Ubisam-MergeTool/doc/history/2026-09-09-3차.md
ksj09406 b82e40a17c 4차 D-1: 변경 이력을 doc/history/ 로 나눔
721줄짜리 진행상황.md 를 날짜별 파일과 changelog 로 나누고 73줄로 줄였다.
지시서 네 개는 git mv 로 doc/작업지시/ 에 모았다. 링크 15개 전부 살아 있다.
옮기기만 하고 내용은 고치지 않았다.
2026-09-10 20:15:57 +09:00

566 lines
35 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.

# 3차 — 버그 정리 + 기능 확장 (2026-09-09)
`진행상황.md` 에 있던 것을 그대로 옮겼다. 내용은 고치지 않았다.
---
지시서: [작업지시/3차.md](../작업지시/3차.md). 실제로 써 보고 나온 문제부터 순서대로 잡는다.
**2026-09-09 마무리.** A → B → C → D → E 를 하루에 다 걷었다. 지시서에 남은 항목이 없다.
D-4(비교 창 여러 개)만 사용자 판단으로 뺐다.
| 묶음 | 항목 | 결과 |
|---|---|---|
| A 죽거나 망가짐 | 5 | 5 완료 |
| B 3-Way 화면 | 3 | 3 완료 |
| C 잔손질 | 3 | 3 완료 |
| D 기능 추가 | 7 | 6 완료 · 1 취소(D-4) |
| E 추가 요청 | 6 | 6 완료 |
커밋 19개(MergeHub 16 · 플랫폼 3). 검사 하네스 16벌 **514건 통과 · 0건 실패**.
빌드 경고 0 · 오류 0. **푸시는 하지 않았다** - 사내 저장소가 생기면 그때 한 번에 올린다.
<b>화면에서 실제로 만져 보지 않으면 못 잡았을 것들이 오늘만 여섯 가지였다</b> -
경로 칸이 끌어다 놓기를 가로채던 것, 놓을 자리 자체가 없던 글자, 놓아도 다시 견주지 않던 것,
3-Way 에서 REMOTE 칸이 통째로 사라지던 것, 반영한 줄 색이 규칙을 바꾸면 뒤집히던 것,
차이로 이동할 때 코드가 바닥 글 위로 겹쳐 그려지던 것. 빌드가 되는 것과 쓸 수 있는 것은 다르다.
### A. 죽거나 화면이 망가지는 문제
| 상태 | 항목 | 내용 | 확인 |
|---|---|---|---|
| **완료** | A-1 폴더 고르기에서 프로그램이 죽음 | 아래 따로 적음 | 왼쪽·오른쪽 10회 연속 · 파일 비교 쪽 2회 |
| **완료** | A-2 화면 이동 시 종료·최소화 | 아래 A-2~A-4 에 함께 적음 | 21회 연속 이동 (누름 60·250·600ms) |
| **완료** | A-3 파일 비교에서 화면 이동이 원래 모니터로 복귀 | A-2 와 같은 원인 | 파일 비교 탭에서 6회 왕복 |
| **완료** | A-4 화면 이동 시 잠금 팝업 | A-2 와 같은 원인 | 팝업 0회 · 잠금/바탕화면/종료 버튼은 그대로 동작 |
| **완료** | A-5 폴더 고르는 창에서 상위 폴더를 고를 수 없음 | 윈도우 폴더 선택 창으로 바꿈. 곁들여 Git 탭은 최상위를 찾아 연다 | 목록에서 상위 폴더 한 번 눌러 고름 · 상위 폴더로 비교해 하위까지 나옴 · PathTest 12건 추가 |
### B. 3-Way 화면
| 상태 | 항목 |
|---|---|
| **완료** | B-1 가로 스크롤 막대가 없음 |
| **완료** | B-2 아래로 내리면 칸마다 보이는 행 수가 다름 |
| **완료** | B-3 Take Mine / Take Theirs 3-Way 동작 확인 (동작하지 않아 고침) |
### C. 잔손질
| 상태 | 항목 |
|---|---|
| **완료** | C-1 코드 위 툴팁 제거 |
| **완료** | C-2 Git 탭 버튼·입력칸 잘림 (목록 글자 잘림 · 설정 이름 잘림) |
| **완료** | C-3 폴더 복사 팝업을 토스트 모양으로 |
### D. 기능 추가
| 상태 | 항목 |
|---|---|
| **완료** | D-1 주석만 다른 파일을 다른 색으로 |
| **완료** | D-2 C++ 문법 강조 (이미 되고 있었음 · 전처리 지시문만 추가) |
| **완료** | D-3 폴더 안 전체 검색 |
| ~~취소~~ | ~~D-4 비교 창 여러 개~~ (2026-09-09 사용자 판단으로 뺌) |
| **완료** | D-5 덩어리 단위 반영 (3-Way 에서 BASE 를 가져오던 것을 REMOTE 로) |
| **완료** | D-6 비교 결과 텍스트 내보내기 |
| **완료** | D-7 드래그 앤 드롭으로 열기 |
### E. 추가 요청 (2026-09-09 오후)
| 상태 | 항목 |
|---|---|
| **완료** | E-1 원본 세 칸에 세로 스크롤 막대 |
| **완료** | E-2 칸 폭을 줄였다 늘리면 행 높이가 안 돌아옴 |
| **완료** | E-3 경로 줄과 코드 사이의 빈 공간 |
| **완료** | E-4 2-Way 로 돌아오면 LOCAL 옆에 빈 칸이 남음 |
| **완료** | E-5 반영한 줄이 규칙을 바꾸면 "직접 수정" 색으로 바뀜 |
| **완료** | E-6 차이로 이동할 때 코드가 바닥 글 위에 겹쳐 그려짐 |
---
### 곁들여. 3-Way 에서 BASE 가 비어 보이던 것 — **의도한 동작이었다** (2026-09-09)
`WeldController.cpp` 를 3-Way 로 열면 BASE 칸이 통째로 비어 보였다. 고장이 아니다.
두 브랜치의 공통 조상(`9b48b82`)에는 그 파일이 <b>아예 없다.</b> master 와 feature/cpp-tuning 이
<b>각자 새로 만든 파일</b>이라 견줄 조상이 없는 것이다. 그래서 모든 줄이 "양쪽에서 새로 넣은 줄"이 되고
충돌 6개가 나온다. 뜻은 맞다.
**다만 화면이 그 사실을 말해 주지 않았다.** 빈 칸은 누가 봐도 고장으로 읽힌다.
BASE 머리글에 그렇게 적었다 - `BASE (merge base) · 이 파일 없음 - 양쪽에서 새로 만든 파일입니다`.
조상에 파일이 있는 경우는 지금까지처럼 파일 이름이 그대로 나온다.
---
### D-3. 폴더 안 전체 검색 — **완료** (2026-09-09)
**왜 필요했나** - 폴더 비교는 "무엇이 다른가"에 답하지만 <b>"이 함수를 쓰는 곳이 어디인가"</b>에는
답하지 못한다. 그때마다 탐색기 검색이나 다른 편집기를 켜야 했다.
`Services/FolderSearcher.cs` 신설. 화면 없이 검사할 수 있게 <b>찾는 일만</b> 여기서 한다.
**양쪽을 다 훑되 같은 줄은 한 번만 보여 준다.** 안 그러면 손대지 않은 파일이 전부 두 번씩 나와
목록의 절반이 쓸모없어진다. 그래서 결과 한 줄에 "왼쪽 / 오른쪽 / 양쪽"이 붙는다 -
`양쪽` 은 두 폴더에 똑같이 있는 줄, `왼쪽`·`오른쪽` 은 한쪽에만 있는 줄이다.
이것만 봐도 "내가 고친 줄인지"를 안다.
내용이 똑같은 파일은 오른쪽을 <b>다시 읽지 않는다</b>. 대신 왼쪽에서 찾아 둔 줄을 `양쪽` 으로 바꾼다.
**결과를 누르면 그 줄에 서서 열린다.** 파일만 열리고 맨 위에 서 있으면 찾은 자리를 눈으로
다시 찾아야 해서 검색을 한 뜻이 없어진다. `GitWorkspace.RequestPlainCompare` 에 줄 번호를
얹고(`RequestedLine`), 파일 비교 화면이 파일을 다 읽은 뒤 그 줄로 옮긴다.
오른쪽에서만 나온 줄은 오른쪽 파일 기준 번호로 찾는다 - 한쪽에만 있는 줄이 끼면
두 번호가 어긋나 엉뚱한 줄에 선다.
**취소하면 여기까지 찾은 것을 그대로 남긴다.** 처음에는 예외를 던져 목록이 비었는데,
큰 폴더에서 원하는 줄이 이미 나왔는데 멈췄다고 다 지우면 처음부터 다시 해야 한다.
- 대소문자 구분 · 확장자 필터(`*.cs;*.h` · `.cs` · `cs` 를 모두 같게 본다) · 하위 폴더 · 제외 규칙
- 이진 파일과 8MB 넘는 파일은 건너뛴다. 훑은 파일 수를 계속 알려 준다
- 한 줄에 여러 번 나와도 한 줄로 센다
**실제로 확인한 것** (화면이 잠겨 있어 UI 자동화로 조작하고 값을 읽었다)
| 확인 | 결과 |
|---|---|
| 양쪽 같은 파일 | `양쪽 · Common.cpp:2` 한 줄만 |
| 한 파일 안에서 다른 줄 | `왼쪽 · :5 int Speed = 100;` / `오른쪽 · :5 int Speed = 250;` |
| 같은 파일 안 안 바뀐 줄 | `양쪽 · :6 Move(Speed);` |
| 한쪽에만 있는 파일 | `왼쪽 · sub\Only.cpp` / `오른쪽 · notes.txt` |
| 결과를 열었을 때 | 두 파일이 열리고 <b>캐럿이 5번째 줄</b>에 섰다 |
| 663개 파일을 훑는 중 | 진행 글자가 계속 바뀌고 [검색 취소]가 눌렸다 |
| 취소한 뒤 | `517줄 · 파일 63개 · 도중에 멈춤` — 찾은 것이 그대로 남았다 |
검사 63건 추가(`SearchTest`). 전체 512건 통과.
---
### D-7. 드래그 앤 드롭으로 열기 — **완료** (2026-09-09)
탐색기에서 파일·폴더를 끌어다 놓으면 그 자리에서 비교가 열린다.
`GitWorkspace.RouteDroppedPaths` 가 <b>명령줄과 같은 판단</b>을 쓴다(`StartupArgs.Parse` 를 그대로).
끌어다 놓는 것과 우클릭·명령줄이 다르게 굴면 그것부터 버그다 - 폴더면 폴더 비교, 파일이면 파일 비교로
가고 해당 탭이 켜진다. 화면 아무 데나 놓으면 이 판단을 타고, 왼쪽·오른쪽 경로 칸에 바로 놓으면
그 칸만 채운다.
**실제로 탐색기에서 끌어다 놓아 보니 세 가지가 걸렸다.** 코드만 보고는 알 수 없는 것들이다.
<b>① 경로 칸 위에 놓으면 아무 일도 일어나지 않았다.</b>
폴더 비교의 경로 칸은 `TextBox` 인데, `TextBoxBase` 의 클래스 처리기가 `DragOver`·`Drop` 을
<b>먼저</b> 먹는다. 그쪽은 글자 끌어다 놓기만 알아서 파일 목록은 버리고 `Handled` 로 막아 버린다.
터널링 쪽(`PreviewDragOver`·`PreviewDrop`)으로 옮겨 클래스 처리기보다 먼저 받게 고쳤다.
<b>② 파일 비교 화면의 경로 글자는 놓을 자리 자체가 없었다.</b>
`TextBlock` 은 `Background` 가 없으면 히트 테스트에 안 잡힌다. `Background="Transparent"` 를 줬다.
<b>③ 칸에 놓아도 다시 견주지 않았다.</b> 칸 값만 바뀌고 화면은 옛 파일을 그대로 보여 줬다 -
"놓았는데 아무 일도 없다"로 읽힌다. `HomeViewModel.DropFile` 과 `FolderViewModel.DropFolder` 를
만들어 넣은 뒤 바로 다시 견주게 했다. 폴더는 양쪽이 다 찼을 때만 견준다.
**파일 하나씩 끌어다 놓아 짝을 만들 수 있게 했다.** 그 전에는 두 번째 파일이 첫 번째를 밀어내
아무리 놓아도 왼쪽만 바뀌었다. 왼쪽만 차 있고 오른쪽이 비어 있으면 오른쪽을 채운다.
**확인한 것**
| 놓은 것 | 결과 |
|---|---|
| 폴더를 왼쪽·오른쪽 칸에 하나씩 | 두 칸이 차고 곧바로 비교가 돌았다 (`전체 4개 · 다름 1 · 왼쪽만 1 · 오른쪽만 1 · 같음 1`) |
| 폴더를 화면 몸통에 | 왼쪽 칸에 들어갔다 |
| 파일을 폴더 비교 몸통에 | 파일 비교 탭으로 넘어가고 BASE 에 들어갔다 |
| 그 뒤 파일을 하나 더 | LOCAL 에 들어가고 짝이 맞춰졌다 (`총 8 라인 · 차이 2 라인`) |
---
### D-6. 비교 결과 텍스트 내보내기 — **완료** (2026-09-09)
**왜 필요했나** - 결과를 남에게 보내거나 결재에 붙일 때 <b>화면을 캡처하는 것 말고는 방법이 없었다.</b>
캡처는 검색도 안 되고 줄 번호를 짚어 말하기도 어렵다.
`Services/CompareReport.cs` 신설. 파일 비교와 폴더 비교 양쪽에서 쓴다.
<b>글을 만드는 일만</b> 여기서 하고, 어디에 저장할지 묻는 것은 화면 쪽 일이다 - 그래야 화면 없이 검사한다.
파일 비교 결과는 이렇게 나온다.
```
MergeHub 파일 비교 결과
만든 때: 2026-09-09 14:46:23
왼쪽 : ...\WeldController_기준.cs
오른쪽: ...\WeldController_내작업.cs
----------------------------------------------------------------
요약: 총 43 줄 · 다름 5 · 오른쪽에만 5 · 왼쪽에만 0
다른 줄
----------------------------------------------------------------
[8/8] 다름
- private const int PowerWatt = 1800;
+ private const int PowerWatt = 2000;
[-/28] 오른쪽에만
+ {
```
**줄 번호를 양쪽 다 적는다.** 한쪽에만 있는 줄이 있어 번호가 어긋나므로, 하나만 적으면
파일에서 그 줄을 찾을 수 없다. 없는 쪽은 `-` 로 둔다.
폴더 비교 결과는 상태와 경로를 줄마다 적는다. 하위 폴더 파일도 상대 경로로 들어가고,
폴더 자체는 줄로 적지 않는다 - 그 안의 파일이 이미 다 적혀 있다.
**곁들여** 요약에 주석만 다른 수를 덧붙였다 - `다름 3(주석만 1)`.
D-1 로 새로 생긴 갈래가 요약에는 안 보였다.
**확인**: 두 화면에서 각각 뽑아 메모장으로 열었다. `ReportTest` 23건 신설. 전체 **451건**.
---
### D-1. 주석만 다른 파일을 다른 색으로 — **완료** (2026-09-09)
폴더 비교 결과에 `주석만 다름` 상태를 새로 두었다. 코드는 같고 주석·공백만 다른 파일이다.
**왜 나누나** - 봐야 할 성격이 다르다. 주석만 손댄 파일은 눈으로 훑고 넘길 수 있는데
다른 것과 같은 색으로 섞여 있으면 전부 열어 봐야 한다.
**판단하는 법**: 주석 토큰을 걷어내고 공백을 지운 뒤 견준다. 주석 기호는 언어마다 다르므로
확장자로 고른 언어표를 따르고, 언어를 모르는 파일(평문·표)은 이 판단을 하지 않는다 -
주석이라는 것이 없다. 양쪽 확장자가 다르면 같은 언어로 볼 근거가 없으니 역시 하지 않는다.
줄 구분은 <b>코드가 있는 줄에만</b> 남긴다. 그래야 주석 한 줄을 통째로 지운 경우도 같다고 보고,
반대로 코드를 다른 줄로 옮긴 것은 다르다고 본다.
큰 파일은 이 판단을 건너뛴다. 전체 비교에서 이미 쓰는 한도(8MB)를 그대로 따른다 -
폴더 하나에 수천 개가 있을 때 이 판단만으로 몇 분이 더 들면 안 된다.
**곁들여 둘**
- <b>색 뜻풀이 줄</b>을 목록 위에 붙였다. 색만 칠해 두면 무슨 뜻인지 아무도 모른다 -
파일 비교 화면의 "색 구분" 줄과 같은 성격이다.
- 보기 필터에 <b>코드가 다른 것만</b>을 더했다. 주석만 다른 것을 빼고 보는 쪽이
실제로 필요한 동작이다.
**확인**: 주석만 고친 `.cs` 는 회색 `주석만 다름`, 값이 바뀐 `.cs` 와 평문 `.txt` 는 노란 `다름`.
`코드가 다른 것만` 을 고르면 앞의 것이 목록에서 빠진다.
`FolderTest` 4건 추가(37 → 41건).
---
### D-2 · D-5 — **완료** (2026-09-09)
#### D-2. C++ 문법 강조
**이미 되고 있었다.** `LanguageProfile.CFamily` 가 `.c .h .cpp .hpp .cc .hh .cxx .hxx .inl` 을
모두 맡고 있고 예약어·주석·문자열도 들어 있다. 지시서에서 확인만 하라고 적어 둔 대로 확인했다.
**빠진 것은 전처리 지시문 하나였다.** `#include` 는 `#` 과 `include` 로 쪼개져 보통 낱말처럼
칠해졌다. 줄 첫머리가 `#` 이면 그 줄 전체를 지시문 색으로 칠하게 했다 - C# 쪽과 같은 규칙이다.
같은 모양인데 언어마다 다르게 보이면 오히려 헷갈린다.
언어표에 `hasPreprocessor` 를 두어 <b>C 계열에서만</b> 켠다. 자바·자바스크립트는 `#` 이 지시문이 아니다.
**검사**: `LangTest` 12건 추가(40 → 52건).
#### D-5. 덩어리 단위 반영
**2-Way 는 이미 됐다**(`이 덩어리 반영` · `Alt+Shift+→`). 확인했다.
**3-Way 가 잘못 동작하고 있었다.** 덩어리를 BASE 에서 가져오고 있었는데, 3-Way 에서 BASE 는
<b>공통 조상</b>이다. 저쪽(REMOTE)을 가져오는 것이 `Take Theirs` 이고 덩어리 반영은 그것을
줄 묶음으로 하는 것인데, BASE 를 가져오면 <b>조상으로 되돌리는 셈</b>이라 뜻이 전혀 달라진다.
덩어리를 묶는 기준도 같이 바꿨다 - 2-Way 는 "BASE 를 가져올 수 있는 줄", 3-Way 는
"저쪽을 가져올 수 있는 줄"이 이어진 구간이다.
**확인**: 충돌 6개짜리 C++ 파일에서 `다음 충돌` → `이 덩어리 반영` 한 번에
네 줄이 REMOTE 값으로 바뀌고 `Conflict 6 → 2`, `머지 반영함 4`.
단축키(`Alt+Shift+→`)도 같은 명령을 부르므로 함께 고쳐졌다.
---
### C. 잔손질 — **완료** (2026-09-09)
#### C-1. 코드 위 툴팁 제거
코드 위에 마우스를 올리면 <b>그 줄 내용이 그대로</b> 툴팁으로 다시 떴다.
글이 이미 보이는데 같은 글이 상자로 겹쳐 뜨니 가리기만 한다. 원본 세 칸에서 뺐다.
버튼·상태 아이콘 툴팁은 그대로 둔다 - 그쪽은 화면에 없는 정보를 알려 준다.
#### C-2. 글자가 잘리던 곳
**Git 탭의 목록들** — 커밋 제목이 `...` 없이 <b>글자 중간에서 뚝 끊겼다</b>
("A-5 다시: 폴더 고르는 창을 윈도우 폴더 선택 ‸"). 줄임표 설정은 이미 있었는데 듣지 않았다.
목록의 가로 스크롤이 켜져 있으면 항목이 <b>제 글 길이만큼 넓게</b> 잡히고, 그러면 잘릴 일이
없으니 줄임표도 나오지 않는다(칸 밖으로 나간 부분이 안 보일 뿐이다).
가로 스크롤을 끄면 항목이 칸 너비에 갇히고 그때 비로소 줄임표가 나온다.
커밋·변경 파일·브랜치·충돌·태그·서브모듈 여섯 목록에 같이 걸었다.
**설정 화면의 항목 이름** — 이름 칸이 200 폭으로 고정이라 긴 이름이 잘렸다
("공백 무시 단계 (0=전부 비교, ‸"). 무슨 설정인지 알 수 없어진다. 접히게 바꿨다.
<b>플랫폼 변경이라 모든 소비 프로그램에 적용된다.</b>
**못 찾은 것**: "작업 취소" 버튼과 Git 탭 입력 칸이 잘리는 것은 재현하지 못했다.
2560×1600 과 1920×1080 에서, 화면 배율 100%·120% 로 여섯 하위 탭을 모두 봤지만
그 자리는 멀쩡했다. 다시 보이면 그때 화면을 받아 고친다.
#### C-3. 폴더 복사 확인 창을 플랫폼 것으로
윈도우 기본 대화상자를 쓰고 있었다. 글꼴도 색도 배치도 혼자 달라 "이 프로그램이 묻는 것"으로
읽히지 않는다. 플랫폼이 이미 가지고 있는 `MessageUtil.ShowYesNo` 로 바꿨다.
<b>묻는 것 자체는 없애지 않았다.</b> 복사·삭제는 되돌릴 수 없다. 지시서에도 그렇게 적었다.
완료 알림은 전부터 토스트였고 그대로 둔다.
---
### E. 추가 요청 세 가지 — **완료** (2026-09-09)
#### E-1. 원본 세 칸에 세로 스크롤 막대
가로(B-1)만 붙이고 세로가 빠져 있었다. 세 칸에 `VerticalScrollBarVisibility="Visible"` 을 켰다.
가로와 달리 세로는 이 칸들이 <b>실제로 굴러가므로</b> 따로 막대를 만들 필요 없이 ScrollViewer 가
주는 것을 그대로 쓴다. 대신 막대가 자리를 차지하는 만큼 <b>여백 보정을 걷어냈다</b> -
전에는 편집기 막대 폭만큼 오른쪽 여백을 줘서 글자 폭을 맞췄는데, 이제 진짜 막대가 그 자리를
차지하므로 여백까지 주면 글자 폭이 좁아져 접히는 자리가 어긋난다.
#### E-2. 칸 폭을 줄였다 늘리면 행 높이가 돌아오지 않던 문제
화면을 좁혔다 되돌리면 접혔던 행이 <b>넓은 자리를 그대로 차지한 채</b> 남았다.
행 높이는 각 칸이 "내 글이 이만큼 높다"고 알려 온 값 중 <b>가장 큰 것</b>을 쓴다.
워드랩에서 한쪽만 접힌 행이 있으면 그쪽에 맞춰야 옆 칸과 줄이 맞물리기 때문이다.
그런데 글자를 그리는 칸이 세로로 <b>늘어나 있어서</b>(WPF 기본값 Stretch) 행이 한 번 커지면
그 다음부터는 "행 높이만큼 높다"고 되돌려 보냈다. 자기가 만든 값을 자기가 다시 읽는 셈이라
<b>높이가 올라가기만 하고 내려오지 않았다.</b>
글자 칸을 위로 붙여(`VerticalAlignment="Top"`) 자기 글 높이만 알리도록 고쳤다.
#### E-3. 경로 줄과 코드 사이의 빈 공간
패널 머리 높이를 상수로 못 박아 두었다(124 / 74). 실제 내용보다 훨씬 커서 경로 줄 아래가
휑하게 비었고 그만큼 코드가 덜 보였다. 상수를 없애고 내용에 맞춰 줄어들게 한 뒤,
나란히 놓인 칸끼리는 `SharedSizeGroup` 으로 <b>가장 큰 것에 맞추게</b> 했다
(REMOTE 만 "Conflict N" 줄이 하나 더 있어서 그렇게 해야 첫 줄이 같은 눈높이에 온다).
2-Way 에서는 MERGED 머리도 같은 무리에 넣고, 3-Way 에서는 뺀다 - 3-Way 의 MERGED 는
아래 줄로 내려가 맞출 상대가 없는데, 넣어 두면 그쪽 경로 줄 때문에 위 세 칸까지 덩달아 커진다.
**확인**: 2-Way 에서 한 화면에 보이는 코드가 <b>29줄에서 34줄로</b> 늘었다.
BASE·LOCAL 의 01행은 같은 높이 그대로다.
#### E-4. 2-Way 로 돌아오면 LOCAL 옆에 빈 칸이 남던 문제
3-Way 에서 <b>구분선을 끌어 칸 폭을 바꾼 뒤</b> 2-Way 로 돌아오면 LOCAL 오른쪽에
칸 하나만 한 빈 자리가 남았다. 구분선을 건드리지 않으면 멀쩡했다.
`GridSplitter` 는 끌고 나면 양옆 칸을 <b>비율(star)이 아니라 고정 픽셀</b>로 바꿔 놓는다.
그러면 두 가지가 어긋난다. 창 크기가 바뀌어도 칸이 따라 늘지 않고, 2-Way 로 돌아와
REMOTE 칸이 접히면서 생긴 자리를 <b>아무도 채우지 않는다</b>(늘어날 star 칸이 없으니까).
- 구분선을 놓는 순간 고정 픽셀을 <b>같은 비율의 star 로 되돌린다.</b> 사용자가 잡아 놓은
폭은 그대로 유지되면서 창 크기를 따라가게 된다.
- 2-Way ↔ 3-Way 를 오갈 때는 세 칸의 폭 지정을 아예 놓아준다. 배치가 통째로 다르기도 하고,
특히 REMOTE 칸은 폭을 Style 이 정하는데 끌어서 생긴 값이 남아 있으면 <b>2-Way 에서 접히지
않는다.</b>
**확인**: 3-Way 에서 구분선을 끌고 2-Way 로 돌아와도 두 칸이 폭을 꽉 채운다.
2-Way 에서 끌어 놓은 비율은 다른 모니터를 다녀와도 그대로다.
#### E-5. 반영한 줄이 규칙을 바꾸면 "직접 수정" 색이 되던 문제
줄을 반영(Accept)한 뒤 비교 규칙을 하나 건드리면 그 줄이 초록(머지 반영함)에서
보라(직접 수정)로 바뀌었다.
**반영 표시가 되돌리기 스택에만 있었다.** 규칙을 바꾸면 화면을 통째로 다시 만들면서 그 스택을
비운다. 그러면 표시가 사라지고 남는 단서는 "글이 원본과 다르다"뿐이라 직접 수정으로 칠해진다.
B-3 에서 Take Mine 이 충돌을 못 지우던 것과 같은 뿌리다.
- 반영할 때 <b>줄 번호로도</b> 남긴다. BASE 줄 번호, 없으면 MINE 줄 번호 - 둘 다 얼어 있는 값이라
다시 정렬해도 살아남는다.
- 다시 만들 때 반영 여부를 <b>먼저</b> 본다. 반영한 줄은 직접 수정 조건에도 걸리므로 순서가 중요하다.
- 되돌리면 그 기록도 함께 지운다.
**곁들여** 머지 주석 줄도 같은 이유로 보라색이 됐다. 주석 줄이라는 표시 역시 줄 객체에만 있어서
다시 만들면 사라진다. 글을 보고 알아보게 했다(`MergeNote.LooksLikeNote`). 사람이 우연히 같은
모양으로 쓴 주석을 잘못 볼 수 있지만, 그래도 색이 달라질 뿐이고 반대로 못 알아보면 반영할 때마다
"직접 수정"이 하나씩 늘어난다.
**확인**: 반영 → 공백 규칙 → Aa 규칙 순으로 눌러도 `머지 반영함 2 · 직접 수정 0` 그대로.
`NoteTest` 13건 추가(16 → 29건).
#### E-6. 차이로 이동하면 코드가 바닥 글 위에 겹쳐 그려지던 문제
`Alt+↑/↓` 로 차이를 옮겨 다니면 <b>칸 아래 상태바 위에 코드가 겹쳐</b> 그려졌다.
표시층은 편집기 글자와 눈높이를 맞추려고 `RenderTransform` 으로 살짝 밀어 둔다. 그런데
RenderTransform 은 <b>배치가 아니라 그리기</b>라 칸 밖으로 나가도 아무도 막지 않는다.
격자(Grid)의 행도 자기 칸 밖으로 나가는 자식을 자르지 않는다.
코드가 놓이는 자리를 자르는 상자로 감쌌다(`ClipToBounds`). 네 칸 모두.
**확인**: `Alt+↓` 8회 · `Alt+↑` 6회 눌러도 마지막 줄이 칸 경계에서 깨끗이 잘린다.
**세 가지 모두 확인**: 세로 막대를 끌면 세 칸이 함께 움직인다.
2560×1600 ↔ 1920×1080 을 오가도, 가운데 구분선을 끌었다 되돌려도 처음과 같은 화면이 된다.
테스트 399건 그대로 통과.
---
### B. 3-Way 화면 — **완료** (2026-09-09)
#### B-1. 원본 세 칸에 가로 스크롤 막대가 없었다
원본 칸은 ScrollViewer 를 <b>가로로 굴리지 않는다.</b> 굴리면 줄 번호까지 화면 밖으로
밀려 나가고 세 칸이 서로 어긋난다. 대신 글자만 `SourceShiftX` 로 민다. 그래서 ScrollViewer 가
만들어 주는 막대는 <b>움직이지 않는 막대</b>가 되어 감춰 두고 있었고, 결과적으로 긴 줄이
있다는 사실 자체를 알 수 없었다(가로 휠을 굴려 봐야 알 수 있었다).
막대를 따로 놓고 `SourceShiftX` 와 서로 값을 맞추게 했다. 세 칸이 늘 같이 움직이므로
세 막대의 손잡이도 같은 자리에 있다. 워드랩을 켜면 밀 곳이 없으므로 사라진다.
**확인**: 손잡이를 끌면 세 칸이 함께 움직이고, 가로 휠을 굴리면 손잡이가 따라온다.
워드랩을 켜면 세 막대가 사라지고 끄면 돌아온다.
#### B-2. 워드랩을 켠 3-Way 에서 세로 스크롤이 아예 안 됐다
증상은 "행 수가 다르다"였는데, 재 보니 <b>스크롤이 한 자리에 눌러앉아 있었다</b>.
스크롤 값을 350 으로 바꿔도 곧바로 80 으로 되돌아왔다.
원인이 둘이었다.
**하나 — 메아리를 막는 자물쇠가 너무 일찍 풀렸다.** 편집기가 굴러가면 원본 칸을 따라
굴리는데, 목록(ListBox)의 스크롤 알림은 <b>배치가 끝난 다음</b>에 온다. 자물쇠를
`try/finally` 로 그 자리에서 풀어 버리니 그때는 이미 열려 있었고, 그 메아리가 다시 편집기를
굴렸다. 워드랩이 꺼져 있으면 환산이 1:1 이라 같은 값이 되돌아와 조용히 끝나지만,
켜면 조금씩 어긋나 서로 밀어내다 멈춘다. 자물쇠를 <b>그 일로 밀려서 오는 알림이 다 지나간
뒤</b>(`DispatcherPriority.Input`)에 풀도록 바꿨다.
**둘 — 두 좌표계를 픽셀로 환산하고 있었다.** 위 세 칸과 아래 MERGED 는 폭이 달라 접히는
횟수가 다르므로, 픽셀로 맞추려면 행마다 실제 높이를 알아야 한다. 그런데 목록은 <b>가상화</b>라
화면에 그려진 행만 자기 높이를 알려 준다. 아직 안 그려진 행은 "한 줄"로 잡혀 환산이 어긋났다.
<b>재지 않고 세는</b> 쪽으로 바꿨다 - 워드랩일 때는 원본 칸을 행 단위로 굴려
"몇 번째 행이 맨 위인가"만 주고받는다. 높이를 몰라도 정확히 맞는다.
곁들여, 편집기 화면 맨 위의 행을 알아낼 때 <b>스크롤 값을 줄 높이로 나누던 것</b>을
좌표로 물어보는 방식으로 바꿨다. 실제로 재 보니 스크롤 160 에서 편집기가 말하는 첫 줄은
11 인데 화면 맨 위에 있는 것은 9행이었다. TextBox 의 줄 번호 계산은 그려진 것과 맞지 않는다.
**확인**: 워드랩을 켠 3-Way 에서 네 칸이 같은 행을 맨 위에 놓는다(원본 09 / BASE 05 /
REMOTE 05 = 같은 줄, MERGED 09). 워드랩을 끈 경우도 그대로다(16 / 12 / 12 / 16).
#### B-3. Take Mine 이 충돌을 정리하지 못했다
Take Theirs 는 되는데 <b>Take Mine 을 눌러도 "남은 충돌" 이 줄지 않았다.</b>
"내 것으로 간다"는 결정은 <b>글을 바꾸지 않는다</b>(이미 내 원본과 같은 줄이다).
그래서 되돌리기 스택에 남지 않는데, 편집 뒤 정리 단계(`RefreshAppliedFlags`)가
<b>되돌리기 스택만 보고</b> 반영 표시를 다시 칠하면서 방금 세운 표시를 지워 버렸다.
줄 번호로 따로 기억해 둔 결정(`_resolvedBaseNumbers` · `_resolvedMineNumbers`)을
같이 보도록 고쳤다. 같은 판단이 두 군데 흩어져 있던 것도 `IsDecidedAsMine` 하나로 합쳤다.
**확인**: 충돌 6개짜리 C++ 파일에서 Take Mine · Take Theirs 를 번갈아 눌러
6 → 5 → 4 → … → <b>충돌 정리 끝</b> 까지 갔다.
**검사**: 전체 399건 그대로 통과.
---
### A-5. 폴더 고르는 창에서 상위 폴더를 고를 수 없던 문제 — **완료** (2026-09-09)
**처음에 요구를 잘못 읽었다.** "최상위 단에서 봐야 하는데 가장 깊은 곳에서 폴더 열기밖에 안 된다"를
Git 저장소 이야기로 읽고 `.git` 찾아 올라가기를 만들었는데, 실제로는 <b>폴더 비교 탭의
폴더 고르는 창</b> 이야기였다. 상위 폴더를 골라 놓고 하위까지 한꺼번에 비교하고 싶은데
그 상위 폴더를 고를 방법이 없다는 것이었다.
**진짜 원인**: 폴더 고르기를 <b>파일 열기 창으로 돌려쓰고</b> 있었다(net48 WPF·WinForms 에는
쓸 만한 폴더 선택 창이 없다). 이 방식은 "지금 서 있는 폴더"를 돌려주기 때문에,
목록에 보이는 폴더를 고르려면 <b>반드시 그 안으로 들어가야</b> 한다. 그래서 늘 가장 깊은
폴더만 골라졌다. 게다가 윈도우가 기억해 둔 "이 프로그램이 마지막으로 본 폴더"에서 열려서
엉뚱한 곳(지난번 Git 저장소를 고르러 들어갔던 자리)에서 시작하기도 했다.
**고친 것**
- 윈도우가 원래 가지고 있는 <b>폴더 선택 창</b>을 셸 COM(`IFileDialog` + `FOS_PICKFOLDERS`)으로
직접 부른다. 목록에서 폴더를 <b>한 번 눌러 고를 수 있고</b>, 주소 칸도 있고, 경로를 쳐 넣을 수도 있다.
COM 이 안 되는 환경이 있을 수 있어 그때는 전에 쓰던 파일 열기 창으로 물러선다.
- `SetDefaultFolder` 가 아니라 `SetFolder` 를 쓴다 — 전자는 윈도우가 기억한 폴더를 우선하기 때문에
지금 경로 칸에 적힌 폴더에서 열리지 않는다.
- 네 자리(폴더 비교 왼쪽·오른쪽, Git 저장소 경로, 변경분 추출 대상)가 이 창 하나를 쓴다.
**곁들여**: Git 탭은 하위 폴더를 골랐더라도 위로 거슬러 올라가 `.git` 이 있는 최상위를 찾아 연다
(`PathUtil.FindRepositoryRoot`). 워크트리·서브모듈은 `.git` 이 파일이라 둘 다 보고,
안쪽에 저장소가 또 있으면 가장 가까운 것을 잡는다. 시작할 때의 자동 열기도 같은 규칙을 쓴다.
**확인**: 폴더 고르기 창을 열어 `위로` 한 번 올라간 뒤 목록의 `right` 폴더를 <b>한 번 눌러</b> 골랐고,
경로 칸에 그대로 들어왔다. 그 상태로 비교하니 `doc / src / src·core / src·ui` 가 트리로 나오고
`src·core·weld.cs` 까지 펼쳐진다. Git 탭은 최상위에서 네 단계 아래 파일을 골라도 저장소가 열린다.
`PathTest` 에 12건 추가(31 → 43건). 전체 **399건**.
---
### A-2 · A-3 · A-4. 화면 이동 결함 세 가지 — **완료** (2026-09-09)
세 가지가 아니라 <b>하나</b>였다.
증상은 셋으로 보였다. 화면 이동을 눌렀는데 창이 최소화되거나, 잠금 창이 뜨거나,
파일 비교 화면에서는 옮겨졌다가 원래 모니터로 되돌아왔다. 셋 다 같은 자리에서 나온다.
스택을 찍어 보니 이렇게 흘렀다.
```
ListBox.OnMouseMove
→ ItemsControl.DoAutoScroll
→ ListBox.FocusItem
→ ListBox.MakeSingleSelection ← 여기서 옆 항목이 선택된다
→ NavList_SelectionChanged ← 그 항목의 동작이 실행된다
```
**하단 탭 바는 ListBox 다.** ListBox 는 <b>누르는 순간</b> 항목을 고르고 마우스를 잡는다
(끌어서 여러 개 고르기 위해서다). 손을 떼기 전에 창이 다른 모니터로 넘어가면 커서는
목록 밖에 남게 되고, ListBox 는 그것을 "끌어서 화면 밖으로 나갔다"고 보고 <b>옆 항목을
대신 고른다.</b> 하단 탭 바의 오른쪽 끝은 `화면 잠금 · 화면 이동 · 바탕화면 · 종료` 순으로
붙어 있어서, 옆 항목이 무엇이냐에 따라 잠금 창이 뜨거나(A-4) 최소화되거나(A-2)
방금 옮긴 창이 도로 넘어온다(A-3). <b>어느 쪽으로 튈지 그때그때 달랐던 이유</b>가 이것이다.
전에 한 번 손을 댄 자리였는데, 그때는 실행을 `DispatcherPriority.Background` 로 미루는
방식이었다. **그것으로는 막히지 않는다** — 사람이 버튼을 누르고 있는 시간(보통 100ms 이상)이
그 지연보다 길기 때문이다. 누르고 있는 동안 창이 움직이면 똑같이 재현된다.
**고친 것**: 동작형 항목(화면 이동·바탕화면·화면 잠금·종료)은 탭이 아니라 <b>버튼</b>이다.
마우스로 누를 때는 ListBox 의 선택 경로를 아예 타지 않게 `PreviewMouseLeftButtonDown` 에서
끊고, 실행은 <b>손을 뗄 때</b> 한다. 창이 움직이는 순간에 눌린 버튼이 없으니 끌어서 스크롤이
일어날 여지가 없다. 누른 뒤 밖으로 끌고 나가 떼면 실행되지 않는 것도 버튼과 같은 규칙이다.
키보드로 항목을 옮겨 고르는 경로는 그대로 두었다.
**확인**: 두 모니터(2560×1600@150% ↔ 1920×1080@100%) 사이를 <b>21회 연속</b> 왕복.
버튼을 누르고 있는 시간을 60ms · 250ms · 600ms 로 나눠 각각 7회씩 했고 실패 0건.
파일 비교 화면에서도 6회 왕복 확인. 바탕화면·화면 잠금·종료 세 버튼도 그대로 동작한다.
**플랫폼 변경이라 모든 소비 프로그램에 적용된다.**
---
### A-1. 폴더 고르기에서 프로그램이 죽던 원인 — **완료** (2026-09-09)
"왼쪽 폴더..." 를 누르면 창이 하얗게 굳었다가 그대로 사라졌다. 윈도우 이벤트 로그에 남은 것:
```
System.ArgumentException
MS.Internal.AppModel.ShellUtil.GetShellItemForPath(System.String)
Microsoft.Win32.FileDialog.PrepareVistaDialog(...)
Microsoft.Win32.CommonDialog.ShowDialog()
MergeHub.ViewModels.FolderViewModel.AskForFolder(...)
```
**원인은 경로에 섞인 슬래시였다.** 왼쪽 폴더 칸에 들어 있던 값이
`C:/Users/Ubisam/AppData/Local/Temp/plain-compare` 였는데, .NET 의 `Directory.Exists` 는
이런 경로도 통과시킨다. 그런데 대화상자의 `InitialDirectory` 는 윈도우 셸(COM)이 그대로
해석하고, 슬래시가 섞이면 거부하며 예외를 던진다. 그 예외가 <b>대입할 때가 아니라
`ShowDialog()` 안쪽에서</b> 올라오기 때문에 대입 줄만 `try` 로 감싸 둔 곳(파일 비교 쪽)도
똑같이 죽게 돼 있었다. 오른쪽 폴더 칸은 비어 있어서 멀쩡했던 것이고, 사실상
"경로를 한 번이라도 채워 둔 채 다시 고르려 하면 죽는" 상태였다.
**고친 것**
- `Services/PathUtil.cs` 신설. 셸에 넘길 경로는 전부 여기를 거친다. 구분자를 역슬래시로
맞추고, 상대 경로를 펴고, 확인할 수 없는 경로는 빈 문자열로 돌려준다. 빈 문자열이면
부르는 쪽이 `InitialDirectory` 를 아예 지정하지 않는다.
- 폴더 고르기·파일 고르기 둘 다 `ShowDialog()` 를 감쌌다. 끊긴 네트워크 드라이브처럼
정규화로 걸러지지 않는 경우가 남기 때문에, 그럴 때는 시작 폴더 없이 한 번 더 연다.
폴더 고르기 자체를 못 쓰게 되는 것보다 낫다.
- 플랫폼(`UbisamBase.Shell/App.xaml.cs`)에 <b>전역 예외 그물</b>을 놓았다. 화면 스레드에서
예외가 새어 나오면 WPF 는 프로그램을 그대로 끝내 버린다. 현장 장비에서 버튼 하나 잘못
눌렀다고 프로그램이 사라지면 무슨 일이 있었는지도 남지 않는다. 이제 로그에 남기고
알림창을 띄운 뒤 계속 돈다. <b>조용히 삼키지는 않는다</b> - 알리지 않으면 "눌렀는데
아무 일도 안 일어난다"가 되어 더 나쁘다.
이 저장소만이 아니라 <b>플랫폼을 쓰는 모든 프로그램에 적용된다.</b>
**검사**: `PathTest` 31건 신설(슬래시 섞인 경로 · 따옴표 · 못 쓰는 글자 · 없는 드라이브 ·
아주 긴 경로 · 드라이브 뿌리). 전체 356 → **387건**.