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

115 KiB
Raw Blame History

12차 진행 기록 (2026-09-23 ~)

지시서: ../작업지시/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.100.14초. 그래도 재정렬 뒤 화면 다시 그리기가 0.150.18초 — 다시 재 보니 색칠(CodeTextBlock) 0.02~0.04초 · 선 맞추기 0.0002초이고 나머지는 칸 목록이 줄 요소를 새로 만드는 비용(줄 객체가 모두 새것). VirtualizationMode=Recycling 은 효과가 없어 되돌렸다.
  2. MergeLineCollection.Patch — 번호 말고 모든 값이 같은 줄(MergeLine.SameStateExceptNumbers: 글 · 있음/없음 · 단어 차이 · 반영/직접 수정/주석/무시 · 옮김 · 책갈피 · 블록 주석 · 3-Way 칸 · 비교 규칙)은 기존 객체를 두고 번호만 고치고(TakeNumbersFrom), 가운데만 빼고 넣는다. 가운데가 200줄을 넘으면 통째로. 값이 하나라도 다르면 새 객체 — 옛 객체에 값을 옮겨 적다 빠뜨리는 일이 없게. 재정렬 자리(ReloadLinesFrom)에만 쓰고, 파일을 여는 자리는 통째로. 결과(엔터 한 번): 재정렬 0.370.42초 → **0.0370.05초**, 뒤 다시 그리기 0.150.2초 → **0.0110.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 ②: t01t21 을 차례로 열자 21번째에서 알림 · t01 이 닫히고 t02t21 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 칸 95148 · 잉크 96146(왼 +1 · 오른 +2), 탭 든 줄 speedLimit · 뒤쪽 speed 도 +12 · 세로 칸 안. ② 색: 키워드 · 형 키워드 · 보통 이름 · 주석이 갈리고 여러 줄 주석 둘째 줄이 주석 색을 이어받는다. 콤보로 평문 을 고르면 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 도 +12. ⑧ 어두운 테마 같은 항목 모두. 밝은 · 어두운 모두 실패 0(사용자가 손을 뗀 동안 마지막으로 돌린 판).
  • 흔들림: 사용자가 메시지를 보내던 동안 돌린 세 판은 판마다 다른 자리가 실패했다('7' 이 안 들어감 → Ctrl+Z 가 재료까지 되돌려 0자, 안내 글 항목 등). 흉내 낸 입력에 사용자 입력이 섞인 것으로 보이나 값으로 확정하지 못했다.
  • 끝날 때마다 설정 네 파일 · 메모를 base-0930 과 해시가 같게 되돌림.

최종 회귀 (G 묶음 커밋 전, 사용자가 손을 뗀 동안)

run-final.ps1 — 아홉 벌 + g1g8(g8 셋은 밝은 · 어두운 두 번) 22벌: 통과 423 · 실패 4, 끝에 설정 · 메모 해시 기준본과 같음 · 세션 보관함 0개. 실패 4건은 모두 g8-verify -Dark ④ 상태 글 — 읽힌 글이 같은 글 · 마킹이 있는 자리. 누르면 그 줄로 갑니다(위치 막대 툴팁)였다. 앞 벌(밝은)이 ⑤에서 막대를 누른 자리에 마우스가 남아 툴팁이 떴다. 같은 판의 배경 값(걸면 0% · 풀면 61% · 지우기 "1개")은 맞았다 → 대본 잘못. 상태 글에서 툴팁을 빼고 시작할 때 마우스를 치우게 고쳐 다시 돌림: 마킹 4건 통과. 그 판에서 ⑦ 마지막 항목("위로 돌아온 탭 줄") 한 번이 실패했다 — speedLimit 칸 130237 안 잉크가 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 는 깊이만큼 줄 전체를 오른쪽으로 밀기 때문에, 칸 폭을 고정해도 깊은 줄일수록 가운데 · 오른쪽 칸이 오른쪽으로 밀려 두 목록으로 읽히지 않는다. 그래서 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.

