Ubisam-MergeTool/doc/history/2026-09-18-11차.md
2026-09-23 09:55:11 +09:00

1055 lines
120 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.

# 11차 진행 기록 (2026-09-18 ~)
지시서: [../작업지시/11차.md](../작업지시/11차.md)
## A-1. 변경분 추출을 먼저 뽑아 보고 적는다 (2026-09-18, 완료 · 고치지 않음)
지시서대로 **고치지 않고 적기만 했다.** 제품 코드는 바꾸지 않았다.
### 무엇으로 했나
- `C1Repo` 로 저장소를 새로 만든 뒤 `e11-a1-setup.sh` 로 추출 시험용 커밋을 더 쌓았다(전부 지어낸 파일).
| 커밋 | 무엇 |
|---|---|
| `4eb9ece` base | 내 쪽 작업 (C1Repo 의 feature/mine) |
| `d3eb15b` c1 | 새 파일 `Extract/Temp.cs` · `Io/Unit12.cs` 한 줄 더함 |
| `f884ed1` c2 | `Recipe/Unit11.cs` 지움 · `Motion/Unit16.cs` → `Motion/Renamed16.cs`(내용 그대로) · 이진 `Extract/logo.bin` · `한글폴더/깊은/경로/아래/설명.cs` |
| `cbcad8a` c3 | `Io/Unit12.cs` 를 c1 전으로 되돌림 · `Extract/Temp.cs` 지움 (범위 안에서 생겼다 사라짐) |
| `f702996` side · `58d5f7f` merge | base 에서 갈라진 옆가지에 `Side/SideOnly.cs`, 그것을 `--no-ff` 로 병합 |
- 화면은 사람 순서로: Git 탭 → 이력에서 줄을 **마우스로** 고르고 [여기부터] · [해제] · [변경분 추출], 폴더 창에는 경로를 쳐 넣고 Enter.
- 뽑은 폴더는 `git diff -M --name-status` 의 기대 목록과 견주고, 파일마다 `git hash-object --no-filters` 로 **바이트(blob 해시)** 를 대조했다.
### 아홉 경우의 결과
| # | 경우 | 알림 | 뽑힌 것 | git 과 견주면 |
|---|---|---|---|---|
| 1 | 커밋 하나(시작점 없음, c1) | "2개 파일 추출" | `Extract/Temp.cs` · `Io/Unit12.cs` | **같음** · 바이트 같음 |
| 2 | 범위 [여기부터] base → c3 | "3개 파일 추출 (삭제되어 건너뜀 1개)" | `Extract/logo.bin` · `Motion/Renamed16.cs` · `한글폴더/…/설명.cs` | **같음** — 범위 안에서 생겼다 사라진 `Temp.cs`, 바뀌었다 되돌아온 `Unit12.cs` 는 **들어가지 않는다**(맞다) |
| 3 | 지운 파일이 있는 범위 | 위와 같은 알림 | — | 건너뛴 **개수만** 알린다. `Recipe/Unit11.cs` 라는 것은 **화면 · 로그 · 폴더 어디에도 없다** |
| 4 | 이름 바뀐 파일 | — | `Motion/Renamed16.cs` (새 이름) | 새 경로로 바이트 그대로. **옛 경로(`Motion/Unit16.cs`)를 알 길이 없다** — 장비에 덮어 넣으면 옛 파일이 남는다 |
| 5 | 병합 커밋 하나(merge) | "1개 파일 추출" | `Side/SideOnly.cs` | **첫 부모(c3)와만** 견준다. 둘째 부모(옆가지)와 견주면 4개인데, 지금 무엇과 견주는지는 화면에 없다 |
| 6 | 거꾸로 범위 [여기부터] c3 → base | "2개 파일 추출 (삭제되어 건너뜀 2개)" | `Motion/Unit16.cs` · `Recipe/Unit11.cs` | git 의 `diff c3 base` 와 **같음**(되돌리는 방향으로 동작). **경고나 방향 표시는 없다** |
| 7 | 이진 · 한글 · 깊은 경로 | 2 와 같음 | `logo.bin` 278B · `설명.cs` 50B | **바이트 · 경로 그대로** |
| 8 | 이미 파일이 있는 폴더 | "3개 파일 추출 (삭제되어 건너뜀 1개)" | 폴더 안 **6개** | 묻지 않고 덮어쓴다. 앞서 뽑아 둔 `Extract/Temp.cs` · `Io/Unit12.cs`(c1 것)와 사람이 넣어 둔 `사용자메모.txt` 가 **그대로 남아 섞인다** |
| 9-a | 대상 파일이 읽기 전용 | **"변경분 추출 실패: … 액세스가 거부되었습니다"** | 2개(logo.bin + 읽기 전용 그대로) | **반쯤 뽑힌 채 멈춘다** — 앞 순서 `logo.bin` 은 써졌고 뒤 순서 `한글…설명.cs` 는 빠졌다. 몇 개까지 됐는지는 알림에 없다 |
| 9-b | 대상 파일이 다른 프로그램에 열려 있음 | **"… 다른 프로세스에서 사용 중이므로 …"** | 2개 | 9-a 와 같은 모양 |
- 알림과 같은 글이 Git 탭 로그에도 남는다 — 성공은 `변경분 추출: N개 파일 → 경로 (삭제되어 건너뜀 M개)`, 실패는 `[변경분 추출] 실패: …`.
- 범위 표시는 제대로 바뀐다: 시작점이 없으면 `부모 → 고른 커밋`, [여기부터] 를 누르면 `4eb9ece → 고른 커밋`.
- 폴더 창은 늘 **저장소가 있는 폴더**에서 열린다.
- 저장소 워킹트리는 아홉 경우 내내 Clean 이었다(추출은 읽기만 한다).
### 이걸로 확정된 것 (A-2 ~ A-4 로 넘긴다)
| 번호 | 확정된 문제 | 어디로 |
|---|---|---|
| ① | 지운 파일 · 이름 바뀐 옛 경로가 결과에 남지 않는다(경우 3 · 4) | **A-2** |
| ② | 무엇과 무엇 사이를 뽑았는지 폴더에 남지 않는다(경우 5 · 6 — 병합 커밋은 첫 부모, 거꾸로 범위도 말없이 됨) | **A-2** |
| ③ | 비어 있지 않은 폴더에 묻지 않고 덮어쓰고 옛 파일이 섞인다(경우 8) | **A-3** |
| ④ | 쓰다 막히면 반쯤 뽑힌 채 멈추고, 어디까지 됐는지 알 수 없다(경우 9-a · 9-b) | **A-3 에 덧** — 먼저 쓸 수 있는지 보고 시작할지, 실패해도 나머지를 계속할지 정한다 |
**맞게 도는 것**(고칠 것 없음): 파일 고르기(1 · 2 · 5 · 6 모두 git 과 같은 목록) · 바이트 보존(이진 · 한글 · 깊은 경로 포함) · 범위 표시 · 로그.
### 남긴 것 (시험 도구 쪽, 제품 아님)
화면 자동 조작에서 세 번 막혔다. 모두 스크립트 문제였고 제품과 무관하다.
- 함수 이름 `History` 가 PowerShell 기본 별칭(`history` = `Get-History`)과 겹쳐, 내 함수 대신 그것이 불렸다 → `GoHistory` 로 바꿈.
- 폴더 창(`#32770`)은 데스크톱 바로 아래가 아니라 **본 창 아래**에 붙는다 → 본 창의 자손에서 찾게 고침.
- 폴더 창의 경로 칸(`id 1152`)과 [이 폴더 선택](`id 1`)은 UIA 에서 **Pane** 으로 보이고, 같은 id 를 가진 다른 칸이 먼저 잡혔다 → id + 클래스(`Edit` · `Button`)로 고르고, 경로를 한 글자씩 친 뒤 Enter 로 닫게 함.
- 백그라운드로 돌리면 앱 창이 앞으로 나오지 못해 마우스가 다른 창을 누른다 → **앞에서** 돌린다.
## A-2. 뽑은 폴더 옆에 "무엇을 뽑았는지" 안내를 남긴다 (2026-09-18, 완료)
사용자 결정(2026-09-18): 안내는 **뽑은 폴더 밖(옆)** 에 두고, 장비에서 지울 파일은 **목록으로만** 적는다(지우는 배치 파일은 만들지 않는다).
### 왜
A-1 에서 값으로 확인한 것 — 지운 파일은 "삭제되어 건너뜀 1개" 라고 개수만 알리고 **어느 파일인지 아무 데도 없었다**. 이름이 바뀐 파일은 새 이름만 뽑혀, 그 폴더를 장비에 덮어 넣으면 **옛 파일이 그대로 남는다.** 무엇과 무엇 사이를 뽑았는지도 폴더만 보고는 알 수 없었다.
### 고친 것
| 자리 | 한 일 |
|---|---|
| `Services/ExportReport.cs` (새 파일) | 안내 글을 만든다. 머리글(만든 때 · 만든 사람 · 저장소 · 뽑은 곳 · 범위 두 줄) · **뽑은 파일 목록**(추가 · 수정 · 이름 바뀜은 옛 이름을 괄호로) · **장비에서 지워야 할 파일 목록**(지워진 파일 · 이름 바뀐 옛 경로). `PathFor` 가 폴더 옆 이름을 정한다 — `…\변경분` → `…\변경분_변경분안내.txt`, 드라이브 뿌리처럼 옆자리가 없으면 폴더 안에 |
| `Services/GitService.cs` | `ExportChangedFiles` 가 `(뽑은 수, 건너뛴 수, **무엇을 어떻게 했는지 목록**)` 을 돌려준다. git 의 상태 글자(Added · Renamed · Copied …)를 안내 글의 말로 옮기고, 이름이 바뀐 파일은 옛 경로를 함께 들고 온다 |
| `Services/IGitService.cs` | 같은 서명으로 |
| `ViewModels/GitViewModel.cs` | 뽑은 뒤 안내를 폴더 옆에 쓴다(`TextFile.Write`). 범위 줄은 이력에 있는 커밋이면 **제목과 날짜까지**, 시작점을 안 잡았으면 "(고른 커밋의 부모)" 를 붙인다. 안내 파일 경로를 로그에 남기고, 알림도 "지워야 할 파일 N개는 안내에" 로 바꿈. **안내를 못 써도 추출 자체는 실패로 만들지 않는다**(파일은 이미 다 뽑힌 뒤다) |
| 하네스 `ReportTest` | **+14건(31 → 45)** — 머리글 · 범위 두 줄 · 요약 수 · 이름 바뀜 표기 · 지울 목록 · 지운 파일이 뽑은 목록에 없음 · 한글 경로 · 빈 목록 · 만든 사람 없음 · 폴더 옆 이름 세 가지 |
### 확인한 것 — 화면에서 뽑아 git 과 대조
| 경우 | 안내에 적힌 것 | git `diff -M --name-status` |
|---|---|---|
| 범위 `4eb9ece` → `cbcad8a` | 뽑은 3개(추가 `Extract/logo.bin` · **이름 바뀜** `Motion/Renamed16.cs (옛 이름 Motion/Unit16.cs)` · 추가 `한글폴더/깊은/경로/아래/설명.cs`) · **지울 2개**(옛 이름 `Motion/Unit16.cs` · 지워짐 `Recipe/Unit11.cs`) | `A logo.bin` · `R100 Unit16.cs → Renamed16.cs` · `D Unit11.cs` · `A 설명.cs` — **같음** |
| 병합 커밋(시작점 없음) | 범위 `cbcad8a … (고른 커밋의 부모)` → `58d5f7f …` · 뽑은 1개 `Side/SideOnly.cs` · 지울 0개 | 같음 |
- 안내 파일은 폴더 **옆**에 생긴다(`note2b_변경분안내.txt` · `note5_변경분안내.txt`). 폴더 안에는 뽑은 파일만 있다.
- 알림: "3개 파일 추출 (지워야 할 파일 1개는 안내에)" · "1개 파일 추출 · 안내 note5_변경분안내.txt". Git 탭 로그에 안내 경로도 남는다.
- 하네스 22벌 **1,173건** 통과(1,159 → +14) · 빌드 경고 0 · 오류 0.
### 남긴 것 (시험 도구 쪽)
- 화면 잠금이 걸린 동안에는 마우스가 데스크톱에 닿지 않는다(커서 좌표가 0,0 으로 돌아가고 화면 캡처가 실패한다). 잠긴 줄 모르고 "클릭이 안 먹는다" 로 두 번 헤맸다 — 앞으로는 커서 자리와 맨 앞 창부터 본다.
- 목록에서 스크롤이 필요한 줄은 **누를 때마다 자리를 다시 읽어야** 한다. 옛 자리로 누르면 조용히 빗나가고, 그 뒤 [여기부터] 가 엉뚱한 커밋(HEAD)으로 잡혀 범위가 거꾸로 됐다.
- PowerShell 변수는 대소문자를 안 가린다 — `$S`(경로)와 `$s`(파일 글)가 겹쳐 경로가 깨졌다(9차 A-2 와 같은 종류).
## A-3. 이미 파일이 있는 폴더 · 쓰다 막힐 때 (2026-09-18, 완료)
사용자 결정(2026-09-18): **고른 폴더 아래 새 폴더를 만든다** · 쓰다 막히면 **먼저 보고 시작한다**(하나라도 못 쓰면 아무것도 쓰지 않는다).
### 고친 것
| 자리 | 한 일 |
|---|---|
| `Services/ExportPlan.cs` (새 파일) | `FolderName` — `변경분_4eb9ece..cbcad8a_2026-09-18` 처럼 범위와 날짜가 들어간 이름. `UniquePath` — 같은 이름이 있으면 `_2` · `_3`. `Blocked` — 쓰기 전에 읽기 전용 · 다른 프로그램이 잡고 있는 파일을 걸러 목록으로. `BlockedMessage` — 사람에게 보일 글(앞 다섯 개 + "외 N개") |
| `Services/GitService.cs` | 뽑기 전에 `ExportPlan.Blocked` 로 **먼저 본다**. 하나라도 막히면 폴더도 만들지 않고 `IOException` 으로 멈춘다 — 예전에는 앞 순서만 써진 반쯤 뽑힌 폴더가 남았다(A-1 경우 9) |
| `ViewModels/GitViewModel.cs` | 고른 폴더에 바로 쏟지 않고 **그 아래 새 폴더**를 만들어 거기에 뽑는다. 안내 글은 그 폴더 옆(= 고른 폴더 안)에 붙는다. 알림도 폴더 이름을 앞에 — "변경분_4eb9ece..cbcad8a_2026-09-18 에 3개 (지워야 할 파일 1개는 안내에)" |
| 하네스 `ReportTest` | **+12건(45 → 57)** — 폴더 이름 · `_2` · `_3` · 읽기 전용은 막힘 · 아직 없는 파일은 막힘 아님 · 열려 있는 파일은 막힘 · 보일 글의 개수와 줄임 |
### 확인한 것 — 화면에서
이미 파일이 있는 폴더(사람이 넣어 둔 `사용자메모.txt` · 옛 폴더 `옛자료`)를 골라 같은 범위를 **두 번** 뽑았다.
| 때 | 결과 |
|---|---|
| 첫 번째 | `…\a3\변경분_4eb9ece..cbcad8a_2026-09-18` 에 3개 · 안내는 그 옆에 |
| 두 번째 | `…_2` 폴더가 새로 생기고 거기에 3개 · 안내도 `…_2_변경분안내.txt` |
| 고른 폴더 안 | 새 폴더 둘 · 안내 둘 · **사용자메모.txt 와 옛자료 그대로** — 섞이지 않았다 |
- **먼저 보기**(`Blocked`)는 하네스 12건으로 확인했다. 화면에서는 재현되지 않는데, 뽑을 때마다 **비어 있는 새 폴더**를 만들기 때문이다 — A-1 경우 9(반쯤 뽑힌 폴더)가 애초에 생기지 않는다. 남겨 둔 까닭은 폴더가 이미 있는 드문 경우(다른 프로그램이 같은 이름을 방금 만든 경우)와, 나중에 폴더를 다시 쓰게 고칠 때를 위한 것이다.
- 하네스 22벌 **1,185건** 통과(1,173 → +12) · 빌드 경고 0 · 오류 0.
### 남은 것
- A-4(ZIP)는 아래(09-22).
- 매뉴얼 · 웹 문서의 변경분 추출 설명(새 폴더 · 안내 파일)은 E-2 에서 함께 고쳤다.
## A-4. 변경분을 ZIP 으로도 (2026-09-22, 완료)
사용자 결정: **폴더에 더해 ZIP 도**(권장안). 장비에는 폴더를 넣고, 메일 · 메신저로 보낼 때 ZIP 을 쓴다.
### 만든 것
| 자리 | 한 일 |
|---|---|
| `Services/ExportZip.cs` (새) | `PathFor` — 뽑은 폴더 옆, 폴더 이름 + `.zip`. `Create` — 안에 **뽑은 폴더와 안내 파일을 나란히** 넣는다(풀면 폴더로 뽑은 것과 같은 모양, 안내가 소스 틈에 섞이지 않는 A-2 결정 그대로). 파일은 바이트 그대로 · 경로는 `/` · 수정 시각 그대로. `.part` 이름에 쓰고 다 되면 옮겨, 쓰다 막혀도 반쯤 된 ZIP 이 남지 않는다 |
| `Services/ExportPlan.cs` | `UniquePath` 가 같은 이름의 `.zip` 이 남아 있어도 그 이름을 쓴 것으로 본다 — 폴더만 지우고 ZIP 을 남긴 채 다시 뽑으면 옛 ZIP 을 덮을 뻔했다 |
| `ViewModels/GitViewModel.cs` | 폴더 · 안내를 쓴 뒤 ZIP 을 만든다. 못 만들어도 폴더는 이미 다 뽑혔으므로 추출은 성공(안내와 같은 규칙), 로그에 까닭. 알림 끝에 "· ZIP 도" / "· ZIP 은 못 만듦(로그)" |
| `MergeHub.csproj` | `System.IO.Compression` 참조 — .NET Framework 에 들어 있어 배포 묶음 파일이 늘지 않는다(빌드 출력에 새 dll 없음) |
| 하네스 `ReportTest` | **+13건(57 → 70)** — ZIP 자리 · 넣은 수 · 풀면 같은 이름 폴더 · 경로 · 해시 목록이 폴더와 같음 · 안내는 폴더 밖 · 안내 바이트 · 빈 파일 · `/` 경로 · 한글 경로 · 한글 이름 UTF-8 표시(bit 11) · 같은 이름 ZIP 이 있으면 `_2`(폴더가 없어도). 고치기 전에 넣어 "폴더 없이 ZIP 만 남은 경우" 가 실패하는 것을 보고 시작 |
### 확인 — 화면에서 아홉 경우 다시 (`e11-a1-setup.sh` · `e11-e1-a.ps1` · `e11-a4-verify.sh`)
- 모든 경우 알림 끝에 **"· ZIP 도"**. 새 폴더 8개마다 옆에 ZIP.
- ZIP 을 앱과 **다른 도구**(Windows `tar.exe`)로 풀어 뽑은 폴더와 견줌 — **8/8 경로 · SHA-256 목록 같음**, 안내 파일 바이트 같음 · 폴더 밖.
- 앱(.NET Framework 4.8)이 만든 ZIP 의 이름 칸 — `변경분_…/한글폴더/깊은/경로/아래/설명.cs` 까지 모두 **UTF-8 표시(bit 11) 켜짐**. 하네스는 .NET 8 에서 돌므로 이 표시는 실제 앱이 만든 파일에서 따로 확인했다.
- 하네스 22벌 **1,260건**(1,247 → +13) · 빌드 경고 0 · 오류 0 · 설정 해시 같음.
### 문서
- 웹 문서(버전 14): 매뉴얼 "변경분 추출" 에 ZIP 한 단계, 남은 일 A-4 완료, "기획안에서 아직 안 한 것" 에서 ZIP 뺌, 기획안 v2 표 변경분 추출 완료, 숫자 1,260건 · 하네스 막대 폭 다시 잼.
- 기획안: 변경분 파일 추출 "일부" → **완료**, Git 작업 버튼 줄 완료, 1,260건. 8쪽 A4 높이 그대로. 원문(`plan/mergetool.html`, 비교 원본 `mergetool-0922.html`)만 고쳐 두었다.
- **웹 문서 · 기획안은 11차가 끝날 때 한 번에** 반영 · 잠금 · 발행한다(2026-09-22 사용자: "11차 끝내기 전에는 웹문서 업데이트하지마", "11차 끝나면 한번에 잠글게"). 버전 14 는 그 말 전에 올라간 것이라 그대로 둔다.
- ZIP 안이 텍스트로 바뀌는지 물음이 있어 앱이 만든 ZIP 두 개를 풀어 git 의 그 커밋과 파일마다 대조 — `.cs` · `.bin` 이 원래 이름 · 확장자 · 폴더 · BOM · 줄끝 그대로 **바이트 같음**. 텍스트는 폴더 옆 안내 파일 한 장뿐.
### 11차 끝에 웹 문서에 넣을 것 (모아 둠)
- 개요 "로컬 커밋" 숫자(버전 14 는 129).
- 이 뒤 11차 항목(F 등)에서 바뀌는 화면 · 설정 · 숫자.
- F-1: 칸 이름 `LOCAL(Mine)` · `REMOTE(Theirs)`, 칸 머리 "어느 쪽 · 어느 파일"(MERGED · 두 칸 LOCAL 은 "(저장할 파일)", mergetool 은 임시 파일 이름), 긴 이름은 가운데 줄임 · 툴팁. 머지 주석 모양 `[사람] 날짜 출처 설명`(출처 = 로그의 말 — Theirs(…) · Mine(…) · REMOTE(…) · BASE(…)), **Take Mine 도 주석**, 두 칸 로그 `BASE(해시) 반영` · 브랜치 3-Way 로그 `REMOTE(브랜치) 반영`. 매뉴얼의 주석 예시 · 칸 설명 · 해결 기록이 남는 곳 표. 숫자 1,312건.
- F-2: [이 메서드 반영](C# · C/C++, 클래스 멤버 통째, 단추 아래 한 줄에 잡은 범위 · 못 잡은 까닭), 덩어리 · 메서드 반영은 주석 한 줄 · 로그 한 줄 · Undo 한 번. 매뉴얼의 반영 단추 설명에. 숫자 23벌 1,395건.
- F-3: 매뉴얼 "탐색기에서 열기" — 여러 개를 골라 보내면 앞의 둘(3개 이상은 알림), 폴더 둘은 바로 폴더 비교, 260자 넘는 경로는 열지 못함(알림), 배포 묶음을 옮겨도 켜면 보내기 바로가기가 따라옴, 설치 파일 필요 없음. 알아 둘 한계에 "떠 있어도 새 창". 숫자 23벌 1,411건.
- F-4: 매뉴얼 파일 비교 화면 — [ACTIONS 감추기] · F9, 아래 탭 줄 [탭 접기] · 띠 · F11(플랫폼), 단축키 표에 F9 · F11. 숫자 23벌 1,414건 · 플랫폼 31건.
- F-5: 매뉴얼 설정 표 "보기 > 코드 글자 크기", 파일 비교 화면 Ctrl+휠 · Ctrl++/- · Ctrl+0 · 상태 줄 `글자 N`, 단축키 표. 숫자 24벌 1,538건.
- F-6: 줄 간격을 좁혀 1920 × 1080 두 칸 40 → 50줄(매뉴얼의 화면 설명 · 그림이 있으면 새로). 숫자 24벌 1,572건.
- F-8: 매뉴얼 되돌리기 설명 — 저장 · 저장 후 커밋 뒤에도 Undo/Redo(커밋은 그대로, 화면만), 편집기에 친 것 · 규칙 바꾸기도 이어짐, 요약 `· 저장됨`, [전체 되돌리기] 는 파일을 연 시점(저장했으면 화면만). Git 탭 충돌 화면 "이번 병합에서 푼 파일" + [다시 충돌로](git.exe 필요 — 알아 둘 한계에도). 숫자 24벌 1,593건.
- F-9: 매뉴얼 설정 표 "저장 > 저장할 때 백업"(기본 끔) — 백업 자리 `{설정 폴더}\Backup\MergeHub\날짜\드라이브\원래 폴더\이름.날짜-시각.확장자` · 목록.txt, 모든 저장(병합 중 Save · 파일 단위 Accept · 폴더 복사 포함), 못 만들면 저장 안 함, 저절로 지우지 않음, ACTIONS [백업 폴더 열기]. 숫자 24벌 1,618건.
- F-10: 매뉴얼 파일 비교 화면 설명과 그림을 새로 — 칸마다 자기 줄(빈 줄로 맞추지 않음) · 두 칸(3분할은 위 세 칸) 사이 잇는 선과 색 · 옮김 점선 · 가운데 기준 스크롤 · 차이 이동이 가운데로 · 지운 줄 되살리기 [>] 가 BASE 칸 끝. 상태 줄 `N행` 은 파일 줄 번호. 숫자 25벌 1,639건.
- F-11: 매뉴얼 Git 탭 원격 칸 — 원격 URL · 계정은 적으면 설정에 남고, 토큰은 fetch/pull/push 가 되면 윈도 자격 증명 관리자에 저장(설정 파일에는 토큰이 없음), 껐다 켜면 다시 묻지 않음, [토큰 지우기] · "저장됨" 표시, 인증 실패 안내 문장. 이 PC · 이 윈도 계정에만 저장된다는 것도. 숫자 25벌 1,654건.
## B-1. 배포 묶음을 다시 만들고 꼭 필요한 파일을 가림 (2026-09-22, 완료)
### 고치기 전 — 매뉴얼이 가리키는 Release 는 09-07 것
| 무엇 | 값 |
|---|---|
| `bin\Release\net48` | 65개 · 96MB. `MergeHub.dll` 09-07 14:11, 셸 09-07 13:41 (플랫폼은 그 뒤 09-16 에 다시 배포됨) |
| 옛 Release 로 켜면 | 오류 없이 켜지지만 **본 탭이 3개**(지금은 4개 — 폴더 비교 탭이 없음). 지금 소스의 형식 142개 가운데 **74개가 옛 dll 에 없다** — 폴더 비교(`FolderView` · `FolderComparer`), 3분할(`ThreeWayAligner`), 병합 저장 판단(`MergeSavePlan`), 단축키(`ShortcutMap`), 변경분 안내(`ExportReport` · `ExportPlan`), 커밋 수정 판단(`CommitSelection` · `CommitReword`) 등 6차 이후 것 (`e11-b1-old.ps1`, 이름 대조) |
### 무엇이 필요한지 가린 길
1. **참조로** — 셸 · MergeHub 에서 시작해 dll 이 부르는 dll 을 따라갔다. 어디서도 닿지 않는 것 7개: `GLWpfControl` · `OpenTK` · `SkiaSharp.Views.WPF` · `SkiaSharp.Views.Desktop.Common` · `LiveChartsCore.SkiaSharpView.WPF` · `Microsoft.Extensions.Options` · `…Options.ConfigurationExtensions`. 플랫폼 dll 안에 이 이름이 글자로도 없다(XAML 로 부르는 것도 아님).
2. **플랫폼이 쓰는 것** — `UbisamBase.Core` 가 `SkiaSharp` · `LiveChartsCore` · `LiveChartsCore.SkiaSharpView` 를 **직접 참조**한다(`Charting/ChartTheme.cs`, 테마를 바꿀 때 `UiSettingsService` 가 부름). MergeHub 는 안 쓰지만 **플랫폼 몫이라 남긴다**(사용자 결정 2026-09-22 — 빼면 약 8MB 가 되지만 플랫폼이 바뀔 때마다 다시 재야 한다).
3. **셸이 켜질 때 하는 일** — 셸은 폴더의 `*.dll` 을 모두 `Assembly.LoadFrom` 으로 열어 `IAppSetup` 을 찾는다(`AppSetupLoader.cs:16`). 쓰지 않는 dll 도 이름 순서상 `MergeHub.dll` 앞이면 켤 때마다 읽힌다 — 빼면 그만큼 덜 읽는다. `Lang` 폴더는 없으면 플랫폼이 만든다(`LanguageService.cs`), 지금은 비어 있다.
4. **다른 OS · CPU** — 맥용 dylib 3개(17.5MB), 리눅스 · 맥용 libgit2 9개(약 19MB), ARM 용(Skia · HarfBuzz 11MB, libgit2). 이 PC 에서 셸은 **64비트**로 뜬다. x86 은 32비트 Windows 를 위해 남긴다.
5. **지워 보고 켜서** — 통째 복사본(65개 · 97MB)과 줄인 묶음(41개 · 44MB)을 `%TEMP%` 에 따로 두고 켰다. 둘 다 오류 없이 켜지고 본 탭 4개가 모두 열렸다(UIA 로만 이동, `e11-b1-run.ps1`).
### 빈 폴더에서 병합 한 길 (`e11-b1-merge.ps1` — 10차 E-1 의 병합 길 그대로, 실행 파일만 묶음으로)
| 무엇 | 결과 |
|---|---|
| 실행 파일 | `%TEMP%\mergehub-beta\UbisamBase.Shell.exe` (줄인 묶음) |
| 병합 | `feature/theirs → feature/mine` 충돌 24건 (머지 주석만 다른 2건 자동) |
| 풀기 | 파일 단위 15 · 줄 단위 7 · 파일 지우기 · 남기기 · 누른 Take 18번 = 줄 고른 로그 18줄 |
| 병합 커밋 | `647f1f8` 부모 둘 · 본문 "충돌 해결 26개" · 줄끝 · BOM · 인코딩 바뀐 파일 0 · 충돌 표식 0 · 워킹트리 깨끗 |
| 두 탭 | Git 탭에서 고른 커밋을 파일 비교 탭이 따라감(10차 B-1) |
| 걸린 시간 | 304초 |
### 만든 것
| 자리 | 한 일 |
|---|---|
| `scripts\make-bundle.ps1` (새) | Release 빌드(vswhere 로 MSBuild 를 찾음, `-NoBuild` 로 건너뜀) → 뺄 것 24개를 **까닭과 함께 한 표**에서 빼고 → `-Out`(기본 `%TEMP%\mergehub-beta`)에 옮기고 → 꼭 있어야 할 것(셸 · Core · MergeHub · LibGit2Sharp · git2 x64)을 확인하고 → 묶음 안에 `배포묶음.txt`(파일 · 크기 · 까닭, 뺀 것 · 까닭, 만든 시각 · 커밋)를 쓴다. **다른 파일이 든 폴더는 비우지 않고 멈춘다**(`배포묶음.txt` 가 있는 폴더 · 빈 폴더만) — 남의 파일을 넣어 둔 폴더로 확인함 |
| 매뉴얼(`doc/web/index.html` 켜기와 탭 구성) | "Release 로 다시 빌드해야 최신" · 다른 PC 에는 스크립트로 묶음을 만들어 폴더째 복사 · `배포묶음.txt` 설명 |
- 스크립트로 만든 묶음이 손으로 만들어 시험한 묶음과 **파일 목록이 같고**, 다시 켜도 오류 없이 네 탭이 열렸다.
- Release 빌드 경고 0 · 오류 0. 제품 코드 변경 없음(스크립트 · 문서만).
### 배포 묶음 (41개 · 43.5MB)
| 묶음 | 파일 | 크기 | 까닭 |
|---|---|---|---|
| 셸 · 플랫폼 | `UbisamBase.Shell.exe` · `.exe.config` · `UbisamBase.Core.dll` | 0.7MB | 켜는 것 · .NET 4.8 · 버전 맞춤 · 플랫폼 |
| MergeHub | `MergeHub.dll` | 0.6MB | 셸이 폴더의 dll 을 훑어 찾는다 |
| Git | `LibGit2Sharp.dll` · `lib\win32\x64\git2-*.dll` · `lib\win32\x86\git2-*.dll` | 4.0MB | Git 조작 · 네이티브 |
| MVVM · 인코딩 | `CommunityToolkit.Mvvm.dll` · `System.Text.Encoding.CodePages.dll` | 0.8MB | 플랫폼 · MergeHub · CP949 판별 |
| 플랫폼 설정 · DI | `Microsoft.Extensions.*` 11개 | 0.5MB | 플랫폼 |
| .NET 보강 | `System.*` 9개 · `Microsoft.Bcl.AsyncInterfaces` | 1.1MB | 플랫폼 · Json |
| 차트(플랫폼 몫) | `LiveChartsCore` 둘 · `SkiaSharp` 둘 · `HarfBuzzSharp` · 네이티브 6개(루트 · x64 · x86) | 35.8MB | 플랫폼 Core 가 참조(`ChartTheme`) |
### 남은 것
- 묶음은 `%TEMP%` 에 있다. 다른 PC 에 줄 자리(공유 폴더 · 문서 폴더)는 베타 때 정한다.
- 지시서의 "다섯 탭" 은 지금 본 탭 4개(폴더 비교 · 파일 비교 · Git · 설정)다 — 지시서 글이 옛 것.
- 웹 문서 발행(아티팩트)은 E-2 에서.
- 플랫폼 배포 폴더(`D:\UbisamPlatform\bin`)는 **손대지 않았다**. 플랫폼 README 가 "UbisamBase.Shell 을 Release 로 빌드하면 이 폴더를 갱신" 한다고 하므로, 플랫폼을 새로 배포하면 이 스크립트를 다시 돌려야 한다.
## B-2. D 드라이브가 없는 PC · 설정이 없는 첫 실행 (2026-09-22, 완료 — 플랫폼에서 고치고 확인 뒤 배포)
지시서대로 **D 드라이브는 건드리지 않았다.** `D:\UbisamConfig` 는 먼저 통째로 백업하고(20개, 파일마다 SHA256 · 크기 · 수정 시각 목록) 이름만 잠시 바꿔 재고, 끝날 때마다 되돌려 목록과 대조했다(두 번 모두 같음). 시험은 모두 UIA 로만 누르고 읽었다(마우스 · 키보드 · 화면 찍기 없음).
### 설정이 없는 첫 실행 (배포 묶음 `%TEMP%\mergehub-beta`, `e11-b2.ps1`)
| 볼 것 | 결과 |
|---|---|
| 오류 창 | **없음** |
| 이름 창 | 빈 칸으로 뜬다 · 빈 채로 [저장] 하면 "이름을 입력하세요." 로 막힘 · 이름을 넣고 [저장] 하면 닫힘 |
| 켜는 순간 생기는 것 | `Settings\MergeHubSettings.json` · `Settings\UiSettingsService.json`. 이름을 저장하면 `CurrentUserService.json`, 첫 기록 때 `Log\` |
| 기본값 | `MergeHubSettings.json` 이 매뉴얼 설정 표와 **모두 같다** — 원격 이름 origin · 줄 필터 켬 · 단어 단위 · 문법 강조 켬 · 커밋 조회 200 · 머지 주석 남기기 · 무시 켬 · C# 스타일 1(AURORA, F-12) · 공백 단계 0 · 나머지 비움 |
| 창 위치 | 주 모니터에 **최대화**(2560×1600). 닫으면 `ShellWindowPlacement.json`(보통 크기 2048×1222 · Maximized true) |
| 네 탭 | 모두 열림, 오류 글 없음 |
### D 드라이브가 없을 때 — 코드로 따라가고, 같은 줄에서 실패하게 해 봄
- 셸 `App.OnStartup` → `UbisamAppBuilder.Run` 의 첫 줄 `UiSettingsService.Initialize(D:\UbisamConfig)` → `Directory.CreateDirectory(D:\UbisamConfig\Settings)`(`UiSettingsService.cs:86`)에 **감싼 곳이 없다**. 드라이브가 없으면 `DirectoryNotFoundException`. 로그 서비스는 그보다 **뒤에** 준비된다.
- 드라이브를 없애는 대신 `D:\UbisamConfig` 자리에 **같은 이름의 빈 파일**을 두면 같은 줄이 `IOException` 으로 실패한다.
- 결과: 셸의 공용 예외 창 **"문제가 생겼습니다 — 작업 중 문제가 생겨 이 동작을 멈췄습니다. 프로그램은 계속 쓸 수 있습니다 … 자세한 내용은 D:\UbisamConfig\Log 아래 로그 파일에 남았습니다."** 두 말 모두 사실이 아니다(본 창 없음 · 로그 없음). [확인] 을 누르면 **창 없는 프로세스가 30초 뒤에도 남았다**. 다시 켜면 하나씩 더 쌓인다.
### 정한 것 (2026-09-22 사용자) · 고친 것 — 플랫폼
- **D 를 못 쓰면 `%LOCALAPPDATA%\UbisamConfig`** 에 쓴다(권장안). **켜는 중 실패는 알리고 끝낸다**(권장안).
- 플랫폼 커밋 `377ffd3` — `Bootstrap/ConfigLocation`(새) · `Bootstrap/StartupMessages`(새) · `UbisamAppBuilder.ConfigRoot` 를 `const` → 속성(+ `ConfigFallbackReason`), 로그를 가장 먼저 준비 · `LockConfirmDialog` 의 ScreenSaver 자리 · `LogService.LogDirectory` · 셸 `App.OnStartup` 에서 켜는 중 실패를 받아 `Shutdown(1)`. 플랫폼 `changelog.md` 에 다른 소비 프로그램 영향과 함께 적음, `platform-guide.md` 11장 · 세 곳 고침.
- 하네스 `PlatformCfgTest`(새, 플랫폼 두 파일을 엮음) **24건** — 자리 고르기 · 둘 다 안 될 때 · 실제 임시 폴더 · 같은 이름 파일 · **없는 드라이브 글자**로 실제로 다른 자리를 고름 · 실패 글(켜는 중에는 "계속 쓸 수 있습니다" 를 안 함 · 로그가 없으면 "남았습니다" 를 안 함).
- 플랫폼은 **Debug 로만** 빌드했다(경고 0 · 오류 0). Release 는 빌드 후 단계가 곧바로 `D:\UbisamPlatform\bin` 을 덮어쓰기 때문이다 — 배포 폴더는 그대로(09-16).
### 고친 뒤 (배포 묶음에 고친 Debug Core · Shell 두 파일만 바꿔 넣은 `%TEMP%\mergehub-beta-b2`)
| 경우 | 결과 |
|---|---|
| D:\ 에 만들 수 있을 때 | 예전처럼 `D:\UbisamConfig` · `%LOCALAPPDATA%` 에 아무것도 안 생김 · 경고 없음 |
| D:\ 자리가 막혔을 때 | `C:\Users\Ubisam\AppData\Local\UbisamConfig` 에 Settings 넷 · Log 가 생기고 이름 창 → 네 탭 → 정상 종료. 로그 첫 줄 `[W] Platform: D:\UbisamConfig 를 쓸 수 없어 C:\Users\Ubisam\AppData\Local\UbisamConfig 에 설정 · 로그를 둡니다 (같은 이름을 가진 파일 또는 디렉터리가 이미 있어서 …)` |
| 두 자리 모두 막혔을 때 | 창 **"켜지 못했습니다"** — "프로그램을 켜지 못해 끝냅니다. 설정 폴더를 만들 수 없습니다. D:\UbisamConfig : … / C:\Users\Ubisam\AppData\Local\UbisamConfig : …" (로그가 없으니 로그 얘기는 없음) → [확인] 뒤 **프로세스 끝남** |
- MergeHub 는 이전 Core 로 빌드된 그대로 새 Core 위에서 돌았다(바뀐 것은 속성 추가뿐, MergeHub 는 `ConfigRoot` 를 읽지 않는다).
- MergeHub 쪽: 매뉴얼 "설정 — … MergeHubSettings.json 에 저장된다" 에 D 없는 PC 의 자리를 덧붙이고, `MergeHubSettings.cs` 주석을 사실대로. 하네스 22벌 1,247건 그대로 · 빌드 경고 0 · 오류 0.
### 배포 (2026-09-22 사용자 확인 뒤)
- `publish-platform.ps1` 로 `D:\UbisamPlatform\bin` 갱신(배포 시각 11:27:25, 경고 0 · 오류 0). 48개 가운데 바뀐 것은 **`UbisamBase.Core.dll` · `UbisamBase.Shell.exe` · `README.txt` 셋**, 없어진 파일 없음(배포 전 목록 · 해시와 대조).
- MergeHub 를 다시 빌드하고 `make-bundle.ps1` 로 묶음을 다시 만듦(41개 · 43.5MB). 묶음 · Debug 출력의 Core · Shell 이 배포 폴더와 해시가 같다. 새 묶음으로 켜 네 탭 오류 없음 · 로그에 자리 경고 없음(D 를 씀).
- 시험이 남긴 것(시험용 설정 폴더 셋 · 빈 파일 셋 · `%LOCALAPPDATA%` 쪽 폴더 · 시험 묶음 둘)은 사용자 말대로 모두 지움. `D:\UbisamConfig` 원본만 남음.
### 남은 것
- 드라이브는 있는데 **읽기 전용**(CD · 쓰기 막힌 USB)인 경우도 같은 길(`CreateDirectory` 실패 → 다른 자리)로 가지만 재 보지는 않았다. 폴더는 있는데 파일만 못 쓰는 경우(권한)는 이번에 다루지 않았다.
## C-1. 있는 [커밋 수정] 을 다시 내 보고 바로잡음 (2026-09-18, 완료 — 8 만 이번 범위 밖)
### 고치기 전에 여덟 가지를 화면에서 다시 냈다
시험 저장소: `C1Repo` 위에 `amend` 브랜치를 만들고 **작성자 박지훈인 커밋**을 HEAD 로, 다른 브랜치(`feature/revert`)에는 **더 새 커밋**을, 그리고 bare 저장소를 원격으로 걸어 upstream 을 두었다(`e11-c1-setup.sh`).
| # | 고치기 전에 본 것 |
|---|---|
| 1 | [모든 브랜치] 켠 채 [마지막 메시지] → 칸에 `딴 브랜치 최신 작업`(HEAD 아님). 그대로 [커밋 수정] 하니 **HEAD 제목이 그 메시지로 바뀌었다** |
| 2 | 앱에서 [전체 스테이징] 한 파일이 **말없이 커밋에 들어갔다**(남은 변경 없음). 알림은 "커밋 수정" 뿐 |
| 3 | 작성자 `박지훈` · 작성 `2026-09-15 10:00` → **`시험` · `2026-09-18 15:48`** 로 바뀜 |
| 4 | 충돌 24개가 남은 채로도 단추가 **켜져 있고**, 누르면 `[커밋 수정] 실패: cannot create a tree from a not fully merged index` |
| 5 | 병합 커밋을 고쳐도 **부모 둘 · 본문(충돌 해결 26개 · 29줄) 그대로** |
| 6 | 원격에 이미 올라간 커밋도 **막지 않고** 고쳐졌고, 그 뒤 [Push] 가 `cannot push non-fastforwardable reference` 로 거부됨 |
| 7 | 알림 · 로그에 **새 해시만**(`커밋: 61653f5 …`). 고치기 전 해시는 어디에도 없음 |
| 8 | 커밋 메시지 칸이 한 줄짜리라 **여러 줄 메시지를 아예 칠 수 없다**. `#` 로 시작하는 글자는 그대로 들어간다 |
### 고친 것 (1 · 3 · 4 · 7)
| 자리 | 한 일 |
|---|---|
| `Services/CommitSelection.cs` | `MessageOfHead` — [마지막 메시지] 는 **HEAD 커밋**의 메시지를 읽는다(목록 순서와 무관). HEAD 가 목록에 없으면 아무것도 주지 않는다. `CanAmend` — **작업 중이면 끈다**. `AmendHint` — 꺼진 까닭을 단추 설명으로. `AmendDone` — "커밋 수정 완료 (옛 abc1234 → 새 def5678) · 파일 N개 포함" |
| `Services/GitService.cs` | 고쳐 쓸 때 **원래 작성자 · 작성 시각을 그대로** 쓴다(git 과 같게). 커밋한 사람 · 시각만 지금 |
| `Services/GitWorkspace.cs` | `HeadFullSha` 를 들고 있는다 — 이력 목록의 Sha 와 맞춰 보려면 짧은 해시로는 모자란다 |
| `ViewModels/GitViewModel.cs` | 위 판단을 붙이고, 고치기 전 해시 · 함께 들어간 파일 수를 알림에. 작업이 시작 · 끝날 때 단추 설명도 다시 그린다 |
| `Views/GitView.xaml` | [커밋 수정] 설명을 상황에 따라 바뀌는 값으로 |
| 하네스 `MergeSaveTest` | **+15건(210 → 225)** — HEAD 메시지 읽기(맨 위가 딴 브랜치여도 · 대소문자 · HEAD 가 목록에 없음 · 빈 목록) · 켜는 조건 넷 · 꺼진 까닭 · 알림 글 셋 |
### 고친 뒤 같은 순서로 다시
| # | 결과 |
|---|---|
| 1 | 칸에 **`남이 만든 커밋`**(HEAD 것) |
| 3 | 고친 뒤에도 작성자 `박지훈` · 작성 `2026-09-15 10:00` **그대로**, 커밋한 사람 · 시각만 지금 |
| 4 | 충돌 24개가 남은 동안 단추 **꺼짐** — 누를 수 없고, 누른 뒤 실패 알림도 없다 |
| 7 | 알림 **`커밋 수정 완료 (옛 bcd2c43 → 새 fd7810c) · 파일 1개 포함`** |
- 하네스 22벌 **1,200건** 통과(1,185 → +15) · 빌드 경고 0 · 오류 0.
### 이어서 고친 것 (2 · 6) — "먼저 묻기" (2026-09-18 사용자 결정)
두 가지 모두 **막지 않고 먼저 묻는다**. 막으면 정말로 그러려는 사람(빠뜨린 파일을 같이 넣기 · 아직 남이 안 받은 커밋 고치기)까지 못 하게 되고, 그대로 두면 모르는 새 벌어진다.
| 자리 | 한 일 |
|---|---|
| `Services/CommitSelection.cs` | `AmendWarning(스테이징 수, 이미 올림, 원격 브랜치)` — 물을 것이 없으면 **null**(안 묻는다). 있으면 해당하는 줄만 모아 "… ¶ 계속할까요?" 한 글로 |
| `ViewModels/GitViewModel.cs` | `StagedFileCount`(충돌 뺀 스테이징 수) · `HeadTrackedBranch` · `IsHeadAlreadyPushed`(추적 브랜치가 있고 앞선 커밋 0개). 물을 것이 있으면 `MessageUtil.ShowYesNo` 로 먼저 묻고, [아니오] 면 **커밋도 메시지 칸도 그대로 두고 아무 일도 하지 않는다** |
| 하네스 `MergeSaveTest` | **+7건(225 → 232)** — 안 묻는 경우 · 스테이징만 · 올린 것만 · 둘 다 · 브랜치 이름을 모를 때 "원격" · "Push 가 거부됩니다" · 끝이 늘 "계속할까요?" |
앞선 커밋 수는 **마지막 fetch 시점** 기준이라, fetch 전에 남이 받아 간 것까지는 알 수 없다. 그래서 막지 않고 묻기만 한다(주석에 남김).
### 고친 뒤 화면에서 (`e11-c1g.ps1`)
스테이징 1개를 만들어 두고, HEAD 가 `origin/amend` 와 같은 자리인 채로 냈다 — **두 가지가 겹치는 자리**다.
| 무엇 | 결과 |
|---|---|
| [커밋 수정] 누름 | 확인 창 `커밋 수정` — "스테이징해 둔 파일 1개도 이 커밋에 들어갑니다. / 이 커밋은 origin/amend 에 이미 올라가 있습니다. 고치면 다음 Push 가 거부됩니다. / 계속할까요?" · 단추 [아니오] [예] (`e11-c1g-ask.png`) |
| [아니오] | HEAD `62dd57f` **그대로** · 스테이징 `Recipe/Unit19.cs` **그대로** · 메시지 칸 `묻는지 확인` **그대로** · 창 닫힘 |
| 다시 누르고 [예] | HEAD `62dd57f` → `6207b26` · 알림 `커밋 수정 완료 (옛 62dd57f → 새 6207b26) · 파일 1개 포함` |
| 스테이징 없음 · 원격보다 앞선 자리에서 다시 | **안 묻고 바로** 고침 (`ceaab5f`, 작성자 `박지훈` 그대로) (`e11-c1g-done.png`) |
- 하네스 22벌 **1,207건** 통과(1,200 → +7) · 빌드 경고 0 · 오류 0 · 설정 파일 해시 같음.
### 남은 것
- **8. 여러 줄 커밋 메시지** — 칸이 한 줄이라 본문을 칠 수 없다. 이번 범위 밖으로 적어 둔다(병합 커밋 본문은 앱이 저절로 붙인다). C-2 의 [메시지 바꾸기] 창은 여러 줄을 받는다.
- C-2 는 아래(2026-09-22 완료).
## C-2. 마지막이 아닌 커밋의 메시지 바꾸기 (2026-09-18 ~ 09-22, 완료 — 좁은 범위)
사용자 결정(2026-09-18): **한다 — 좁은 범위로.** 화면은 기획안 문구대로 **이력 줄 우클릭 메뉴**.
### 고치기 전
- 이력 줄을 우클릭해도 **메뉴가 없고**, 이력 탭 단추는 Revert · Cherry-pick · Reset · 여기부터 · 해제 · 변경분 추출뿐이었다(`e11-c2-before.ps1`). 고른 커밋의 메시지를 바꿀 길이 없었다.
- 메시지는 커밋 해시에 들어가므로, HEAD 가 아닌 커밋을 바꾸면 **그 커밋부터 HEAD 까지가 새 해시로 다시 써진다**. 아직 안 올린 커밋이면 브랜치가 새 줄기로 옮겨 갈 뿐이지만, 올린 뒤라면 남의 이력과 갈라진다(사용자에게 설명함).
### 만든 것
| 자리 | 한 일 |
|---|---|
| `Services/CommitReword.cs` (새) | 막을 까닭 — 고른 커밋 없음 · 작업 중 · 커밋 안 한 변경 · 지금 브랜치 조상 아님 · 원격에 올라감 · 다른 로컬 브랜치도 품음 · 구간에 태그 · 사이에 병합 커밋. 먼저 풀어야 할 것부터 말하고 **푸는 길**까지 적는다. 확인 글 · 완료 알림 · 메뉴 설명. `Clean` — 새 메시지를 git `-m` 처럼 다듬는다(아래) |
| `Services/GitService.cs` · `IGitService.cs` · `Models/GitModels.cs` | `InspectReword` — 저장소에서 사실만 읽는다(다시 쓸 수 · 병합 수 · 올라간 원격 브랜치 · 다른 브랜치 · 태그). `RewordCommit` — LibGit2Sharp `Refs.RewriteHistory` 로 **고른 커밋의 메시지만** 갈고 트리 · 작성자 · 작성 시각 · 부모 관계는 그대로. 바꾸기 전 자리는 `refs/original/날짜-시각/` 에 남긴다(같은 자리에 두 번 쓰면 libgit2 가 막아서 시각을 붙임). 고친 커밋의 새 해시도 돌려준다 |
| `Views/RewordWindow.xaml(.cs)` (새 창) | 새 메시지를 받는 창. 고친 커밋 한 줄(해시 · 작성자 · 날짜) · 몇 개가 다시 써지는지를 늘 보이고, **여러 줄**을 받는다. 지금 메시지를 미리 넣어 전체를 골라 둔다. [바꾸기] 는 기본 단추가 아니다(Enter 는 줄바꿈) |
| `Views/GitView.xaml(.cs)` | 이력 목록의 우클릭 메뉴 **[메시지 바꾸기...]** — MergeHub 의 첫 우클릭 메뉴. 목록은 오른쪽 누름으로 선택이 안 바뀌므로 **우클릭한 줄을 먼저 고른다**(10차 B-1 처럼 저장 안 한 편집이 있으면 먼저 묻고 [취소] 면 메뉴도 안 연다) |
| `ViewModels/GitViewModel.cs` | 막힐 자리면 메뉴를 끄지 않고 **눌렀을 때 까닭을 알림으로** — 까닭은 저장소를 읽어 봐야 알고, 꺼 두면 왜 안 되는지 알 길이 없다. 되면 창 → 확인 창 → 실행 → 고른 커밋이 **새 해시**를 따라간다 |
| 하네스 `MergeSaveTest` | **+40건(232 → 272)** — 막는 여덟 가지와 보는 순서 · 확인 글 · 완료 알림 · 빈 메시지 · 안 바뀐 메시지 · 다듬기 아홉 |
### 화면에서 찾아 고친 것 — 끝 줄바꿈
첫 회차에 새 메시지가 **줄바꿈 없이** 저장됐다(`셋` 으로 끝남). git 이 만든 커밋은 늘 `\n` 으로 끝나고, MergeHub 의 [커밋] · [커밋 수정]도 libgit2 가 다듬는다. 칸을 어떻게 고쳤느냐에 따라 끝 줄바꿈이 있다 없다 했다.
libgit2 의 다듬기(`PrettifyMessages`)를 켜면 **손대지 않은 뒤 커밋들 메시지까지** 다듬어지므로, 새 메시지 하나만 `CommitReword.Clean` 으로 다듬는다(줄 끝 공백 · 앞뒤 빈 줄 없앰, 빈 줄 여럿은 하나, 끝은 줄바꿈 하나, `#` 줄 · 들여쓰기는 둠). 검사 9건을 먼저 넣어 실패를 보고 고쳤다.
같은 회차의 다른 둘은 **스크립트 탓**으로 가렸다(`e11-c2-probe.ps1`): 첫 우클릭이 안 먹은 것은 창을 앞으로 부르는 도우미가 다른 프로그램 창을 앞으로 가져와 클릭이 앱에 안 닿은 것, 한글이 한 글자만 들어간 것은 8ms 간격으로 몰아 친 것. 150ms 간격으로 치면 그대로 들어갔다.
### 고친 뒤 화면에서 (`e11-c2-setup.sh` · `e11-c2a.ps1` · `e11-c2b.ps1` · `e11-c2c.ps1`)
시험 저장소: `reword` 브랜치에 커밋 넷(작성자 박지훈 · 이수진, 셋째는 일부러 **"셋제 커밋"** 오타). 막힐 자리로 `pushed`(bare 원격에 올림) · `merged`(사이에 병합) · `shared` + `shared-copy` · `tagged`(v9.9).
**되는 자리** — "셋제 커밋" 우클릭 → [메시지 바꾸기...]
| 무엇 | 결과 |
|---|---|
| 우클릭 | 그 줄이 먼저 골라지고 메뉴 `메시지 바꾸기...` |
| 창 | `83ed0b2 · 박지훈 · 2026-09-16 11:00` · "이 커밋과 그 뒤 1개, 모두 2개가 새 해시로 다시 써집니다" · 칸에 `셋제 커밋` (`e11-c2-window.png`) |
| [취소] | HEAD `4c0263e` 그대로 |
| "셋째 커밋" 치고 [바꾸기] | 확인 창 "83ed0b2 "셋제 커밋" 의 메시지를 바꿉니다. 이 커밋과 그 뒤 1개, 모두 2개가 새 해시로 다시 써집니다. 내용 · 작성자 · 작성 시각은 그대로입니다. 바꾸기 전 HEAD 4c0263e 는 로그에 남겨…" (`e11-c2-confirm.png`) |
| [예] | 알림 `메시지 바꿈 완료 (옛 HEAD 4c0263e → 새 HEAD 1b11c03) · 커밋 2개 다시 씀` · 로그 `… · 되돌릴 자리 refs/original/20260922-102604/` |
| git 대조 | 넷째 · 셋째: **트리 해시 · 작성자 · 작성 시각 같음**, 해시만 새것. 제목은 셋째만 바뀜. 둘째 아래는 해시까지 그대로 · 커밋 수 같음 · 워킹트리 깨끗 |
| 메시지 끝 | `셋째 커밋\n` — 손 안 댄 넷째(`넷째 커밋\n`)와 같은 꼴 |
| 고른 커밋 | 새 해시 `b2d9880` 을 따라감 |
| 되돌리기 | `git reset --hard refs/original/20260922-102604/heads/reword` → `4c0263e … 셋제 커밋` 이 그대로 돌아옴 |
**막히는 자리** — 모두 창이 안 뜨고 HEAD 그대로, 알림에 까닭
| 자리 | 알림 |
|---|---|
| 올린 커밋 | "이 커밋은 origin2/pushed 에 이미 올라가 있습니다. 올린 커밋을 다시 쓰면 남의 이력과 갈라져 막습니다." |
| 사이에 병합 | "고른 커밋과 HEAD 사이에 병합 커밋이 1개 있습니다. 병합 이력은 다시 쓰지 않습니다." |
| 다른 브랜치도 품음 | "이 커밋을 다른 브랜치도 품고 있습니다(shared-copy). 그 브랜치까지 다시 써야 해서 막습니다." |
| 구간에 태그 | "다시 쓸 구간에 태그가 있습니다(v9.9). 태그가 가리키는 커밋이 사라져 막습니다." |
| 작업 중(병합 충돌 26개) | "병합 · rebase · cherry-pick · revert 를 끝낸 뒤에 쓸 수 있습니다." |
| 커밋 안 한 변경 | "커밋하지 않은 변경이 있습니다. 먼저 커밋하거나 stash 한 뒤에 바꾸세요." |
| 다른 브랜치에만([모든 브랜치]) | "지금 브랜치에서 닿지 않는 커밋입니다. 그 커밋이 있는 브랜치로 체크아웃한 뒤에 바꾸세요." (`e11-c2-other.png`) |
| 파일 비교 탭에 저장 안 한 편집 | 우클릭하자 `저장하지 않은 편집` 창("Common/Util/Unit103.cs 에 저장하지 않은 편집이 있습니다. 다른 커밋을 봅니다.") → [취소] 면 메뉴도 안 열리고 선택 그대로 (`e11-c2-unsaved.png`) |
- 하네스 22벌 **1,247건** 통과(1,207 → +40) · 빌드 경고 0 · 오류 0 · 설정 파일 해시 같음.
### 알아 둘 것
- 판단은 **마지막 fetch 시점** 기준이다. fetch 전에 남이 올린 것까지는 모른다(C-1 과 같음).
- 되돌릴 자리 `refs/original/…` 은 이력 목록에 안 보인다(목록은 브랜치만 훑음). 지우는 단추는 두지 않았다 — 되돌릴 일이 없으면 그대로 두어도 된다.
- HEAD 커밋도 이 메뉴로 바꿀 수 있다. [커밋 수정] 과 달리 **스테이징한 파일이 들어가지 않는** 메시지만 고치는 길이다.
## D-1. 시험 도구 둘 (2026-09-22, 완료 — 제품 코드 변경 없음)
| 도구 | 고치기 전 | 고친 것 |
|---|---|---|
| `d1-verify.ps1` (줄끝 · BOM · 표식 검사) | 부모 하나인 커밋(rebase · cherry-pick · revert)에도 없는 둘째 부모로 `git merge-base` 를 불러 사용법 글 · `null` 오류를 찍었다(판정 숫자는 맞았음) | 부모가 하나면 **첫 부모와만 견준다고 적고** merge-base 를 부르지 않는다. 견줄 부모 쪽 파일이 없는 새 파일은 가운데 BOM · 줄끝 섞임만 본다 |
| `d7lib.ps1` 의 `FileState` (파일 비교 탭 요약 읽기) | 요약 글을 "남은 충돌 · 충돌 정리 끝 · 남은 차이" 로만 찾아, 제품이 쓰는 **"차이 없음"**(편집이 있으면 "차이 없음 · 편집 N건 …")을 못 읽고 커밋 뒤 요약을 빈칸으로 적었다 | "차이 없음*" 도 요약으로 읽는다 (`HomeViewModel.cs:3486` 의 글과 맞춤) |
확인:
- 앱 없이 — 시험 저장소 HEAD(revert 커밋 `d59bc42`, 부모 하나)에 돌려 **오류 · 사용법 줄 0**, 결과는 E-1 revert 때와 같음(36개 · 다른 파일 0).
- 판정대로 rebase 한 길을 다시(`e11-e1-merge.ps1 -Way rebase`, 298초) — **오류 줄 0**, `[계속]` 뒤 파일 비교 탭 요약 **"차이 없음"**, "부모: … 하나 — 첫 부모와만 견준다", 누른 Take 18 = 로그 18 · 줄끝 · BOM 0/31 · 표식 0 · class 0 · 워킹트리 Clean · 설정 해시 같음.
- 고치기 전 두 파일은 `*.d1bak` 로 남김(시험 도구 폴더, 저장소 밖).
## E-1. 바꾼 길을 다시 끝까지 (2026-09-22, 완료 — 새로 걸린 것 없음)
11차에서 바뀐 것에 맞춰 넷을 돌렸다. A(변경분 추출) · C-1 · C-2 를 고쳤고, F-12 가 비교 · 저장에 걸리는 C# 스타일 기본값을, B-2 가 플랫폼 시작부를 바꿨으므로 **다섯 길 + pull** 도 다시 했다. 모두 Debug 출력(배포한 플랫폼 09-22 11:27 과 해시 같음), 사람 순서로(마우스 · 키보드).
### 1. A-1 의 아홉 경우 (`e11-a1-setup.sh` · `e11-e1-a.ps1` · `e11-e1-a-verify.sh`)
모두 첫 회차에 끝. 뽑은 곳은 고른 폴더 **아래 새 폴더**, 안내는 그 옆(A-2 · A-3 설계대로).
| # | A-1 때 | 지금 |
|---|---|---|
| 1 커밋 하나 | 2개 · 바이트 같음 | `변경분_228995f..8145f3b_2026-09-22` 에 2개 · 바이트 같음 · 안내 범위 "(고른 커밋의 부모)" |
| 2 범위 | 3개 · 바이트 같음 | 3개 · 바이트 같음 · 안내에 이름 바뀜 "(옛 이름 Motion/Unit16.cs)" |
| 3 지운 파일 | **어디에도 없음** | 안내 "장비에서 지워야 할 파일" 에 `지워짐 Recipe/Unit11.cs` |
| 4 이름 바뀐 옛 경로 | **알 길이 없음** | 같은 목록에 `옛 이름 Motion/Unit16.cs (→ Motion/Renamed16.cs 로 바뀜)` — git 기대 지울 것 2개와 같음 |
| 5 병합 커밋 | 첫 부모와만 · 화면에 없음 | 같은 동작(1개) · **안내에 범위 `37a2ced … (고른 커밋의 부모) → 84961da`** 로 무엇과 견줬는지 남음 |
| 6 거꾸로 범위 | 말없이 됨 | 같은 동작(2개 · 지울 것 3개) · 안내에 방향 그대로(`37a2ced → 228995f`) |
| 7 이진 · 한글 · 깊은 경로 | 바이트 그대로 | 바이트 그대로 |
| 8 이미 파일이 있는 폴더 | 옛 추출 · 사용자 파일과 **섞임** | 두 번 뽑으니 **새 폴더 둘**(범위별 이름) · `사용자메모.txt` 그대로 |
| 9-a · 9-b 읽기 전용 · 잠긴 파일 | **반쯤 뽑힌 채 멈춤** | 새 폴더에 써서 막힘 없이 3개 · 미리 둔 파일은 손대지 않음 |
- 대조 스크립트가 경우 8 의 첫 폴더(c1 범위)를 둘째 범위와 견줘 "다름" 이라고 찍었다 — 그 폴더는 안내대로 c1 범위이고 경우 1 과 같은 내용이다(스크립트가 폴더마다 범위를 따로 안 봄).
- 저장소 워킹트리 내내 Clean.
### 2. C-1 의 여덟 경우 (`e11-c1-setup.sh` · `e11-e1-c.ps1` · `e11-e1-c1again.ps1`)
| # | C-1 고치기 전 | 지금 |
|---|---|---|
| 1 | [모든 브랜치] 에서 [마지막 메시지] 가 딴 브랜치 메시지 | 목록 맨 위 `딴 브랜치 최신 작업` 인데 칸에 **`남이 만든 커밋`**(HEAD) |
| 2 | 스테이징 파일이 말없이 들어감 | 확인 창 "스테이징해 둔 파일 1개도 이 커밋에 들어갑니다" → [예] → 알림 "… · 파일 1개 포함" |
| 3 | 작성자 · 작성 시각이 바뀜 | 두 번 고쳐도 작성자 `박지훈` · 작성 `2026-09-15 10:00` 그대로 |
| 4 | 작업 중에도 켜져 실패 | 병합 충돌 26개 동안 **꺼짐** · 설명 "병합 · rebase · cherry-pick · revert 를 끝낸 뒤에 쓸 수 있습니다" |
| 5 | (부모 둘 그대로) | [마지막 메시지] 가 `Merge branch e1-side` · 고쳐도 **부모 둘** |
| 6 | 막지 않고 Push 거부 | 확인 창 "이 커밋은 origin/amend 에 이미 올라가 있습니다. 고치면 다음 Push 가 거부됩니다" → [예] 뒤 [Push] `cannot push non-fastforwardable reference` (정한 대로 묻기만) |
| 7 | 새 해시만 | 알림 "커밋 수정 완료 (옛 4816f3e → 새 953816a)" |
| 8 | 한 줄 칸 | 그대로(범위 밖으로 둠) |
### 3. C-2 로 이력을 다시 쓴 저장소에서 병합 한 길 (`e11-e1-merge.ps1 -Way reword`)
새 저장소의 `feature/mine` 에 커밋 둘(박지훈 · 이수진)을 얹고, 앞 커밋 `첫 추가 커밋(오타 잇음)` 을 우클릭 → [메시지 바꾸기...] 로 `첫 추가 커밋` 으로 고친 뒤 그대로 병합.
- 확인 창 "… 이 커밋과 그 뒤 1개, 모두 2개가 새 해시로 다시 써집니다 …" → 알림 `메시지 바꿈 완료 (옛 HEAD e1d924c → 새 HEAD 3145f3f) · 커밋 2개 다시 씀`.
- 앞 네 커밋의 작성자 · 작성 시각 · **트리 4/4 같음**, 제목은 고른 것만. 되돌릴 자리 `refs/original/20260922-115615/heads/feature/mine`.
- 병합: 충돌 24(+자동 2) → 누른 Take 18 = 줄 고른 로그 18 → 병합 커밋 `ae20f56`(부모 첫째 = 다시 쓴 `3145f3f`) · 줄끝 · BOM 바뀐 파일 0 · 충돌 표식 0 · class 0 · 워킹트리 Clean · 두 탭 같은 커밋.
### 4. 다섯 길 + pull (`e11-e1-merge.ps1 -Way merge|pull|rebase|pick|revert` — 10차 E-1 과 같은 순서)
| 길 | 누른 Take = 로그 | 마지막 Save 뒤 기준 | 커밋 | 줄끝 · BOM 바뀐 파일 | 충돌 표식 · class | 두 탭(10차 B-1) | 걸린 시간 |
|---|---|---|---|---|---|---|---|
| 병합 | 18 = 18 | HEAD · N=3 = git | `cac5366` 부모 둘 · 본문 해결 26개 | 0/33 | 0 · 0 | 같음 | 303초 |
| pull | 18 = 18 | 같음 | `1b32e2f` "Merge remote-tracking branch 'origin/feature/mine' …" | 0/33 | 0 · 0 | 같음 | 303초 |
| rebase | 18 = 18 | 같음 | `b108182` 부모 하나 · "내 쪽 작업" | 0/31 | 0 · 0 | 같음 | 302초 |
| cherry-pick | 18 = 18 | 같음 | `631234e` "상대 쪽 작업" · 빈 cherry-pick 은 [커밋] 꺼짐 → [작업 취소] (8차 B-2) | 0/33 | 0 · 0 | 같음 | 350초 |
| revert | 6 = 6 | HEAD · N=6 = git | `d59bc42` `Revert "내 쪽 작업"` · REVERT_HEAD 안 남음 | 0/36 | 0 · 0 | 같음 | 199초 |
- 10차 E-1 과 숫자가 같다. **새로 걸린 것 없음.**
### 시험 도구 쪽에서 고친 것 (제품 아님)
- **창을 앞으로 부르는 도우미가 최대화를 풀었다** — `ShowWindow(SW_RESTORE)` 를 늘 불러, 첫 누름 때 창이 복원 크기로 줄고 옛 자리를 눌러 로그 칸에 떨어졌다(누르기 전 · 뒤 사진으로 확인). 경우 1 이 처음에 "칸이 빔" 으로 나온 까닭이다. **최소화돼 있을 때만** 되살리게 고쳤다(`e11-c1lib.ps1` · `e11-e1-a.ps1`). 앞선 차수의 결과는 누른 뒤 자리를 다시 읽어 눌렀으므로 그대로 믿을 수 있다.
- C-1 시험 저장소의 "다른 브랜치의 더 새 커밋" 날짜가 09-18 이라 오늘(09-22)에는 목록 맨 위가 HEAD 가 되어 경우 1 이 성립하지 않았다 → 12-31 로.
- rebase · cherry-pick · revert 뒤 `d1-verify.ps1` 이 부모가 하나인 커밋에도 `git merge-base` 를 불러 사용법 글을 찍는다 — 판정에는 영향 없음(10차와 같음).
- 이 PC 의 Caps Lock 이 켜져 있어 영문 입력이 대소문자 뒤집혀 들어갔다(경우 5 의 메시지). 키보드 상태라 바꾸지 않았다 — 판정에는 영향 없음.
- 하네스 22벌 1,247건 · 플랫폼 24건 그대로 · 빌드 경고 0 · 오류 0 · 설정 파일 되돌림 해시 매번 같음. 제품 코드 변경 없음.
## E-2. 웹 문서 · 기획안을 11차에 맞게 (2026-09-22, 완료)
### MergeHub 문서 (`doc/web/index.html`, 아티팩트 "MergeHub 문서" 버전 11 → **13**)
- 올리기 전에 발행본(버전 11)을 받아 로컬과 견줌 — 09-17 에 올린 그대로였고(다른 사람 손 없음), 그 뒤 로컬에서만 바뀐 것은 11차 F-12 · B-1 · B-2 문장뿐.
- 고친 것:
- 개요 — 09-22 기준 · 22벌 1,247건 · 11차 · 커밋 129 · 지시서 11차.
- 매뉴얼 — Git 탭 표에 우클릭 [메시지 바꾸기...] 와 아래 두 절 연결, **"커밋 메시지 고치기"**(두 길을 한 표로 — 고치는 커밋 · 메시지 넣기 · 함께 들어가는 것 · 이미 올린 커밋 · 남는 것 · 꺼지는 때, 되돌리는 길), **"변경분 추출"**(새 폴더 · 옆 안내 파일 · 먼저 보고 쓰기 · 병합 커밋 · 거꾸로 범위), 목차 둘, 알아 둘 한계 셋(메시지 칸 한 줄 · 올렸는지는 마지막 Fetch 기준 · D 없는 PC 는 계정마다 설정). 실행 위치(B-1) · 설정 위치(B-2) · C# 스타일 기본 AURORA(F-12)는 먼저 적어 둔 것이 함께 올라감.
- 남은 일 — 11차 표(완료 · 대기 · 진행).
- 지금까지 만든 것 — 09-22 11차 기록 · 커밋 129 · 1,247건 · 하네스 막대 11차(폭을 새 최댓값으로 다시 잼) · 기능 목록 Git 한 줄 · 배포 한 줄.
- 기획안 v2 — 커밋 메시지 수정 **완료**, 변경분 추출 설명, 3단계에 11차, 4단계 **진행**(베타 준비).
- WinMerge 쪽 숫자 1,247건.
- 대조: 문서의 단추 이름 91개를 소스(MergeHub · 플랫폼)에서 찾음 — **"맞추지 않음"(09-18 F-12 때 적은 말)이 실제 콤보 글자 "C# 스타일 끔" 과 달라** 문서 두 곳 · 이 기록 한 곳을 고침. 남은 하나(`비교 규칙 펴기` 의 공백 두 칸)는 화면에 보이는 글자가 같다. 태그 짝 모두 맞음.
- 공유 링크를 받은 사람에게는 **고정된 옛 버전**이 보이도록 되어 있다 — 새 버전을 보이려면 페이지 Share 메뉴에서 사용자가 바꾼다.
### 기획안 v1.2 (잠금 아티팩트 "UbiSam MergeTool 개발 기획안" 버전 **17**)
- 발행본을 받아 로컬 잠금 파일(09-17 17:06)과 견줌 — 암호문 · salt · IV 가 같음. 그러므로 고칠 원문은 09-17 원문(`plan/mergetool.html`).
- 원문에서 고친 것: 3.2 Git 작업 UI화의 **커밋 메시지 수정 → 완료**("마지막 커밋은 [커밋 수정], 앞 커밋은 트리에서 우클릭 → [메시지 바꾸기]"), 변경분 파일 추출 설명(새 폴더 · 지울 파일 안내, ZIP 이 없어 "일부" 그대로), 일정 요약(09-22 기준 · 1,247건 · 4단계 베타 준비 중), 3단계 "Git 작업 버튼" 한 줄, 4단계 상태 "대기" → "준비 중", 4단계 베타 줄, 개정일 09-22.
- **A4 쪽 높이**: 고치기 전 · 뒤 원문을 헤드리스 Edge 로 재니 8쪽 모두 1123px · 넘침 없음으로 같다. 기능 목록 쪽 그림으로 한 줄씩인 것 확인.
- **잠금은 사용자가 했다**(`lock-plan.js`, 비밀번호는 화면에 안 보이게 받아 어디에도 남기지 않음 · 비교할 원본은 09-17 원문). 첫 "잠갔어" 때는 잠금 파일이 09-17 것 그대로여서 올리지 않고 멈췄다(PowerShell 문법을 CMD 에서 돌린 것으로 보임). 두 번째에 새 salt · IV 의 잠금 파일(13:12)이 생겨 올렸고, 발행본의 암호문이 그 파일과 같음을 되읽어 확인했다. 스크립트 출력은 다른 창이라 직접 보지 못했다.
## F-1. 칸 이름을 맞추고 칸마다 어느 파일인지 · 머지 주석에 어디서 가져왔는지 (2026-09-22, 완료)
사용자 결정(2026-09-22): 칸 이름은 **표준 용어 + 영문 쪽 이름**(`LOCAL(Mine)` · `REMOTE(Theirs)` — 4차 · 6차의 "표준 용어 그대로" 를 이어 가고 로그의 Mine/Theirs 와 맞춤), 주석 출처는 **쪽 이름 + 브랜치/해시**, 설명이 있으면 **출처 먼저, 설명 뒤**.
### 고치기 전 — 화면에서 (`f1-probe.ps1`)
| 길 | 칸 머리에 보인 것 |
|---|---|
| 병합 네 칸 | BASE `(merge base) Weld/Unit1.cs` · LOCAL (Mine) `feature/mine` · MERGED `feature/mine` · REMOTE (Theirs) `REMOTE · feature/theirs` — **파일 이름은 BASE 칸에만**, REMOTE 칸은 제목의 말을 되풀이 |
| 두 칸 | BASE `9b064cc · Weld/Unit1.cs` · LOCAL `feature/mine` — 저장하는 칸에 파일이 없음 |
| git mergetool · `--merge` (코드로) | **Mine 칸이 MERGED 칸과 같은 값**(저장할 파일 이름)을 써서, 실제로 읽은 `--local` 임시 파일과 다른 이름이 보였다 |
머지 주석은 `// [김승준] 2026-09-22` 뿐이고 Take Mine 은 주석을 남기지 않았다(로그에만 "정함").
### 고친 것
- **칸 머리** — 칸마다 한 줄 "어느 쪽 · 어느 파일"(`PaneHeaders`). 네 칸: `feature/mine · Weld/Unit1.cs` · `(merge base) Weld/Unit1.cs` · `Weld/Unit1.cs (저장할 파일)` · `feature/theirs · Weld/Unit1.cs`. 두 칸: `3a12077 · Weld/Unit1.cs` · `feature/mine · Weld/Unit1.cs (저장할 파일)`. mergetool · `--merge`: 네 칸이 제 임시 파일 이름(Mine 칸은 새 `MineHeader`). rebase 는 `올릴 자리 …`, cherry-pick · revert 는 `가져오는 커밋 …` · `되돌리는 커밋 …` 에 파일이 붙는다.
- 제목 `LOCAL (Mine)` · `REMOTE (Theirs)` → `LOCAL(Mine)` · `REMOTE(Theirs)`. 네 칸 머리 글씨를 모두 BASE 와 같은 모양(읽으라고 쓰는 글씨)으로.
- **한 줄 · 가운데 줄이기** — WPF 끝 자르기는 파일 이름부터 잘라서, 폭에 맞게 가운데를 `…` 로 줄이는 `MiddleTrim`(앞 조각 굵게는 그대로). 전체는 툴팁.
- **머지 주석 출처** — `[사람] 날짜 출처 설명`(`MergeNote.Build` 에 출처). 출처는 로그와 같은 말(`MergeNames.TheirsSource` · `MineSource` · `BaseSource`): 네 칸 `Theirs(feature/theirs)` · `Mine(feature/mine)`, rebase · cherry-pick · revert `Theirs(커밋 42e68b2)`, 브랜치 3-Way `REMOTE(develop)`, 두 칸 `BASE(3a12077)`(Git 아니면 왼쪽 파일 이름).
- 로그도 같은 말로 — 두 칸 `9행 BASE 반영` → `9행 BASE(af3925e) 반영`, 브랜치 3-Way `7행 develop 반영` → `7행 REMOTE(develop) 반영`. 네 칸 로그는 원래 같은 말이라 그대로.
- **Take Mine 도 주석을 남긴다**(지시서 판정 기준) — 병합의 두 쪽에서 내 것으로 정하면 `Mine(…)` 주석 한 줄. 같은 줄을 또 눌러도 쌓이지 않고(처음 한 번), 되돌리기가 주석 줄까지 뺀다. 브랜치 3-Way 의 "내 원본으로 되돌림" 은 되돌리기라 주석 없음(그대로).
- **무시 조건** — 알아보는 규칙(`[사람] 날짜` 뒤는 따지지 않음)이 원래 출처를 받아들이는 모양이라 `NoteBody` · `LooksLikeNote` · `TailStart` · `CompareRules` · `FolderComparer` · `NoteConflict` · `NoteRows` 는 코드를 바꾸지 않았다. 대신 옛 모양 · 새 모양 · 출처만 다른 모양을 하네스로 굳힘.
### 확인한 것
- 하네스: NoteTest(출처 자리 · 옛 모양 바이트 같음 · XAML 닫는 기호 · 줄 끝 조각 · 무시 키 · 출처만 다른 충돌 자동 해결 두 주석 다 남김 · 코드가 다르면 안 풂) · RulesTest(무시 · 규칙 끄면 차이) · FolderTest(출처만 다름 · 옛↔새 → **같음(머지 주석만 다름)**, 규칙 끄면 다름(코드는 같음)) · MergeSaveTest(출처 = 로그의 말 · 칸 머리 · 가운데 줄이기). 22벌 **1,312건** 통과(1,260 → +52) · 빌드 경고 0 · 오류 0.
- 화면(`f1-verify.ps1` · `f1-trim.ps1`, UIA 만 — 마우스 안 씀):
| 길 | 칸 머리 | 누른 것 → 로그 | 파일에 남은 주석 |
|---|---|---|---|
| 병합 네 칸 | `feature/mine · Weld/Unit1.cs` · `(merge base) Weld/Unit1.cs` · `Weld/Unit1.cs (저장할 파일)` · `feature/theirs · Weld/Unit1.cs` | `101행 Theirs(feature/theirs) 반영` · `802행 Mine(feature/mine) 으로 정함` (Take Mine 한 번 더 → 단추 꺼짐) | `// [김승준] 2026-09-22 Theirs(feature/theirs)` · `… Mine(feature/mine)` — 범례 무시 2 |
| 두 칸 | `af3925e · Weld/Unit1.cs` · `feature/mine · Weld/Unit1.cs (저장할 파일)` | `9행 BASE(af3925e) 반영` | 화면의 주석 줄 `// [김승준] 2026-09-22 BASE(af3925e)`(두 칸은 Save & Commit 뿐이라 커밋하지 않고 화면에서 읽음) |
| `--merge` | `Unit1_LOCAL_4321.cs` · `Unit1_BASE_4321.cs` · `Unit1.cs (저장할 파일)` · `Unit1_REMOTE_4321.cs` | `Theirs(Unit1_REMOTE_4321.cs) 반영` · `Mine(Unit1_LOCAL_4321.cs) 으로 정함` | 같은 말 두 줄 |
| git mergetool (`git -c` 로만 도구 설정 — 설정 파일에 안 남음 확인) | `Unit1_LOCAL_290.cs` · `Unit1_BASE_290.cs` · `Unit1.cs (저장할 파일)` · `Unit1_REMOTE_290.cs` | `Theirs(Unit1_REMOTE_290.cs) 반영` | 같은 말 |
- 칸 머리는 모두 한 줄(높이 21). 117자 파일 이름으로 `--merge`: `WeldSequenceController_Laser…ipeParameterValidation_Unit1_LOCAL_4321.cs` 처럼 가운데가 줄고 끝(쪽 · 파일 이름)이 남음, 툴팁에 전체. 창 폭을 UIA 로 줄이는 것은 이 창에서 먹지 않아 긴 이름으로 대신 봄.
- 회귀: 11차 E-1 병합 길(`e11-e1-merge.ps1 -Way merge`, 마우스 확인 B-1 · 스크린숏만 끔) — 누른 Take 18 = 로그 18, 병합 커밋 본문 26개, 줄끝 · BOM 바뀐 파일 0 · 충돌 표식 0 · class 검사 0, 옛 모양 주석만 다른 2건 자동 해결 그대로. 결과 파일의 Take Mine 줄 위에 `Mine(feature/mine)` 주석이 새로 들어간다(이번 변경).
- 첫 번째 회차는 1) 파일 여러 개 골라 Accept Theirs 가 한 개만 골라져(24 → 23, 20초) 줄 단위로 처음 가 보는 파일들에서 Take 가 꺼져 있었다. 그 파일들을 새로 열어 보면 Take 둘 다 켜짐(`f1-take-probe.ps1`), 두 번째 회차는 1) 이 24 → 9(1초)로 예전과 같아 끝까지 — 시험 도구의 여러 개 고르기가 한 번 빗나간 것으로 본다.
### 알아 둘 것
- Take Mine 주석이 새로 들어가므로 같은 순서로 풀어도 결과 파일이 전보다 주석 줄 수만큼 길다(무시 규칙이 켜져 있으면 차이로 세지 않음). 코드 줄 · 줄끝 · BOM 은 그대로.
- 파일 단위 Accept(충돌 탭)는 여전히 소스에 주석을 남기지 않는다 — 병합 커밋 본문(8차 F-1)이 그 기록이다.
## F-2. 메서드 단위로 골라 반영 · 덩어리 반영의 주석 한 줄 (2026-09-22, 완료)
사용자 결정(2026-09-22): 언어는 **C# + C/C++**, "메서드" 는 **클래스 멤버 전부**(메서드 · 생성자 · 소멸자 · 속성 · 인덱서 · 이벤트 · 연산자 · 필드 · enum — 로컬 함수 · 람다 안이면 감싸는 멤버 통째), 덩어리 반영의 주석도 **첫 줄 위 한 줄로**.
### 고치기 전 — 화면에서 (`f2-probe.ps1`)
- 지시서에는 "덩어리를 반영하면 머지 주석은 덩어리 첫 줄 위에 한 줄" 이라고 적었지만 사실과 달랐다 — 이어진 두 줄 차이에 [이 덩어리 반영] → **주석이 줄마다 하나씩 두 줄**. `AcceptBlock` 이 줄마다 `Apply` 를 불러 줄마다 주석 · 로그 · 되돌리기 한 건씩이었다.
- 메서드 단위 반영은 없었다.
### 만든 것
- **`MemberRanges`**(새 서비스, Roslyn 없음) — 파일 전체를 훑어 주석 · 문자열을 가리고(C# 축자 · 보간 · raw `"""`, C++ raw `R"x(...)x"` · 숫자 구분자 `1'000` · 문자 `'{'`), 중괄호 짝으로 멤버 범위(시작 · 이름 줄 · 끝)를 잡는다.
- `#if` 는 갈래(#elif · #else)마다 `#if` 자리의 상태에서 다시 읽고 `#endif` 뒤에는 첫 갈래 상태로 잇는다 — 여는 줄만 갈린 메서드도 짝이 맞고, 갈래마다 따로 있는 메서드는 따로 잡힌다.
- 멤버 몸통이 아닌 중괄호: 필드 초기화(`= new X { }`) · 람다 · switch 식(`=>` 뒤) · 속성 초기화(`{ get; set; } = 1;`) · C++ 생성자 초기화 목록(`items_{1, 2}`) · 특성 안.
- 시작은 바로 위 설명 주석(`///` · `//`) · 특성 · 감싸는 `#if` 까지. **머지 주석에서 멈춘다**(이력이라 반영으로 지우면 안 됨). 클래스 · 네임스페이스 선언 줄은 어느 멤버에도 들지 않는다. 짝이 틀어져 겹치면 뒤의 것을 버린다(틀린 범위 대신 없음).
- **[이 메서드 반영]**(덩어리 반영 아래) — 캐럿이 놓인 멤버를 반영해 올 쪽(두 칸 BASE · 네 칸 REMOTE)에서 찾아, 그 범위의 행(사이의 내 쪽에만 있는 줄 포함)을 덩어리 반영과 같은 규칙으로 반영. 네 칸은 Take Theirs 와 같아 저쪽이 바꾼 줄 · 충돌 줄만, 내 쪽만 고친 줄은 그대로.
- 단추 아래 한 줄: 잡으면 `Target · BASE 7~17행 · 바뀐 줄 4`, 못 잡으면 까닭(`캐럿이 BASE 쪽 메서드 안에 있지 않습니다` · `C# · C/C++ 파일에서만` · `… 안에 가져올 차이가 없습니다`) — 단추는 꺼지고 툴팁도 같은 말.
- **묶음 반영**(`ApplyGroup`) — 덩어리 · 메서드 모두: 머지 주석은 **한 줄**(메서드는 이름 줄 위, 덩어리는 첫 줄 위, 출처는 F-1 과 같은 말), 로그도 한 줄(`7~11행 메서드 Target BASE(left.cs) 반영 (4줄)` · `10~11행 덩어리 BASE(left.cs) 반영 (3줄)`), **Undo · Redo 한 번에 통째로**(편집에 묶음 번호). 주석만 든 편집(`NoteOnly`)은 "직접 수정" · "편집 N건" 에 세지 않는다. 머지 주석 줄은 건드리지 않는다.
### 확인한 것
- 하네스 **MemberTest**(새, 83건): C# 시험 파일 29개 멤버 · C++ 13개 멤버의 시작 · 이름 줄 · 끝이 표시 주석과 모두 같다(한 줄 메서드 · 중첩 중괄호 · 문자열 · 축자 · 보간 · raw 안 중괄호 · 주석 안 중괄호 · `#if` 로 여는 줄이 갈린 메서드 · 갈래마다 다른 메서드 · 로컬 함수 · 람다 필드 · switch 식 · 속성 초기화 · 이벤트 · 인덱서 · 연산자 · 머지 주석 위 멤버 · 설명 주석 · 중첩 클래스 · C++ 템플릿 · 초기화 목록 · `operator=` · raw 문자열 · 숫자 구분자 · `extern "C"` · `enum class` · `#ifdef`), 기대하지 않은 범위 없음 · 겹침 없음, 이름, 캐럿 행 → 멤버 · 멤버 → 화면 행, 짝이 틀어진 파일에서 멈추지 않음. 23벌 **1,395건**(1,312 → +83) · 플랫폼 24건 · 빌드 경고 0 · 오류 0.
- 화면(`f2-verify.ps1`, `--diff` · `--merge` 로 C# 짝, BOM · CRLF):
| 길 | 결과 |
|---|---|
| 두 칸 · 주석 켬 | 연 직후 단추 꺼짐 + `캐럿이 BASE 쪽 메서드 안에 있지 않습니다…`, [다음 차이] 뒤 `Target · BASE 7~17행 · 바뀐 줄 4` · 켜짐. 반영 → 머지 주석 **1줄**(설명 주석과 선언 줄 사이, 메서드 이름 위) · 범례 반영 4 · 직접 수정 0 · 편집 4건. **Undo 한 번** → 주석 0 · 남은 차이 5줄(처음과 같음), Redo 한 번 → 다시. 저장한 파일: Target 은 BASE 그대로, 다른 메서드 Other 는 내 쪽 그대로 |
| 두 칸 · 주석 끔 | 메서드 반영 저장본 = **같은 네 줄을 하나씩 골라 Accept 한 저장본 = 기대(Target = BASE, 나머지 = 내 쪽, BOM · CRLF 그대로)** — SHA-256 같음 |
| 두 칸 · 덩어리 | [이 덩어리 반영] 세 줄 → 머지 주석 **1줄**, Undo 한 번에 처음으로 |
| 세 칸 `--merge` | `Target · REMOTE 7~18행 · 바뀐 줄 2` → 주석 `Theirs(Station_REMOTE_1.cs)` 한 줄, 저장본 Target = REMOTE(`limit = 150` · `Trace(power);`), 내가 고친 Other(`return 2;`)는 그대로 |
- 회귀: 11차 E-1 병합 길(마우스 확인 B-1 · 스크린숏만 끔) — 누른 Take 18 = 로그 18, 병합 커밋 본문 26개, 줄끝 · BOM 바뀐 파일 0 · 충돌 표식 0 · class 검사 0.
- E-1 의 1) "15개 골라 Accept Theirs" 가 가끔 24 → 23 에서 멈췄다(F-1 · F-2 회차에 모두 12번 중 3번). 따로 재 보니(`f2-multisel.ps1`) 첫 Select 뒤 AddToSelection 이 하나도 안 먹은 것 — 충돌 목록에서 고를 때 앱은 단추 상태만 새로 고치고 F-1 · F-2 가 고친 파일 비교 쪽은 타지 않는다. F-1 전 빌드(`2f36f1b`, 임시 폴더에 꺼내 빌드) 5번 · 지금 빌드 5번 모두 15개 골라짐. 병합 직후 목록이 한 번 더 그려지며 시험 도구가 잡아 둔 항목이 낡은 것으로 보고, **시험 도구가 고른 수를 세어 모자라면 목록을 다시 잡아 다시 고르게** 고쳤다(`d7lib.ps1`, 제품 아님).
### 남은 것 · 알아 둘 것
- [전체 반영](두 칸) · [자동 병합](네 칸)은 지금도 줄마다 주석이 붙는다 — 이번에 정한 것은 덩어리 · 메서드. 같은 규칙(한 줄)으로 맞출지는 정하지 않았다.
- 범위는 반영해 올 쪽(BASE · REMOTE) 글로 잡는다. 그쪽에 없는 메서드(내 쪽에서만 새로 만든 메서드)는 잡히지 않아 단추가 꺼진다.
- XAML · 그 밖의 언어는 단추가 꺼지고 까닭을 말한다(사용자 결정: C# + C/C++).
## F-3. 탐색기에서 폴더 두 개를 [보내기] 로 바로 비교 · 설치 없이 되는지 (2026-09-22, 완료)
### 어떻게 쟀나 — 사용자의 보내기 메뉴는 건드리지 않고
탐색기 [보내기] 는 SendTo 폴더의 바로가기에 고른 항목들을 **떨어뜨리는 것**(바로가기의 IDropTarget::Drop)이다. 같은 셸 길을 부르는 도구(`SendToSim` — 고른 항목의 데이터 개체를 임시 폴더의 바로가기에 Drop)와 받은 명령줄을 적기만 하는 `ArgsEcho` 로 쟀다. 바로가기는 [탐색기 보내기 등록] 과 같은 모양(대상 · 작업 폴더).
| 경우 | 탐색기가 넘긴 것 | 고치기 전 MergeHub |
|---|---|---|
| 파일 2개 | **프로세스 1개에 인자 2개** | 파일 비교로 열림 |
| 폴더 2개 | 프로세스 1개에 인자 2개 | **창 하나에 왼쪽 · 오른쪽 폴더, 폴더 비교까지**(`전체 1개 · 같음 1`) |
| 폴더 1개 | 인자 1개 | 폴더 비교, 왼쪽만 채움(오른쪽은 사용자가 고름) |
| 3개 이상 | 프로세스 1개에 인자 3개 | 앞의 둘만 비교, 셋째는 **말없이 버림** |
| 경로에 공백 | 그대로(`left side`) | 폴더 비교까지 |
| 한글 | 그대로(`왼쪽 폴더`) | 폴더 비교까지 |
| 260자 넘는 경로 | **8.3 짧은 이름**(123자, `ME0359~1\긴폴더~1\…\RIGHTL~1`) | **아무것도 안 열림** — 로그엔 폴더인데 "파일 비교 요청" |
- 탐색기는 여러 개를 **한 번, 프로세스 하나**로 넘긴다 — "두 번 뜬 프로세스를 하나로 모으는 길" 은 필요 없다.
- 긴 경로: net48 에서 짧은 이름을 `Path.GetFullPath` 가 긴 이름(277자)으로 펼친 뒤 못 찾는다(`Directory.Exists` 거짓, `\\?\` 를 붙이면 "잘못된 문자" — `LongPathProbe`). 열려면 플랫폼 실행 파일 설정(긴 경로 스위치)을 바꿔야 한다.
- MergeHub 가 **이미 떠 있을 때** 보내면 창이 하나 더 뜬다(프로세스 2개).
### 정한 것 (2026-09-22 사용자)
- 긴 경로: **열 수 없다고 알린다**(지원은 플랫폼 설정을 바꿔야 해서 하지 않음).
- 3개 이상: **앞의 둘 + 알림**.
- 배포 묶음을 옮겨 바로가기가 옛 자리를 가리키면: **켜질 때 저절로 고친다**.
- 이미 떠 있을 때: **지금처럼 새 창**(하나로 모으려면 플랫폼에 한 번만 실행 · 창 사이 전달이 필요 — 남은 것).
### 고친 것
- `StartupArgs` — 셋째부터는 `Extra`, 앞의 둘 가운데 파일도 폴더도 아닌 것은 `Missing`(있으면 **아무것도 열지 않는다**). 알림 한 줄 `Notice`: `3개를 골랐습니다 - 앞의 둘만 비교하고 1개(Third)는 두었습니다` · `LeftLong 외 1개 - 경로가 277자라 열 수 없습니다(260자가 넘는 경로는 아직 열지 못합니다)` · 그 밖은 `… - 찾을 수 없습니다`. 이름표 붙은 경로(`git difftool` 의 지워진 쪽)는 따지지 않는다.
- `GitWorkspace` — 켤 때 넘어온 것 · 끌어다 놓은 것 모두 같은 말로 로그 한 줄 + 경고 알림. 켤 때는 사용자 이름 창(모달)이 먼저 떠서 그 뒤로 알림이 지나가 버렸다 — **본 창이 활성이 된 뒤에** 띄운다(이름 창을 닫고 0.4초 뒤에 보임).
- `ShellIntegration.RepairIfMoved` — 등록해 둔 사람만, 바로가기 대상이 지금 실행 파일과 다르면 **바로가기 파일만** 다시 쓰고 로그 한 줄(`보내기 바로가기가 옛 자리(…)를 가리켜 지금 실행 파일로 고쳤습니다`). 레지스트리는 손대지 않는다. 이미 맞으면 파일을 건드리지 않는다.
### 설치 파일 없이 되는가
**된다.** 보내기는 `%APPDATA%\Microsoft\Windows\SendTo` 에 바로가기 하나를 두는 것이 전부다(관리자 권한 · HKLM · 설치 관리자 없음). 배포 묶음을 옮기면 켤 때 바로가기가 따라온다. 레지스트리에 닿는 것은 [탐색기 보내기 등록] 을 누를 때 **예전 판이 HKCU 에 남긴 항목이 있으면 지우는 것** 하나뿐이다(새로 쓰지 않음).
### 확인한 것
- 하네스 ArgsTest +16(52건): 3개 → 앞의 둘 · 남은 것 · 알림 글, 2개 · 1개 → 알림 없음, 없는 경로 → 안 열고 그 경로 · "찾을 수 없습니다", 260자 넘는 경로 → 안 열고 "260자", 이름표 붙은 difftool 경로는 안 따짐, 빈 인자, 바로가기 대상 판정(같은 곳 · 대소문자 · 빈 대상 · 지금 실행 파일 모름). 23벌 **1,411건** · 빌드 경고 0 · 오류 0.
- 화면(`f3-sendto.ps1` · `f3-e2e.ps1` · `f3-e2e2.ps1` · `f3-toast.ps1` · `f3-second.ps1`, 바로가기 대상을 실제 MergeHub Debug 로):
- 폴더 2개 · 공백 · 한글 → 창 하나에서 폴더 비교까지(로그 `폴더 비교: 왼쪽 ↔ 오른쪽 · 전체 1개 · 같음 1`). 파일 2개 → 파일 비교.
- 3개 → 폴더 비교(앞의 둘) + 로그 · 알림 `3개를 골랐습니다 - …1개(Third)는 두었습니다`, 알림은 이름 창을 닫은 직후.
- 긴 경로 → 아무것도 열지 않고 로그 · 알림 `LeftLong 외 1개 - 경로가 277자라 열 수 없습니다…`.
- 바로가기 저절로 고침: 사용자의 실제 바로가기를 바이트 그대로 떠 두고 대상을 `D:\옛자리\bin\…` 로 바꿈 → 켜자 지금 실행 파일 · 작업 폴더로 고쳐지고 로그 한 줄, 한 번 더 켜도 파일 그대로 → 원래 파일 **바이트 그대로 되돌림**(원래 대상이 이미 Debug 실행 파일이라 평소 시험으로는 바뀌지 않는다).
- 시험 재료(`%TEMP%\mergehub-f3`, 긴 경로 포함)는 지웠다.
### 사용자가 실제 탐색기에서 찾은 것 — 왼쪽 칸이 기본 저장소로 덮임 (2026-09-22, 고침)
사용자: "폴더 2개를 선택해 보내기에서 머지허브를 켰는데 한개의 폴더만 읽혀 기능이 안된거같아".
- 받은 인자를 파일 로그에 남기게 한 뒤(`켤 때 받은 인자 N개: …`) 다시 해 받아, 떠 있는 프로세스의 명령줄 · 파일 로그 · 폴더 비교 탭 칸을 **읽기만** 했다(업무 폴더의 파일은 열지 않음). 탐색기는 두 폴더를 모두 넘겼고 `StartupArgs` 도 왼쪽 · 오른쪽을 맞게 잡았다. 그런데 **왼쪽 칸이 설정의 기본 저장소 `C:/Users/Ubisam/AppData/Local/Temp/plain-compare`** 였고 결과가 "왼쪽만" 투성이였다.
- 원인: `FolderViewModel` 생성자가 넘어온 요청을 먼저 받아 비교를 시작한 **뒤에** "열어 둔 저장소를 왼쪽 기본값으로" 가 왼쪽을 덮어썼다. 비교는 비동기라 덮인 왼쪽을 읽을 수 있다. 위 시험(`f3-e2e.ps1`)에서는 로그가 맞게 나와 지나쳤다 — 칸 값을 읽는 필터가 기대 경로만 골라 덮인 왼쪽 칸이 안 보였다(시험이 모자랐다).
- 고침: 기본값을 먼저 두고 요청을 나중에 받는다 — 요청이 늘 이긴다. 폴더 하나만 보낸 경우도 같이 고쳐진다.
- 확인(`f3-fix.ps1`, 기본 저장소 설정 그대로): 폴더 2개 → 칸 `…\Left` · `…\Right`, 요약 `전체 3개 · 다름 1 · 왼쪽만 1 · 같음 1`(재료와 같음). 폴더 1개 → 왼쪽 칸 = 보낸 폴더, 오른쪽 빈 칸 + "오른쪽 폴더를 고르고 비교를 누르세요".
- 켤 때 받은 인자를 파일 로그(`D:\UbisamConfig\Log`)에 남기는 한 줄은 그대로 둔다 — 다음에 같은 말이 나오면 무엇이 넘어왔는지 바로 안다. 커밋 `c35280a`.
- 사용자가 같은 두 폴더로 다시 보내기 → "잘되네"(2026-09-22).
### 남은 것
- 이미 떠 있는 창으로 보내기(한 번만 실행 · 창 사이 전달) — 플랫폼 일이라 이번 범위 밖.
- 260자 넘는 경로를 여는 것 — 플랫폼 실행 파일의 긴 경로 설정과 전체 시험이 필요.
- 여러 개를 골랐을 때 **어느 것이 왼쪽인지**는 탐색기가 넘기는 순서를 따른다(도구는 고른 순서대로 넘겼다). 실제 탐색기 창에서 누른 순서와의 관계는 재지 않았다.
## F-4. 툴박스(ACTIONS) 감추기 + 아래 탭 줄 접기 (2026-09-22, 완료 — 플랫폼 배포함)
사용자: F-4 에 더해 "밑에 탭화면도 접을 수 있게 해줘 머지 메인 화면이 더 많이 나와야해".
### 먼저 본 것
- 파일 비교 화면(HomeView) 안에는 아래쪽 탭이 없다. 화면에서 아래에 있는 것은 **플랫폼 창의 탭 줄**(`NavList` — 파일 비교 · Git · 폴더 탭과 화면 잠금 · 바탕화면 · 종료 동작 항목), 1440p 150% 에서 높이 95px. 플랫폼에는 탭 줄 위치(아래 · 왼쪽 · 오른쪽) 설정은 있었지만 접는 기능은 없었다.
- ACTIONS 칸은 "접기" 가 있어도 폭은 그대로였다(4차 C-5).
### 정한 것 (2026-09-22 사용자)
탭 줄은 **얇은 띠 + 펴기 단추 + 키**, **아래 탭 줄만**(위 로고 · 시계 줄은 창 손잡이라 둠), 플랫폼에 넣되 **모든 소비 프로그램이 쓰게 · 기본은 펼침**. ACTIONS 는 **단추 + F9**.
### 만든 것
- **플랫폼**: 탭 줄 끝 동작 항목 **[탭 접기]**(새 아이콘), 접으면 16 DIP 띠 `탭 펴기 · F11`, **F11** 로 접고 폄(잠금 중 제외), 상태는 `UiSettingsService.NavCollapsed` — 그 항목만 파일에 고쳐 씀(`UiSettingsFile`). 왼쪽 · 오른쪽 탭 줄이면 세로 띠. 전체 단추 스타일의 `MinHeight 38` 때문에 띠가 처음엔 57px 로 두꺼워 최소 크기를 0 으로.
- **MergeHub**: 위 상태 바에 **[ACTIONS 감추기 ▶ (F9)]** / **[◀ ACTIONS 보이기 (F9)]**, 단축키 표에 F9(도움말에도 나옴). 감추면 ACTIONS 칸과 앞 구분선 폭을 0 으로 해 코드 칸이 넓어지고, 펴면 **끌어 둔 폭 그대로** 돌아온다(폭을 Style 로 쥐지 않는 까닭 — 구분선을 끌면 로컬 값이 박힌다). 상태는 설정 `ActionsHidden`(설정 화면에는 안 나옴).
### 확인한 것
- 하네스: PlatformCfgTest +7(31건 — 옛 파일 · 망가진 글은 펼침, 접기 항목만 바뀌고 다른 값 · 모르는 항목 그대로), ShortcutTest +3(F9 = ACTIONS, F11 은 머지툴 표에 없음). MergeHub 23벌 **1,414건** · 빌드 경고 0 · 오류 0(플랫폼 Debug · MergeHub Debug).
- 화면(`f4-verify.ps1` — MergeHub Debug 를 임시 폴더에 복사하고 플랫폼 Core · Shell 만 고친 Debug 로 바꿔 띄움, 배포 폴더 · 사용자 Debug 폴더 안 건드림, 화면 설정 파일은 바이트 그대로 되돌림):
| 한 것 | 결과(1440p · 150%) |
|---|---|
| [ACTIONS 감추기] | 코드 칸 두 칸이 각각 **+163~164px**(1083 → 1247 · 985 → 1148), 단추 글 `◀ ACTIONS 보이기 (F9)` |
| 감춘 채 Alt+↓ · Alt+→ · Ctrl+S | 차이로 가서 반영하고 저장됨(파일에 반영 줄 · 주석) |
| F9 | 원래 폭으로 |
| [탭 접기] · F11 | 탭 줄 95px → 띠 24px, 코드 칸 **+71px**(1198 → 1269, 약 2줄). 설정 파일에 `"NavCollapsed": true` 한 항목만 더해지고 나머지 그대로 |
| 띠 [탭 펴기] · F11 다시 | 원래대로 |
| 둘 다 감추고 다시 켬 | 두 상태 모두 남음 |
| 탭 줄 왼쪽 | 세로 띠 24px 로 접히고 폄. 왼쪽 탭 줄에서 코드 칸 자리는 고친 플랫폼 · 지금 플랫폼이 같다 |
### 남은 것
- ~~플랫폼 배포~~ — 사용자 "배포해줘" 뒤 `publish-platform.ps1`(16:22, 경고 0 · 오류 0, 48개 가운데 Core · Shell · README 셋 바뀜). MergeHub Debug 를 다시 빌드해 두 파일 해시가 배포본과 같음, 배포본으로 [탭 접기] → 24px 띠 → 띠로 펴기 확인(`f4-deployed.ps1`, 화면 설정 · MergeHub 설정 바이트 그대로 되돌림).
- F-6(넓게 보기)에서 몇 줄이 늘었는지 1920 × 1080 기준으로 다시 잰다.
## F-5. 코드 글자 크기 조절 (2026-09-22, 완료)
사용자: "모니터 해상도와 관계없이 모든 모니터에 적용될 수 있도록 하면 좋겠는데".
### 먼저 본 것
- 코드 칸은 `Consolas 12` · 줄 높이 20 으로 박혀 있었다 — 행 템플릿 넷 · 편집기 · `CodeLineHeight` 리소스(StaticResource) · `MergeLine.MinimumRowHeight = 20` 상수. 여백도 번호 46(LOCAL 은 표식 6 + 번호 40) + 단추 20 = 66, 단추 높이 14 · 글자 9 로 박혀 있어 글자만 키우면 번호가 잘리고, 줄이면 단추가 줄보다 커지고 행이 20 아래로 줄지 않는다.
- 플랫폼은 PerMonitorV2 라 WPF 단위(1/96 인치)의 크기는 창이 있는 모니터의 Windows 배율에 맞춰 다시 그려진다.
### 정한 것 (2026-09-22 사용자)
**한 값을 모든 모니터에**(모니터 배율은 플랫폼이 맞춤), **코드 칸만**(앱 전체는 이미 UI Setting 화면 배율), **Ctrl+휠 · Ctrl++/- · Ctrl+0 + 설정**, 8~24.
### 만든 것
- `CodeFontMetrics`(새) — 글자 크기 하나에서 줄 높이 · 여백 · 번호 칸 · 좁은 번호 칸 · 표식 · 단추 칸 · 단추 높이 · 단추 글자를 같은 비율로. 12 에서 지금 값과 정확히 같다(20 · 66 · 46 · 40 · 6 · 20 · 14 · 9).
- 화면: 이 값들을 **DynamicResource** 로(행 템플릿 넷 · 편집기 · 줄 컨테이너 · 편집기 여백), `ApplyCodeFont` 가 한 벌로 갈아 끼운다. `MergeLine.MinimumRowHeight` 를 설정할 수 있는 값으로 바꾸고 바뀌면 모든 행을 다시 잰다.
- 줄 번호에 코드와 같은 줄 높이 규칙(`LineHeight` · `BlockLineHeight`) — 원래 번호가 코드보다 몇 px 위에 떠 있었는데(12 에서 약 6px), 글자를 키우면 그만큼 커져 24 에서 눈에 띄었다. 이제 기준선이 같다.
- 설정 "보기 > 코드 글자 크기 (8~24, 기본 12)"(`CodeFontSize`). 바꿀 때마다 저장, 설정 화면에서 바꾸고 돌아오면 따라감. 단축키 Ctrl+= · Ctrl+숫자판 + · Ctrl+- · Ctrl+숫자판 - · Ctrl+0 · Ctrl+숫자판 0, Ctrl+휠. 상태 줄 커서 위치 옆에 기본이 아닐 때만 `글자 14`(누르면 기본).
### 확인한 것
- 하네스 **FontTest**(새, 110건 — 8~24 모든 크기에서 여백 합 · 단추 ≤ 줄 · 번호 다섯 자리 · 정수 줄 높이, 끝에서 멈춤, NaN · 0 은 기본) · ShortcutTest +14(85건 — 새 키 · 수식키 없는 +/- 는 편집기 몫). 24벌 **1,538건** · 빌드 경고 0 · 오류 0.
- 화면(`f5-verify.ps1`, 1440p · 150%, 코드 칸만 창 그림(PrintWindow)으로 — 다른 창이 찍히지 않게):
| 크기 | 두 칸 행 높이(BASE · LOCAL) | 네 칸 행 높이(Mine · BASE · REMOTE · MERGED) | 여백 단추 |
|---|---|---|---|
| 8 | 19 · 20 번갈아(13 DIP × 1.5) — 두 칸 같음 | 넷 모두 같은 순서로 19 · 20 | 행 안 |
| 12 | 30 | — | 행 안 |
| 24 | 60 | 넷 모두 60 | 행 안 |
- 상태 줄 `글자 8` · `글자 24`, 12 는 비움. 그림: 줄 번호 · 여백 단추 · 코드 · 편집기 글자가 행마다 맞음(24 에서 번호가 뜨던 것을 고친 뒤 다시 찍음).
- 키 시험 — 처음 두 번은 시험 도구가 앱 창을 앞으로 올리지 못했다(화면이 꺼져 있었다). 사용자가 화면을 켠 뒤(`f5-keys2.ps1`, 1440p · 150%):
- **Ctrl+휠**: 위 한 칸 12 → 13(행 30 → 33px) · 아래 한 칸 → 12 · 위 → 13, 매번 설정에 저장, **껐다 켜도 13**.
- 편집기에 포커스가 있으면 **Ctrl+=** 13 → 14(행 34px) · **Ctrl+-** → 13 · **Ctrl+0** → 기본(12, 행 30px), 매번 저장.
- **켜자마자는 포커스가 아무 데도 없어**(`FocusedElement` 빈 값) Ctrl+= 뿐 아니라 F9 같은 **다른 단축키도 듣지 않는다** — F-5 가 아니라 원래 있던 동작(코드 칸을 한 번 누르면 된다). 고칠지는 사용자에게 물음.
### 이어서 고친 것 — 켜자마자 단추키가 안 듣던 것 (2026-09-22 사용자 "응 고쳐줘")
- `HomeView.FocusEditorWhenIdle` — 화면이 처음 뜰 때 · 파일 비교 탭으로 돌아올 때 · 창이 활성이 될 때, 키보드 포커스가 **비어 있거나 이 화면 밖**(아래 탭 줄 등)이면 편집기로 준다. 이 화면 안(찾기 칸 · 가는 줄 칸 등)이나 **다른 창**(대화상자)에 있는 포커스는 건드리지 않는다. 켤 때는 사용자 이름 창이 먼저 떠 있으므로 논리 포커스만 정해 두고, 본 창이 활성이 되면 WPF 가 거기로 돌려준다.
- 확인(`f5-focus.ps1`): 켠 직후 아무 데도 안 누르고 포커스 `LocalEditor` · F9 들음, Git 탭(포커스는 탭 줄 항목)에 갔다 돌아오면 `LocalEditor` · F9 들음, Ctrl+F 뒤 `FindBox` 가 2초 뒤에도 그대로. 빌드 경고 0 · 오류 0.
- 회귀: 11차 E-1 병합 길(마우스 확인 B-1 · 스크린숏만 끔) — 1) 24 → 9 · 누른 Take 18 = 로그 18 · 병합 커밋 본문 26개 · 줄끝 · BOM 바뀐 파일 0 · 충돌 표식 0 · class 검사 0.
- 첫 회차는 "다음 충돌 안 보임" 으로 멈췄다 — 사용자가 그 사이 직접 MergeHub 를 켜 글자 9 · ACTIONS 감춤으로 바꿔 두었고, 시험 도구는 설정을 그대로 썼다. 시험 도구(`d7lib.ps1` `WithSettings`)가 **시험하는 동안만** ACTIONS 보임 · 글자 12 로 두고 끝나면 바이트 그대로 되돌리게 고쳤다(제품 아님).
- 같은 때 시험 스크립트가 사용자가 켜 둔 MergeHub 를 강제로 껐다(16:55). 앞으로 시험 전에 떠 있는 창을 보고, 있으면 먼저 묻는다.
## F-6. 글자를 되도록 많이 보이게 — 줄 간격 좁힘 (2026-09-22, 완료)
지시서는 "한 단추로 넓게 보기" 를 방향으로 적었지만, 사용자는 **넓게 보기라는 기능이 아니라 넓게 보기 위한 방향을 물은 것**이었다. 그래서 방법마다 1920 × 1080 에서 몇 줄이 느는지 재서 드렸고, 사용자가 "**줄 간격 줄여주고 선택 사항은 만들지 마**" 로 정했다.
### 잰 것 (`f6-measure.ps1` — 두 번째 모니터 1920 × 1080(WPF 단위, 물리 2880 × 1620 · 150%)에 최대화, 글자 12)
| 방법 | 두 칸 | 네 칸(위 세 칸 · 아래 MERGED) | 상태 |
|---|---|---|---|
| 고치기 전 | **40줄** | 17 · 18 | — |
| 아래 탭 줄 접기(F11) | 42 | 18 · 19 | 이미 있음(F-4) |
| 글자 12 → 10(Ctrl+-) | 47 | 20 · 22 | 이미 있음(F-5) |
| 줄 간격 좁힘(줄 높이 20 → 16) | 약 50 | 22 · 23 | 새로 |
| 칸 제목 두 줄 → 한 줄 | 약 +0.7 | 약 +1.4 | 새로(안 함) |
| 색 범례 줄 감추기 | 약 +1 | 약 +1 | 새로(안 함) |
| ACTIONS 감추기(F9) | 0(칸 폭 +163px 씩) | 0 | 이미 있음(F-4) |
### 고친 것
- 줄 높이를 글자의 **1.67배 → 1.33배**로(`CodeFontMetrics`, 12 → 16 · 8 → 11 · 24 → 32). 켜고 끄는 단추 · 설정은 만들지 않았다(사용자 결정). 여백 · 번호 칸 · 단추 비율은 그대로.
- `MergeLine.MinimumRowHeight` · `CodeLineHeight` 기본값 · 화면의 줄 높이 대체값을 16 으로.
- 북마크 표식(위아래 여백 3 + 높이 14 = 20)이 16 줄을 넘으므로 표식 높이도 줄에 맞춤(`BookmarkHeight` = 줄 높이 − 6, 12 에서 10).
### 확인한 것
- 하네스 FontTest 144건(+34 — 8~24 모든 크기에서 줄 높이 1.3~1.4배, 북마크가 줄 안, 기본 12 의 값). 24벌 **1,572건** · 빌드 경고 0 · 오류 0.
- 화면(1920 × 1080, 글자 12): **두 칸 40 → 50줄**(탭 줄까지 접으면 53), **네 칸 17 · 18 → 22 · 23줄**(탭 줄 접으면 23 · 24). 행 높이 24px(16 DIP × 1.5).
- 8 · 12 · 24 (`f5-verify.ps1`, 한글 주석 든 코드): 두 칸 행 높이 서로 같음(8 은 16 · 17 번갈아, 12 는 24, 24 는 48), 여백 단추 행 안, 그림에서 한글이 잘리거나 위아래 줄이 겹치지 않고 줄 번호 · 편집기 커서가 행에 맞음.
- 회귀: 11차 E-1 병합 길 — 24 → 9 · 누른 Take 18 = 로그 18 · 병합 커밋 본문 26개 · 줄끝 · BOM 바뀐 파일 0 · 충돌 표식 0 · class 검사 0.
## F-7. 대형 모니터에서 오른쪽(종료 · ACTIONS)이 잘림 (2026-09-22, 완료 — 플랫폼 배포함)
사용자 증상: "대형 모니터에서 화면이 짤리는 현상, 오른쪽이 — 프로그램 종료 및 액션 패널이 안 보였다". 사용자 바람: "모든 해상도의 배율에서 화면이 정상적으로 출력되길".
### 지금 화면이 무엇에 맞춰져 있나 (사용자 물음에 답한 것)
특정 해상도에 맞춘 것이 아니다 — 칸 폭은 비율로 늘고 준다. 플랫폼 창 최소 크기 1280 × 720(WPF 단위 — 100% 1280×720 · 150% 1920×1080 · 200% 2560×1440 px), 화면이 그보다 작으면 최소 크기를 작업 영역에 맞춰 줄인다. 모니터마다 배율이 달라도(PerMonitorV2) 그 모니터 배율로 다시 그리고 최대화 크기도 그 모니터로 잡는다.
### 가려낸 것
- 종료(플랫폼 탭 줄 오른쪽 끝)와 ACTIONS 가 **함께** 안 보였다 → 안쪽 화면이 넓은 것이 아니라 **창이 모니터 밖으로 나간 것**.
- 켤 때 기억한 창 모드 자리를 되살리는 `PreparePlacement` 가 모니터에 200×50px 만 걸쳐도 "보인다" 로 보고 그대로 놓았다. 모니터 안으로 밀어 넣는 `KeepInside` 는 모니터를 옮길 때만 썼다.
### 재현 (`f7-repro.ps1`, 창 자리 파일에 창 모드 자리를 넣고 켬)
| 경우 | 고치기 전 | 고친 뒤 |
|---|---|---|
| 창 모드, 주 모니터(2560) 오른쪽을 넘는 자리 1200~3400 | **창 1200~3400 · 840px 넘침**, 종료 3400 · Accept 3366 · 시계 · 단축키 등 75개 요소가 모니터 밖 | 창 360~2560(크기 그대로 밀림), 종료 2560 · Accept 2526, 넘친 것 없음 |
| 같은 자리를 최대화로 | 모니터에 맞음 | 같음 |
| 가장 좁은 창(1280 DIP) × 화면 배율 80 · 100 · 120 · 133%, 파일 비교 · Git · 폴더 | 넘친 것 없음 | 같음 |
- 안쪽 화면 배치(칸 최소 폭 · 도구 줄)는 가장 좁은 창과 가장 큰 화면 배율에서도 넘치지 않았다 — 원인이 아니다.
### 고친 것 (플랫폼)
- `WindowPlacementMath.FitToMonitors` — 가장 많이 겹치는 모니터의 작업 영역 안으로 밀고 크면 줄인다. `ShellWindow.PreparePlacement` 가 기억한 자리를 이것으로 고쳐 놓는다.
- PlacementTest +8(45건) · 플랫폼 Debug 빌드 경고 0 · 오류 0. MergeHub 쪽 코드 변경 없음.
### 남은 것
- ~~플랫폼 배포~~ — 사용자 "배포해줘" 뒤 `publish-platform.ps1`(경고 0 · 오류 0, Core · Shell · README 셋 바뀜). MergeHub Debug 다시 빌드 → 두 파일 해시가 배포본과 같음, 배포본으로 같은 재현 → 창 360~2560 · 종료 2560 · Accept 2526 · 넘침 0.
- 배율이 **서로 다른** 모니터 사이를 옮길 때는 이 PC(두 모니터 모두 150%)에서 재현할 수 없다. 대형 모니터에서 같은 증상이 또 나면 그 모니터의 해상도 · 배율 · 창 모드였는지를 알려 받는다(창 자리 파일 `ShellWindowPlacement.json` 한 벌이면 그대로 재현할 수 있다).
## F-8. 저장한 뒤에도 되돌리기 · 푼 파일을 다시 충돌로 (2026-09-22, 완료)
사용자 결정: "되돌리면 다시 충돌로", "커밋은 두고 화면만 되돌림", [전체 되돌리기] 는 "파일을 연 시점", 병합 중 [Save] 는 지금 흐름 그대로 두고 **Git 탭 충돌 목록에 [다시 충돌로]**("1번으로 — UNDO, REDO 기능이 중요해"). [다시 충돌로] 는 LibGit2Sharp 에 길이 없어 **이 단추만 git.exe 를 부름**(사용자 결정).
### 고치기 전 (재현 `f8-probe.ps1` · `f8-git.ps1`)
| 경우 | 고치기 전 |
|---|---|
| 두 파일 비교: 반영 → [저장] | Undo · Redo **꺼짐** — 저장하는 네 길(저장 · 저장 후 커밋 · 해결로 저장 · 파일 바꿀 때 "저장하고 계속")이 모두 목록을 비웠다 |
| Git: 반영 → [Save & Commit] | Undo 꺼짐 — 저장 때 비우고, 이어지는 새로고침이 **새 HEAD 기준으로 줄을 다시 세우며** 또 비움(9차 A-2 로 BASE 가 새 HEAD 를 따라감) |
| 편집기에 글자를 치거나 비교 규칙(Aa · 공백 · 필터)을 바꿈 | 재정렬이 목록을 통째로 비워 그 전 Accept 들을 Undo 할 수 없었다 |
| "저장할 것 있음" | 되돌리기 목록이 비었는지로 판단 — 저장 뒤 목록을 두면 저장 단추가 켜진 채로 남으므로 비울 수밖에 없었다 |
### 고친 것
- `SaveBaseline`(새) — 편집 칸의 두 기준 글: **파일을 연 시점**(전체 되돌리기가 가는 곳 · 직접 고친 줄을 가리는 기준)과 **마지막 저장**. "저장할 것 있음" = 지금 글 ≠ 마지막 저장(또는 스타일 맞춤 대기). 저장 뒤 Undo 하면 "있음", Redo 로 저장 지점에 오면 "없음".
- 저장 · 저장 후 커밋 · "저장하고 계속" 은 목록을 **두고** 저장 지점만 옮긴다(`MarkSaved`). 병합 중 [Save](해결로 저장)는 다음 충돌로 넘어가므로 예전처럼 비움.
- **글 단위 이력** — 줄 단위 목록은 줄 객체를 가리켜 줄을 다시 세우면 못 쓴다. 다시 세우기 **전에** 각 지점의 전문을 적어 옮긴다(`FlattenHistory` — 한 묶음이 한 걸음, 덩어리 · 메서드 반영은 11차 F-2 대로 한 번). 쓰는 곳: 커밋 뒤 같은 파일 · 같은 글을 다시 읽을 때, 비교 규칙을 바꿀 때, 편집기에서 친 뒤 재정렬할 때. 글 단위 걸음은 줄을 다시 세워 되돌린다. 걸음마다 파일 전문 한 벌이라 한쪽에 300걸음까지 둔다.
- **편집기에서 친 것도 한 걸음** — 치는 동안 화면을 바로 보이려고 줄 객체를 먼저 고치는데(되돌리기 목록 밖) 그 줄의 원래 글을 적어 두었다가 재정렬 때 한 걸음으로 올린다.
- 커밋 뒤 새로고침은 한 번에 **두 번 읽고 그 사이 화면을 잠깐 비운다**(고른 커밋 바뀜 · 목록 다시 세우기). 한 번 쓰고 버리는 표시로 했더니 두 번째가 비웠다 → 조건만 본다(같은 파일 · 두 칸 · 충돌 아님 · 읽은 글이 화면 글과 같음 — 파일의 CRLF · 끝 줄바꿈은 같은 글로 봄). 화면을 비울 때는 이력 · 두 기준을 맡겨 두고, 같은 파일 · 같은 글을 다시 실으면 되찾는다.
- [전체 되돌리기] — 저장한 뒤에도 켜짐(연 시점과 다르면), 파일을 연 시점으로. 이미 저장했으면 확인 창이 "저장한 파일 · 커밋은 그대로, 화면만 돌아감 — 넣으려면 다시 저장, 되돌리기 목록은 비워짐" 이라고 알린다.
- 요약 줄 — 저장한 지점이면 `· 저장됨` 을 붙인다(`남은 차이 1줄 · 편집 1건 · 되돌리기 0건 · 저장됨`).
- **Git 탭 충돌 목록 아래 "이번 병합에서 푼 파일 N개" + [다시 충돌로]** — 병합 중이고 푼 파일이 있을 때만 보인다. 목록 = 처음 충돌 목록(`.git/MERGEHUB_CONFLICTS`) ∩ git 의 해결 기록(REUC) − 지금 충돌(`MergeConflictRecord.Resolved` — REUC 만 보면 저절로 병합된 파일도 들어온다, 6차 C-1). 누르면 먼저 묻고(저장해 둔 해결 내용이 충돌 표시 글로 바뀜), `git checkout -m -- 파일` 로 세 벌 · 충돌 표시를 되살린 뒤 저장소를 다시 열고(열어 둔 객체는 인덱스를 다시 안 읽어 충돌이 0 으로 보였다 — `UnresolveProbe` 로 확인), 해결 기록(8차 F-1)에서 그 파일을 뺀다(`MergeResolutionLog.Forget`). git.exe 가 없으면 "Git 이 설치되어 PATH 에 있어야" 로 알린다.
### 확인
| 경우 | 결과 |
|---|---|
| 두 파일: 반영 → 저장 → Undo → Redo (`f8-probe.ps1`) | 저장 뒤 Undo 켜짐 · 저장 꺼짐 · `저장됨` → Undo: 저장 켜짐 · 파일은 그대로 → Redo: 저장 꺼짐 · `저장됨`. 전체 되돌리기 켜짐 |
| Git: 반영 → Save & Commit → Undo → Redo (`f8-git.ps1`) | 커밋됨(워킹 변경 0) · 요약 `차이 없음 · 편집 1건 · 되돌리기 0건 · 저장됨` · Undo 켜짐 → Undo: 새 HEAD 대비 `남은 차이 1줄` · 커밋 · 파일 그대로 → Redo: `차이 없음 · 저장됨` |
| 반영 → 편집기에 침 → 규칙(Aa) 바꿈 → Undo ×3 → Redo ×3 (`f8-typing.ps1`) | 친 것이 한 걸음(편집 2건), 규칙을 바꿔도 이력 그대로, Undo 가 친 것 → Accept 차례로, Redo 가 거꾸로 |
| 병합: Accept Theirs 로 푼 파일 · 3분할 [Save] 로 푼 파일을 [다시 충돌로] (`f8-reopen.ps1`, C1Repo) | 푼 파일 목록에 나옴 → 확인 창 → git `UU` · 충돌 표시 줄 되살아남 · 충돌 23 → 24 · 충돌 목록에 다시 · 푼 파일 목록에서 빠짐 · 해결 기록에서 빠짐. 병합 직후 머지 주석만 달라 저절로 푼 2개도 목록에 나옴(MergeHub 가 푼 것이라 맞음) |
| 병합 한 길 회귀(`e11-e1-merge.ps1`) | 충돌 24 · 풀기 26 · 누른 Take 18 = 로그 18 · 병합 커밋 부모 둘 · 줄끝/BOM/인코딩 다른 파일 0 · 충돌 표식 0 · 305초 |
- 하네스 MergeSaveTest **+21건(296 → 317)** — 두 기준 글(연 직후 · 고침 · 저장 · 저장 뒤 Undo/Redo · 전체 되돌리기 대상 · 스타일 대기 · 다시 열기 · null), 줄바꿈만 다른 글, 맡겨 둔 기준 되찾기, 푼 파일 목록(지금 충돌 · 저절로 병합 · 해결 기록 없는 파일 · 겹친 이름), 해결 기록에서 한 파일 빼기. 먼저 넣고 시작. Debug 빌드 경고 0 · 오류 0.
- 시험 도구: `d7lib` 의 머지 주석 줄 패턴이 F-1 뒤 주석 모양(날짜 뒤 출처)을 못 알아봐 "기준 대조 → 다름" 이 F-2 부터 나고 있었다 — 패턴을 고침(앱 문제 아님).
### 남은 것
- 세 칸(3-Way) 화면에서 규칙을 바꾸거나 치면 이력은 지키지만, 커밋 뒤 다시 읽기처럼 **파일에서 새로 읽는** 경우는 두 칸일 때만 지킨다(충돌 · 3-Way 는 저장이 곧 다음 충돌로 넘어감).
- [다시 충돌로] 는 MergeHub 로 시작한 병합만(처음 충돌 목록이 있어야). rebase · cherry-pick · revert 중에는 보이지 않는다.
## F-9. 저장할 때 백업 — 원본을 남기기 (2026-09-22, 완료)
사용자 결정: 방법은 **설정 "저장할 때 백업"**(기본 끔, [다른 이름으로 저장] 은 두지 않음), 자리는 **한곳에 모아 날짜 이름**, **저절로 지우지 않음**, 범위는 **모든 저장**(병합 중 Save 포함).
### 고치기 전
- 저장은 늘 원본을 덮어썼다. `.bak` 도 다른 이름으로 저장도 없었다. 덮어쓰는 자리 여섯: 두 파일 비교 [저장](`WritePlainFile`) · mergetool 저장(같은 함수) · Git [Save & Commit] · 병합 중 [Save] · Git 탭 파일 단위 Accept Mine/Theirs/Both · 폴더 비교 복사(파일 · 폴더).
### 고친 것
- 설정 `저장 > 저장할 때 백업` (`MergeHubSettings.BackupBeforeSave`, 기본 끔).
- `FileBackup`(새) — 쓰기 직전 지금 파일을 **바이트 그대로** 복사한다. 자리 `{설정 폴더}\Backup\MergeHub\2026-09-22\D\원래\폴더\A.20260922-210501.cs` — 날짜 폴더 아래 드라이브와 원래 폴더 모양 그대로, 이름에 날짜-시각(확장자는 끝이라 두 번 누르면 원래 프로그램으로 열림). 같은 초에 또 저장하면 `-2`. 네트워크 경로는 `UNC\서버\공유`. 240자를 넘으면 `_긴경로\폴더해시8_A.날짜-시각.cs`. 날짜 폴더마다 `목록.txt`(시각 · 원본 · 백업). 새로 만드는 저장(파일이 없음)은 백업할 것이 없다.
- 백업을 **못 만들면 저장하지 않는다** — "백업을 만들지 못해 저장하지 않았습니다 - 까닭 (설정을 끄면 백업 없이 저장)". 백업을 약속한 설정이라 원본을 그냥 덮지 않는다.
- 여섯 자리에 모두 붙임. Git 이 쓰는 길(병합 중 Save · 파일 단위 Accept · 머지 주석만 다른 충돌 자동 풀기)은 `IGitService.BeforeOverwrite` 로, 폴더 복사는 `FileOps.CopyDirectory` 의 덮어쓰기 직전 알림으로.
- 로그에 `백업: 원본 → 백업` 한 줄. 파일 비교 ACTIONS 저장 단추 밑 **[백업 폴더 열기]**(켰을 때만).
- 원본 폴더 · Git 저장소 밖에 모이므로 폴더 비교 · 변경분 추출 · git status 에 섞이지 않는다(따로 빼는 규칙이 필요 없음).
### 확인 (`f9-ui.ps1`, 설정을 켜고)
| 경우 | 결과 |
|---|---|
| 두 파일 비교 반영 → [저장] | 백업 1개 = 저장 전 파일(SHA-256 같음), 원본은 바뀜, 자리 `…\2026-09-22\C\Users\…\mergehub-f9\right.20260922-210729.cs`, 원본 폴더에는 `left.cs · right.cs` 뿐 |
| 저장 → Undo(F-8) → 다시 [저장] | 백업 2개, 두 번째 = 첫 저장 뒤 파일. `목록.txt` 두 줄 |
| 병합 직후 | 머지 주석만 다른 충돌 자동 풀기 2개도 백업 |
| Git 탭 Accept Theirs | 백업 = 충돌 표시 든 파일(해시 같음, `<<<<<<<` 줄 있음) |
| 3분할 [Save] | 백업 = 저장 전 파일 |
| 저장소 안 새 파일 | untracked 0 |
| 로그 | `백업: …\Motion\Unit0.cs → {백업}\2026-09-22\…\Unit0.20260922-211310.cs` |
| 끈 채(기본) | F-1~F-8 시험 내내 저장을 많이 했지만 백업 폴더가 생기지 않았다 · 하네스로도 확인 |
- 하네스 MergeSaveTest **+18건(317 → 335)** — 이름 규칙(날짜-시각 · 같은 초 `-2` · 점 파일 · 확장자 없음), 자리(드라이브 · UNC · 긴 경로), 끄면 아무것도 안 함, 켜면 바이트 그대로 · 원본 그대로, 없는 파일 · 빈 경로, 목록.txt, 알림, 원본 폴더에 안 생김, 못 만들면 던짐. FolderTest **+7건(81 → 88)** — 폴더 복사가 덮기 직전에만 알림 · 새 파일은 안 알림 · 알림이 던지면 안 덮음. Debug 빌드 경고 0 · 오류 0.
- 시험이 만든 백업 폴더(`D:\UbisamConfig\Backup`, 시험 전에 없었음)는 시험 끝에 치웠다.
### 남은 것
- 백업은 저절로 지워지지 않는다(사용자 결정) — 오래 쓰면 쌓인다. [백업 폴더 열기] 에서 날짜 폴더째 지우면 된다(매뉴얼에 적는다).
- 폴더 비교의 [삭제] 는 저장이 아니라 백업하지 않는다(지금처럼 먼저 묻는다).
## F-10. 칸마다 자기 줄 · 바뀐 덩어리를 잇는 선 (Araxis 식) (2026-09-22, 완료)
사용자 요청: 지시서의 "옮긴 덩어리를 선으로" 에서 넓혀 "Araxis 머지툴처럼 라인 바뀐 위치 UI 표시를 중점으로"(Araxis 화면 두 장). 물어 정한 것: **지금 보기를 Araxis 식으로 바꿈**(두 보기를 두지 않음), **3분할에서도 잇는 선**, 3분할은 **위 세 칸(LOCAL(Mine) | BASE | REMOTE) 사이만 잇고 MERGED 는 따라 스크롤**.
### 고치기 전
- 모든 칸이 **정렬 행 하나**(`Lines`)를 같은 번호로 보이고, 한쪽에 없는 줄 자리에는 빈 줄(필러)을 채워 같은 줄을 같은 눈높이에 두었다. 편집기에도 빈 줄이 들어가 저장 · 재정렬 전에 걷어냈다(`StripFillers`).
- 자리 옮김은 줄 색과 "N행으로 옮김" 글로만 알렸다 - 어디로 갔는지는 두 칸을 눈으로 따라가야 했다.
### 고친 것
- **속은 그대로, 보이는 것만** — 정렬 행 `Lines` 는 그대로 두고(차이 · 반영 · 되돌리기 · 차이 이동 · 머지 주석 · 미니맵이 그대로 쓴다) 칸마다 **그 칸에 있는 줄만** 걸러 보인다(`ListCollectionView` 필터, LOCAL 은 줄이 생기고 사라지면 바로 따라감). 칸마다 자기 행 높이(워드랩에서 서로 맞추지 않음).
- **편집기에서 빈 줄을 없앰** — 편집기 글이 곧 파일 글. 편집기 줄 ↔ 정렬 행은 `EditorSnapshot.Rows` 표로 옮긴다(캐럿 · 친 줄 바로 보이기 · 차이 이동). `StripFillers` 는 쓸 곳이 없어져 지웠다(매 재정렬마다 돌던 줄 정렬 계산도 없어짐). 상태 줄 `N행` 이 이제 파일 줄 번호 그대로다.
- **스크롤 맞추기** — 한 칸을 굴리면 나머지가 **가운데 기준**으로 따라온다(`PaneMapping`). 두 칸에 함께 있는 줄의 가운데를 짝점으로 삼아 그 사이는 비율로 잇는다 - 같은 줄이 이어지는 곳은 1:1, 한쪽에만 있는 덩어리를 지날 때는 한쪽이 경계에 멈춰 있고 다른 쪽만 지나간다(한 번에 튀지 않음).
- **잇는 선** — 칸 위에 겹친 투명한 그림판(`PaneLinkOverlay`, 마우스 통과)에 바뀐 덩어리마다 두 칸 안의 위 · 아래 경계선과 틈의 사다리꼴을 그린다. 한쪽에 없는 덩어리는 그 칸에 굵은 선 하나(들어갈/빠진 자리). 색: 추가 파랑 · 삭제 빨강 · 수정 주황 · 충돌 분홍 · 상대만 바꿈(REMOTE) · 직접 수정/반영. 자리 옮김은 떠난 자리(BASE)에서 옮겨 간 자리(LOCAL)까지 청록 점선 곡선(두 칸 비교). 칸 사이 틈 4 → 48px(끌면 구분선처럼 폭이 바뀜).
- 칸 좌표는 **그려진 줄이면 그 자리**, 화면 밖이면 보이는 첫 줄에서 모델 높이로 어림(`PaneLinks.Tops`).
- **지운 줄 되살리기 [>]** — 예전에는 LOCAL 의 빈 줄에 달려 있었다. 이제 BASE 칸 그 줄 끝에 단다(Araxis 처럼).
- **차이 이동은 그 자리를 가운데로** — 예전엔 캐럿 줄을 위쪽에 두었는데, LOCAL 에서 지운 덩어리로 가면 지운 줄이 화면 위로 벗어났다. 이제 그 줄 윗변(= 지운 자리)을 가운데로 → 지운 줄들이 BASE 칸 가운데에 온다.
- 3분할: 위 세 칸 사이 두 틈에 같은 선. 충돌 줄은 칸마다 줄 번호가 달라도(Mine 36 · BASE 31 · REMOTE 31) 같은 높이로 이어진다.
- 계산은 화면 없이 검사: `PaneLinks`(윗변 · 덩어리) · `PaneMapping`(가운데 짝점 · 비율) · `EditorLayout.LineOfRow/RowOfLine`.
### 확인 (화면)
| 경우 | 결과 |
|---|---|
| 메서드 옮김 · 두 줄 추가 · 한 줄 수정 · 두 줄 삭제 (`f10-shot.ps1`) | 칸마다 자기 줄(BASE 49 · LOCAL 49), 추가 BASE 18 경계 → LOCAL 18~19, 삭제 BASE 25~32 → LOCAL 26 경계, 수정 37 ↔ 31, 옮긴 Gamma 청록 점선 |
| 400줄 · 60줄 추가 · 40줄 삭제 스크롤 (`f10-sync.ps1`) | 가운데 줄: 0% 25/25 · 20% 99/99 · 추가 지날 때 BASE 101 멈춤 · LOCAL 새 줄 36 · 55% 169/169 · 65% 206/206. BASE 를 굴려 삭제를 지날 때 LOCAL 250 멈춤 |
| 다음 차이 → 삭제 덩어리 | BASE 251~290 가 칸 가운데, LOCAL 310/311 사이 빨간 선 |
| BASE 칸 [>] (지운 줄 되살리기) · Undo | 편집기 420 → 422줄(줄 + 머지 주석) → Undo 420 |
| 3분할 병합 충돌(C1Repo · 새 시험 저장소) | 세 칸 사이 선, 내가 넣은 5줄 → BASE 경계로 비스듬히(파랑), 상대가 지운 5줄 BASE 40~44 → REMOTE 경계(빨강), MERGED 따라 스크롤 |
| 워드랩 켬 | 세 줄로 접힌 LOCAL 31 ↔ BASE 37 사다리꼴이 덩어리 위아래에 붙음 |
| 회귀 | 병합 한 길(e11-e1-merge) 충돌 24 → 26 풀기 · Take 18 = 로그 18 · 기준 대조 같음 · 303초. F-8 저장 · 입력/규칙 · Save & Commit 되돌리기 모두 전과 같음 |
- 하네스 **PaneLinkTest 새로 17건**(윗변 · 덩어리 · 같은 칸 1:1 · 추가/삭제 사이 비율 · 단조 · 워드랩 · 공통 줄 없음 · 빈 칸), **EditorTest 14 → 18**(빈 줄 없이 LOCAL 줄만 · 행 ↔ 줄 표 — 옛 "빈 줄 넣고 걷어내기" 검사는 그 동작이 없어져 새 규칙으로 바꿈). MergeSaveTest · AlignTest · RulesTest · MemberTest · MoveTest · NoteTest 그대로 통과. Debug 빌드 경고 0 · 오류 0.
- 처음 그렸을 때 선이 안 보였다(그림판이 칸의 조상이 아니라 좌표 변환이 예외 → `TransformToVisual`). 스크롤 직후 LOCAL 표시층이 늦게 구르는 사이 옛 자리로 선이 한 번 그려졌다 → 어느 칸이 굴러도 다시 그림. 3분할 LOCAL(Mine)|BASE 에서 "내가 넣은 줄" 이 삭제 색으로 → BASE 가 오른쪽인 짝의 방향을 따름.
### 남은 것
- 옮김 점선은 두 칸 비교만(3분할은 옮김을 찾지 않는다).
- 워드랩을 켜면 화면 밖 줄의 높이는 어림이라, 긴 파일을 멀리 건너뛴 직후에는 가운데 맞춤이 몇 줄 어긋날 수 있다(그려지면 바로잡힘 — 예전 방식에도 있던 한계).
- 3분할 MERGED 와 위 칸 사이 선은 두지 않았다(사용자 결정).
## F-11. Git 계정 · URL · 토큰이 한 번 쓰면 저장되게 (2026-09-23, 완료)
사용자 결정: 토큰은 **윈도 자격 증명 관리자에 저장**(평문 금지 — 지시서 조건), Git 탭에 적은 **원격 URL · 계정은 적으면 설정에 바로 남김**.
### 고치기 전 (화면으로 확인, `f11-check.ps1` · 가짜 서버 `f11-server.ps1`)
| 확인 | 결과 |
|---|---|
| 계정 · 토큰을 넣고 Fetch | 서버가 `tester:토큰` 을 받았다 — 보내는 것은 정상 |
| 설정 파일 | Git 탭에 적은 URL · 계정 · 토큰이 **아무것도 저장되지 않음**(설정 탭 "사내 Gitea" 값만 켤 때 읽음) |
| 껐다 켬 | 원격 URL · 계정 · 토큰 칸이 **모두 빔** |
| 그 상태로 Fetch | 인증 없이 보내고 실패. 로그는 영어로 `remote authentication required but no callback set` 뿐 |
| 설정 폴더 전체에서 토큰 글자 | 0곳(저장하지 않으니 당연) |
### 고친 것
- `GitCredentialStore`(새) — 윈도 자격 증명 관리자(advapi32 `CredWrite`/`CredRead`/`CredDelete`, 사용자 계정 키로 암호화). 이름은 **서버마다 하나** `MergeHub:git:스킴://호스트[:포트]`(`GitCredentialKey.For`) — 같은 서버의 다른 저장소는 한 번만 넣으면 된다. SSH(`git@…`) · 로컬 경로 원격은 토큰을 쓰지 않아 저장하지 않는다. 못 읽거나 못 쓰면 조용히 넘어간다(토큰은 덤이다).
- **저장은 된 것만** — fetch · pull · push · 클론이 **성공했을 때** 그때의 계정 · 토큰을 맡긴다. 켤 때와 원격 주소를 바꿀 때 그 서버의 토큰을 가져와 토큰 칸에 채운다(PasswordBox 는 바인딩이 안 돼 화면 코드가 넣는다).
- 다른 서버로 주소를 바꾸면 **앞 서버에서 가져온 토큰은 치운다** — 남의 서버로 보내지 않는다. 사람이 직접 적은 토큰은 건드리지 않는다.
- **[토큰 지우기]** 단추와 "저장됨" 표시(맡겨 둔 것이 있을 때만). PC 를 넘기거나 토큰을 새로 받았을 때 쓴다.
- Git 탭 원격 칸의 **URL · 계정을 설정에 남긴다**(글자마다 쓰지 않게 0.8초 모았다가 한 번).
- 실패한 영어 말을 사람 말로(`GitErrors.Friendly`) — "원격이 계정 · 토큰을 요구합니다. Git 탭 원격 칸에 계정과 토큰을 넣고 다시 해 보세요." · "계정 또는 토큰이 맞지 않습니다…". 원래 영어 말은 괄호로 함께 남긴다.
### 확인 (`f11-verify.ps1`, 127.0.0.1 가짜 사내 서버 — 인증이 없거나 토큰이 틀리면 401, 맞으면 `git http-backend` 로 진짜 fetch)
| 경우 | 결과 |
|---|---|
| 계정 · 토큰 넣고 Fetch | `fetch 완료` · 서버가 받은 인증 `tester:f11-secret-…` · 자격 증명 관리자에 저장됨 · 화면에 "저장됨" |
| 설정 파일 | URL 있음 · 계정 있음 · **토큰 글자 없음** |
| 껐다 켬 | URL · 계정 칸이 채워져 있고 "저장됨" 표시 |
| 아무것도 안 넣고 Fetch | `fetch 완료` · 서버가 같은 토큰을 받음 — **다시 묻지 않는다** |
| [토큰 지우기] | 자격 증명 관리자에서 사라짐 · "저장됨" 꺼짐 |
| 그 뒤 Fetch | `[fetch] 실패: 계정 또는 토큰이 맞지 않습니다. Git 탭 원격 칸에서 다시 넣어 주세요. (too many redirects or authentication replays)` |
| 시험 뒷정리 | 시험이 만든 자격 증명 삭제 확인 |
- 하네스 MergeSaveTest **+15건(335 → 350)** — 이름 규칙(서버마다 하나 · 저장소가 달라도 같음 · 대소문자 · 포트 · SSH · 로컬 경로 · 빈 주소 · 말이 안 되는 주소)과 실패한 말 옮기기(토큰 없음 · 틀림 · 401 · 인증과 무관 · 빈 말). Debug 빌드 경고 0 · 오류 0.
- 회귀: 저장 후 커밋 되돌리기(f8-git) · 병합 [다시 충돌로](f8-reopen) 그대로.
### 남은 것
- 사내 Gitea 가 실제로 서면 그 서버로 한 번 확인한다(지금은 127.0.0.1 가짜 서버).
- 자격 증명은 **이 PC · 이 윈도 계정**에만 있다(DPAPI). 다른 PC 에서는 한 번 더 넣어야 한다.
## F-12. C# 코드 스타일 맞춤의 기본값을 AURORA 로 (2026-09-18, 완료)
사용자: "기본값은 원본형태를 따르고 C#인 경우에 AURORA 스타일로 하자" → 물어서 정한 것은 **제품 기본값 AURORA · C# 파일만 · 저장 동작은 지금 그대로**(켜져 있으면 저장도 그 스타일로 쓴다).
### 알아본 것
- C/C++ · XAML 은 고칠 것이 없었다. 스타일 맞춤은 `CSharpStyle.Applies` 가 **확장자 `.cs` 만** 보므로(`Services/CSharpStyle.cs:60`) 설정이 무엇이든 다른 파일은 원본 그대로 견준다.
- 그래서 바뀐 것은 설정 기본값 한 줄뿐이다.
### 고친 것
| 자리 | 고친 것 |
|---|---|
| `MergeHubSettings.cs:77` | `CSharpBraceStyle` 기본값 `None` → **`Aurora`**. 왜 그렇게 정했는지와 "C# 파일에만 걸린다" 를 주석으로 남김 |
| 웹 문서 매뉴얼 | "기본은 끔" → "**기본은 AURORA**(2026-09-18 결정) · 끄려면 콤보에서 [C# 스타일 끔]" · "C# 파일에만 걸린다" 한 줄 더함 |
| 웹 문서 설정 표 | 기본값 칸 `끔` → `AURORA`, "`.cs` 파일에만 걸린다" |
| 웹 문서 시연 대본 | "스타일 맞춤은 끔으로 둔다" → "기본이 AURORA 이므로 시연 전에 [C# 스타일 끔] 으로 바꿔 둔다" |
| 하네스 `StyleTest` | **+12건(78 → 90)** — 설정 기본값이 AURORA 인지, `.cpp` · `.h` · `.hpp` · `.c` · `.xaml` · `.py` · `.txt` · `.cs.txt` 는 안 걸리는지, `.cs` · `.CS` · 한글 경로 `.cs` 는 걸리는지. 고치기 전에 먼저 넣어 **기본값 검사만 실패**하는 것을 보고 시작했다 |
### 확인한 것 — 설정 파일이 없는 첫 실행 (`e11-f12.ps1`)
설정 파일을 백업해 치우고, 중괄호 자리만 다른 짝 두 벌을 명령줄(`--diff`)로 열었다.
| 무엇 | 결과 |
|---|---|
| C# 짝(`left.cs` Allman · `right.cs` AURORA) | 요약 **"차이 없음"** · 상태바 `무시: 머지 주석 · C# AURORA` (`e11-f12-cs.png`) |
| C++ 짝(`left.cpp` · `right.cpp`, 같은 모양의 중괄호 차이) | 요약 **"남은 차이 4줄"** — 원본 그대로 (`e11-f12-cpp.png`) |
| 앱이 새로 만든 설정 파일 | `"CSharpBraceStyle": 1` (= Aurora) |
| 설정 파일 되돌림 | 해시 같음 |
- 하네스 22벌 **1,159건** 통과(1,147 → +12) · 빌드 경고 0 · 오류 0.
- 이미 설정 파일이 있는 PC 는 그 값을 그대로 쓴다. 이번 변경은 **설정이 없는 첫 실행**에만 닿는다.
## G. 11차 뒤 추가 (2026-09-23 사용자 증상)
### G-1. 배율이 크게 다른 새 모니터에서 최대화하면 오른쪽이 넘침 (2026-09-23, 완료 — 플랫폼 배포함)
사용자 증상: "지금 모니터 새로운거 열었는데 최대화하면 화면이 짤려 오른쪽으로 넘쳐".
F-7 은 **창 모드로 기억한 자리**를 고친 것이라 최대화는 손대지 않았다. 이번은 최대화 자체다.
### 잰 것 (쓰던 창은 건드리지 않고 읽기만 → 시험용 창으로 되풀이)
| 무엇 | 값 |
|---|---|
| 주 모니터 | 2560×1600 · 150% (작업 영역 2560×1528) |
| 새 모니터 | 3840×2160 · **300%** (작업 영역 3840×2016) |
| 새 모니터에서 최대화한 창 | **5120×2186** — 오른쪽 **1280px** · 아래 170px 모니터 밖 |
| 모니터 밖으로 나간 화면 요소 | 25개(시계 · 모듈 · 최근 칸 등) |
| 주 모니터에서 최대화 | 2560×1600 — 넘침 0(정상) |
- 처음 잰 값이 뒤섞여 보였다(주 150% · 새 300%) — 재는 스크립트를 **배율을 아는 상태(PerMonitorV2)** 로 돌려야 실제 픽셀이 나온다.
### 까닭
- 실행 중인 셸 창의 DPI 인식을 물어보니 **모니터별 V1**(PerMonitorV2 아님). 매니페스트에는 `PerMonitorV2, PerMonitor` 가 적혀 있고 exe 에도 박혀 있지만, .NET Framework WPF 가 V1 로 올린다.
- V1 창에서 `WM_GETMINMAXINFO` 에 모니터 크기를 줘도 윈도가 더 크게 잡는다. 값을 바꿔 가며 재 보니 **작은 값은 그대로 따르고**(1920×1080 · 2560×1440), 어느 한도를 넘으면 가로가 1280px 더 붙고 세로는 2186 에 고정됐다(3000×2000 → 4280×2186, 3840×2160 → 5120×2186).
- 윈도 기본값에 맡겨도(우리가 손대지 않아도) 3874×2186 으로 테두리만큼 넘쳤다.
### 고친 것 (플랫폼)
- `ShellWindow.FitMaximizedToMonitor` — 최대화된 창이 그 모니터와 다르면 **한 번 다시 놓는다**. 상태 바뀜 · 배율 바뀜 · 켤 때부터 최대화인 세 자리에 건다.
- 판단은 화면 없이 검사하는 `WindowPlacementMath.NeedsMaximizeFix`(자리 · 크기가 모니터와 같으면 하지 않는다 — 되풀이 방지).
- 최대화가 작업 표시줄까지 덮는 정책(7차 E-4 사용자 결정)은 그대로다.
### 확인
| 경우 | 고치기 전 | 고친 뒤 |
|---|---|---|
| 새 모니터(300%) 최대화 | 5120×2186 · 오른쪽 1280px 넘침 | **3840×2160 · 넘침 0** |
| 되풀이 최대화 · 복원 | 같은 값으로 계속 넘침 | 3840×2160 그대로 |
| 주 모니터(150%) 최대화 | 2560×1600 | 2560×1600 (달라지지 않음) |
| 두 모니터 왕복 | — | 각 모니터에 맞음 |
| 최대화 때 모니터 밖 화면 요소 | 25개 | **0개** |
- PlacementTest **+6(45 → 51건)**. 플랫폼 Debug 빌드 경고 0 · 오류 0.
- 배포: 사용자 "배포해 줘" 뒤 `publish-platform.ps1`(2026-09-23, 경고 0 · 오류 0 — 42개 가운데 `UbisamBase.Core.dll` · `UbisamBase.Shell.exe` · `README.txt` 셋). MergeHub Debug 를 다시 빌드해 `Core.dll` 해시가 배포본과 같음을 확인하고, 배포본으로 같은 시험을 다시 돌려 넘침 0.
### 남은 것
- 근본은 **셸이 모니터별 V1** 이라는 점이다. V2 로 올리려면 WPF 보다 먼저 프로세스 DPI 인식을 정해야 하는데(.NET Framework WPF 는 V1 전제), 모든 소비 프로그램의 창틀 · 그리기에 영향이 커서 이번에는 하지 않았다. 다른 배율 관련 증상이 또 나오면 그때 함께 본다.
- 300% 모니터에서는 **창 모드 최소 크기가 곧 모니터 폭**이다(최소 1280 DIP = 3840px). 그 화면의 논리 폭이 1280 DIP 뿐이라 그런 것으로, 창을 그보다 좁게 만들 수는 없다.