Ubisam-MergeTool/doc/HANDOFF.md
ksj09406 8a3ddf338a 인수인계 문서 추가 - 다음 세션이 먼저 읽을 것
세션을 새로 여는 참에, 다음 사람이 맨땅에서 시작하지 않게 정리한다.
"어디에 무엇이 있는지" 보다 "왜 그렇게 되어 있고 무엇을 건드리면 깨지는지" 를
적었다. 진행상황.md 맨 위에서 이 문서를 가리키게 했다.

담은 것
- 지켜야 할 것: 푸시 금지, 설정 파일을 바이트까지 되돌리기, D:\UbisamConfig
  아래는 Remove-Item 이 막히니 IO.File.Delete 를 쓸 것
- 항목 하나를 끝내는 순서(재현 -> 하네스 -> 빌드 0/0 -> 화면 -> 세 문서 -> 커밋)와
  "검증 안 된 동작 변경은 남기지 않는다"
- 시험 도구가 있는 자리. 새 세션은 scratchpad 경로가 달라지므로 지금 경로를
  그대로 적어 뒀다. 함정도 함께 - .ps1 은 BOM 필요, bash 히어독은 역슬래시를
  먹음, 빌드 전에 시험용 앱을 꺼야 함(안 그러면 옛 빌드로 시험하게 된다)
- 흉내 낸 입력으로 못 하는 것: Alt+Shift 조합(윈도우가 입력 언어 전환으로 채감),
  창 끌기(DragMove), 큰 파일 타이핑 지연. 셋 다 사람 손이 필요하다
- 저장소·배포 지금 상태. 공용 플랫폼에 dist 커밋 하나(028a50b)가 안 올라가 있고,
  그것을 올리면 사내 다른 PC 가 업데이트를 묻는다는 것까지
- 12차 G 의 남은 여섯 항목과 사용자가 이미 정해 준 결정
- G-1 에서 두 번 헛짚은 기록과 교훈(추리하지 말고 기록을 찍어라)
- 코드 지형과 자주 걸리는 것들(되돌리기 두 겹, 덩어리 명령은 CaretRow 를 본다,
  CommunityToolkit 커맨드는 RaiseCanExecuteChanged 에 넣어야 단추가 켜진다)

G-4 를 고치기 직전에 찾은 것도 적어 뒀다. TextFile.KeepOriginal 은 화면에
보이는 글이면서 동시에 세션 보관본에 저장되는 값이라(SessionStore 목록.txt
7·8번째 칸), 문자열만 바꾸면 이미 보관된 세션이 어긋난다. 보여 주는 글과
저장하는 값을 나누는 쪽을 권해 뒀다.

Agent: main (claude-opus-5)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 13:21:23 +09:00

14 KiB

인수인계 — MergeHub (2026-09-30 세션 끝)

다음 세션(다른 AI 또는 사람)이 먼저 읽는 문서. "어디에 무엇이 있는지" 보다 "왜 그렇게 되어 있고, 무엇을 건드리면 깨지는지" 를 적는다.

문서 내용
doc/진행상황.md 차수별 항목과 상태 — 무엇이 남았는지
doc/history/2026-09-23-12차.md 12차 진행 기록 — 항목마다 재현·고침·확인값
doc/history/changelog.md 변경 이력, 최신이 위
doc/작업지시/12차.md 이번 차수 지시서

1. 지켜야 할 것 (어기면 사고)

  • 커밋은 로컬만. 절대 푸시하지 않는다. 올리는 것은 사용자가 한다.
  • 커밋 메시지 끝에 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>, 그 위에 Agent: 이름 (모델).
  • 실제 업무 저장소를 읽지 않는다. 시험 재료는 만들어 쓴다.
  • 화면 시험 전에 떠 있는 창을 확인하고, 사용자 창을 끄기 전에 묻는다.
  • 웹 문서는 차수 끝에 한 번에 — 차수 중간에는 고치거나 올리지 않는다. (11차 F-1~F-11 + G-1 + 12차 전체가 아직 밀려 있다.)
  • D:\UbisamConfig 아래는 하네스가 Remove-Item 을 막는다 → [IO.File]::Delete($f) 로 지운다.
  • HKLM 쓰기 · 셸 확장 설치 · 보안/백신/디스크/디스플레이 설정 변경은 하지 않는다.