남긴 것

  • 왼쪽 칸은 타이핑으로는 못 고친다. 편집기는 오른쪽에만 겹쳐 있다 - 왼쪽은 [<] 로 반영해서 고친다 (지시서가 정한 것: "칸마다 저장 단추 · 왼쪽 칸에도 반영 단추 [<]").
  • 왼쪽으로 반영한 줄에는 "머지 반영함" 색을 칠하지 않는다. 반영하고 나면 양쪽이 같아져 색이 필요 없다.

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) · 네 줄
무엇이 있었나 ① 경로 두 개 + 최근 ② 빠른 비교 · 하위 폴더 · 보기 · 포함 · 제외 · [비교] · [취소] ③ 폴더 안 찾기 한 벌 ④ 색 구분 범례

비교하기 전에도 네 줄이 그대로 있었고, 자주 쓰는 [비교] 는 옵션 줄 끝에 묻혀 있었다.

고친 것

  • 위 한 줄: […] 왼쪽 경로 ⇆ […] 오른쪽 경로 [최근▾] 켠 옵션 [비교] [취소] [옵션 ▾]
    • 폴더 고르기는 글자 대신 … 로. 경로 칸 바로 앞에 두어 어느 쪽 것인지 헷갈리지 않는다.
    • 좌우 바꾸기(⇆) 를 새로 넣었다(사용자 결정) - 반대로 견주려고 두 번 고르지 않아도 된다.
    • 최근 목록은 화살표만 남겼다.
  • 옵션 서랍: 빠른 비교 · 하위 폴더 포함 · 보기 · 포함 · 제외를 [옵션 ▾] 안으로. [폴더 안 찾기] 도 여기에 뒀다.
  • 켠 옵션 표시: 서랍에 넣으면 "내가 뭘 켜 뒀더라" 를 알 수 없게 된다 - 특히 제외 규칙은 켠 줄 모르고 "왜 이 파일이 안 보이지" 로 이어진다. 기본과 다른 것만 한 줄에 짧게 적는다(FolderOptionText). 기본 그대로면 아무것도 적지 않는다 - 늘 떠 있는 글은 눈에 띄지 않는다.
  • 찾기: 늘 한 줄을 먹던 것을 Ctrl+F 로 부를 때만 편다. Esc · [찾기 접기] 로 접는다.
  • 색 범례: 위에서 빼고 아래 상태 줄로 내렸다 - 색은 목록을 보는 동안 필요한 것이라 결과 요약 옆이 제자리다.

화면으로 확인 (값)

확인 값
위쪽 높이 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 으로 뒀더니 플랫폼 기본 스타일이 스위치 모양이라 같은 줄의 다른 단추와 따로 놀았다. 여느 단추로 바꾸고 누를 때마다 서랍을 여닫게 했다. 최근 목록도 폭을 못 박지 않아 넓게 벌어지던 것을 고쳤다.

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, 사용자 보고)

사용자가 그림으로 짚어 준 것이 이것이었다. 아래 "왼쪽 줄 번호" 건은 이것을 쫓다 따로 찾은 다른 흠이다.

먼저 재현 (화면, 값)

[<] 단추가 보이는 줄만 코드가 오른쪽으로 밀려, 같은 칸 안에서도 줄마다 글 시작 자리가 달랐다.

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" 는 자리까지 없앤다 - 자리를 지키려면 칸 폭이 고정이어야 한다.

  • 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 줄 끝의 [>](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).

아직 확인하지 못한 것

"왼쪽에만 있는 줄을 [<] 로 빼면 왼쪽 줄 수가 하나 준다" 는 것은 값으로 확정하지 못했다. 따로 열어 한 번만 누르면 잘 빠지는데(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 의 여러 사건과 한 자리짜리 약속 (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 를 여는 것만으로 "저장할 것 있음" 이 되어, 손대지도 않은 탭에 점이 붙었다. 늘 붙어 있는 점은 뜻이 없다. "저장하지 않은 편집이 있습니다" 를 묻는 자리(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 포함).