인수인계 문서 추가 - 다음 세션이 먼저 읽을 것

세션을 새로 여는 참에, 다음 사람이 맨땅에서 시작하지 않게 정리한다.
"어디에 무엇이 있는지" 보다 "왜 그렇게 되어 있고 무엇을 건드리면 깨지는지" 를
적었다. 진행상황.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>
This commit is contained in:
김승준 2026-09-30 13:21:23 +09:00
parent a86dc1596f
commit 8a3ddf338a
2 changed files with 253 additions and 0 deletions

250
doc/HANDOFF.md Normal file
View File

@ -0,0 +1,250 @@
# 인수인계 — 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 진행해줘`). 여러 개를 한꺼번에 주면 **순서를 제안하고**
하나씩 끝낸다.
- 갈리는 결정(문구 · 단축키 · 화면 모양)은 **묻는다.** 사용자가 골라 주면 그대로 간다.
- **추측으로 원인을 말하지 않는다.** 값으로 확인한 뒤 말하고, 못 했으면
**"값으로 확정하지 못했다"** 고 분명히 적는다(문서와 커밋 메시지 양쪽에).
- 헛짚은 것도 기록에 남긴다 — 다음 사람이 같은 길을 또 파지 않게.

View File

@ -1,5 +1,8 @@
# MergeHub 진행 상황
> **이어받는 사람은 [HANDOFF.md](HANDOFF.md) 를 먼저 읽는다** — 지켜야 할 것,
> 항목 하나를 끝내는 순서, 시험 도구가 있는 자리와 함정, 지금 하던 일이 적혀 있다.
**지금 무엇이 남았는지만** 적는다. 끝난 것의 상세는 [history/](history/) 로 옮겼다.
항목을 끝내면 이렇게 한다.