설정 파일은 바이트까지 되돌린다

화면 시험은 사용자의 설정을 건드린다. 시험 뒤 반드시 아래 넷을 시작 기준본과 바이트까지 같게 되돌린다(기준본은 scratchpad 의 h8-*.bak).

D:\UbisamConfig\Settings\MergeHubSettings.json
D:\UbisamConfig\Settings\UiSettingsService.json
D:\UbisamConfig\Settings\MergeHub.Recent.txt      ← Settings 아래다. 헷갈리기 쉽다
D:\UbisamConfig\Settings\ShellWindowPlacement.json

보관함도 비운다: D:\UbisamConfig\Memo, D:\UbisamConfig\Session\MergeHub.


2. 항목 하나를 끝내는 순서

이 순서를 줄이지 않는다. 특히 먼저 재현 을 건너뛰면 엉뚱한 것을 고친다 (12차 G-1 에서 두 번 헛짚었다 — 아래 6절).

1) 먼저 재현한다            — 값으로. "그럴 것이다" 로 시작하지 않는다
2) 하네스를 먼저 만든다      — 순수 로직은 scratchpad 의 net8 콘솔 프로젝트로
3) 빌드 0 경고 · 0 오류      — 경고가 나면 고치고 넘어간다
4) 화면으로 확인(UIA)        — 값을 찍는다. "보인다" 가 아니라 "몇 px" 로
5) 세 문서에 적는다          — 차수 기록 · 진행상황 · changelog
6) 로컬 커밋                — 담당 단위로 나눠서

검증 안 된 동작 변경은 남기지 않는다. 만들어 봤는데 증상이 안 없어지면 되돌린다 (G-1 에서 EditorUndoGate 와 재정렬 타이머 고침을 둘 다 되돌렸다).


3. 시험 도구

자리

C:\Users\Ubisam\AppData\Local\Temp\claude\C--Users-Ubisam\8f71cefa-dbf5-4aa8-bcb8-fdb89ccc518f\scratchpad

새 세션은 scratchpad 경로가 달라진다. 위 폴더는 디스크에 그대로 남아 있으니, 새 세션에서 쓰려면 그 경로를 직접 가리키거나 복사해 온다. 안에 든 것:

무엇 것
공용 라이브러리 e2fold.ps1(재료 만들기 · 폴더 비교 · 앱 띄우기), e2lib.ps1(Editor-Text · Set-Editor · Dialog-Window), a2lib.ps1(프로세스 · 창 잡기)
화면 시험 벌 g1-verify g2-verify f1-verify d1-verify e1-verify e2-verify e2-reg e2-start e3-verify e4-verify c3-verify
몰이 대본 run-all-merge.ps1(아홉 벌 차례로 · 앞 벌 뒤처리까지)
하네스 EditorTest ShortcutTest DocTabTest MemoTest FolderTest SessionTest PlatformCfgTest 등 31개
설정 기준본 h8-*.bak (세션 시작 상태)

하네스는 net8 콘솔이고 .csproj 에서 MergeHub 의 .cs 를 직접 가리켜 컴파일한다. 새 파일을 시험하려면 그 .csproj 에 <Compile Include="D:\Sorce\...\X.cs" /> 를 더한다.

