Ubisam-MergeTool/doc/history/2026-09-23-12차.md
ksj09406 df06947afc Shift+휠 가로 · Ctrl+↓ 와 아래 "지금 줄" 칸 · Ctrl+W 로 폴더 비교 · 다크 모드
- Shift+휠을 가로 스크롤로(파일 비교 · 메모). 파일 비교는 가로 휠(틸트)과 같은
  길로 보내 마우스가 놓인 칸이 움직이고 나머지가 따라온다. 휠 아래 = 오른쪽.
- Ctrl+↓ / Ctrl+↑ 를 다음 · 이전 차이의 대체 키로. 칸과 상태 줄 사이에 캐럿 행의
  BASE(변경 전) · LOCAL(변경 후)을 위아래로 보이는 칸 - 칸과 같은 CodeTextBlock
  (문법 색 · 바뀐 글자 칠하기 · 줄 색), 긴 줄은 접는다. HomeViewModel.CaretLine.
- 폴더 비교에서 연 문서 탭을 닫으면(Ctrl+W) 폴더 비교 탭으로 간다.
  RequestPlainCompare(fromFolder:) 로 표시를 보내 DocTab.FromFolder 에 붙이고,
  ◀ 이전 파일 · 다음 파일 ▶ 로 짝을 바꿔도 이어 받는다. 다른 길로 연 탭은 그대로.
- 어두운 테마에서 폴더 비교 목록만 흰 바탕 · 검은 글자였다 - 플랫폼 테마가
  TreeView 는 칠하지 않는다. 바탕 · 글자색(항목 포함)을 테마에서 받는다.
  색 구분의 0 항목 흐리기 0.35 -> 0.55(어두운 바탕에서 묻혔다).

시험: 빌드 경고 0 · 오류 0. 화면 g27-five(새, 어두운 테마) - Shift+휠 편집기 가로
0 -> 20% · 세로 그대로 · BASE 칸 위에서도 · 메모, Ctrl+↓ 로 차이 이동 · 아래 칸
표시, 폴더 비교에서 연 탭만 Ctrl+W 뒤 폴더 비교로, 사진으로 폴더 목록 · 아래 칸
확인. 실패 0. 기존 회귀는 돌리지 않았다.