반드시 아는 함정

  • .ps1 은 UTF-8 BOM 으로 저장한다. Write 도구나 bash 히어독은 BOM 을 안 붙이고, PowerShell 5.1 은 그러면 한글을 ANSI 로 읽어 문법 오류로 죽는다. 쓴 뒤 붙일 것.
  • bash 히어독은 역슬래시를 먹는다. Python 편집 대본은 Write 도구로 만들고 raw 문자열(r'...')을 쓴다.
  • 빌드 전에 시험용 앱을 반드시 끈다. 떠 있으면 dll 이 잠겨 빌드가 조용히 실패하고, 옛 빌드로 시험하게 된다(실제로 겪었다).
  • PowerShell 함수 이름이 기본 별칭과 부딪힌다 — Type 은 Get-Content 다. Send-Text 처럼 짓는다.
  • 하네스 변수 $keep 은 라이브러리 $Keep 을 가린다. 다른 이름을 쓴다.

흉내 낸 입력으로 못 하는 것 (도구 한계 · 사람 손이 필요)

무엇 왜
Alt+Shift+← · Alt+Shift+→ 윈도우가 입력 언어 전환으로 먼저 채간다. SendKeys 는 아예 안 닿고 keybd_event 는 Shift 가 빠져 Alt+← 처럼 동작한다. 기존 Alt+Shift+→ 로 견줘도 같아 도구 한계임을 확인했다 → 단추로 우회해 확인한다
창 끌기(DragMove) 흉내 낸 마우스로는 WPF 가 안 움직인다. 기존 헤더 끌기로 견줘도 같다
큰 파일 타이핑 지연 UIA 로 읽는 값이 타이핑보다 비싸 잴 수 없다

UIA 참고: Border 와 맨 ItemsControl 은 컨트롤로 안 잡힌다(ListBox 는 잡힌다).


4. 저장소 · 배포 지금 상태

무엇 자리 상태
MergeHub D:\Sorce\MergeTool\MergeHub 깨끗 · HEAD a86dc15 · 푸시 안 함
공용 플랫폼 D:\Sorce\UbisamCommonPlatform 깨끗 · 028a50b 한 커밋이 안 올라감(dist 배포본)
옛 플랫폼 사본 D:\Sorce\ubsrepo 사용자가 지웠다. 플랫폼은 공용 저장소에서만 고친다
배포 폴더 D:\UbisamPlatform\bin Release 51개 · 되돌릴 자리 bin-backup-merge · bin-backup-c3

플랫폼은 빌드하는 순간 D:\UbisamPlatform\bin 에 반영된다. 시험 빌드로 덮지 않으려면 -p:UbisamPublishDir=<딴 자리>\ 로 돌린다(명령줄 전역 속성이 PropertyGroup 보다 세다).

028a50b 를 올리면 사내 다른 PC 가 다음 실행 때 업데이트를 묻는다. 앞의 세 커밋은 dist 를 안 건드려 조용했지만 이것은 다르다. 올리기 전에 Release 빌드 위에서 화면 회귀를 한 번 더 돌리기를 권했고, 사용자는 아직 답하지 않았다.

공용 플랫폼 저장소는 일하는 방식이 다르다 — 그 CLAUDE.md·doc/HANDOFF.md 가 "토큰을 아낀다, 앱을 띄워 클릭·캡처하는 검증 루프를 쓰지 않는다, 화면 확인은 사용자에게 한 줄로 부탁한다" 고 못 박았다. 거기서는 UIA 회귀를 돌리지 않는다.


5. 지금 하던 일 (12차 G)

사용자가 준 다섯 가지 중 둘은 끝났고 셋이 남았다. 그 뒤 네 가지가 더 들어왔다.

끝난 것

G-1 undo 가 계속 이어진다 LocalEditor 에 IsUndoEnabled="False" 한 줄. 커밋 b95a07d
G-2 base 머지 단축키 Alt+← 한 줄 왼쪽으로 · Alt+Shift+← 구간 왼쪽으로 · [이 덩어리 왼쪽으로] 단추. 커밋 a86dc15

남은 것 — 결정은 다 받아 뒀다

# 무엇 사용자가 정한 것
G-3 "덩어리" 라는 말이 어색하다 구간 반영 · 구간 왼쪽으로 로 바꾼다
G-4 저장 형식 칸에 원본 유지 콤보가 둘 이라 구별이 안 된다 글자를 다르게 — 인코딩 쪽과 줄바꿈 쪽 문구를 갈라 적는다
G-5 사내 문의처가 배포본에 안 따라간다 csproj 에 받침값을 박는다. ⚠ 문구를 아직 못 받았다 — 물어야 한다
G-6 프로그램 개요 탭 읽기 전용 안내 화면(개요 · 만든사람 · 문의처 · 버전)
G-7 Alt+↓ 로 이동하다 보면 화면이 잘린다 아직 재현 안 함
G-8 메모 탭에 문법 강조 + 마킹 RichTextBox 로 바꾸고 기존 CSharpTokenizer 재사용. 마킹 셋 다: 고른 글과 같은 글 모두 칠하기 · 색 골라 여러 개 · 오른쪽 끝 위치 막대

G-3 · G-4 를 고칠 자리 (이미 찾아 뒀다)

파일 줄 무엇
Views/HomeView.xaml 1783 · 1785 이 덩어리 반영 단추 글 · 툴팁
Views/HomeView.xaml 1789 · 1793 이 덩어리 왼쪽으로 단추 글 · 툴팁
Services/ShortcutMap.cs 132 · 142 단축키 설명 두 줄
ViewModels/HomeViewModel.cs ApplyGroup(..., what: "덩어리") · ApplyGroupToBase(block, "덩어리") 로그 문구에 들어간다
Services/TextFile.cs 57 · 60 · 63 KeepOriginal = "원본 유지" 와 두 목록

⚠ G-4 를 고치기 직전에 찾은 것 — 그냥 바꾸면 안 된다

TextFile.KeepOriginal 은 화면에 보이는 글이면서 동시에 저장되는 값이다.

  • HomeViewModel.SaveEncoding · SaveNewLine 의 기본값이 이 문자열이고,
  • 그 값이 세션 보관본에 그대로 쓰인다 — SessionStore.cs:165-166(쓰기), :212-213(읽기), 목록.txt 의 7·8번째 칸.

그래서 문자열만 바꾸면 이미 보관된 세션의 값이 목록에 없는 값이 되어 되살릴 때 어긋난다. 갈 수 있는 길:

  1. 보여 주는 글과 저장하는 값을 나눈다 — 목록은 (값, 보일 글) 짝으로 하고 콤보에 DisplayMemberPath 를 준다. 저장되는 것은 값 그대로라 옛 보관본이 산다. (권함)
  2. 문자열을 바꾸고 SessionStore 읽을 때 옛 값("원본 유지")을 새 값으로 옮긴다.

어느 쪽이든 세션 되살리기 회귀(e1-verify)를 반드시 돌린다.


6. 12차 G-1 에서 배운 것 (같은 실수 반복 금지)

undo 가 계속 이어진다 를 잡는 데 오래 걸렸다. 두 번 헛짚었다.

짚었던 것 어떻게 아니라고 알았나
"문서가 바뀔 때 TextBox 기록을 비우면 된다" — EditorUndoGate 를 만들어 하네스 16건까지 붙였다 앞 파일로 넘어가는 것은 막았지만 오가는 것은 그대로였다
"재정렬 타이머가 되돌린 뒤 뒤늦게 터진다" 타이머를 꺼도 그대로였다

결국 답을 준 것은 기록(trace)이다. 코드를 읽어 추리하지 말고, Undo · Reflow · SyncEditorFromLines 에 한 줄씩 찍어 고리를 떴더니 바로 보였다. 임시 기록은 환경 변수로 켜고(MERGEHUB_G1_TRACE) 끝나면 뺐다.

교훈 셋:

  1. 재현 대본이 고치기 전에 실패하는지 먼저 확인한다. 첫 g1-verify 는 고치기 전에도 통과했다 — 재현을 못 하고 있었던 것이다.
  2. Set-Editor(UIA 로 통째로 넣기)와 사람이 치는 것은 기록이 다르다.
  3. 증상이 여러 겹이면 하나씩 떼어 본다(글자 없이 · 글자 치고).