Agent: main (claude-opus-5-5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:02:25 +09:00

1475 lines
115 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 12차 진행 기록 (2026-09-23 ~)
지시서: [../작업지시/12차.md](../작업지시/12차.md)
## Shift+휠 가로 · Ctrl+↓ 와 아래 "지금 줄" 칸 · Ctrl+W 로 폴더 비교 · 다크 모드 (2026-10-01, 사용자 요청 · 결정 · 완료)
1. **Shift+휠 = 가로 스크롤**(파일 비교 · 메모, 사용자 결정). 예전에는 Ctrl+휠(글자 크기)만 따로 받고 Shift+휠은 세로로 굴렀다.
파일 비교는 가로 휠(틸트)과 같은 길(`ScrollSourcePanes` · `ScrollEditor`)로 보낸다 — 마우스가 놓인 칸이 움직이고 나머지는 지금처럼 따라온다. 휠 아래 = 오른쪽.
2. **Ctrl+↓ / Ctrl+↑ = 다음 · 이전 차이**(대체 키, 단축키 표에 더함 — 설명 창에도 나온다). 그리고 **아래 "지금 줄" 칸**(사용자 결정: 캐럿 줄 두 줄) —
칸과 상태 줄 사이에 캐럿 행의 BASE(변경 전) · LOCAL(변경 후)을 위아래로. 칸과 같은 `CodeTextBlock`(문법 색 · 바뀐 글자 칠하기 · 줄 색), 긴 줄은 접어 끝까지,
한쪽에 없는 줄이면 `(이 줄 없음 …)`. `HomeViewModel.CaretLine` — 캐럿이 옮겨지거나 행이 다시 만들어지면(`UpdateSummaries`) 다시 알린다.
3. **Ctrl+W 로 닫으면 폴더 비교로**(사용자 결정: 폴더 비교에서 연 파일일 때만). `RequestPlainCompare(fromFolder:)` 로 표시를 실어 보내고
(폴더 비교 두 번 누르기 · 폴더 검색 결과), 파일 비교가 문서 탭(`DocTab.FromFolder`)에 붙인다. ◀ 이전 파일 · 다음 파일 ▶ 로 짝을 바꿔도(탭을 새로 만드는 `Replace`) 이어 받는다.
그 탭을 닫으면 `TabManager.Activate(ViewIds.Folder)`. 남은 탭은 그대로 다음 탭을 띄워 둔다.
"폴더 비교에서 파일을 열면 하단 탭도 파일 비교로" 는 플랫폼 `8158bbf`(배포본 `2026-10-01 13:37:46`)에서 이미 고쳤다 — 그 전 플랫폼인 PC 에서 보인 것.
4. **다크 모드 가독성** — 어두운 테마로 켜서 찍어 봤다. 가장 큰 것은 **폴더 비교 목록만 흰 바탕**: 플랫폼 테마가 ListBox · ListView · DataGrid 는 칠하지만
TreeView 는 칠하지 않아 WPF 기본(흰 바탕 · 검은 글자)으로 남았다 → 바탕 · 글자색을 테마에서 받고 항목(TreeViewItem) 글자색도 준다(줄 상태 색은 반투명이라 그대로 맞는다).
색 구분의 0 항목 흐리기 0.35 → 0.55(어두운 바탕에서 거의 묻혔다). 차이 줄 바탕색 대비는 두 테마가 비슷해(1.2~1.5) 그대로. 꺼진 단추 글자가 흐린 것은 플랫폼 테마 몫이라 손대지 않았다.
시험(이것만): `g27-five`(새, 어두운 테마) — Shift+휠 편집기 가로 0 → 20%(세로 그대로) · BASE 칸 위에서도 · 메모 0 → 5.7%, Ctrl+↓ 로 차이 2/2 → 1/2 · 아래 칸 BASE · LOCAL 이름표,
기본 짝은 Ctrl+W 뒤 파일 비교 그대로 · 폴더 비교에서 연 alpha 는 하단 탭 파일 비교(1) → Ctrl+W 뒤 폴더 비교(0), 사진으로 폴더 목록 어두운 바탕 · 아래 칸 확인. 실패 0. 기존 회귀는 돌리지 않았다.
## 전체 반영 · 자동 병합의 주석이 너무 많다 (2026-10-01, 사용자 요청 · 결정 · 완료)
[전체 반영] 은 반영할 줄마다 `Apply` 를 따로 불러 **바뀐 줄마다 머지 주석 한 줄**, 되돌리기도 줄마다 한 걸음이었다.
[구간 반영] 은 이어진 차이 구간을 `ApplyGroup` 으로 묶어 구간마다 주석 한 줄(`N~M행 구간 … 반영 (k줄)`)만 남긴다.
- **두 파일 비교 [전체 반영]** → 반영할 수 있는 줄이 이어진 구간(구간 반영의 `BlockAround` 와 같은 기준)을 먼저 다 나눈 뒤
구간마다 `ApplyGroup` — 주석은 구간마다 한 줄, 지우는 반영이 섞이면 `삭제 N줄`. 모든 구간을 한 묶음 번호로 넘겨 **Undo 한 번에 전체가** 돌아간다.
`ApplyGroup` 에 `group` · `finish` 를 더했다(여러 구간을 이을 때 화면 맞추기는 끝에 한 번). 끝나면 `N구간 M줄 반영` 알림.
- **3-Way [전체 반영] 은 끈다**(사용자 결정). 3-Way 에서 이 단추는 차이 나는 모든 줄을 **공통 조상(BASE)** 으로 바꿨다 — 나와 저쪽 변경을 함께 지우는 일이라 병합에서 쓸 데가 없고,
"저쪽 전부" 로 바꾸면 충돌 줄까지 말없이 덮는다. 꺼져 있어도 툴팁(`ShowOnDisabled`)으로 까닭을 보인다.
- **3-Way [자동 병합]** 도 같은 방식(사용자 결정) — 저쪽만 고친 줄이 이어진 구간마다 주석 한 줄, Undo 한 번. 사이에 낀 충돌 · 내가 고친 줄은 구간을 끊고 그대로 둔다.
시험(사용자 지시로 이것만): `g22-accept-all`(새 — 고친 3줄 · 오른쪽에만 2줄 · 오른쪽에서 빠진 2줄): 주석 **3줄**(예전 방식 7) · `삭제 2줄` 1 ·
주석을 빼면 BASE 와 같음 · Undo 한 번에 처음으로. `g23-three-way`(새, `--merge` 로 네 파일 — 저쪽만 3줄 · 2줄 구간, 나만 1줄, 충돌 1줄):
[전체 반영] 꺼짐 · [자동 병합] 뒤 주석 **2줄**(예전 방식 5) · 저쪽 5줄 들어옴 · 내 줄 · 충돌 줄 그대로 · Undo 한 번에 처음으로. 실패 0. 기존 회귀는 돌리지 않았다.
## 스크롤을 내리면 프로그램이 뻗는다 (2026-10-01, 사용자 보고 · 완료)
어제 커밋(`f105bab` 칸 어긋남 고침) 뒤부터 생겼다. Windows 이벤트에 `UbisamBase.Launcher.exe` **응답 없음(AppHang)** 두 번(13:44 · 13:45) — 죽은 것이 아니라
화면 실이 되풀이에 갇혔다. 같은 재료로 BASE 칸을 휠로 끝까지 내리자 재현(9초 만에 꺼짐). 임시 기록으로 찾은 원인 넷(기록 코드는 뺐다):
1. **사람이 BASE 를 굴려도 BASE 를 LOCAL 쪽으로 되돌렸다.** BASE 맞추기는 늘 LOCAL 을 기준으로 봐서, BASE 를 휠로 굴린 만큼(48px)을 매번 끌어당겼다.
그 사이 보정값이 오르내리며(694 → 22) 다른 칸을 다시 움직여 초당 32번 밀고 당겼다. → **누가 굴렸는지**를 손이 닿은 칸(휠 · 누름 · 키)으로 정하고,
LOCAL 쪽일 때만 BASE 를 맞춘다. 스크롤 이벤트로 정하면 글자 크기를 바꿀 때 목록이 저절로 내는 스크롤까지 "BASE 를 굴렸다" 로 읽어 확대 뒤 BASE 를 안 맞췄다(14초 동안 0번).
확대 · 다음 차이 · 줄 이동(프로그램이 편집기를 옮김)은 LOCAL 기준.
2. **따라 굴러가는 도중에 쟀다.** 다른 칸에 굴리라고 한 자리는 다음 배치에야 들어가는데, 배치마다 도는 맞추기가 그 사이에 재서 휠 한 칸(48px)을 늘 어긋남으로 보고
보정값에 쌓았다(6,619px). 그 엉터리 보정값으로 BASE 를 기준 삼으면 편집기 · LOCAL 이 수십~백 줄 엉뚱한 곳으로 갔다. → 메아리 막기 중이면 크게 굴리는 맞추기를 하지 않고
**앱이 한가할 때(ApplicationIdle) 한 번** 맞춘다 — 한가할 때 도는 일은 입력을 밀어내지 않는다. 못 굴리는 끝에서는 보정값 · 다시 그리기를 건드리지 않고 **실제로 움직인 만큼만** 보정한다.
3. **목록 항목 번호를 정렬 행 번호로 썼다.** 칸 목록은 그 칸에 있는 줄만 거른 목록(`PaneView`, 11차 F-10)이라, 한쪽에만 있는 줄이 앞에 있으면 그만큼 밀린다.
어제 넣은 코드가 `ContainerFromIndex(row)` · `IndexFromContainer` 로 둘을 같게 써서 엉뚱한 줄끼리 견줬다(한쪽에만 있는 줄 10개 → 157px, 마지막 차이에서 칸이 통째로 228px 밀림).
→ 줄 객체로 오간다(`RowOf` · `ContainerOfRow`). 실제 파일처럼 한쪽에만 있는 줄이 많으면 이 어긋남이 커져 되풀이가 길어졌을 것이다.
4. **사람이 빠르게 굴리면 스크롤이 버려졌다.** 메아리 막기 중에 온 스크롤은 버리는데, 빠르게 굴리면 사람의 다음 휠까지 버려져 따라가는 칸이 중간에 멈췄다
(BASE 784 · LOCAL 555, 편집기를 굴리면 BASE 37 · LOCAL 473). → 사람이 굴리는 칸의 스크롤이 버려지면 표시만 남기고 멈춘 뒤 한 번 따라잡는다.
따라잡기로 다른 칸이 움직여 생기는 스크롤은 사람이 굴린 칸이 아니라 표시하지 않는다 — 되풀이가 없다. BASE 를 굴릴 때는 보정값을 **지금 보이는 줄에서 다시 잰다**(`MeasureBias`).
시험(사용자 지시로 이것만): `g20-scroll-end`(새, 보통 · 빠르게 — 편집기 · BASE 휠로 끝까지 · 처음까지) 모든 단계 **응답**, BASE · LOCAL 가운데 줄 한 줄 안.
`g21-blocks-fast`(새, 60줄마다 오른쪽에서 5줄 빼고 7줄 넣은 재료) 응답 · 따라옴 — 화면 가운데가 한쪽에만 있는 묶음에 걸리면 그 자리는 일부러 맞추지 않아 10줄 남짓 벗어날 때가 있다.
`g7-verify`(Alt+↓) 실패 0, `g17`(엔터 · 확대 · 내리기) 통과(남은 ✗ 는 대본 한계). **사용자 파일로는 확인하지 않았다** — 실제 저장소를 읽지 않는다. 기존 회귀는 돌리지 않았다.
## 끌 때 저장하지 않은 편집을 묻는다 (2026-09-30 밤, 사용자 요청 · 결정 · 완료)
사용자 보고: 편집이 있는데 하단 [종료] 를 누르면 그냥 꺼진다. 편집은 보관본(E-1)이 다음에 되살렸지만(g16 로 확인), 사용자 판단은
"잘못 고치고 있었으면 어떻게 하나 - 버릴 수 있어야 한다" 였다. 결정(사용자): **파일 비교의 모든 탭**(Git 모드 포함), 메모는 제외,
[저장하고 종료] · [저장하지 않고 종료] · [취소], 버리면 **보관본도 비워** 다음에 되살아나지 않게.
- 플랫폼에 이미 있는 `ICloseGuard` 를 `HomeViewModel` 이 구현(`HomeViewModel.Close.cs`) — 셸이 열린 탭 화면의 DataContext 에 묻는다. 플랫폼은 고치지 않음.
- 묻는 범위: 지금 화면(두 파일 · Git 모드 · 네 칸 — 스타일 맞춤만 바뀐 글은 빼는 `WillAskBeforeDiscard` 와 같은 기준) · 뒤에 맡겨 둔 탭 · 닫았지만 편집이 남은 짝(`(닫은 탭)` 이라고 적는다). 창에 파일 이름을 10개까지.
- 저장: 지금 화면은 기존 `SaveWorkingCopy`(Git 충돌 파일 · 왼쪽 칸까지), 맡겨 둔 글은 그 짝의 저장 형식으로 `WritePlainFile`(백업 포함). **하나라도 못 쓰면 끄지 않는다.**
- 보관본: 평소 쓰기는 다른 실(`Task.Run`)이라, 끌 때는 걸려 있던 쓰기(`_sessionWrite`)를 기다린 뒤 **그 자리에서** 비운다 — 늦게 도착한 옛 쓰기가 방금 비운 것을 다시 채우지 않게. 묻지 않고 끄는 경우(고친 것 없음)도 비워 맞춘다.
- **처음엔 창이 꺼지지 않았다** — 기록: `Shell: 닫기 확인 중 오류 … 창을 닫는 중에는 … Close 를 호출할 수 없습니다`. 셸은 답을 받은 뒤 `Close()` 를 다시 부르는데,
곧바로 답하면 그게 아직 Closing 안이다. `await Task.Yield()` 로 한 차례 비켜선 뒤 묻게 해 풀었다(플랫폼은 그대로 — 동기로 답하는 가드가 있으면 같은 일이 난다는 점만 적어 둔다).
시험 `g19-exit-confirm`(새, 사용자 지시로 이것만): ① 고친 것 없음 → 묻지 않고 꺼짐 ② 고친 뒤 종료 → 창(파일 이름 · 단추 셋) → [취소] 면 앱 · 편집 그대로
③ [저장하지 않고 종료] → 꺼짐 · 파일 그대로 · 보관본 빔 · 다시 켜도 안 살아남 ④ 고친 직후(0.3초) [저장하고 종료] → 파일에 씀 · 보관본 빔
⑤ 뒤 탭에만 편집(alpha, 화면은 bravo) → 창에 alpha.cs · [저장하고 종료] 로 alpha 만 씀. **실패 0.** 대본의 [종료] 누름은 다른 실에서 한다(창이 뜨면 그 누름이 돌아오지 않는다).
**Git 모드는 화면으로 시험하지 않았다** — 지금 화면의 저장은 기존 `SaveWorkingCopy` 를 그대로 쓴다. **기존 회귀는 사용자 지시로 돌리지 않았다.**
## 엔터를 꾹 누르면 줄이 어긋난다 · Ctrl+휠로 키우면 화면이 잘린다 · 내리면 BASE 와 LOCAL 이 따로 논다 (2026-09-30 밤, 사용자 보고 · 완료)
### 엔터 꾹 — 캐럿과 LOCAL 칸의 줄이 다르다 (사진)
편집기 줄 → 정렬 행 자리를 **글자 수**로 지켜, 엔터가 여러 번 쌓이는 동안 캐럿 줄과 LOCAL 칸 줄이 벌어졌다.
글을 갈아 끼운 뒤 캐럿은 글에서 직접 센 줄 자리로 둔다(`TakeCaretLine` · `LineSpan`, 앞 커밋의 방식 그대로).
또 편집기가 스크롤될 때 **메아리 막기 중에도 LOCAL 칸은 따라가게** 했다 — 막는 동안 LOCAL 만 멈춰 칸이 편집기에 뒤처졌다.
### Ctrl+휠 확대 · 쭉 내리기 — 칸이 통째로 밀리고 BASE 가 다른 줄을 본다
임시 기록(`MERGEHUB_ZOOM_TRACE`, 끝나고 뺌)으로 잰 것:
1) 칸 목록은 가상화라 안 그린 줄 높이를 어림으로 센다 — 1,806줄 847행에서 전체 길이가 편집기 28,896 · 목록 27,940, 같은 픽셀이 다른 줄을 가리킨다.
이 차이를 네 칸 공통 이동값(`_decorationShift`)으로 메우다 467px 까지 커져 **칸 위가 텅 비었다**. →
두 줄 넘게 어긋나면 이동값 대신 **LOCAL 목록을 그만큼 실제로 굴리고** 그 차이를 칸별 보정(`_paneBias`)으로 기억한다(다음 편집기 스크롤도 더해 굴림, 목록을 다시 세우면 비움).
2) BASE · REMOTE 는 **같은 줄끼리** 맞춘다(`AlignOtherPanesByRow`): LOCAL 가운데 줄이 그 칸에도 있으면 두 행 윗변을 견줘 두 줄 넘게 다르면 굴린다.
한쪽에만 있는 줄 묶음 한가운데는 맞추지 않는다 — 처음엔 짝(묶음 아래 줄)을 불러왔는데 가운데 맞추기가 도로 밀어내 **초당 600번 넘게 되풀이하며 타이핑 입력을 밀어냈다**(기록 16,081번). 같은 줄은 한 번만 부른다.
3) 맞추기 기준을 **편집기에 보이는 첫 줄**로 바꿨다. 예전엔 "목록에 보이는 첫 행" 을 편집기에서 찾았는데 목록이 수십 줄 앞서 있으면 그 줄이 편집기 화면 밖이라 **95번 중 95번 맞추기를 포기**했다.
4) 편집기에 묻는 값 둘이 틀렸다(UIA 로 실제 보이는 줄을 읽어 대조):
- `TextBox.GetFirstVisibleLineIndex` — 글꼴 기본 줄 간격으로 어림해 늘린 줄 높이를 모른다. 실제 886 에서 1033.
- `GetRectFromCharacterIndex` — **글자 크기를 바꾼 뒤 몇 초씩 옛 배치로 답했다**(보이는 첫 줄 728 을 1,892px 위라고). 그래서 매번 "화면 밖" 으로 빠져 LOCAL 이 69줄 어긋난 채 남았다.
→ 줄 바꿈이 꺼져 있으면 줄 높이가 고정(`BlockLineHeight`)이니 **계산**한다: 첫 줄 = ⌈VerticalOffset ÷ 줄 높이⌉, 그 줄 위치 = 줄 × 줄 높이 − VerticalOffset + 글자 위 여백.
정상 상태에서 계산값과 편집기 답의 차이 0 을 기록으로 확인했다. 줄 바꿈을 켜면 예전처럼 점으로 찾는다.
시험(`g17-hold-enter`, 사용자 지시로 이 두 건만): 엔터 40번 → LOCAL 칸 번호 = 편집기 줄 번호(936) · 같은 높이(y 1379).
확대 뒤 편집기(UIA) 첫 줄 702 · LOCAL · BASE 가운데 722/722 · 칸 위 비지 않음. 30칸 내린 뒤 776 · 795/795. 확대만 하는 판도 708/708 · 775/775.
**키를 꾹 누른 흉내에서는 40번 중 30~34번만 들어갔다** — 고치기 전 커밋도 같았고, 따로따로 누르면 40번 다 들어간다. 앱이 바쁠 때 Windows 가 반복 키를 합쳐 보내는 것이라 편집을 잃는 것이 아니다.
대본의 "확대 뒤 Method100 높이" 칸은 확대로 Method100 이 화면 밖에 나가 견줄 것이 없어 ✗ 로 남는다(대본 한계).
**기존 회귀는 사용자 지시로 돌리지 않았다.**
## 머지 주석 · 타이핑 속도 · 탭 알림 (2026-09-30 저녁, 사용자 요청 · 완료)
### Accept 뒤 Alt+↓ 를 두 번 눌러야 다음 차이로 간다 (사용자 보고)
재현(`g12-note`): Accept 직후 캐럿이 **새로 끼운 주석 줄**에 서서 첫 Alt+↓ 는 방금 반영한 줄로 갔다. 편집기는 글을 갈아 끼울 때
캐럿을 글자 수 자리로 지키는데, 캐럿 위에 주석 한 줄이 끼면 그 자리가 주석 줄이 된다.
고침: 주석 줄을 끼우거나 빼기 **직전 캐럿 행(MergeLine)** 을 기억했다가(`NoteRowMoved` → `_caretAnchor`), 글을 갈아 끼운 뒤
그 행이 몇 번째 편집기 줄이 됐는지(`EditorLayout.LineOfRow`)로 캐럿을 둔다(`TakeCaretLine`). 처음엔 "끼운 줄 수만큼 옮기기" 로 했는데
지운 줄(편집기에 줄이 없는 행)에서 어긋났다. 또 **임시 기록으로 확인한 것**: 글을 막 갈아 끼운 순간 TextBox 의 줄 정보는 아직 **옛 글 기준**이다
(새 글 7번 줄을 물으니 옛 글 주석 줄의 자리 · 길이가 왔다) → 줄 시작 자리는 새 글 문자열에서 직접 센다(`LineSpan`). 기록 코드는 뺐다.
### 줄을 지우는 반영에도 주석 · 삭제 표시 (사용자 요청 · 표시 모양은 사용자 선택)
BASE 에 없는 줄을 반영하면 LOCAL 에서 지우는데 주석이 남지 않았다(`Apply` 가 BASE 에 줄이 있을 때만 주석을 만들었다). 이제 지운 자리에도 남긴다 —
한 줄이면 `… BASE(a.cs) 삭제`, 구간 · 메서드로 여러 줄이면 한 번만 `… 삭제 N줄`(`MergeNote.DeletionTag` · `WithDeletion`).
왼쪽으로 반영(BASE 에서 지움) · 3-Way 저쪽 반영 · 구간/메서드 반영 모두. 묶음이 전부 지우는 반영이면 첫 줄 자리에. 들여쓰기는 지우기 전 글에서.
하네스 `NoteTest` 99 → 106건. `g12-note`: 삭제 주석 · 구간 반영(바뀐 1 + 지운 2) → 주석 한 줄에 `삭제 2줄` · Ctrl+Z 로 지운 줄과 주석이 함께 돌아감.
### 색 구분 "줄 안에서 바뀐 부분" 에 줄 수 (사용자 요청)
이 칸만 개수가 없었다. "BASE와 다름" 줄 가운데 단어 단위로 달라진 구간이 있는 줄 수(`InlineChangeCount`). `g14-inline`: 재료에서 1 → 전체 반영 뒤 0.
### 파일 비교 타이핑이 느리다 · 엔터 · 들여쓰기는 더 느리다 (사용자 보고)
임시 기록(`MERGEHUB_PERF_TRACE`, 끝나고 뺌)으로 1,806줄 C# 에서 사람 속도로 쳐서 쟀다(`g13-perf`).
글자 하나 처리는 1.4ms 로 빨랐고, **엔터 · 줄 합치는 지우기마다 도는 재정렬이 0.37~0.42초** 였다. 단계별로 쪼개 보니
파일 비교(`BuildLines`)는 0.012초, **1,800줄을 목록에 한 줄씩 넣으며 알림이 1,800번 가는 것이 0.29~0.31초**, 비우기 0.04초.
1) `MergeLineCollection.ReplaceAll` — 한 번에 넣고 알림은 Reset 한 번 → 재정렬 0.10~0.14초. 그래도 재정렬 뒤 화면 다시 그리기가 0.15~0.18초 —
다시 재 보니 색칠(CodeTextBlock) 0.02~0.04초 · 선 맞추기 0.0002초이고 **나머지는 칸 목록이 줄 요소를 새로 만드는 비용**(줄 객체가 모두 새것).
`VirtualizationMode=Recycling` 은 효과가 없어 되돌렸다.
2) `MergeLineCollection.Patch` — **번호 말고 모든 값이 같은 줄**(`MergeLine.SameStateExceptNumbers`: 글 · 있음/없음 · 단어 차이 · 반영/직접 수정/주석/무시 ·
옮김 · 책갈피 · 블록 주석 · 3-Way 칸 · 비교 규칙)은 기존 객체를 두고 번호만 고치고(`TakeNumbersFrom`), 가운데만 빼고 넣는다. 가운데가 200줄을 넘으면 통째로.
값이 하나라도 다르면 새 객체 — 옛 객체에 값을 옮겨 적다 빠뜨리는 일이 없게. 재정렬 자리(`ReloadLinesFrom`)에만 쓰고, 파일을 여는 자리는 통째로.
결과(엔터 한 번): 재정렬 0.37~0.42초 → **0.037~0.05초**, 뒤 다시 그리기 0.15~0.2초 → **0.011~0.018초**, 다시 칠한 줄 약 290 → 0~8.
하네스 `PatchTest`(새) 16건 — 먼저 실패 확인. 화면 `g15-typing-tabs` ①: 엔터 · 공백 4 · 123 → 새 줄 제자리, LOCAL 번호 896 → 897 → 898, BASE 그대로.
**기존 회귀(22벌)는 사용자 지시로 돌리지 않았다** — "방금 요청한 것만". ReplaceAll 단계까지는 파일 비교 여덟 벌이 통과했다(Patch 전).
### 탭이 20개를 넘어 조용히 닫힐 때 알림 (사용자 요청)
로그 한 줄뿐이던 것에 알림(`ShowWarningToast`)을 더했다 — "탭이 20개를 넘어 오래 안 본 '…' 탭을 닫았습니다 (고친 것이 없던 탭)".
닫히는 규칙은 그대로(저장할 것 없는 탭 가운데 가장 오래 안 본 것, 고친 탭 · 보고 있는 탭은 안 닫음, 모두 고쳤으면 20을 넘겨서라도 연다).
`g15-typing-tabs` ②: t01~t21 을 차례로 열자 21번째에서 알림 · t01 이 닫히고 t02~t21 20개(사진). 처음 두 판은 대본이 1번을 열지 않아(켤 때 기본 짝이 탭이 되리라 가정)
20개에 그쳐 알림이 안 떴다 — 사용자가 화면을 보고 짚었다. 대본이 탭 줄 이름을 1개만 읽는 것은 읽기 한계라 사진으로 셌다.
### 이 PC 메모
시험 중 앱이 빈 메모(`01.txt` 0바이트)와 `목록.txt` 를 지운 적이 있다 — 대본은 메모 폴더를 건드리지 않는다. 앱이 빈 메모를 정리한 것으로 보이나 값으로 확정하지 않았다. 끝에 기준본으로 되돌림.
## 커밋 뒤 사용자 요청 셋 (2026-09-30 · 완료)
### 폴더 비교 → 두 번 눌러 파일 비교로 가도 하단 탭이 "폴더 비교" 에 남는다 (사용자 보고 · 플랫폼에서 고침)
사용자가 쓰던 앱을 **읽기만** 해서 재현 — 화면은 파일 비교(편집기 보임, 폴더 칸 안 보임)인데 하단 탭 선택은 0번.
원인은 MergeHub 가 아니라 공용 플랫폼: 셸의 `NavList` 는 동작형 항목을 섞느라 `SelectedMainTab` 에 바인딩하지 않고 켤 때 한 번만 넣는다.
사람이 누를 때만 화면 쪽으로 알리므로, 코드가 탭을 바꾸면(`TabManager.Activate` — 폴더 비교 두 번 누르기 두 곳 · Git 충돌 열기 ·
파일 비교 → 폴더) 화면만 바뀐다. 사용자 허락("플랫폼 고쳐줘")을 받아 `UbisamCommonPlatform` 의 `ShellWindow` 가
`SelectedMainTab` 변화를 듣고 선택만 맞추게 고쳤다(플랫폼 저장소 changelog · 가이드에 적음). 빌드 = 이 PC 배포 폴더에 바로 반영,
전 상태는 `D:\UbisamPlatform\bin-backup-navsync`. 화면 `g10-drop` ①: 두 번 누른 뒤 하단 탭 선택 **[0] → [1]**.
### Git 탭에 폴더를 끌어다 놓으면 연다 (사용자 요청)
- `PathUtil.RepositoryFolderFor` — 파일이면 그 폴더, 저장소 안이면 최상위, 저장소가 아니면 그 폴더 그대로(열기가 알린다). `[폴더...]` 도 같은 함수로 바꿨다.
- Git 탭 전체에서 `PreviewDragOver` · `PreviewDrop` 으로 받는다 — 경로 칸 · 커밋 메시지 칸(TextBox)이 파일 놓기를 먼저 먹는 문제(7차 E-2)를 피한다.
파일 놓기만 가로채고 글자 끌어다 놓기는 그대로. 여럿이면 첫 것.
- 하네스 `PathTest` 6건을 먼저 넣어 **컴파일 실패** 확인 → 62 → 68건.
- 화면: 흉내 낸 OLE 끌기(작은 창에서 `DoDragDrop` + 다른 실이 마우스를 옮김)는 **이미 되는 폴더 비교 칸에도 안 들어갔다**(결과 None) —
시험 도구의 한계로 보고(창 끌기와 같은 부류), **사용자가 직접 끌어다 놓아 확인**: 로그 17:03 · 17:04 `저장소 열기: D:\Sorce\UbisamCommonPlatform\`
(그 사이 [폴더...] · [열기] 누름 기록 없음, 오류 없음).
### 파일 비교 편집 칸 바탕 (사용자 요청 "회색 말고, 어중간하게 말고")
칸에 바탕이 없어 페이지 바탕(밝음 #F2F2F3 · 어두움 #23262B, 둘 다 회색)이 비쳤다. 플랫폼 토큰에 맞는 짝이 없어 `PaneBackgroundConverter` 가
물려받은 글자색 밝기로 테마를 가린다(코드 글자색과 같은 방법). 흰색 · 순검정 → "너무 까맣다" → #1E1E1E → "둘 다 보기 편하게" 에서
후보 네 개씩을 실제 코드 색으로 그려 보여 드렸고 **사용자가 밝음 #FAFAFA · 어두움 #252830** 을 골랐다. 네 칸(BASE · LOCAL · 원본 LOCAL · REMOTE)에.
화면 `g11-pane` 두 테마: 빈 자리 화소 #FAFAFA / #252830 (BaseLines · LocalEditor 모두).
## G 묶음 마무리 — 알파 · 베타 전 정리 (2026-09-30, 사용자 지시 · 완료)
### 설정의 "프로그램 정보" 칸을 뺐다
`만든 사람` · `사내 문의처` 두 칸 — 설정 → 개발자 정보가 담당을 보이므로 필요 없다(사용자). [정보] 창은 이제 **실행 파일에 박힌 값만**
읽는다(만든 사람 = 회사 이름, 문의처 = csproj `<SupportContact>`). 그래서 `ProgramInfoReader.For` 의 "설정값 먼저" 인수를 걷고,
빈 칸 안내 글을 `적혀 있지 않습니다 (설정 > MergeHub 에서 적습니다)` → **`적혀 있지 않습니다`** 로 줄였다(없는 칸을 가리키게 되므로).
옛 설정 파일에 남은 두 키는 읽을 때 버려진다 — `g5-verify` 2단계가 옛 키에 값을 적어 두고 켜서 안 쓰이는 것을 확인.
`OverviewViewModel` 은 설정을 읽을 일이 없어져 인수 없는 생성자로(플랫폼 `AddSub` 가 DI 로 만든다).
하네스 `AboutTest` 를 새 모양으로 먼저 고쳐 **컴파일 실패** 확인 → 30건 통과. 화면 `g5-verify` 3건 · `g6-verify` 2단계
(설정 → MergeHub 에 `프로그램 정보` 묶음 · 두 칸 없음, 다른 칸 그대로) 실패 0.
### 작성자가 비면 반영하지 않고 알린다
사용자 요청 "반영한 줄에 주석 남기기에서 작성자를 입력하지 않으면 알람". 방식은 물어 정했다 — **반영할 때 알림 + 반영 멈춤**.
예전에는 비어 있으면 git `user.name` → 윈도우 계정으로 조용히 채웠다(장비를 여럿이 쓰면 주석 이름이 틀리게 남는다).
- 판단은 `MergeNote.AuthorMissing(주석 켬, 작성자)` — 주석을 끈 사람에게는 묻지 않는다. 알림 글도 `MergeNote` 에(설정 자리를 댄다).
- 거는 자리: 반영 **명령 맨 앞** 여덟 곳(한 줄 · 모두 · 구간 · 메서드 · 왼쪽으로 한 줄 · 왼쪽으로 구간 · 3-Way 내 것 · 저쪽).
줄마다(`Apply` 안)가 아니라 명령마다 — [모두 반영] 이 줄 수만큼 알리거나 구간이 반쯤 반영된 채 멈추지 않게.
보고서 내보내기 · 변경분 추출의 이름은 반영이 아니므로 그대로(옛 물러서기).
- 화면에서 하나 더: 작성자가 비었는데 "머지 설명" 미리보기가 `// [Ubisam] …`(윈도우 계정)을 보였다 — 그 이름으로는 반영되지 않으므로
`(작성자가 비어 있어 반영할 수 없습니다 - 설정 > 머지 주석 > 작성자)` 로.
- 설정 칸 이름 `작성자 (비우면 git config user.name)` → `작성자 (주석을 남기면 꼭 적습니다 - 비어 있으면 반영하지 않고 알립니다)`.
- 한때 사용자 지시로 "주석 남기기를 늘 켬"(설정 칸 감춤)으로 바꿨다가 **곧 "고를 수 있게 원복"** 지시 — 되돌렸다. 늘 켬 판은 커밋되지 않았다.
- 하네스 `NoteTest` 에 6건을 먼저 넣어 **컴파일 실패** 확인 → 93 → 99건 통과. 화면 `g9-author.ps1`(새): ① 주석 켬 · 작성자 빔 →
알림 창("작성자를 적어 주세요", 설정 자리를 댐) · 편집기 그대로 · 미리보기 안내 ② 주석 끔 → 묻지 않고 반영 ③ 작성자 적음 →
`[G9시험자]` 주석과 함께 반영. 실패 0. (처음 판 3단계 실패 2건은 2단계 편집이 저장 안 한 편집 보관함으로 되살아난 것 — 단계 사이에 보관함을 비움.)
### 데모 걷어냄 · 이 PC 의 시험 흔적
- 코드: 설정 `데모` 묶음(`DemoGitPath` · `DemoPlainPath`), 파일 비교 위 [데모 → .git 있음/없음] 단추와 `SwitchDemoAsync`. 켤 때 쓰는
`LoadDefaultPlainFiles` 는 남김. 데모를 예로 든 주석 네 곳도 고침. `g6-verify` 가 설정 화면에 `데모` 가 없는 것을 본다.
- 이 PC(사용자 선택 "시험 흔적까지"): 시험 대본이 만드는 `%TEMP%` 재료 13개(e2-fold · g2 · mergehub-demo · plain-compare 등, 약 3MB)를
**휴지통으로**(영구 삭제 아님). 설정의 기본 저장소 · 기본 왼쪽/오른쪽 파일(시험 경로)을 비우고 데모 키 둘을 뺐다.
최근 목록에서 시험 경로 6줄만 뺐다(사용자가 실제로 비교한 18줄은 그대로). 이 상태를 새 기준본으로(`base-0930`, 옛 설정은 옆에 따로).
## G-6 고침. 담당 카드 · 프로그램 소개 문구 (2026-09-30, 사용자 지시 · 완료)
- 담당 카드를 **갈래 둘**로: `Platform` 최민영 대리 · `MergeHub` 김승준 사원. 이름표는 영문(사용자 선택). 사람은
`ProgramOverview.PlatformOwner` · `MergeHubOwner` 두 상수 — 설정의 "만든 사람" 은 정보 창의 회사 이름 받침이라 따로 둔다.
- **사내 문의처는 카드에서만 뺐다**(사용자 선택). [정보] 창의 문의처 줄과 G-5 구조(csproj `<SupportContact>` · 설정)는 그대로 — `g5-verify` 3건 통과.
- 소개 문구 `사내 Git Merge/Diff 도구` → **`사내 Merge/Diff + Git 도구`**(Merge 가 먼저 — 사용자 지시). 네 곳을 같은 말로:
csproj `<Description>` · `ProgramOverview.DefaultSubtitle` · 정보 창 받침 글 · 창 제목 `GetAppName()`(`MergeHub - 사내 Merge/Diff + Git`).
창 제목은 플랫폼이 왼쪽 위 글로만 쓰는 것을 읽어 확인했다(설정 폴더 등 열쇠로 안 쓴다).
- 시험: 하네스 `OverviewTest` 새 기대값으로 먼저 고쳐 **고치기 전 4건 실패** → 15건 통과, `AboutTest` 30건. 화면 `g6-verify` 실패 0
(두 담당자 위아래 · 같은 가운데, 설정에 만든 사람 · 문의처를 적어도 카드에 안 나옴, 창 제목) · 사진 확인. 끝에 기준본으로 되돌림.
## G-8. 메모 문법 강조 · 같은 글 칠하기 · 색 마킹 (2026-09-30, 사용자 요청 · 완료)
### 정한 것 (사용자 결정)
- 방식: 파일 비교처럼 **투명한 TextBox + 아래층**. 처음 지시의 `RichTextBox` 는 버렸다 — 칠하는 일이 되돌리기 기록에
섞이고, 줄 번호 · 캐럿 · 찾기 · 보관을 다시 짜야 해서.
- 강조 언어: 메모마다 콤보로 고른다. 기본은 파일이면 확장자, 이름 없는 메모는 C#. **보관하지 않는다.**
- 같은 글 칠하기: 한 줄 · 공백 아님 · 200자 이하 선택만. 낱말을 통째로 고르면 낱말 단위, 아니면 글자 단위.
대소문자를 가린다. 상한 5,000곳. 한 곳뿐이면(고른 그 자리) 칠하지 않는다.
- 마킹: 색 다섯, **Ctrl+1~5**(숫자판 포함) · 단추. 같은 글에 같은 색이면 풀고 다른 색이면 바꾼다. 고른 것이 없으면
캐럿 아래 낱말. 메모마다 따로, **보관하지 않는다.** 선택을 풀어도 남는다(글을 기억해 두었다가 그릴 때마다 찾아 칠한다).
- 오른쪽 위치 막대 12px: 같은 글은 왼쪽 반, 마킹은 오른쪽 반, 캐럿 줄은 가는 선. 누르면 그 줄로.
### 만든 것
- `Services/MemoHighlight.cs` — 셈하는 부분(`SmartTarget` · `Occurrences` · `LineStarts` · `LineOf` · `BarTicks` ·
`Tokenize`(여러 줄 주석 이어받기) · `SplitLines`), 줄 토큰 캐시 `MemoSyntaxCache`, 마킹 목록 `MemoMarkBook`.
로직을 먼저 쓰고 하네스를 붙였다 — **"고치기 전 실패" 단계는 없었다.**
- `Views/Controls/MemoPaintLayer.cs` — 아래층(보이는 줄만 · 배경 → 글자). 글자 자리는 스스로 셈하지 않고 편집기
`GetRectFromCharacterIndex` 에 묻고, 탭이 든 줄은 탭마다 끊어 다시 묻는다. `MemoMarkBar` — 위치 막대.
- `Memos.cs` — `Memo.Language`(고르지 않으면 기본값을 셈) · `Memo.Marks`.
- `MemoView.xaml(.cs)` — 콤보 · 마킹 단추 다섯 · [마킹 지우기] · 세 열 편집기. 다시 칠하기는 `Background` 우선순위로
한 번에 모은다(타이핑 한 번에 TextChanged · SelectionChanged 가 함께 온다). 글 · 언어가 그대로면 줄 토큰을 다시 셈하지 않는다.
세로 **또는 가로** 스크롤 · 크기 바뀜 · Ctrl+휠 뒤에 다시 그린다. 상태 줄 캐럿 글 끝에 `· 같은 글 N곳`.
### 화면에서 나온 것 (셋, 모두 값 · 사진으로 확인)
1. **아래층이 통째로 안 보였다.** 첫 `g8-verify` 에서 칠한 글자 잉크가 **0px**, 사진에는 편집기 선택 색만 있었다.
`같은 글 5곳` 은 나왔으니 칠할 값까지는 셈했다 — 파일 비교 `LocalEditor` 주석에 적힌 것과 같은 까닭:
플랫폼 암시적 TextBox 스타일이 템플릿 안에서 배경을 직접 칠해 `Background="Transparent"` 가 무시된다.
편집기에 `Style="{x:Null}"`. 줄 번호 칸은 "편집기와 같은 템플릿이라 같은 자로 잰다" 가 전제라 **함께** 뺐다
(줄 번호 7 과 7번째 줄 윗변 403px = 403px 로 확인). 선택 색은 LocalEditor 와 같은 값(Accent · 0.35)을 주었다 —
**선택 색은 사진으로 확인 못 함**(시험이 편집기에 포커스를 주지 않아 WPF 가 선택을 안 그렸다).
2. **강조 콤보에 `MergeHub.Services.LanguageProfile` 이 보였다**(사진). 플랫폼 콤보 템플릿은 선택 칸에 `DisplayMemberPath` 를
안 쓴다 — G-4 `SaveChoice` 와 같은 길로 `LanguageProfile.ToString() => Name`. 부르는 곳이 없는 것을 확인하고 넣었다.
3. `MemoMarkBar` 는 `FrameworkElement` 라 `Foreground` 가 없어 빌드 오류 — `TextElement.Foreground` 로.
### 어두운 테마에서 메모 탭 제목이 안 보인다 (사용자 보고, 같은 날)
`g8-tabs.ps1`(키 입력 없이 UIA + 사진)로 재현: 제목 `메모 1` 글 칸에서 바탕(#23262B)과 가장 다른 점의 차이 **116**(검정 글씨).
탭 줄의 `ItemContainerStyle` 이 플랫폼 `ListBoxItem` 스타일을 대신해서 글자색이 WPF 기본(검정)으로 떨어졌다.
`Foreground = Ubisam.Brush.ContentForeground` 를 그 스타일에 넣음 → 어두운 587 · 밝은 635.
파일 비교 문서 탭(`DocTabStrip`)도 같은 구조라 사용자 지시로 이어서 확인했다. `g8-doctabs.ps1`(폴더 비교에서 alpha · bravo 를
열어 탭 둘)로 재현 — 어두운 테마에서 `alpha.cs` 157 · `bravo.cs` 116. 같은 한 줄을 넣어 → 어두운 546 · 587, 밝은 673 · 635.
### 시험
- 빌드 경고 0 · 오류 0. 하네스 `MemoHlTest` 45건, `MemoTest` 69건(그대로 — `.csproj` 에 새로 필요한 소스 여섯 개를 넣음).
- 화면 `g8-verify.ps1 [-Dark]` — 편집기가 UIA 로 알려 주는 글자 칸(= 캐럿이 서는 자리)과 그 칸 안에 실제로 찍힌 잉크의
왼끝 · 오른끝을 px 로 비교한다(반 글자만 밀려도 칸 밖에 잉크가 걸린다).
① 겹침: `speed` 칸 95~148 · 잉크 96~146(왼 +1 · 오른 +2), 탭 든 줄 `speedLimit` · 뒤쪽 `speed` 도 +1~2 · 세로 칸 안.
② 색: 키워드 · 형 키워드 · 보통 이름 · 주석이 갈리고 여러 줄 주석 둘째 줄이 주석 색을 이어받는다. 콤보로 `평문` 을 고르면
`public` 이 보통 글 색, C# 으로 돌리면 돌아온다. ③ 낱말 통째 `같은 글 5곳`(speedLimit 안은 빼고) · 일부 `6곳` ·
`Speed` 는 표시 없음, 다른 자리 배경 칸 안 바탕 0%. ④ Ctrl+1 · 다시 누르면 풀림 · [마킹 2] · Ctrl+숫자판3 으로 색 바꿈 ·
[마킹 지우기] 1개 · 낱말 없는 자리는 안내만. ⑤ 노랑 눈금 y 81 · 95 · 111 · 1122 · 1138(기대 80 · 96 · 112 · 1123 · 1139),
막대를 누르면 71행. ⑥ **'7' 을 치고 마킹 · 선택을 바꾼 뒤 Ctrl+Z 한 번에 타이핑 전 글자 수로**(G-1 회귀 없음).
⑦ 가로로 굴린 뒤 71줄 끝 `speed` 도 +1~2. ⑧ 어두운 테마 같은 항목 모두.
**밝은 · 어두운 모두 실패 0**(사용자가 손을 뗀 동안 마지막으로 돌린 판).
- 흔들림: 사용자가 메시지를 보내던 동안 돌린 세 판은 판마다 다른 자리가 실패했다('7' 이 안 들어감 → Ctrl+Z 가 재료까지
되돌려 0자, 안내 글 항목 등). 흉내 낸 입력에 사용자 입력이 섞인 것으로 보이나 **값으로 확정하지 못했다.**
- 끝날 때마다 설정 네 파일 · 메모를 `base-0930` 과 해시가 같게 되돌림.
### 최종 회귀 (G 묶음 커밋 전, 사용자가 손을 뗀 동안)
`run-final.ps1` — 아홉 벌 + g1~g8(g8 셋은 밝은 · 어두운 두 번) 22벌: **통과 423 · 실패 4**, 끝에 설정 · 메모 해시 기준본과 같음 · 세션 보관함 0개.
실패 4건은 모두 `g8-verify -Dark` ④ 상태 글 — 읽힌 글이 `같은 글 · 마킹이 있는 자리. 누르면 그 줄로 갑니다`(위치 막대 **툴팁**)였다.
앞 벌(밝은)이 ⑤에서 막대를 누른 자리에 마우스가 남아 툴팁이 떴다. 같은 판의 배경 값(걸면 0% · 풀면 61% · 지우기 "1개")은 맞았다 →
**대본 잘못.** 상태 글에서 툴팁을 빼고 시작할 때 마우스를 치우게 고쳐 다시 돌림: 마킹 4건 통과.
그 판에서 ⑦ 마지막 항목("위로 돌아온 탭 줄") 한 번이 실패했다 — `speedLimit` 칸 130~237 안 잉크가 127~154 로 일부만.
실패할 때 사진을 남기게 하고 **다섯 번 더 돌렸으나 모두 실패 0** 이라 재현하지 못했다. 가로 스크롤을 되돌린 직후의 그림을 뜬 것으로
보이나 **값으로 확정하지 못했다**(사진이 없다). 다시 나오면 `g8-dark-7b.png` · `7c.png`(2초 뒤 다시 잰 것)가 남는다.
## G-7. `Alt+↓` 로 이동하다 보면 화면이 잘린다 (2026-09-30, 사용자 보고 · 완료)
### 먼저 재현 (값)
600줄 재료에 모양이 다른 차이 여덟 개(수정 한 줄 · 추가 5줄 · 삭제 10줄 · 400자 긴 줄 · 수정 · 50줄 큰 추가 ·
수정 · 마지막 줄)를 만들고 `Alt+↓` 를 차례로 누르며 칸마다 위치를 쟀다(`g7-probe.ps1`, 사진 `g7-00`~`g7-10`).
증상이 **둘** 나왔다.
| 언제 | 값 | 화면 |
|---|---|---|
| 앞으로 갈 때(2~7번째) | 차이 줄 위 **1,361px** · 칸 아래 1,394px · 줄 높이 약 35px | 가운데(842px)에 와야 할 차이 줄이 **칸 맨 아래에 걸려 잘림** |
| 마지막 → 첫 차이로 돌 때(9번째) | 두 칸의 위가 289 → **832px**(+543) | 두 칸이 통째로 반 칸 밀려 **위는 비고 아래는 잘림** |
### 원인 (기록으로 확인)
`MERGEHUB_G7_TRACE` 로 켜는 임시 기록을 `OnScrollToRowRequested` · `AlignDecorationToEditor` 에 심었다(끝나고 뺐다).
```
GOTO row=29 edOff=8950.5 listOff=8948.5 shift=-6.5
ALIGN row=29 ... wanted=-5.0 edOff=436.7 listOff=436.7 ← 편집기가 캐럿을 들이려고 먼저 구름, 목록도 따라옴
ALIGN row=29 ... wanted=357.2 edOff=74.4 listOff=436.7 ← 9ms 뒤 가운데 맞추기로 편집기만 74.4, 목록은 그대로
```
1. 차이로 가면 캐럿을 옮기는데, 그 순간 편집기가 **"캐럿을 화면에 들이려고" 스스로 한 번 구른다**(WPF 기본).
이 스크롤은 다른 칸을 따라오게 하면서 메아리 막기(`HoldSync`)를 건다 — 이 막기는 `Input` 우선순위에서 풀린다.
2. 곧이어 `CenterCharInView` 가 그 줄을 가운데로 올리려고 편집기를 **한 번 더** 굴린다. 이것은 `Loaded` 우선순위라
`Input` 보다 **먼저** 돈다 → 두 번째 스크롤은 메아리로 여겨져 **버려진다.**
3. 그래서 목록 칸들은 첫 스크롤 자리(= 차이 줄이 칸 맨 아래)에 남았다. 뒤로 크게 돌 때는 편집기와 목록이 반 칸
어긋난 채로 표시층 맞추기(`AlignDecorationToEditor`)가 그 차이를 **이동값 357**(화면 배율 1.5 → 약 543px)로
메워, 네 칸에 함께 걸린 이동값 때문에 두 칸이 통째로 밀렸다.
### 고침
`CenterCharInView` 가 편집기를 굴린 뒤 **다른 칸을 직접 맞춘다** — `HoldSync()` · `Scroll(LocalLines, y)` ·
`SyncPanesFrom(Local, y, 뷰포트)`. 편집기 ScrollChanged 가 하던 일과 같은 호출이라 동작이 갈리지 않고,
메아리 막기에 걸릴 일이 없다. 굴릴 값도 `Math.Max(0, …)` 에서 `Clamp(…, ScrollableHeight)` 로 위아래를 함께 막았다.
### 시험
- 화면 `g7-verify.ps1` — `Alt+↓` 9번(한 바퀴) · `Alt+↑` 3번(뒤로 돌기) · [처음] · [마지막] · [처음] 마다
① 두 칸이 제자리(위 ±12px) ② 차이 줄이 칸 안에 보임 ③ 파일 끝이 아니면 **칸 가운데 근처(칸 높이 ±25%)**.
**고치기 전 10건 실패**(맨 아래 걸림 8 · 칸 밀림 2) → **고친 뒤 실패 0**: 차이 줄 위 837px · 칸 가운데 842px,
칸 위 289px 그대로, 표시층 이동값 −5~−7 (예전 357 없음). 파일 끝 차이(8번)에서 칸이 2~3px 움직이는 것은
원래 있던 미세 맞춤(이동값 −5 → −6.5)이라 이번 증상과 별개다.
- 빌드 경고 0 · 오류 0. 회귀는 아래 G-6 과 함께 적는다.
## G-6. 개발자 정보 (설정 안 메뉴) (2026-09-30, 사용자 요청 · 완료)
### 자리가 바뀌었다 (사용자 지시)
처음에는 하단 탭 맨 끝의 `개요` 로 만들었는데, 사용자가 **"탭으로 만들지 말고 설정에 메뉴 하나 만들어서 개발자
정보라는 이름으로"** 옮겼다. 플랫폼은 건드리지 않고(사용자: "이제 웬만하면 플랫폼은 건들지 않을 거야") 플랫폼이
이미 주는 `AddSub<OverviewView, OverviewViewModel>(TabManager.SettingsTabKey, "개발자 정보")` 로 붙였다 — 플랫폼의
UI Setting · 백업 · 로그와 같은 길이다. 설정 하위 메뉴 차례는 `MergeHub · 개발자 정보 · UI Setting · 설정값 · 백업 ·
클리너 · 로그`, 하단 탭은 예전 그대로(`폴더 비교 · 파일 비교 · Git · 메모 · 설정`).
### 무엇을 만들었나
읽기 전용 안내 화면. 사용자가 준 참고 화면(가운데 제목 + 버전 배지 ·
소개 한 줄 · 목적 문장 · 카드)의 모양을 따랐다. 참고 화면의 이름들은 다른 서비스의 예시라 가져오지 않았다.
| 자리 | 값 | 어디서 |
|---|---|---|
| 제목 · 배지 | `MergeHub` · `v0.12` (배지에 올리면 빌드 날짜) | 실행 파일 (`ProgramInfo`) |
| 소개 한 줄 | `사내 Git Merge/Diff 도구` | csproj `<Description>` |
| 목적 문장 | `MergeHub 는 폴더 비교 · 파일 비교(2-Way · 3-Way 머지) · Git 커밋을 한 곳에서 하는 UbiSam 사내 머지 도구입니다.` | 코드(`ProgramOverview.PurposeText`) |
| 카드 `담당` | 만든 사람 · 사내 문의처 | 설정 → 실행 파일 받침값(G-5) — **정보 창과 같은 길** |
| 아래 | `빌드 2026-09-30 14:14` | 실행 파일 |
사용자 결정(추천안 그대로): ① (처음) 맨 뒤 탭 → 뒤에 설정 메뉴로 옮김 ② 카드 한 장(`담당`) — 이 프로그램에는 Backend/Frontend 같은 갈래가 없다
③ 목적 문장은 코드에, 사람 이름은 기존 설정 · csproj 받침값을 다시 씀 ④ [정보] 창은 그대로 둔다.
### 짠 방식
- `Services/ProgramOverview.cs` — 화면에 올릴 값을 셈하는 순수 모델(`From(ProgramInfo, description)`).
버전을 못 읽으면 `v알 수 없음` 이 되지 않게 `v` 를 붙이지 않는다.
- `OverviewViewModel` — 정보 창과 같은 `ProgramInfoReader.For(어셈블리, 설정 만든 사람, 설정 문의처)`.
설정에서 이름을 고칠 수 있으므로 **화면이 보일 때마다 다시 읽는다**(`IsVisibleChanged`).
- `OverviewView.xaml` — 가운데 정렬 · 카드는 `WrapPanel`(늘어나면 가로로, 좁으면 아래로). 색은 기존 토큰만 썼다
(`ContentBackground` · 카드 `Input.Background`/`Input.Border` · 배지 `Tag.Accent*`).
- `AppSetup.RegisterSettings` — `AddSub<OverviewView, OverviewViewModel>(TabManager.SettingsTabKey, "개발자 정보")`(처음의 `AddMain` · `ViewIds.Overview` 는 뺐다).
### 화면을 보고 고친 것
처음에는 카드 한 줄에 `이름표 값` 을 나란히 두었는데, 사진으로 보니 문의처 안내 글(`적혀 있지 않습니다 (설정 >
MergeHub 에서 적습니다)`)이 카드 폭에서 **`에 / 서` 로 낱말 가운데가 꺾였다.** 또 `Run` 사이 줄바꿈이 공백으로
들어가 이름표와 값 사이가 4칸이 됐다. → **이름표를 위에 작게, 값을 아래에** 두었다. 안내 글 높이 26px = 짧은 값
`UbiSam` 26px(한 줄), 폭 483px.
### 시험
- 하네스 `OverviewTest` **14건 새로** — 고치기 전에는 모델 파일이 없어 컴파일 실패. 제목 · 배지(`v0.12`, 못 읽으면
`v` 없음) · 소개(없으면 기본) · 목적 · 빌드 줄 · 카드 한 장 · 두 줄 · **문의처가 비면 정보 창과 같은 안내 글**.
- 빌드 경고 0 · 오류 0.
- 화면 `g6-verify.ps1` **22건 · 실패 0**(설정 메뉴로 옮긴 뒤):
하단 탭에 개요가 없음(`폴더 비교,파일 비교,Git,메모,설정,…`) · 설정 → 개발자 정보가 열림 · 글 값 전부 · 머리 가운데 1,271 / 목적 1,280 /
카드 1,281 (px, ±20 안) · 설정에 적은 만든 사람 · 문의처가 카드에 나옴 · 파일 비교 · 메모 탭 회귀 · 오류 글 없음.
시험 대본의 헛짚음 둘: 하단 탭 이름은 항목 Name 이 아니라 템플릿 안 TextBlock 이라 **Raw 보기**로 읽어야 했다.
### 값으로 확정하지 못한 것
- **설정 화면에서 이름을 고치고 돌아왔을 때 바로 바뀌는가**(`IsVisibleChanged` 로 다시 읽기) — 시험은 설정 파일을
고치고 **다시 켜서** 확인했다. 켜 둔 채 설정 화면을 흉내 내 고치는 길은 만들지 않았다.
- 시험 뒤 **메모 한 장에 `TL` 두 글자가 들어가 있었다.** 대본은 키를 보내지 않는다. 같은 탭 이동(개요 → 파일 비교 →
메모)만 따로 돌려 보니 메모는 0바이트 그대로라 재현되지 않았다 — 앱에 초점이 간 사이 밖에서 들어온 키 입력으로
보이나 확정하지 못했다. 메모 보관함은 기준본으로 되돌렸다.
## G-5. 사내 문의처가 배포본에 따라가지 않는다 (2026-09-30, 사용자 요청 · 구조 완료 · 문구 대기)
### 무엇이 문제였나
정보 창(12차 B-2)의 만든 사람은 설정이 비면 `csproj` 의 `<Company>` 로 받쳐지지만, 문의처는
`Prefer(contact, null)` — **받칠 것이 없었다.** 설정(`D:\UbisamConfig\Settings\MergeHubSettings.json`)은
PC 마다 따로라, 배포본을 새로 깐 PC 에서는 늘 `적혀 있지 않습니다` 가 보였다.
### 고친 방식 (사용자 결정: csproj 에 받침값)
- `csproj` 에 `<SupportContact></SupportContact>` 를 두고, 빌드할 때 `AssemblyMetadata("SupportContact", …)` 로
실행 파일에 박는다 — 이미 있는 `BuildDate` 와 같은 길이다. **비어 있어도 키는 박는다**(읽는 쪽이 "자리는 있다" 를 확인).
- `ProgramInfoReader.StampedContact` 가 그것을 읽고, `Contact = Prefer(설정, 박힌 값)`. 순서: 설정 → 박힌 값 → 안내 글.
`StampedBuildDate` 와 같은 읽기를 `Stamped(assembly, key)` 하나로 모았다.
- 설정 화면 글: `사내 문의처 (담당자 · 연락처 한 줄, 비우면 실행 파일에 박힌 값)`.
- **문구는 아직 받지 않았다**(사용자 결정: 구조만 먼저). 지금은 비어 있어 화면은 예전과 같다.
나중에 `csproj` 의 `<SupportContact>` 한 줄만 채우면 된다.
### 시험
- 하네스 `AboutTest`(옛 세션 scratchpad 에서 이번 도구 자리로 옮김) 28 → **30건**(+3, 기존 "안내 글" 1건을 바꿈).
고치기 전에는 `StampedContact` 가 없어 **컴파일부터 실패**.
`ABOUT_DLL` · `ABOUT_CONTACT` 로 dll 을 갈아 끼울 수 있게 해 **두 빌드를 모두 읽었다** —
원래 빌드(비어 있음) 30건 · 문구를 채운 시험 빌드(`-p:SupportContact=G5 시험 문의처 (내선 0000)`, scratchpad 로 출력) 30건 실패 0.
- 빌드 경고 0 · 오류 0.
- 화면 `g5-verify.ps1` — 두 빌드에서 각각 3건 · 실패 0:
| 빌드 | 설정 비움 | 설정에 `G5 설정 문의처` |
|---|---|---|
| 원래(비어 있음) | `적혀 있지 않습니다 (설정 > MergeHub 에서 적습니다)` | `G5 설정 문의처` |
| 시험(문구 채움) | **`G5 시험 문의처 (내선 0000)`** | `G5 설정 문의처` (설정이 앞선다) |
만든 사람은 두 경우 모두 `UbiSam` 그대로. 시험 빌드는 같은 자리(`bin\Debug`)에 빌드해 띄운 뒤 **원래대로 다시 빌드**했고,
되돌린 dll 이 안내 글을 내는 것을 하네스로 확인했다.
## G-4. 저장 형식 칸의 `원본 유지` 콤보 둘이 구별되지 않는다 (2026-09-30, 사용자 요청 · 완료)
### 무엇이 문제였나 (값)
저장 형식 칸에 콤보 둘이 나란히 붙어 있고, 기본값이 **둘 다 `원본 유지`** 였다 —
`TextFile.SaveEncodings[0]` 과 `TextFile.SaveNewLines[0]` 이 같은 상수 `KeepOriginal` 이다.
툴팁을 올려 봐야 어느 쪽이 인코딩인지 알 수 있었다.
### 그냥 문자열을 바꾸면 안 되는 까닭
`KeepOriginal` 은 **보이는 글이면서 저장되는 값**이다. `HomeViewModel.SaveEncoding`·`SaveNewLine` 의 값이
그대로 문서 탭 → 세션 보관본(`목록.txt` 의 7·8번째 칸)에 적힌다. 문자열을 바꾸면 이미 보관된 세션의 값
`원본 유지` 가 목록에 없는 값이 되어 되살릴 때 콤보가 빈 채로 뜬다. 또 `TextFile.Encode` 는 모르는 값을
원본 유지로 보므로 동작은 같겠지만, 화면과 값이 어긋난다.
### 고친 방식 — 보이는 글과 값을 가른다 (인수인계의 1안)
- `TextFile.SaveChoice(Value, Label)` 를 새로 두고, 목록을 `SaveEncodingChoices` · `SaveNewLineChoices` 로 바꿨다.
첫 칸만 글이 다르다 — `(원본 유지, "원본 인코딩 유지")` · `(원본 유지, "원본 줄바꿈 유지")`. 나머지는 글 = 값.
- 콤보는 `DisplayMemberPath="Label"` · `SelectedValuePath="Value"` · `SelectedValue="{Binding SaveEncoding}"`.
**ViewModel · 저장 · 세션 보관은 한 줄도 안 바뀌었다** — 여전히 문자열 값을 주고받는다.
- `SaveChoice.ToString()` 이 `Label` 을 돌려준다 — UIA 가 콤보 칸의 이름으로 이것을 읽는다.
- 화면 시험이 집을 수 있게 콤보에 `SaveEncodingBox` · `SaveNewLineBox` 이름을 달았다.
### 시험
- 하네스를 먼저 넣었다. `EncTest` 에 G-4 8건 — 고치기 전에는 새 목록이 없어 **컴파일부터 실패**, 고친 뒤
**56건 · 실패 0**(두 콤보 글이 겹치지 않음 · 저장값은 그대로 `원본 유지` · 원본 유지로 쓰면 원본 줄끝을 따름).
`SessionTest` 에 3건 — 보관본에 `원본 유지` 가 값으로 적히고 그대로 되읽힌다. 고치기 전에도 통과했다
(보관은 값을 글자 그대로 옮길 뿐이라는 뜻 = **값만 안 바꾸면 옛 보관본이 산다**), 고친 뒤 **68건 · 실패 0**.
- 빌드 경고 0 · 오류 0.
- 화면 `g4-verify.ps1` **25건 · 실패 0**:
| 확인 | 값 |
|---|---|
| 연 직후 콤보 글 | `원본 인코딩 유지` · `원본 줄바꿈 유지` |
| 콤보 칸들 | `원본 인코딩 유지,UTF-8,UTF-8 (BOM),CP949` · `원본 줄바꿈 유지,CRLF,LF` |
| 콤보 폭 | 330px (글이 잘리지 않음) |
| LF · UTF-8 (BOM) 골라 오른쪽 저장 | CRLF 로 만든 파일이 **CR 0개** · BOM 붙음 — 고른 값이 저장까지 간다 |
| 원본 유지로 돌리고 강제로 끔 | 보관본 7·8칸 = `원본 유지 \| 원본 유지` (옛 값 그대로) |
| 다시 켬 | 고친 글이 되살아나고 콤보 = `원본 인코딩 유지` · `원본 줄바꿈 유지` |
| CP949 · CRLF 로 같은 시험 | 보관본 `CP949 \| CRLF` → 되살린 콤보 `CP949` · `CRLF` |
- 회귀 `e1-verify`(세션 되살리기) — **첫 번은 12건 실패, 같은 빌드로 다시 돌리니 29건 · 실패 0.**
첫 번은 1단계에서 폴더 비교 줄을 **진짜 마우스로 더블클릭**해 둘째 짝을 여는 것이 안 먹어 탭이 하나뿐이었고,
뒤 단계가 줄줄이 따라 실패했다. 코드가 같은데 결과가 갈렸으므로 G-4 와는 무관하다. 다만 **더블클릭이 왜
빗나갔는지는 값으로 확정하지 못했다**(창이 가려졌거나 앞에 오지 않았던 것으로 보이나 찍어 두지 않았다).
## G-3. "덩어리" 라는 말이 어색하다 (2026-09-30, 사용자 요청 · 완료)
### 무엇을 바꿨나
사용자 결정대로 화면에 보이는 "덩어리" 를 **"구간"** 으로 바꿨다. 툴팁은 원래 "차이가 이어지는 구간" 이라
적고 있었다 — 단추 글만 다른 말을 쓰고 있던 셈이다.
| 자리 | 전 | 후 |
|---|---|---|
| 단추 (`AcceptBlockButton`) | `이 덩어리 반영` | **`구간 반영`** |
| 단추 (`AcceptBlockToBaseButton`) | `이 덩어리 왼쪽으로` | **`구간 왼쪽으로`** |
| 단축키 설명 `Alt+Shift+→` | `이 덩어리 반영 - 차이가 이어지는 구간을 한 번에` | `구간 반영 - 차이가 이어지는 구간을 한 번에` |
| 단축키 설명 `Alt+Shift+←` | `이 덩어리를 왼쪽 파일로 - 이어지는 구간을 한 번에 (2-Way)` | `구간 왼쪽으로 - 차이가 이어지는 구간을 한 번에 왼쪽 파일로 (2-Way)` |
| 로그 (Git 탭 아래) | `5~7행 덩어리 BASE(…) 반영 (3줄)` | `5~7행 구간 BASE(…) 반영 (3줄)` |
| 로그 (왼쪽으로) | `5~7행 덩어리 왼쪽에 반영 (3줄)` | `5~7행 구간 왼쪽에 반영 (3줄)` |
토스트(`3줄 반영` · `3줄 왼쪽에 반영`)는 원래 "덩어리" 가 없어 그대로다. 옆의 `이 메서드 반영` 도 그대로다.
**바꾸지 않은 것**: 코드 안 이름(`AcceptBlock` · `BlockAround` 등)과 주석의 "덩어리"(개념을 가리키는 말이라
화면에 안 나온다). 단추 이름을 그대로 인용하던 주석 두 곳만 맞췄다. **웹 문서(`doc/web/index.html`)에 옛 문구가
네 곳 남아 있다** — 웹 문서는 차수 끝에 한 번에 고친다.
### 시험
- 하네스 `ShortcutTest` 101 → **104건**(+3): 두 설명의 첫머리가 `구간 반영` · `구간 왼쪽으로` 인가,
단축키 표 **전체**에 "덩어리" 가 남지 않았는가. **고치기 전에 돌려 새 3건이 실패하는 것을 먼저 확인했다**
(`통과 101 · 실패 3`) → 고친 뒤 `통과 104 · 실패 0`.
- 빌드 경고 0 · 오류 0.
- 화면 `g3-verify.ps1` **18건 · 실패 0** — 단추 글(UIA Name)이 `구간 반영` · `구간 왼쪽으로`, 두 단추 폭 270px
(잘리지 않음), 누르기 전후 화면 어디에도 "덩어리" 없음, 두 방향 모두 세 줄이 한 번에 가고 한 번에 돌아옴,
**로그 칸(Git 탭, ValuePattern)** 에 `5~7행 구간 왼쪽에 반영 (3줄)` · `5~7행 구간 BASE(block.cs) 반영 (3줄)`.
- 회귀 `g2-verify` 23건 실패 0(단추는 AutomationId 로 찾아 문구와 무관).
- `f2-verify`·`f2-probe` 는 단추를 **글로** 찾아 새 문구로 맞췄다. 다만 `f2-verify` 는 이번 변경보다 앞 단계에서
`'저장' 안 보임` 으로 멈춘다 — `저장` 단추는 12차 B-1 에서 `◀ 왼쪽 저장` · `오른쪽 저장 ▶` 로 나뉘어 이미 없고,
이 대본은 회귀 몰이(`run-all-merge`)에도 없다. **원래 낡은 대본이라 G-3 과 무관**하다. 멈춘 채 뒤처리를 못 해
설정 두 파일 · 세션 보관함이 흐트러졌고, 이번 세션 시작 때 떠 둔 기준본으로 바이트까지 되돌렸다.
## G-2. base 칸으로 머지하는 단축키 (2026-09-30, 사용자 요청 · 완료)
### 무엇이 없었나
12차 B-1 로 **왼쪽 파일로 반영**하는 길이 생겼는데(`[<]` 단추 · `TakeLineToBase`) **단축키가 없었다.**
덩어리를 왼쪽으로 보내는 것은 명령 자체가 없었다 — `AcceptBlock`(`Alt+Shift+→`)은 오른쪽으로만 간다.
### 키를 어디에 둘지
사용자가 "`Alt+←` 도 비어 있지 않냐" 고 물었다. **비어 있지 않았다** — `CopyRightToLeft` 가 쓰고 있었는데,
2-Way 에서 그것이 하는 일은 이름·화살표와 달리 **"이 줄 되돌리기"** 였다. 그런데 이름과 화살표가
가리키는 "왼쪽으로 반영" 이 B-1 로 **실제로 생겼다.** 그래서 감각이 맞는 쪽으로 넘겼다(사용자 결정).
| 키 | 전 | 후 |
|---|---|---|
| `Alt+→` | 한 줄 오른쪽으로 | 그대로 |
| `Alt+Shift+→` | 덩어리 오른쪽으로 | 그대로 |
| `Alt+←` | **이 줄 되돌리기**(3-Way: Take Theirs) | **한 줄 왼쪽 파일로**(3-Way 는 Take Theirs 그대로) |
| `Alt+Shift+←` | 비어 있음 | **덩어리 왼쪽 파일로**(새로 만듦) |
| 이 줄 되돌리기 | `Alt+←` | **단축키 없음 — `Ctrl+Z` 만**(사용자 결정) |
`Alt+→` 가 이미 2-Way/3-Way 로 갈라져 있으므로 `Alt+←` 도 같은 식으로 갈랐다. 3-Way 는 안 건드렸다.
### 만든 것
- `AcceptBlockToBase` · `ApplyGroupToBase` — `AcceptBlock`/`ApplyGroup` 의 거울.
주석은 덩어리 첫 줄 위 한 줄, 되돌리기는 한 번에(11차 F-2 와 같은 규칙).
덩어리를 잡는 `BlockAround` 는 2-Way 에서 이미 `CanTakeBase` 로 잡으므로 **그대로 썼다** —
"가져올 수 있는 줄" 과 "보낼 수 있는 줄" 이 같은 줄이고 방향만 반대다.
- **[이 덩어리 왼쪽으로] 단추** — `[이 덩어리 반영]` 옆에. 두 파일 비교(`BothWays`)에서만 뜬다.
단축키만 두면 찾을 길이 없다.
### 겪은 것 둘
**`RaiseCanExecuteChanged` 에 새 명령을 안 넣어 단추가 꺼진 채였다.** CommunityToolkit 의 커맨드는
`CommandManager` 를 구독하지 않아 직접 알려야 한다(파일에 이미 그 까닭이 주석으로 있었다).
`OnCaretRowChanged` 에도 넣어야 줄을 옮길 때 켜지고 꺼진다.
**덩어리 명령은 고른 줄이 아니라 `CaretRow`(편집기 캐럿)를 본다.** 시험이 왼쪽 목록을 고르기만 해서
한 줄만 갔다 — `Alt+↓`(다음 차이)로 캐럿을 놓아야 했다.
### 화면으로 확인 (값)
`g2-verify.ps1` **21건 · 실패 0**. 연속 세 줄이 다른 재료(`speed` · `power` · `delay`)를 따로 만들어 썼다.
| 확인 | 값 |
|---|---|
| `Alt+←` 한 줄만 | `speed = 10 → 11`, `power`·`delay` 는 그대로 |
| `Ctrl+Z` | 되돌아간다 |
| 덩어리 왼쪽으로 | 셋이 **한 번에** 간다 |
| 덩어리 되돌리기 | 셋이 **한 번에** 돌아온다 |
| 회귀 | `Alt+→` · `[이 덩어리 반영]` 그대로 · `g1`·`e2`·`e3`·`e4` 실패 0 |
하네스 `ShortcutTest` 88 → **101건**(G-2 +13): 키가 겹치지 않음 · 화살표 넷의 모양이 맞음
(Alt = 한 줄, Alt+Shift = 덩어리) · 충돌 이동(`Alt+Shift+↑↓`)을 안 빼앗음 ·
**`Alt+←` 설명이 "되돌리기" 라고 거짓말하지 않음**.
### 값으로 확정하지 못한 것
**`Alt+Shift+←` 와 `Alt+Shift+→` 를 흉내 낸 키로는 못 눌렀다.** 윈도우가 `Alt+Shift` 를
입력 언어 전환으로 먼저 채간다 — `SendKeys` 로는 아예 안 닿고, `keybd_event` 로는 `Shift` 가 빠져
`Alt+←` 처럼 동작했다. **기존 `Alt+Shift+→` 로 견줘 봐도 똑같았으므로 시험 도구 한계다**(C-3 의
`DragMove` 와 같은 부류). 그래서 덩어리는 **같은 명령을 부르는 단추로** 확인했다.
사람 손으로 `Alt+Shift+←` 를 한 번 눌러 보는 것이 남았다.
## G-1. undo 가 계속 이어진다 (2026-09-30, 사용자 보고 · 완료)
### 먼저 재현 (화면, 값)
짝 A(`alpha.cs`)를 보다 짝 B(`bravo.cs`)로 옮기고 `Ctrl+Z` 를 누르면 **끝나지 않는다.**
```
5회: 228자 10회: 236자 15회: 228자 20회: 236자 … 40회: 236자
```
**글자를 하나도 안 쳐도 난다** — 짝만 바꾸면 A 의 글(234자)과 B 의 글(228자)을 영원히 오간다.
### 무엇이었나
`Ctrl+Z` 는 우리 목록(`_undo` · `_textUndo`)이 비면 **키를 먹지 않는다**(`HomeView.xaml.cs` 의
`RunShortcut` — *"되돌릴 머지 작업이 없으면 키를 먹지 않는다, 그래야 편집기의 글자 되돌리기가 살아 있다"*).
그러면 그 키가 `TextBox` 자체 되돌리기로 넘어간다. WPF 의 그것은 **한도가 없고, 프로그램이 코드로
넣은 글까지 기록한다.** 짝을 바꿔 새 파일을 실으면 그 글도 기록에 쌓이므로, `Ctrl+Z` 가
**앞 파일 글을 편집기에 되살렸다.**
거기서 끝이 아니다. 기록을 심어 고리를 떴다.
```
Sync 끝났다 편집기=228 ← B 를 편집기에 실었다
편집알림 편집기=234 모델=216 ← TextBox 되돌리기가 A 의 글을 되살렸다
Reflow 모델=216 편집기=221 같나=False ← 그걸 "사용자가 친 것" 으로 보고 모델을 A 로 되돌린다
StepText 목표=216 … 편집기=234 ← 우리 undo 가 다시 B 로
… 끝없이 되풀이
```
`221 = 234 − 13`(줄 13개의 CRLF↔LF 차이)이라 편집기가 쥔 것이 A 의 글임을 값으로 확인했다.
`Reflow` 는 다시하기(redo)까지 지우므로 빠져나갈 길도 없었다.
### 고친 것 — 한 줄
`HomeView.xaml` 의 `LocalEditor` 에 **`IsUndoEnabled="False"`**. 되돌리기는 우리 목록만 쓴다 —
줄 단위(`_undo`)와 글 단위(`_textUndo`, 타이핑이 멈출 때마다 한 걸음).
### 헛짚은 것 둘 (기록으로 남긴다)
| 짚었던 것 | 어떻게 아니라고 알았나 |
|---|---|
| "실린 문서가 바뀔 때 `TextBox` 기록을 비우면 된다"(`EditorUndoGate` 를 만들어 하네스 16건까지 붙였다) | 앞 파일로 넘어가는 것은 막았지만 **오가는 것은 그대로**였다 — 버렸다 |
| "타이핑 때 건 재정렬 타이머가 되돌린 뒤 뒤늦게 터진다" | `SyncEditorFromLines` 에서 타이머를 꺼도 **그대로**였다 — 되돌렸다 |
둘 다 만들어 시험까지 했지만 증상이 안 없어져 되돌렸다. **검증 안 된 동작 변경은 남기지 않는다.**
특히 `IsUndoEnabled="False"` 를 효과 없이 남기면 글자 단위 되돌리기만 잃는 꼴이 된다.
### 화면으로 확인 (값)
`g1-verify.ps1` **14건 · 실패 0**
| 확인 | 값 |
|---|---|
| **끝없이 오가지 않는다** | 길이 자취 `228 → 228 → 228 → 228 → 228 → 228`(30번 눌러도) |
| **앞 파일로 안 돌아간다** | A 는 234자 — 자취에 한 번도 안 나온다 |
| 친 뒤에도 같다 | 자취 `228 → …`, 친 글은 되돌려졌다 |
| 회귀 — 되돌리기는 살아 있다 | 친 글이 `Ctrl+Z` 로 되돌아간다 |
| 회귀 — 편집 관련 다섯 벌 | `e1` · `e2` · `e2-reg` · `e3` · `e4` 전부 실패 0 |
### 남은 것
글자 **한 자 단위** 되돌리기는 없어졌다. 이제 한 걸음은 "타이핑이 멈춘 자리" 다.
쓰다가 너무 굵게 느껴지면 `_textUndo` 를 더 잘게 쌓는 것으로 따로 잡는다.
## A-1. 폴더 줄의 크기가 늘 0B (2026-09-27, 완료)
### 무엇이 문제였나
크기를 **파일 줄에만** 채웠다(`FolderComparer` 의 파일 루프에서 `FileInfo.Length`). 폴더 줄은 `LeftSize` ·
`RightSize` 가 0 인 채로 화면에 내려가 어느 폴더든 `0 B` 로 보였다. 어느 쪽이 더 큰지, 어느 폴더가 무거운지를
목록에서 알 수 없었다.
### 어떻게 고쳤나
`Services/FolderSizeText.cs` 를 새로 두었다. 화면 없이 검사할 수 있게 계산만 떼어 낸 것이다.
| 하는 일 | 함수 |
|---|---|
| 사람이 읽는 크기로 적기 | `Of(long bytes)` — 1KB 미만은 바이트 그대로(`999 B`), 그 위는 소수 한 자리(`1.5KB` · `2.4MB` · `3GB`) |
| 폴더 줄에 합계 채우기 | `FillFolderSizes(items)` — 아래에서 위로, 폴더는 자기 아래 것들의 합 |
- 부르는 자리는 `FolderComparer.Compare` 의 **`ClassifyAll` 다음, `Tally` 앞**이다. 파일 갈래를 정하는 판단은
파일 크기를 보고 하므로(크기가 다르면 열지 않는다) 그보다 **먼저 채우면 판단이 달라진다** — 그래서 뒤에 둔다.
- 걸러 낸 것은 합계에서도 빠진다. 포함 · 제외 규칙에 걸린 파일은 애초에 목록에 없으므로 따로 뺄 일이 없다.
- 한쪽에만 있는 폴더는 그쪽만 합계고 반대쪽은 0 이다(화면에는 `-`).
- 화면 쪽은 `FolderViewModel.Describe` 가 `{크기} · {수정일}` 을 만들 때 `FolderSizeText.Of` 를 쓰게 바꿨다.
예전에는 `{바이트:N0} B` 라 300,000 B 처럼 읽기 어려웠다.
### 값으로 확인한 것
시험 재료는 만들어 썼다(`%TEMP%\mergehub-a1`, 실제 업무 저장소는 읽지 않는다).
| 폴더 | 화면 | 탐색기 속성(파일 크기 합) |
|---|---|---|
| `Docs` | 2.4MB · 2.4MB | 2,500,000 · 2,500,000 |
| `Legacy`(왼쪽만) | 50KB · `-` | 51,200 · 없음 |
| `NewOnly`(오른쪽만) | `-` · 40KB | 없음 · 40,960 |
| `Src` | 490.2KB · 491.7KB | 502,000 · 503,500 (기본 제외 규칙에 걸린 `Src\bin` 9MB 는 빠짐) |
| `Src\Core` | 472.7KB · 474.1KB | 484,000 · 485,500 |
| `Src\Core\Motion` | 179.7KB · 181.2KB | 184,000 · 185,500 |
| `Src\Ui` | 17.6KB · 17.6KB | 18,000 · 18,000 |
- 파일 줄도 함께 읽기 쉬워졌다 — `Unit0.cs 293KB` · `Home.cs 62.5KB / 64KB` · `manual.txt 2.4MB`.
- **비교 시간**: 4,608 줄(136MB · 파일 8,922개) 짜리 묶음에서 합계 채우기 **0ms**(빠른 비교 203ms 대비 0.0%).
디스크를 다시 읽지 않고 이미 만들어 둔 목록을 한 번 훑을 뿐이라 늘어날 여지가 없다.
### 하네스
`FolderTest` **88 → 111건(+23)** · 실패 0.
- 글자 8건: 1KB 미만 · 1024 · 소수 한 자리 · 딱 떨어질 때 · MB · GB · TB 를 넘을 때 · 음수
- 합계 9건: 여러 겹 · 양쪽 따로 · 가운데 폴더 · 맨 안쪽 · 한쪽에만 있는 폴더 · 파일 줄은 그대로 ·
탐색기 속성과 같은 값(양쪽)
- 걸러 내기 3건: 제외한 폴더 · 제외해도 다른 줄은 그대로 · 포함 규칙에 안 맞는 파일
- 판단이 안 바뀌는지 4건: 크기가 다른 이진 파일 · 같은 파일 · 한쪽에만 있는 폴더 · 하위 폴더를 안 볼 때
### 남긴 것
- 폴더 줄의 **수정일**은 그대로 비워 둔다. 폴더의 수정 시각은 안의 파일과 무관하게 바뀌어(윈도가 하위 항목을
더하거나 지울 때만 갱신) 사람이 기대하는 값과 다르다 — 크기만 적는 편이 오해가 적다.
## A-2. 좌우로 나눠 보이기 (2026-09-27, 완료)
### 무엇이 문제였나
한 줄에 `이름 | 상태 | 왼쪽(크기·수정일) | 오른쪽(크기·수정일)` 이 늘어서 있었다. 이름이 한 번만 나오니
"이 줄이 어느 쪽 것인가"를 상태 글자를 읽어야 알 수 있었고, 한쪽에만 있는 항목도 `-` 한 글자로만 표시됐다.
### 어떻게 고쳤나
한 줄을 **왼쪽 목록 · 가운데 상태 · 오른쪽 목록** 세 칸으로 나눴다(11차 F-10 파일 비교와 같은 모양).
- 칸마다 **자기 쪽 이름과 크기**를 적는다. 그쪽에 없으면 그 칸을 통째로 비운다 — 빈칸이 "여기엔 없다"를
상태 글자보다 먼저 말해 준다. 없는 쪽 크기를 `-` 로 적던 것도 뺐다.
- 가운데 칸이 두 목록을 가르는 선이다(같음 · 다름 · 왼쪽만 · 오른쪽만).
**줄마다 칸이 어긋나는 문제**가 하나 있었다. TreeView 는 깊이만큼 <b>줄 전체</b>를 오른쪽으로 밀기 때문에,
칸 폭을 고정해도 깊은 줄일수록 가운데 · 오른쪽 칸이 오른쪽으로 밀려 두 목록으로 읽히지 않는다.
그래서 `TreeDepthMarginConverter`(새로 둠)로
1. 줄 바깥틀을 민 만큼 **되돌리고**(`Negative=True`, 깊이 × 19 DIP 음수 여백) — 칸 경계와 가운데 구분선이 줄마다 같은 자리에 온다,
2. 이름에만 **다시 같은 값을 들여쓴다** — 계층은 그대로 보이고, 왼쪽 · 오른쪽 이름이 같은 높이 · 같은 들여쓰기로 맞는다.
트리 자체는 그대로 쓴다(목록 둘을 따로 두고 스크롤 · 펼침을 맞추는 길은 택하지 않았다) — 접기 · 펼치기,
선택, 복사 · 삭제, 걸러 보기, 더블클릭으로 파일 비교 열기가 손대지 않은 채로 남는다.
### 화면으로 확인한 것
| 판정 기준 | 결과 |
|---|---|
| 같은 이름이 같은 높이에 온다 | `Src > Core > Motion > Axis.cs` 까지 네 겹 모두 양쪽 같은 줄 · 같은 들여쓰기 |
| 한쪽만 있는 항목이 어느 쪽인지 한눈에 | `Legacy`(왼쪽만)는 오른쪽 칸이 빈칸, `NewOnly`(오른쪽만)는 왼쪽 칸이 빈칸 |
| 접기 · 펼치기 | 모두 펼침 · 모두 접기 모두 그대로. 칸 경계는 깊이가 달라도 한 자리 |
| 걸러 보기 | 왼쪽에만 2줄 · 오른쪽에만 2줄 · 같은 것만 2줄 · 다른 것만 6줄 |
| 복사 단추 | `Legacy` 고르고 [→ 오른쪽으로 복사] → 확인 창(본 창 가운데) → 복사됨. 다시 비교하니 `왼쪽만 1 · 같음 4` 가 `왼쪽만 0 · 같음 5` 로 |
| 좁은 창 | 1750×1100(1166 DIP) 창모드에서도 세 칸(420+170+420 DIP) 이 다 보인다 |
### 하네스
`FolderTest` **111 → 118건(+7)** · 실패 0 — 뿌리 여백 0 · 한 겹 19 · 세 겹 57 · 되돌리는 쪽은 음수 ·
되돌린 뒤 다시 들여쓰면 제자리 · 위아래 여백은 안 건드림 · 깊이가 아니거나 음수면 0.
### 남긴 것
- 칸 폭은 고정(420 · 170 · 420 DIP)이다. 트리가 깊이마다 폭을 다르게 잡아서 비율(`*`) 폭을 쓰면 줄마다 칸이
달라진다 — 화면이 아주 좁으면 오른쪽 목록이 잘린다(1166 DIP 까지는 확인했다).
## B-1. 2-Way 에서 양쪽으로 머지 (2026-09-27, 완료)
지시서에 남겨 둔 "정할 것"을 물어 정했다 — **두 파일 비교에서만 양방향**(사용자 결정 2026-09-27).
Git 모드에서 왼쪽은 지난 커밋이라 고쳐 쓸 파일이 없고, 네 칸(3-Way)의 BASE 는 공통 조상이라 거기에 쓰는 것이 뜻이 없다.
### 무엇이 문제였나
반영이 BASE → LOCAL 한 방향뿐이고 저장도 오른쪽 파일에만 됐다. 두 파일을 견주다 **왼쪽이 맞는 경우**
(오른쪽에서 잘못 지웠거나, 왼쪽 파일도 같이 맞춰야 하는 경우)에는 도구 밖에서 손으로 고쳐야 했다.
### 어떻게 고쳤나
| 무엇 | 어디 |
|---|---|
| 줄 반영 `[<]` | BASE 칸 여백(왼쪽에 있는 줄) · LOCAL 칸 여백(왼쪽에 **없는** 줄 - BASE 칸에는 그 줄 자체가 없어 달 자리가 없다) |
| 칸마다 저장 | ACTIONS 의 `◀ 왼쪽 저장` · `오른쪽 저장 ▶`. 고친 것이 없는 쪽은 꺼진다 |
| 반영 규칙 | `Services/PaneText.cs`(새로 둠) — 두 방향이 **같은 규칙**을 쓴다 |
| 되돌리기 | 편집 기록(`LineEdit`)에 `Side` 를 더해 어느 칸을 고친 편집인지 들고 다닌다. 한 스택에 쌓여 누른 차례대로 풀린다 |
| 저장 기준점 | 왼쪽 · 오른쪽을 따로 센다(`_leftBaseline`) — 한쪽을 저장해도 다른 쪽의 "저장할 것 있음"은 그대로 |
반영 규칙은 어느 쪽으로든 같다. **반대쪽에 줄이 있으면 그 글을 가져오고(없던 줄이면 생긴다), 반대쪽에 없으면
이 칸에서도 지운다.** 머지 주석(설정 "머지 주석 넣기")도 양쪽 모두 남는데, 왼쪽에 넣는 주석은
`LOCAL(파일이름)` 을 출처로 적고 **왼쪽에만 있는 줄**로 들어간다 — 오른쪽 파일에는 섞이지 않는다.
`MergeLine.HasBase` · `BaseNumber` 가 이제 바뀔 수 있는 값이다(예전에는 정렬 결과라 고정이었다).
BASE 칸 목록도 그 변화를 따라가게 했다(`PaneView` 의 live filtering) — 이게 없으면 왼쪽에 넣은 줄이 안 보이고
왼쪽에서 지운 줄이 빈 칸으로 남는다(화면 시험에서 실제로 걸려 고쳤다).
### 화면으로 확인한 것
시험 재료는 만들어 썼다(`%TEMP%\mergehub-b1`, left.cs · right.cs).
| 판정 기준 | 결과 |
|---|---|
| 왼쪽 파일에 반영 · 저장된다 | 값이 다른 줄 · 왼쪽에만 있는 줄 · 오른쪽에만 있는 줄 셋을 `[<]` 로 반영 → `◀ 왼쪽 저장` → 왼쪽 파일이 오른쪽 내용 + 머지 주석 2줄 |
| 오른쪽은 그대로다 | 오른쪽 파일 해시 `07f16198…` 저장 전후 같음. `오른쪽 저장 ▶` 은 고친 것이 없어 꺼져 있었다 |
| 되돌리기(11차 F-8) | Undo 세 번에 BASE 칸이 처음 모습으로(값 100 · 왼쪽에만 있던 줄 · 주석 사라짐), Redo 켜짐. 저장한 뒤라 `◀ 왼쪽 저장` 은 다시 켜진다(저장한 글과 달라졌으므로 - F-8 의 뜻 그대로) |
| 저장할 때 백업(11차 F-9) | `{설정}\Backup\MergeHub\2026-09-27\…\left.20260927-191108.cs` |
| 두 파일 비교에서만 | git mergetool 네 칸(`--merge --base --local --remote --merged`)에서 `[<]`(새 단추) 0개 · `◀ 왼쪽 저장` 안 보임. 네 칸의 기존 `<`(Take Mine · Take Theirs)는 그대로 |
### 하네스
`TwoWayTest` **29건 새로** · 실패 0 — 칸의 글 만들기 3 · 반영 규칙 7(양방향 · 한쪽에만 있는 줄 · 같은 줄 ·
멱등) · 왼쪽으로 반영한 뒤 5 · 오른쪽으로 반영한 뒤 3(예전 길 그대로) · 양쪽 저장 기준점 5 · 로그 문구 4 · 기타 2.
### 남긴 것
- 왼쪽 칸은 **타이핑으로는 못 고친다**. 편집기는 오른쪽에만 겹쳐 있다 - 왼쪽은 `[<]` 로 반영해서 고친다
(지시서가 정한 것: "칸마다 저장 단추 · 왼쪽 칸에도 반영 단추 [&lt;]").
- 왼쪽으로 반영한 줄에는 "머지 반영함" 색을 칠하지 않는다. 반영하고 나면 양쪽이 같아져 색이 필요 없다.
---
## B-2. 프로그램 정보 창 (2026-09-27, 완료)
지시서의 "정할 것"을 물어 정했다(사용자 결정 2026-09-27).
- **넣을 것**: 기본 4가지(프로그램 이름 · 버전 · 빌드 날짜 · 만든 사람) + **사내 문의처**.
- **버전 출처**: `MergeHub.csproj` 의 `<Version>` 을 실행 파일에서 읽는다. 12차 = `0.12.0`.
### 어떻게 만들었나
| 값 | 어디서 |
|---|---|
| 이름 · 만든 곳 | 어셈블리의 `Product`(MergeHub) · `Company`(UbiSam) — csproj 에 적는다 |
| 버전 | 어셈블리 버전. 뒤가 0 인 자리는 떼고 적는다(`0.12.0.0` → `0.12`) |
| 빌드 날짜 | **csproj 가 빌드할 때 박아 둔 값**(`AssemblyMetadata "BuildDate"`) |
| 만든 사람 · 문의처 | 설정 `프로그램 정보 > 만든 사람 · 사내 문의처`. 비우면 만든 사람은 회사 이름, 문의처는 "적혀 있지 않습니다" |
빌드 날짜를 어셈블리에 박는 까닭 — 실행 파일(PE) 머리의 시각 자리를 읽어 보니 **요즘 SDK 는 결정적 빌드라
그 자리에 내용 해시**가 들어간다(날짜로 읽으면 2100년 같은 값). 그래서 csproj 에서 빌드 시각을 박고,
못 찾을 때만 PE 시각 → 파일 수정 시각 순으로 물러난다(`ProgramInfoReader.ChooseBuildDate`).
파일 수정 시각을 첫째로 두지 않는 까닭은 복사 · 압축 풀기로 바뀌기 때문이다.
창은 [단축키] 옆 **[정보]** 단추로 연다. 단축키 창과 같은 규칙 — 이미 떠 있으면 새로 띄우지 않고 앞으로 가져온다.
[정보 복사] 로 네 줄을 그대로 클립보드에 담는다(결함을 알릴 때 첫 질문이 늘 "어느 버전이냐" 라서).
### 화면으로 확인한 것
| 판정 기준 | 결과 |
|---|---|
| 버전이 실제 실행 파일 값과 같다 | 창 `0.12` ↔ `MergeHub.dll` 어셈블리 버전 `0.12.0.0` |
| 빌드 날짜가 실제 값과 같다 | 창 `2026-09-27 19:51` ↔ 실행 파일에 박힌 `BuildDate` `2026-09-27 19:51` |
| 창을 닫아도 화면 상태가 그대로다 | 닫기 전후 화면 같음(BASE · LOCAL 칸 · 선택 줄 · ACTIONS 상태 그대로) |
| 설정에서 받은 값 | 만든 사람 · 문의처를 적으니 그대로 보이고, 비우면 회사 이름 · 안내 글 |
### 하네스
`AboutTest` **28건 새로** · 실패 0 — 버전 글 5 · PE 시각 읽기 9 · 빌드 날짜 고르기 4 ·
적혀 있지 않은 칸 4 · 실제 실행 파일 대조 5(버전 · 박힌 날짜 · 창에 적을 값 한 벌 · 문의처 빈칸)
### 남긴 것
- **만든 사람 · 사내 문의처는 비워 두었다.** 사람 이름 · 연락처는 지어낼 값이 아니다 -
설정 > MergeHub > 프로그램 정보 에서 적으면 그때부터 창에 나온다.
- 버전은 차수에 맞춰 손으로 올린다(12차 = 0.12.0). 올리는 자리는 csproj 한 곳이다.
## B-3. 칸 아래에 무엇이 달라졌는지 (2026-09-27, 완료)
### 무엇이 문제였나
요약이 창 맨 아래 상태 줄 한 곳에만 있었다("총 7줄 · 남은 차이 3줄"). 칸이 둘 · 넷이 되면서
**이 칸에 몇 줄이 있고 저쪽과 무엇이 다른가**는 눈으로 세야 했다.
### 어떻게 고쳤나
- **칸마다 한 줄**을 칸 아래에 붙였다 — `6줄 · 추가 1 · 삭제 1 · 수정 1`.
다른 것이 없으면 `6줄 · 같음`, 0 인 칸은 아예 적지 않는다(0 을 늘어놓으면 읽을 것만 는다).
- **가운데(칸 사이 틈) 아래에 진행** — 위에 `3 / 7`(몇 번째 차이를 보고 있나), 아래에 `남은 2`(아직 다른 줄).
틈이 48 DIP 라 두 줄로 짧게 적는다. 남은 것이 없으면 아랫줄은 비운다.
- 세는 규칙은 `Services/PaneSummary.cs`(새로 둠)에 모았다.
**칸을 기준으로 읽는다** — `추가` 는 *이 칸에만* 있는 줄, `삭제` 는 *저쪽에는 있는데 이 칸에 없는* 줄,
`수정` 은 양쪽에 있고 글이 다른 줄이다. 그래서 두 칸의 추가 · 삭제는 서로 거울이 된다.
무시 규칙에 걸린 줄과 우리가 넣은 머지 주석은 차이로 세지 않는다(화면 색 · 차이 이동과 같은 기준) —
다만 **줄 수에는 들어간다**(파일에 있는 줄이므로).
3분할에서는 네 칸 모두 **BASE(공통 조상)와** 견준다 — "누가 무엇을 고쳤나" 가 그 화면에서 읽어야 할 것이라서다.
BASE 칸 자신은 견줄 것이 없어 `4줄 · 기준` 이라고만 적는다.
### 화면으로 확인한 것
| 판정 기준 | 결과 |
|---|---|
| 반영 · 되돌리기 · 저장마다 숫자가 따라간다 | 반영 전 `6줄 · 추가 1 · 삭제 1 · 수정 1`(양쪽) · `남은 3` → `[>]` 하나 → 오른쪽 `8줄 · 추가 1 · 수정 1` · 왼쪽 `6줄 · 삭제 1 · 수정 1` · `남은 2` → Undo → 처음 값 그대로 |
| 3분할에서도 칸마다 맞는 숫자 | LOCAL(Mine) `4줄 · 수정 1` · BASE `4줄 · 기준` · REMOTE(Theirs) `4줄 · 수정 1` · MERGED `4줄 · 수정 1` · 가운데 `- / 1` · `남은 1` |
| 긴 파일에서 느려지지 않는다 | 5만 줄 × 두 칸 × 네 번 = **22ms**(한 번에 3ms 남짓). 목록을 한 번 훑을 뿐 파일을 다시 읽지 않는다 |
### 하네스
`SummaryTest` **25건 새로** · 실패 0 — 두 칸 요약 7 · 다른 것이 없을 때 4 · 세지 않는 줄 3 ·
세 칸(BASE 기준) 5 · 가운데 진행 5 · 긴 파일 1.
### 남긴 것
- 맨 아래 상태 줄의 "파일 전체" 요약은 그대로 둔다 — 칸 요약과 다른 값이다(전체 줄 수 · 남은 차이 · 편집 건수).
- 가운데 진행은 두 줄뿐이다. 틈을 넓히면 더 적을 수 있지만, 틈은 잇는 선(11차 F-10)의 자리라 좁게 두었다.
## C-2. 플랫폼 탭 줄 크기 줄이기 (2026-09-28, 완료 · 배포됨)
플랫폼(`D:\Sorce\ubsrepo`) 쪽이라 **모든 사내 프로그램에 함께 간다.** 지시서 원칙대로 코드와 값은 다 만들고
**배포는 사용자 확인 뒤**에 한다 — 아래 "배포" 참고.
### 무엇을 얼마나 줄였나
`Themes/ShellStyles.xaml` 의 `Ubisam.Style.SidebarItem.Horizontal`(하단 탭 줄 항목) 한 곳이다.
| 값 | 전 | 후 |
|---|---|---|
| 위아래 여백 | 13 / 14 | **9 / 10** |
| 아이콘 | 22×22 | **20×20** |
| 아이콘 아래 틈 | 7 | **5** |
| 글자 | 13.5 | **12.5** |
| 항목 높이(DIP) | 74 | **61** |
### 값으로 잰 것
2560×1600 · 배율 150% · 셸 배율 1.2 에서 실제 화면 픽셀로 쟀다.
| | 전 | 후 |
|---|---|---|
| 탭 줄 높이 | **133px** | **109px** (−24px, −18%) |
| 파일 비교 코드 칸 높이 | 1,081px | **1,105px** |
| 코드 줄 수(줄 높이 23px) | 47줄 | **48줄** (+1) |
배율 1.2 기준으로는 89 → 73px 이다(사용자가 말한 "95px" 과 같은 자리 - 재는 방법에 따라 테두리 · 밑줄이 붙는다).
글자 · 아이콘이 작아졌지만 한글 라벨(폴더 비교 · 파일 비교 · 화면 잠금 · 바탕화면 · 종료)은 잘리지 않고,
선택 밑줄 · 탭 접기 띠 · 오른쪽 동작 항목도 그대로다(화면 확인).
### 배포
이 항목만 빌드하면 `D:\UbisamPlatform\bin` 에 바로 반영된다. 처음에는 배포본을 백업해 두고 잠깐 바꿔치기해서
시험하고 되돌렸는데, **그 뒤 새 플랫폼(`D:\Sorce\ubsrepo`)을 빌드하면서 함께 나갔다.**
- 영향: 이 플랫폼으로 만든 **모든 프로그램의 하단 탭 줄**이 함께 작아진다. 기능 · 배치는 그대로고 높이만 준다.
- **배포 확인 (2026-09-28)**: 배포본 `UbisamBase.Core.dll` 이 지금 소스의 빌드 결과와 **해시가 같고**(BA27EE48…, 738,304바이트, 04:09),
실제로 켜서 잰 하단 탭 줄이 **111px · 항목 109px**(배율 150% 기준 72.7 DIP)로 "후" 값과 맞았다.
앞서 적었던 "지금 배포 폴더에는 예전 것이 그대로" 는 그 뒤 빌드로 낡은 글이 되어 고쳤다.
## C-3. 플랫폼 헤더 줄도 접을 수 있게 (2026-09-28, 완료 · 배포됨)
### 먼저 재현 (화면, 값)
| | |
|---|---|
| 헤더가 먹던 자리 | 모듈 화면이 **144px** 부터 시작(시계 줄 아래 끝 88px) |
| 접는 길 | **설정 화면 체크박스뿐** — `UiSettings.TopBarCollapsed` 는 있었지만 그것을 켜고 끄는 자리가 설정 화면 하나였다 |
| 접으면 | 되살릴 **띠도 단축키도 없다** — 다시 펴려면 설정 화면으로 들어가야 했다 |
### 고친 것
아래 탭 줄 접기(11차 F-4)와 **같은 방식**으로 맞췄다. 두 곳이 다르게 움직이면 배우는 것이 둘이 된다.
- **접은 띠**(`TopBarExpandStrip`) — 창 맨 위에 16 DIP 짜리 얇은 띠. `⌄ 헤더 펴기 · Ctrl+F11`.
사용자 결정대로 **화살표와 안내 글만** 남기고 시계 · 이름은 넣지 않았다.
- **Ctrl+F11** 로 접고 편다(사용자 결정 — 아래 탭 줄 F11 과 짝이 맞는다).
- 하단 동작 줄에 **[헤더 접기]** 를 [탭 접기] 옆에 두었다 — "접는 것들" 을 한 곳에서 찾게.
- 접은 상태는 **그 항목만** 파일에 남긴다(`UiSettingsFile.WithTopBarCollapsed`) —
설정 화면에서 바꾸고 아직 [저장] 하지 않은 테마 · 배율까지 함께 써지면 안 된다(F-4 와 같은 까닭).
- 설정 화면 체크박스로 바꿔도 띠가 따라온다(`OnUiSettingsChanged`).
**띠는 단추가 아니라 `Border` 다.** 한 자리에서 **끌기와 누르기를 둘 다** 받아야 하는데, 단추에 `DragMove` 를
붙이면 마우스를 놓는 순간 `Click` 이 오지 않아 펴지지 않는다. 그래서 끌린 거리를 보고 가른다 —
움직이지 않았으면 누른 것이다. 전체화면 · 최대화에서는 끌 것이 없으므로 바로 편다.
### 하네스
`PlatformCfgTest` 29 → **42건** 통과(C-3 +13) — 항목 없는 옛 파일은 펼침 · 망가진 글도 펼침 ·
다른 값(테마 · 배율 · 위치 · 알림)을 건드리지 않음 · **탭 줄 접힘과 한 파일에 나란히 살고 서로 덮지 않음** ·
모르는 항목도 지우지 않음. 하네스가 옛 플랫폼 소스를 가리키고 있던 것도 새 저장소로 옮겼다.
### 화면으로 확인 (값)
| 확인 | 값 |
|---|---|
| **접으면 코드 칸이 높아진다** | 모듈 시작 **144px → 51px** (93px · 배율 150% 기준 **62 DIP** 벌었다) |
| 남는 띠 | 51px(34 DIP)만 — 화살표와 안내 글뿐 |
| 다시 켜도 | **접힌 채**(`"TopBarCollapsed": true` 가 남는다) · 코드 칸도 그대로 높다 |
| 띠 누르기 | 펴진다 · 자리도 144px 로 돌아온다 |
| Ctrl+F11 | 접고 펴기 둘 다 |
| 회귀 | **F11 은 그대로 아래 탭 줄**만 접고 편다 · 헤더는 건드리지 않는다 |
| 최대화 | 그대로 2560×1600 |
### 값으로 확정하지 못한 것
**접은 띠를 끌면 창이 따라오는가** — 흉내 낸 마우스로는 WPF 의 `DragMove` 가 움직이지 않는다.
예전부터 되던 **기존 헤더 끌기로 견줘 봐도 똑같이 안 움직였다**(`c3-drag.ps1`) — 시험 도구 한계다.
띠는 헤더와 **같은 `DragMove()`** 를 부르고, 끌리지 않았을 때 펴지는 쪽은 값으로 확인했다.
사람 손으로 한 번 끌어 보는 것이 남았다.
### 배포
사용자 확인 뒤 **빌드해서 바로 반영**했다(`D:\UbisamPlatform\bin`). 빌드 오류 0 ·
경고 4(전부 기존 `ChartTheme.cs` 것). 되돌릴 자리로 배포본을 `D:\UbisamPlatform\bin-backup-c3` 에 떠 두었다.
**이 플랫폼으로 만든 모든 사내 프로그램의 헤더 줄에 함께 간다** — 기본은 지금까지와 같은 "펴짐" 이라
아무것도 하지 않으면 달라지는 것이 없다.
### 배포 뒤 전체 화면 회귀 (2026-09-30)
플랫폼을 바꿔 배포했으니 **그 위에 얹혀 도는 12차 화면 시험을 전부 다시 돌렸다**.
플랫폼만 달라졌고 MergeHub 쪽은 손대지 않았다.
| 벌 | 보는 것 | 통과 |
|---|---|---|
| `f1-verify` | 메모장 탭 | 38 |
| `d1-verify` | 폴더 비교 위쪽 한 줄 | 39 |
| `e1-verify` | 껐다 켜도 남는 편집 | 24 |
| `e2-verify` | 문서 탭 | 34 |
| `e2-reg` | 문서 탭 회귀 | 18 |
| `e2-start` | 시작 때 기본 짝 탭 | 8 |
| `e3-verify` | 왼쪽 줄 번호 | 16 |
| `e4-verify` | `[<]` 줄 밀림 | 6 |
| `c3-verify` | 헤더 줄 접기(배포 직후에 돌렸다) | 25 |
**합 208건 · 실패 0 · 예외 0.** 한 벌에 들어가기 전 앞 벌이 남긴 창과 보관함
(`D:\UbisamConfig\Memo` · `Session\MergeHub`)을 치워 서로 섞이지 않게 했다(`run-all-c3.ps1`).
끝나고 `MergeHubSettings.json` · `UiSettingsService.json` · `MergeHub.Recent.txt` ·
`ShellWindowPlacement.json` 을 시작 기준본과 **바이트까지 같게** 되돌렸다.
### 공용 플랫폼에 합친 뒤 다시 (2026-09-30)
우리가 따로 쓰던 플랫폼(`ubsrepo`)의 두 커밋을 **공용 플랫폼**(`D:\Sorce\UbisamCommonPlatform`)에
합쳐 배포했다. MergeHub 가 얹히는 바닥이 또 달라졌으니 아홉 벌을 다시 돌렸다.
MergeHub 코드는 이번에도 손대지 않았다.
| 벌 | 통과 | | 벌 | 통과 |
|---|---|---|---|---|
| `c3-verify` | 25 | | `e2-verify` | 34 |
| `f1-verify` | 38 | | `e2-reg` | 18 |
| `d1-verify` | 39 | | `e2-start` | 8 |
| `e1-verify` | 24 | | `e3-verify` | 16 |
| | | | `e4-verify` | 6 |
**합 208건 · 실패 0 · 예외 0** (09:48 → 09:58, `run-all-merge.ps1`).
MergeHub 를 새 플랫폼으로 다시 빌드해도 **경고 0 · 오류 0** — 플랫폼 API 는 깨지지 않았다.
**값 하나가 흔들렸다** — 이 회귀에서 C-3 의 모듈 시작 자리가 `144px → 51px` 이 아니라
`154px → 61px` 로 나왔다. 벌어드린 높이는 **93px · 62 DIP 로 똑같다**. 뒤에 `c3-verify` 를
따로 돌리니 다시 `144px → 51px` 이 나와 문서 값이 맞는 것은 확인했다.
| 아니었던 것 | 어떻게 알았나 |
|---|---|
| 공용 플랫폼의 네 커밋 | 셸 배치에 닿는 것이 없다 — 더한 암시적 스타일은 `ContextMenu` · `MenuItem` 뿐 |
| 뜨는 모드(전체화면 ↔ 최대화) | `mode-probe.ps1` 로 둘 다 재 보니 **똑같이 144px** |
**무엇이 10px 을 밀었는지는 확정하지 못했다.** 몰이 대본 안에서 한 번 나왔고 따로 돌리면
나오지 않는다 — 시험 환경 쪽 변덕으로 보이지만 끝까지 캐지 않았다. 다음에 또 나오면
그때 잡는다. 통과 건수와 다른 값은 모두 그대로다.
## F-1. 하단 탭에 메모장 자리 (2026-09-28, 완료)
사용자가 정한 것 — **메모 + 파일 열기 둘 다**, **저장 단추 없이 계속 남는다**,
이름은 **`메모 1` · `메모 2` 번호만**, 편집기는 줄 번호 · 글자 크기 · 줄바꿈 **+ 찾기/바꾸기**.
### 무엇을 만들었나
- `Services/Memos.cs` — `Memo`(한 장) · `MemoBook`(번호 · 이름 · 넘기기 · 닫기, 계산만) ·
`MemoStore`(보관, `{설정 폴더}\Memo\`).
- `ViewModels/MemoViewModel.cs` — 새 메모 · 파일 열기 · 저장 · 다른 이름으로 · 닫기 · 자동 보관 · 되살리기.
- `Views/MemoView.xaml(.cs)` — 동작 한 줄 · 메모 탭 줄 · 찾기/바꾸기 줄 · 줄 번호 편집기 · 아래 상태 줄.
- `AppSetup` — 하단 탭 **맨 뒤**에 넣었다. 앞 세 탭의 차례가 바뀌면 손버릇도 바뀐다.
문서 탭(12차 E-2)과 **같은 모양**을 쓴다 — 탭 줄 · 저장 안 한 가운뎃점 · Ctrl+Tab · 가운데 단추로 닫기.
두 화면이 서로 다르게 움직이면 배우는 것이 둘이 된다.
메모는 문서 탭과 달리 **글을 스스로 들고 있다** — 메모는 원래 작은 글이고, 이름 없는 메모에는
다시 읽어 올 파일 자체가 없다.
### 하네스
`MemoTest` **69건** 통과 — 번호 매기기(빈 번호 다시 쓰기) · 파일 이름 겹칠 때 폴더 덧붙이기 ·
같은 파일 두 장 금지 · 이름 붙이기 · 넘기기 · 닫기 자리 · 떠 있는 장 표시 ·
글로 옮기고 되읽기(빈칸 · 한글 · UNC) · 파일로 쓰고 되읽기 · 옛 글 파일 치우기 · 되살려 담기.
### 화면으로 확인 (값)
| 확인 | 값 |
|---|---|
| 세 장 만들어 각각 쓰기 | `메모 1` `메모 2` `메모 3` · 가운뎃점 3개 · 글이 섞이지 않음 |
| 손 멈춘 뒤 | 보관본 `01.txt` `02.txt` `03.txt` `목록.txt` |
| **강제 종료 → 다시 켜기** | **세 장이 차례 · 글 그대로**(저장 단추를 누른 적이 없다) |
| 찾기/바꾸기 | Ctrl+F 로 펴짐 · `사과`→`포도` 모두 바꾸기 3곳 |
| 닫기 | 고친 메모는 **세 갈래로 물음** · [취소] 면 그대로 3장 · [저장하지 않고 닫기] 면 2장 · **빈 메모는 묻지 않음** |
| 줄 번호 | 40줄에서 번호 40개, 글과 한 줄씩 맞음 |
| 다른 탭 | 폴더 비교 · 파일 비교 그대로 · 탭을 오가도 메모 글 40줄 그대로 |
하네스 29벌 **1,934건** 통과 · 실패 0, 화면 여덟 벌 모두 통과(빌드 경고 0 · 오류 0).
### 화면을 보고 고친 세 가지 (사용자 보고)
**(가) 다른 탭에 갔다 오면 메모 글이 비어 보였다.** 탭을 오가면 화면이 다시 만들어지는데 그때 편집기가
잠깐 비어 있다. 그 틈에 글을 거두면 빈 글로 메모를 덮어썼다. 편집기가 **지금 어느 장을 싣고 있는지**(`_shown`)를
두고, 맞지 않으면 거두지 않게 했다.
**(나) 줄 번호와 글이 어긋났다.** 줄 번호를 `TextBlock` 으로 그렸더니 줄 높이가 조금씩 달라 아래로 갈수록
벌어졌다. **편집기와 같은 종류(TextBox)** 로 바꾸고 글꼴 · 크기 · 여백을 같은 값으로 두니 같은 자로 줄을 잰다.
**(다) 글이 칸 한가운데에서 시작했다.** 플랫폼 입력 칸 스타일이 글 칸(PART_ContentHost)을 세로 가운데에
붙박아 둔다 — 한 줄짜리 칸을 겨냥한 값이다. `VerticalContentAlignment` 로는 안 풀려(템플릿 안에 박힌 값)
그 칸을 직접 늘려 **맨 위부터** 쓰게 했다.
### 시험 도구
시험 스크립트가 변수 이름을 `$keep` 로 써서 **라이브러리의 `$Keep`(되돌릴 설정 파일 목록)을 덮어쓰고** 있었다 —
그래서 시험이 끝나도 설정이 안 돌아왔다(두 번 겪고 나서야 찾았다). 이름을 갈랐다.
묻는 창을 찾는 도구도 단추 이름을 `저장하지 않고 계속` 로 못 박아 두어, 말이 다른 창(`저장하지 않고 닫기`)을
못 알아봤다 — 앞부분만 맞춰 보게 했다.
## D-1. 폴더 비교 위쪽을 한 줄로 · 옵션은 서랍으로 (2026-09-28, 완료)
### 먼저 재현 (화면, 값)
| | |
|---|---|
| 위쪽이 먹던 높이 | **305px (배율 150% 기준 203 DIP)** · 네 줄 |
| 무엇이 있었나 | ① 경로 두 개 + 최근 ② 빠른 비교 · 하위 폴더 · 보기 · 포함 · 제외 · [비교] · [취소] ③ 폴더 안 찾기 한 벌 ④ 색 구분 범례 |
비교하기 <b>전에도</b> 네 줄이 그대로 있었고, 자주 쓰는 [비교] 는 옵션 줄 끝에 묻혀 있었다.
### 고친 것
- **위 한 줄**: `[…] 왼쪽 경로 ⇆ […] 오른쪽 경로 [최근▾] 켠 옵션 [비교] [취소] [옵션 ▾]`
- 폴더 고르기는 글자 대신 `…` 로. 경로 칸 <b>바로 앞</b>에 두어 어느 쪽 것인지 헷갈리지 않는다.
- **좌우 바꾸기(⇆)** 를 새로 넣었다(사용자 결정) - 반대로 견주려고 두 번 고르지 않아도 된다.
- 최근 목록은 화살표만 남겼다.
- **옵션 서랍**: 빠른 비교 · 하위 폴더 포함 · 보기 · 포함 · 제외를 `[옵션 ▾]` 안으로. `[폴더 안 찾기]` 도 여기에 뒀다.
- **켠 옵션 표시**: 서랍에 넣으면 "내가 뭘 켜 뒀더라" 를 알 수 없게 된다 - 특히 제외 규칙은 켠 줄 모르고
"왜 이 파일이 안 보이지" 로 이어진다. <b>기본과 다른 것만</b> 한 줄에 짧게 적는다(`FolderOptionText`).
기본 그대로면 아무것도 적지 않는다 - 늘 떠 있는 글은 눈에 띄지 않는다.
- **찾기**: 늘 한 줄을 먹던 것을 **Ctrl+F 로 부를 때만** 편다. Esc · [찾기 접기] 로 접는다.
- **색 범례**: 위에서 빼고 <b>아래 상태 줄</b>로 내렸다 - 색은 목록을 보는 동안 필요한 것이라 결과 요약 옆이 제자리다.
### 화면으로 확인 (값)
| 확인 | 값 |
|---|---|
| **위쪽 높이** | **305px → 77px** (203 → **51 DIP**, −75%) |
| 한 줄에 있는 것 | […] ⇆ […] · 최근 · [비교] [취소] [옵션 ▾] · 켠 옵션 |
| 좌우 바꾸기 | 누르면 두 경로가 맞바뀌고, 다시 누르면 되돌아온다 |
| 옵션 서랍 | 빠른 비교 · 하위 폴더 · 보기 · 포함 · 제외 · [폴더 안 찾기] 모두 있음 |
| 켠 옵션 표시 | 빠른 비교를 켜면 줄에 "빠른 비교", 처음 제외 규칙이 있어 "규칙 1" |
| 찾기 | Ctrl+F 로 77 → 158px, Esc 로 다시 77px |
| 비교 | 그대로 동작(`전체 2개 · 다름 2 · 왼쪽만 0 · 오른쪽만 0 · 같음 0`) |
| 없어지지 않은 것 | 결과 내보내기 · 줄 단위 비교 · 양쪽 복사 · 양쪽 삭제 · 색 구분(아래로) |
| 좁은 창(1920px = 1280 DIP) | 여전히 **77px 한 줄**, [비교] · [옵션] 다 보임 |
하네스 `FolderTest` 118 → **131건**(D-1 +13), 전체 28벌 **1,865건** 통과 · 실패 0(빌드 경고 0 · 오류 0).
### 화면을 보고 고친 것
`[옵션 ▾]` 를 `ToggleButton` 으로 뒀더니 플랫폼 기본 스타일이 <b>스위치 모양</b>이라 같은 줄의 다른 단추와
따로 놀았다. 여느 단추로 바꾸고 누를 때마다 서랍을 여닫게 했다. 최근 목록도 폭을 못 박지 않아 넓게 벌어지던 것을 고쳤다.
## E-1. 꺼도 작업 내용이 남아 있게 (2026-09-28, 완료)
E-2 가 만든 **보관함**(탭이 사라져도 고친 글이 짝 이름으로 남는 자리)을 그대로 파일로 옮겨 적는 일이다.
새로 정할 것은 **언제 쓰고 언제 되살리는가**뿐이었다.
### 어디에 · 어떤 모양으로
`{설정 폴더}\Session\MergeHub\` — 11차 F-9 백업과 **다른 자리**다. 백업은 "저장하기 전 파일",
이것은 "저장하지 않은 편집" 이라 뜻도 수명도 다르다.
```
목록.txt 차례 · 짝 경로 · 캐럿 · 스크롤 · 저장 형식 · 보고 있던 탭 · 그때 파일의 자취
01-오른쪽.txt 저장하지 않은 오른쪽 글(있을 때만)
01-왼쪽.txt 저장하지 않은 왼쪽 글(12차 B-1, 있을 때만)
```
- **JSON 을 쓰지 않는다**(최근 목록과 같은 까닭). 사람이 열어 보고 손으로 고칠 수 있고, 무엇보다
**글 안에 무엇이 들었든**(따옴표 · 줄바꿈 · 제어 문자) 본문을 따로 파일로 두면 탈이 없다.
- **원본 글은 담지 않는다.** 파일에서 다시 읽으면 되고, 담아 두면 그 사이 파일이 바뀌었을 때 낡은 것을 보여 준다.
대신 그때 파일의 **길이 · 마지막으로 쓴 때**(`FileStamp`)만 적어 두고, 다를 때 알린다.
- 목록은 **맨 나중에** 쓴다 — 목록이 있다는 것이 곧 글 파일이 다 있다는 뜻이다.
- 못 읽거나 못 써도 **조용히 넘어간다**. 보관 때문에 프로그램이 안 켜지거나 저장이 막히면 안 된다.
### 언제 쓰나
| 때 | 어떻게 |
|---|---|
| 글자가 멈춘 뒤 | **2초**에 한 번(`DispatcherTimer`). 글자마다 쓰면 큰 파일에서 걸린다 |
| 탭을 옮길 때 · 닫을 때 · 새 짝을 열 때 | **그 자리에서 바로** — 그 순간이 곧 끄기 직전일 수 있다 |
| 고친 것이 생기거나 없어질 때 | 2초 뒤에 한 번(저장하면 보관본이 빠져야 한다) |
쓰는 일은 **다른 실로 넘긴다**(`Task.Run`). 지금 화면의 상태를 뜰 때 `ParkCurrentTab` 을 쓰지 않는다 -
그쪽은 되돌리기 목록을 글 단위로 눌러 담는데(`FlattenHistory`), 2초마다 이력을 주무를 까닭이 없다.
보관할 것은 **글과 자리**뿐이고 되돌리기 이력은 되살리지 않기로 정했다(사용자 결정).
### 언제 되살리나
켤 때 **묻지 않고 그대로**(사용자 결정). 다만 **사용자가 지금 열라고 한 것이 먼저**다 -
충돌 파일 열기 · `git mergetool` · 폴더 비교에서 고른 파일이 있으면 그것을 연다.
설정의 기본 저장소 · 기본 짝보다는 **앞선다**.
되살리는 것은 탭 짝 · 저장하지 않은 글 · 캐럿 · 스크롤 · 어느 탭을 보고 있었는지다.
원본 글은 파일에서 다시 읽고 그 위에 편집을 얹는다(E-2 의 `ResumePendingTab` 이 하던 그대로).
두 파일이 다 없어진 짝은 되살릴 자리가 없으므로 버린다.
### 하네스
`SessionTest` **65건** 통과 — 글로 옮기고 되읽기(빈칸 · 한글 · UNC 경로, 알아볼 수 없는 줄 건너뛰기) ·
파일로 쓰고 되읽기 · 보관할 것이 없으면 치우기 · 고친 것 없는 줄 빼고 번호 다시 매기기 ·
옛 글 파일 치우기 · 글 파일이 없어져도 넘어가기 · `FileStamp` · 보관함을 내주고 되받기.
### 화면으로 확인 (값)
두 짝을 열어 둘 다 고치고 **작업 관리자로 끄듯 강제로** 끝낸 뒤 다시 켰다.
| 확인 | 값 |
|---|---|
| 손을 멈춘 뒤 | 보관본 `01-오른쪽.txt` · `02-오른쪽.txt` · `목록.txt` 생김 |
| 강제 종료 뒤 | 보관본 그대로 남음 |
| 다시 켜면 | **묻지 않고** 탭 2개 되살아남(`alpha.cs` · `bravo.cs`), 이름 · 차례 그대로 |
| 보고 있던 탭 | `bravo` 의 고친 글 그대로 · 가운뎃점 2개 |
| 다른 탭 | `alpha` 의 고친 글도 그대로 · 서로 섞이지 않음 |
| 한쪽을 저장 | 가운뎃점 2 → 1 · 파일에 써짐 |
| 둘 다 저장 | 가운뎃점 0 · **보관본이 치워짐** |
| 다시 켜면 | 되살릴 것 없음 · 설정의 기본 짝이 열림 |
| 원본이 그 사이 바뀌면 | 고친 글 되살아나고 **"그 사이 파일이 바뀌었습니다 … 저장하면 바뀐 파일을 덮어씁니다"** 알림 |
하네스 28벌 **1,852건** 통과 · 실패 0, 화면 여섯 벌 모두 통과(빌드 경고 0 · 오류 0).
### 빠르기에 대해 말할 수 있는 것과 없는 것
5,003줄(113KB) 짝에서 글자를 보내 보니 **글자당 31ms 로 4줄짜리와 같았다** - 앱이 입력에 밀리지 않는다.
보관은 손을 멈춘 뒤 한 번, 다른 실에서 107KB 를 적었다.
다만 **"글자를 친 뒤 화면에 보이기까지" 는 값으로 확정하지 못했다.** 그것을 재려면 편집기 글을
UIA 로 읽어야 하는데, 5,003줄 창에서는 그 읽기가 타이핑보다 훨씬 오래 걸려(한 번에 수 초) 값이 통째로 오염된다.
재는 법을 따로 마련해야 한다.
### 시험 도구도 함께 고쳤다
E-1 이 도니 **지난 시험이 남긴 "저장하지 않은 편집" 이 다음 시험에서 되살아나** 화면이 달라졌다
(`e3-verify` 가 왼쪽 7줄 대신 14줄을 봤다). 시험마다 보관 자리를 치우고 시작해 끝에 되돌리도록
`Backup-Cfg` · `Restore-Cfg` 를 고쳤다 - 기능이 제대로 도는 증거이기도 하다.
## B-1 을 쓰다 나온 것 (2) - [<] 단추가 있는 줄만 글이 밀린다 (2026-09-28, 사용자 보고)
사용자가 그림으로 짚어 준 것이 <b>이것</b>이었다. 아래 "왼쪽 줄 번호" 건은 이것을 쫓다 따로 찾은 다른 흠이다.
### 먼저 재현 (화면, 값)
`[<]` 단추가 보이는 줄만 코드가 오른쪽으로 밀려, <b>같은 칸 안에서도 줄마다 글 시작 자리가 달랐다.</b>
```
BASE LOCAL
01 class Axis 01 class Axis
02 { 02 {
< 03 int speed = 100; 03 > int speed = 200; ← 밀림
< 04 // left only line 04 void Home() { }
05 void Home() { } 05 > < void Stop() { } ← 더 밀림
06 } 06 }
```
오른쪽 칸은 단추가 하나 늘 때마다 한 칸씩 더 밀렸다.
### 까닭
단추가 든 칸의 폭이 `Auto` 였다. 단추가 안 보이는 줄에서는 0 이 되고 보이는 줄에서는 단추 폭만큼 벌어지니,
그 줄만 뒤 칸이 통째로 오른쪽으로 밀린다. `Visibility="Collapsed"` 는 <b>자리까지 없앤다</b> -
자리를 지키려면 칸 폭이 고정이어야 한다.
- BASE 는 `[<]` 가 맨 앞 칸(`Auto`)이라 **번호까지 함께** 밀렸다.
- LOCAL 은 `[<]` 가 번호 뒤 칸(`Auto`)이라 **코드만** 밀렸다.
### 고친 것
`GutterBothWaysWidth` 를 두고 두 칸이 같은 값을 쓴다. 화면이 모드에 따라 0 ↔ 단추 폭으로 갈아 끼운다(`ApplyGutterReserve`).
- **두 파일 비교**: 단추 폭만큼 늘 잡아 둔다 - 단추가 없는 줄도 같은 자리에서 글이 시작한다.
- **Git 모드 · 네 칸**: `[<]` 가 아예 없으므로 0 - 여백이 예전 그대로라 코드 칸이 좁아지지 않는다.
- **편집기 여백**(`EditorGutterMargin`)도 같은 값만큼 민다. 투명한 편집기와 아래 표시층이 어긋나면
캐럿이 글자와 다른 자리에 선다.
- 글자 크기(11차 F-5)를 바꾸면 단추 폭도 따라 바뀌므로, 잡아 두는 자리도 `CodeFontMetrics` 한 벌에서 나온다.
### 화면으로 확인 (값)
| 확인 | 값 |
|---|---|
| 두 파일 비교 BASE | 여섯 줄 모두 `x=137` (전에는 단추 줄만 달랐다) |
| 두 파일 비교 LOCAL | 여섯 줄 모두 `x=1283` |
| 글자 크기 키운 뒤 | 모두 한 x(`147`) |
| Git 모드(회귀) | 모두 한 x(`459`) · 여백 예전 그대로 |
### 남겨 둔 것
BASE 줄 <b>끝</b>의 `[>]`(LOCAL 에서 지운 줄 되살리기, 11차 F-10)는 아직 `Auto` 다. 이것은 왼쪽 가장자리를
밀지 않고 오른쪽에서 코드 칸을 조금 좁힐 뿐이라 눈에 띄지 않는다. 워드랩을 켜면 그 줄만 접히는 자리가
달라질 수 있다 - 워드랩은 기본이 꺼짐이고, 손대면 네 칸 폭을 다시 맞춰야 해서 이번에는 두었다.
## B-1 을 쓰다 나온 것 - 왼쪽 줄 번호가 밀린다 (2026-09-28, 사용자 보고)
### 먼저 재현 (화면, 값)
왼쪽 6줄 · 오른쪽 8줄(오른쪽에만 있는 줄 둘)로 열고, 오른쪽에만 있는 줄을 `[<]` 로 왼쪽에 넣었다.
| | 왼쪽 칸 번호 |
|---|---|
| 연 직후 | `01 02 03 04 05 06` |
| 한 줄 넣은 뒤 | `01 02 · 03 04 05 06` |
| 두 줄 넣은 뒤 | `01 02 · 03 04 05 · 06` |
`·` 는 번호가 **비어 있는 줄**이다. 왼쪽 파일은 8줄이 됐는데 번호는 6까지만 있고, 새로 들어간 줄에는
번호가 없다. 저장하면 8줄로 써지므로 "몇 번째 줄을 고쳤나" 를 번호로 짚을 수가 없다.
### 까닭
8차 B-3 에서 만든 `RenumberMerged` 는 **오른쪽(MERGED) 칸만** 다시 번호를 맨다. 그때는 BASE 가 얼어 있었다.
12차 B-1 로 **왼쪽에도 반영**할 수 있게 되면서 왼쪽 줄이 늘고 줄게 됐는데, 왼쪽은 정렬기가 처음 준 번호를
그대로 달고 있었다. `MergeLine.BaseNumber` 의 설명에는 이미 "이제 두 파일 비교에서는 … 번호도 따라 바뀐다" 고
적혀 있었지만 실제로 다시 매기는 자리가 없었다 - 적어 둔 뜻과 코드가 어긋나 있었다.
### 고친 것
`RenumberBase` 를 `RenumberMerged` 옆에 두고, 그릴 때마다(`UpdateSummaries`) 왼쪽에 있는 줄만 1부터 다시 맨다.
- **두 파일 비교에서만**(`BothWays`). Git 모드 · 네 칸의 BASE 는 지난 커밋 · 공통 조상이라 고칠 수 없고,
그때 번호는 그 파일의 번호이므로 건드리면 안 된다.
- **"머지 반영함" 표시의 열쇠를 함께 옮긴다.** 그 표시는 BASE 줄 번호로 붙들려 있어서(`_appliedBaseNumbers`),
번호만 바꾸면 규칙을 한 번 건드리는 순간 엉뚱한 줄이 반영함으로 칠해진다. 번호가 실제로 달라질 때만
옛 번호 → 새 번호로 옮겨 담는다(안 바뀌면 손대지 않는다 - 연 직후가 그렇다).
### 화면으로 확인 (값)
왼쪽 7줄(왼쪽에만 있는 줄 하나) · 오른쪽 8줄로 다시 잡고 봤다.
| 확인 | 값 |
|---|---|
| 연 직후 | `01 … 07` · 왼쪽 줄 수 = 파일 줄 수 7 |
| 오른쪽 줄 둘을 왼쪽에 넣음 | `01 … 09` 로 이어짐 · 왼쪽 9줄 |
| 왼쪽에만 있는 줄을 반영 | 번호가 이어짐 · 번호 빠진 줄 0 |
| 되돌리기 | 번호가 이어짐 · 번호 빠진 줄 0 |
| 왼쪽 저장 | 저장한 파일 9줄 = 칸의 끝 번호 09 · 넣은 줄이 파일에 있음 |
| Git 모드(회귀) | BASE 번호 그대로 이어짐 |
하네스 27벌 **1,787건** 통과 · 실패 0, E-2 화면 세 벌도 그대로 통과(빌드 경고 0 · 오류 0).
### 아직 확인하지 못한 것
"왼쪽에만 있는 줄을 `[<]` 로 빼면 왼쪽 줄 <b>수</b>가 하나 준다" 는 것은 **값으로 확정하지 못했다.**
따로 열어 한 번만 누르면 잘 빠지는데(3줄 → 2줄 확인), 다른 반영을 몇 번 한 뒤에 같은 것을 하면
시험 도구가 읽은 줄 칸이 낡아 옆 줄 단추를 누르는 일이 생겨 값이 흔들렸다.
번호 쪽 불변식(늘 1부터 이어지고 빠진 번호가 없다)은 어느 경우에도 지켜졌다.
**빼는 길 자체가 제대로 도는지는 따로 봐야 한다.**
## E-2. 여러 짝을 동시에 (문서 탭) (2026-09-28, 완료)
### 먼저 재현 (화면, 값)
| 단계 | 값 |
|---|---|
| 짝 A 열기 | 오른쪽 15줄 · `public int Version = 2;` |
| 고치고 저장하지 않음 | 16줄 · 마지막 줄 `// E2-MARK-ALPHA` · `[오른쪽 저장 ▶]` 켜짐 · 아래 줄 "남은 차이 3줄 · **편집 1건**" |
| 다른 짝 열기 | 모달 "**a-right.cs 에 저장하지 않은 편집이 있습니다**" · [취소] [저장하지 않고 계속] [저장하고 계속] |
| [저장하지 않고 계속] | 화면이 15줄로 돌아감 — **고친 글이 사라졌다**(원본 파일은 안 바뀜) |
한 번에 한 짝만 들기 때문이다. 짝을 바꾸는 순간 앞 짝의 편집 글이 갈 곳이 없어서 묻고, 버리면 그대로 사라진다.
### 고친 방식
**탭마다 `HomeViewModel` 을 따로 만들지 않았다.** 이 화면은 `GitWorkspace` 의 여러 사건과 <b>한 자리짜리 약속</b>
(`ConfirmDiscardEdits` · `ConfirmLeaveCommit`)에 물려 있다 — 여럿을 만들면 같은 사건에 여러 화면이 동시에 반응하고
약속은 마지막 것만 남는다. 그래서 **화면은 하나로 두고 탭이 편집만 맡는다.**
- `Services/DocTabs.cs` (새로 만듦) — 계산만 한다(화면 없이 검사할 수 있다).
- `DocTab` : 짝 하나 · 이름(`Title`) · 저장 안 한 표시(`IsDirty`) · 떠 있는지(`IsCurrent`) · 맡아 둔 편집(`Parked`)
- `DocTabState` : **파일에 없는 것만** 담는다 — 고친 글 · 왼쪽 글(B-1) · 되돌리기/다시하기 이력 · 두 기준점 ·
캐럿 자리 · 스크롤 행 · 저장 형식. 파일에서 다시 읽을 수 있는 것(원본 글 · 줄 정렬)은 담지 않는다 —
담아 두면 파일이 그 사이 바뀌었을 때 낡은 것을 보여 준다.
- **이름 짓기** : 파일 이름, 양쪽이 다르면 `왼 ↔ 오`(최근 목록 `RecentPair.Label` 과 같은 규칙).
같은 이름이 겹치면 **갈릴 때까지 위 폴더를 한 겹씩** 덧붙인다(`Unit0.cs` 가 여러 폴더에 있는 사내 소스 때문).
- **보관함** : 탭을 닫거나 짝을 바꿔 탭 자체가 사라져도 고친 글은 짝 이름으로 남는다(50개).
◀ ▶ 로 훑다가 앞 파일로 돌아왔을 때 고친 것이 사라지면 그것이 곧 E-2 가 없애려던 일이다.
**E-1(껐다 켜도 남는 편집)은 이 보관함을 파일로 옮겨 적으면 된다.**
- **상한 20** : 넘으면 저장할 것 없는 탭 가운데 가장 오래 안 본 것을 닫는다. **고친 탭은 절대 닫지 않는다**(넘겨서라도 연다).
- `ViewModels/HomeViewModel.Tabs.cs` (새로 만듦) — 화면과 이어 주는 자리.
`ParkCurrentTab`(맡기기) · `ResumePendingTab`(되살리기) · `OpenPairInTab` · `EnsureTabForCurrentPair` ·
`GoToTab` · 명령 네 개(ActivateTab · CloseTab · NextTab · PreviousTab).
- `HomeViewModel.cs` 세 군데만 고쳤다.
- `OnPlainCompareRequested` : 두 파일 비교끼리 짝을 바꿀 때 **묻지 않는다**. Git · 네 칸에서 넘어올 때는 그대로 묻는다.
- `MoveFile`(◀ ▶) : 묻지 않고 **지금 탭의 짝만 바꾼다**(사용자 결정 — 탭이 늘지 않아 목록을 훑기 좋다).
- `LoadPlainFilesAsync` : 맨 앞에서 `EnsureTabForCurrentPair`, 맨 끝(파일을 다 읽은 뒤)에서 `ResumePendingTab`.
- `HomeView.xaml` : 맨 위에 문서 탭 줄. **ItemsControl 이 아니라 ListBox** 다 — ItemsControl 은 UIA 에 "컨트롤" 로
보이지 않아 화면 시험이 탭 줄을 아예 못 읽고, 키로 옮기는 것도 공짜로 얻을 수 없다. 고르기가 곧 탭 옮기기다.
탭이 둘 이상일 때만 보인다(한 짝만 볼 때 줄을 낭비하지 않는다).
- 단축키 : **Ctrl+Tab · Ctrl+Shift+Tab · Ctrl+W**(notepad++ · 브라우저와 같은 손버릇). 가운데 단추로도 닫힌다.
- 닫을 때 · 옮길 때 **묻지 않는다** — 글이 보관함에 남기 때문이다.
### 원본 파일이 그 사이 바뀌었으면
**되살리고 알린다.** 사용자가 아까운 것은 자기가 고친 글이다. "파일을 연 시점"은 **새로 읽은 글**로 두어
[전체 되돌리기]가 지금 파일로 가고, 고친 글은 편집으로 남는다 — 그래야 어느 쪽도 잃지 않는다.
알림으로 "저장하면 바뀐 파일을 덮어씁니다" 를 말해 준다.
### 화면을 켜 놓고 보다 고친 두 가지 (2026-09-28)
만들어 놓고 실제 설정으로 켜서 눈으로 보다 찾았다. 둘 다 사용자가 고르게 했다.
**(가) 켜자마자 가운뎃점이 붙었다.** 설정에 C# 중괄호 맞춤이 켜져 있으면 `.cs` 를 <b>여는 것만으로</b>
"저장할 것 있음" 이 되어, 손대지도 않은 탭에 점이 붙었다. 늘 붙어 있는 점은 뜻이 없다.
"저장하지 않은 편집이 있습니다" 를 묻는 자리(`WillAskBeforeDiscard`)는 이미 스타일 맞춤만 바뀐 것을 빼고 세고 있었다 —
**점도 같은 기준**(`HasTypedEdits`)을 쓰게 했다. 맡길 때도 같다(스타일로만 바뀐 글은 맡기지 않는다 — 다시 열면 똑같이 만들어진다).
**(나) 긴 짝 이름이 탭 줄을 많이 먹었다.** `WeldController_기준.cs ↔ WeldController_내작업.cs` 하나가 **421px(280 DIP)**,
탭 줄의 1/9 를 차지하고 뒤가 잘려 `WeldController_내작...` 으로 보였다. 사내 소스에는 이름이 살짝만 다른 짝이 흔하다.
**양쪽이 같은 부분은 한 번만** 적게 했다(사용자 결정).
| | 전 | 후 |
|---|---|---|
| 탭 이름 | `WeldController_기준.cs ↔ WeldController_내작...` | `WeldController_기준 ↔ 내작업.cs` |
| 탭 폭 | 421px | **288px** (−32%, 잘림 없음) |
뒤쪽은 **확장자일 때만** 접는다. 글자 단위로 같은 꼬리를 세면 낱말 가운데서 잘린다 —
`a-left.cs ↔ a-right.cs` 에서 `t.cs` 가 같다고 `a-lef ↔ right.cs` 가 되어 읽을 수 없었다(하네스에서 걸렸다).
앞이 한 글자만 같은 것은 접지 않는다(`abc.cs ↔ axy.cs` 는 그대로).
### 하네스
`DocTabTest` **106건** 통과 — 이름 짓기(겹칠 때 폴더 덧붙이기 · 한쪽만 있는 짝) · 저장 표시 · 같은 짝 두 탭 금지 ·
Replace · 닫기 자리 이동 · Ctrl+Tab 돌기 · 상한 · 보관함(짝 바꿔도 · 닫아도 · 모두 닫아도 · 치워도 되살아남 · 50개 한도) ·
**같은 부분 접기**(사내 소스 짝 · 앞뒤가 다 겹치는 짝 · 한 글자만 같으면 접지 않음 · 폴더까지 겹치는 경우).
### 화면으로 확인 (값)
| 확인 | 값 |
|---|---|
| 폴더 비교에서 두 번 누르기 | alpha.cs → 탭 1개(줄 안 보임) · bravo.cs → **탭 2개** `alpha.cs \| bravo.cs` |
| 짝을 바꿀 때 | **묻는 창이 뜨지 않는다** |
| 저장 안 한 표시 | alpha 만 고쳤을 때 가운뎃점 **1개** → 둘 다 고치면 **2개** |
| 탭을 오간 뒤 | alpha 15줄(`E2-MARK-ALPHA` 그대로) · bravo 15줄(`E2-MARK-BRAVO` 그대로) · **서로 섞이지 않음** |
| 되돌리기 | Undo 가 그 탭 것만 되돌리고 Redo 로 돌아옴 · 다른 탭은 그대로 |
| Ctrl+Tab | alpha 로 넘어가고 그 탭의 편집도 그대로 |
| 저장 | 그 탭의 가운뎃점만 사라짐(2 → 1) · 저장 단추 꺼짐 · 파일에 실제로 써짐 · 다른 파일은 안 바뀜 |
| 탭 닫기 | 묻지 않음 · 한 짝만 남자 탭 줄이 사라짐 · 오류 글 0 |
회귀로 함께 본 것.
- ◀ ▶ 로 훑기 : `1 / 2` → `2 / 2` → `1 / 2`, **탭이 늘지 않고**(0개 = 줄 안 보임) 돌아오니 고친 글 그대로(15줄).
- 켤 때 저절로 열리는 짝(설정의 기본 짝)에서 고친 것도 탭이 맡는다 — `EnsureTabForCurrentPair` 를 넣기 전에는
그 짝만 탭이 없어 편집이 사라졌다(화면에서 찾아 고쳤다).
- Git 모드로 넘어갈 때는 **여전히 묻는다**(탭이 맡을 수 없는 자리). 데모 전환은 예전부터 **두 번** 묻는다 —
화면이 한 번(`데모 폴더를 바꿉니다`), `GitWorkspace` 가 한 번(`Git 작업을 합니다: 데모 저장소 열기`). 이번 변경과 무관하다.
- 하네스 전체 **27벌 1,777건 통과 · 실패 0**(빌드 경고 0 · 오류 0).
## 시험 도구를 런처에 맞추기 (2026-09-28)
새 플랫폼부터 프로그램을 `UbisamBase.Launcher.exe` 로 켜고, 런처가 셸을 **같은 프로세스 안에** 올린다
(별도 프로세스로 띄우면 디버거가 안 붙어서 소비 프로젝트 중단점이 안 걸린다 — 런처 주석). 그래서 창을 든
프로세스 이름이 `UbisamBase.Shell` 에서 `UbisamBase.Launcher` 로 바뀌었고, 예전 이름만 찾던 화면 시험
도구가 전부 **"창이 안 뜬다"** 로 멈춰 있었다.
| | 전 | 후 |
|---|---|---|
| 켜는 파일 | `UbisamBase.Shell.exe` | `UbisamBase.Launcher.exe` (없으면 셸로 되돌아감) |
| 창 주인 프로세스 | `UbisamBase.Shell` 만 | `UbisamBase.Launcher` · `UbisamBase.Shell` 둘 다 |
- `a2lib.ps1` 에 `Shell-Procs` · `Shell-Proc` · `Stop-App` 을 두고 `Start-App` · `Get-Win` 이 그것을 쓰게 했다.
런처가 업데이트 진행 창(제목 없는 창)을 먼저 띄우는 경우를 대비해 **제목이 있는 창을 먼저** 고른다.
- 스크립트 **76개 · 156곳**을 함께 고쳤다(옛 이름만 찾던 `Get-Process`, 지금 빌드 자리를 가리키던 실행 경로).
고치기 전 파일은 스크래치패드 `_prelauncher\` 에 그대로 뒀다.
- 확인: 문법 검사 187개 통과, 끄기 → 켜기 → 창 찾기 → 네 탭 넘기기 → 단추 찾기 → 끄기까지 실제로 돌았고,
설정 · 창 자리 네 파일은 해시 그대로 되돌렸다.
- 시험 도구일 뿐 **제품 코드는 바뀌지 않았다.**
## 여기까지의 하네스 (2026-09-27)
A-1 · A-2 · B-1 · B-2 · B-3 을 넣은 뒤 **30벌 1,830건 통과 · 실패 0**(빌드 경고 0 · 오류 0).
- 새로 만든 것: `TwoWayTest` 29건(B-1) · `AboutTest` 28건(B-2) · `SummaryTest` 25건(B-3)
- 늘어난 것: `FolderTest` 88 → 118건(A-1 +23 · A-2 +7)
- 나머지 26벌은 그대로 통과 — `MergeLine.HasBase` · `BaseNumber` 를 바뀔 수 있는 값으로 바꾼 것이
정렬 · 이동 · 3-Way · 저장 길에 영향을 주지 않았다는 뜻이다(MergeSaveTest 350 · NoteTest 93 · MemberTest 83 포함).