7. 코드 지형 (자주 가는 곳)

src/MergeHub/
├─ AppSetup.cs              탭 등록(AddMain 순서 = 화면 순서) · ViewIds
├─ MergeHubSettings.cs      설정 항목([DisplayName] 이 곧 설정 화면 글)
├─ Models/MergeLine.cs      줄 하나(BaseText/LocalText/RemoteText · Has* · CanTake*)
├─ Services/
│   ├─ ShortcutMap.cs       단축키 표 — 키 처리와 도움말 창이 <b>둘 다 이것만</b> 본다
│   ├─ TextFile.cs          인코딩 · 줄바꿈 · 저장
│   ├─ DocTabs.cs           문서 탭(12차 E-2) · 보관함(attic)
│   ├─ SessionStore.cs      껐다 켜도 남는 편집(12차 E-1)
│   ├─ Memos.cs · MemoStore 메모 탭(12차 F-1)
│   └─ CodeTokenizer.cs     문법 강조 — 메모에 쓸 재료
├─ ViewModels/
│   ├─ HomeViewModel.cs         4천 줄. 줄 편집 · 되돌리기 · 저장
│   ├─ HomeViewModel.Tabs.cs    문서 탭 · 세션
│   └─ MemoViewModel.cs
└─ Views/
    ├─ HomeView.xaml(.cs)   파일 비교. LocalEditor 는 맨 TextBox(글자 투명 + 아래층에 색칠)
    ├─ Controls/CodeTextBlock.cs · CSharpTokenizer.cs   문법 강조(보여주기 전용)
    └─ MemoView.xaml(.cs)   메모 탭. 지금은 맨 TextBox

꼭 아는 것

  • 되돌리기는 두 겹이다 — _undo(줄 단위 LineEdit)와 _textUndo(글 전문, 타이핑이 멈출 때마다 한 걸음). FlattenHistory() 가 줄 단위를 글 단위로 옮긴다. ClearHistory() 는 저장 직후에도 불린다 — "파일이 바뀌었다" 의 신호로 쓰면 안 된다.
  • 덩어리 명령은 고른 줄이 아니라 CaretRow(편집기 캐럿)를 본다.
  • CommunityToolkit 커맨드는 CommandManager 를 구독하지 않는다. 새 명령을 만들면 RaiseCanExecuteChanged() 와 (캐럿을 보는 것이면) OnCaretRowChanged 에 꼭 넣는다. 안 넣으면 단추가 꺼진 채로 남는다(G-2 에서 겪었다).
  • 편집기 글자는 투명이다. 색은 아래층이 그린다 — 그래서 "한참 뒤에 반응한다" 가 곧 "아무것도 안 보인다" 가 된다.
  • 만든 사람은 설정이 비면 csproj 의 <Company> 로 받쳐지지만(ProgramInfo.Prefer), 문의처는 받칠 것이 없다(Prefer(contact, null)) — 그래서 G-5 가 나왔다.

8. 사용자와 일하는 방식

  • 한국어로 답한다. 코드 주석도 한국어이고 "무엇을" 이 아니라 "왜" 를 적는다.
  • 사용자는 항목을 하나씩 준다(X 진행해줘). 여러 개를 한꺼번에 주면 순서를 제안하고 하나씩 끝낸다.
  • 갈리는 결정(문구 · 단축키 · 화면 모양)은 묻는다. 사용자가 골라 주면 그대로 간다.
  • 추측으로 원인을 말하지 않는다. 값으로 확인한 뒤 말하고, 못 했으면 "값으로 확정하지 못했다" 고 분명히 적는다(문서와 커밋 메시지 양쪽에).
  • 헛짚은 것도 기록에 남긴다 — 다음 사람이 같은 길을 또 파지 않게.