Ubisam-MergeTool/doc/history/2026-09-23-12차.md
ksj09406 b7ade6a800 12차 G-3 ~ G-8 · 알파/베타 전 정리 (사용자 지시로 한 번에 커밋)
G-3 "덩어리" 를 "구간" 으로 - 단추 [구간 반영] · [구간 왼쪽으로], 단축키 설명, 로그.
G-4 저장 형식 콤보 둘을 구별 - 원본 인코딩 유지 · 원본 줄바꿈 유지. 저장값은
    "원본 유지" 그대로(세션 보관본 7·8칸) - SaveChoice(Value, Label) 로 보이는 글만 가른다.
G-5 사내 문의처 받침값 - csproj <SupportContact> 를 실행 파일에 박는다(문구는 아직 비움).
G-6 설정 > 개발자 정보. 담당 카드는 Platform 최민영 대리 · MergeHub 김승준 사원,
    소개 문구는 "사내 Merge/Diff + Git 도구"(Merge 먼저 - 창 제목 · csproj · 정보 창).
G-7 Alt+Down 화면 잘림 - 가운데 맞추기의 두 번째 스크롤이 메아리 막기에 걸려 버려졌다.
    CenterCharInView 가 다른 칸을 직접 맞춘다(g7-verify 고치기 전 10건 실패 -> 0).
G-8 메모 문법 강조 · 같은 글 칠하기 · 색 마킹(Ctrl+1~5) · 위치 막대. 투명 TextBox +
    아래층(사용자 결정). 화면에서 찾은 셋: 플랫폼 TextBox 스타일이 아래층을 덮음
    (Style={x:Null}), 콤보에 형 이름(LanguageProfile.ToString), 어두운 테마에서 메모 탭 ·
    문서 탭 제목이 검정(ItemContainerStyle 에 Foreground).

마무리(사용자 지시)
- 설정의 "프로그램 정보" 칸(만든 사람 · 사내 문의처)을 뺐다. [정보] 창은 실행 파일 값만.
- 주석 남기기가 켜져 있는데 작성자가 비면 반영 명령 맨 앞에서 알리고 반영하지 않는다
  (예전에는 git 이름 · 윈도우 계정으로 조용히 채웠다). 주석을 끄면 묻지 않는다.
- 데모(설정 두 칸 · [데모 ->] 단추 · 전환 코드)를 걷어냈다.

시험: 빌드 경고 0 · 오류 0. 하네스 MemoHlTest 45 · MemoTest 69 · OverviewTest 15 ·
AboutTest 30 · NoteTest 99. 화면 최종 회귀 22벌 423건(실패 4건은 대본이 툴팁을
읽은 것 - 고쳐 통과), 마무리 뒤 g5 · g6 · g9-author 실패 0. 한 번 나오고 재현 안 된
g8 가로 스크롤 항목은 12차 기록에 "값으로 확정 못 함" 으로 남겼다.

Agent: main (claude-opus-5-5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:32:42 +09:00

1300 lines
92 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.

# 12차 진행 기록 (2026-09-23 ~)
지시서: [../작업지시/12차.md](../작업지시/12차.md)
## G 묶음 마무리 — 알파 · 베타 전 정리 (2026-09-30, 사용자 지시 · 완료)
### 설정의 "프로그램 정보" 칸을 뺐다
`만든 사람` · `사내 문의처` 두 칸 — 설정 → 개발자 정보가 담당을 보이므로 필요 없다(사용자). [정보] 창은 이제 **실행 파일에 박힌 값만**
읽는다(만든 사람 = 회사 이름, 문의처 = csproj `<SupportContact>`). 그래서 `ProgramInfoReader.For` 의 "설정값 먼저" 인수를 걷고,
빈 칸 안내 글을 `적혀 있지 않습니다 (설정 > MergeHub 에서 적습니다)` → **`적혀 있지 않습니다`** 로 줄였다(없는 칸을 가리키게 되므로).
옛 설정 파일에 남은 두 키는 읽을 때 버려진다 — `g5-verify` 2단계가 옛 키에 값을 적어 두고 켜서 안 쓰이는 것을 확인.
`OverviewViewModel` 은 설정을 읽을 일이 없어져 인수 없는 생성자로(플랫폼 `AddSub` 가 DI 로 만든다).
하네스 `AboutTest` 를 새 모양으로 먼저 고쳐 **컴파일 실패** 확인 → 30건 통과. 화면 `g5-verify` 3건 · `g6-verify` 2단계
(설정 → MergeHub 에 `프로그램 정보` 묶음 · 두 칸 없음, 다른 칸 그대로) 실패 0.
### 작성자가 비면 반영하지 않고 알린다
사용자 요청 "반영한 줄에 주석 남기기에서 작성자를 입력하지 않으면 알람". 방식은 물어 정했다 — **반영할 때 알림 + 반영 멈춤**.
예전에는 비어 있으면 git `user.name` → 윈도우 계정으로 조용히 채웠다(장비를 여럿이 쓰면 주석 이름이 틀리게 남는다).
- 판단은 `MergeNote.AuthorMissing(주석 켬, 작성자)` — 주석을 끈 사람에게는 묻지 않는다. 알림 글도 `MergeNote` 에(설정 자리를 댄다).
- 거는 자리: 반영 **명령 맨 앞** 여덟 곳(한 줄 · 모두 · 구간 · 메서드 · 왼쪽으로 한 줄 · 왼쪽으로 구간 · 3-Way 내 것 · 저쪽).
줄마다(`Apply` 안)가 아니라 명령마다 — [모두 반영] 이 줄 수만큼 알리거나 구간이 반쯤 반영된 채 멈추지 않게.
보고서 내보내기 · 변경분 추출의 이름은 반영이 아니므로 그대로(옛 물러서기).
- 화면에서 하나 더: 작성자가 비었는데 "머지 설명" 미리보기가 `// [Ubisam] …`(윈도우 계정)을 보였다 — 그 이름으로는 반영되지 않으므로
`(작성자가 비어 있어 반영할 수 없습니다 - 설정 > 머지 주석 > 작성자)` 로.
- 설정 칸 이름 `작성자 (비우면 git config user.name)` → `작성자 (주석을 남기면 꼭 적습니다 - 비어 있으면 반영하지 않고 알립니다)`.
- 한때 사용자 지시로 "주석 남기기를 늘 켬"(설정 칸 감춤)으로 바꿨다가 **곧 "고를 수 있게 원복"** 지시 — 되돌렸다. 늘 켬 판은 커밋되지 않았다.
- 하네스 `NoteTest` 에 6건을 먼저 넣어 **컴파일 실패** 확인 → 93 → 99건 통과. 화면 `g9-author.ps1`(새): ① 주석 켬 · 작성자 빔 →
알림 창("작성자를 적어 주세요", 설정 자리를 댐) · 편집기 그대로 · 미리보기 안내 ② 주석 끔 → 묻지 않고 반영 ③ 작성자 적음 →
`[G9시험자]` 주석과 함께 반영. 실패 0. (처음 판 3단계 실패 2건은 2단계 편집이 저장 안 한 편집 보관함으로 되살아난 것 — 단계 사이에 보관함을 비움.)
### 데모 걷어냄 · 이 PC 의 시험 흔적
- 코드: 설정 `데모` 묶음(`DemoGitPath` · `DemoPlainPath`), 파일 비교 위 [데모 → .git 있음/없음] 단추와 `SwitchDemoAsync`. 켤 때 쓰는
`LoadDefaultPlainFiles` 는 남김. 데모를 예로 든 주석 네 곳도 고침. `g6-verify` 가 설정 화면에 `데모` 가 없는 것을 본다.
- 이 PC(사용자 선택 "시험 흔적까지"): 시험 대본이 만드는 `%TEMP%` 재료 13개(e2-fold · g2 · mergehub-demo · plain-compare 등, 약 3MB)를
**휴지통으로**(영구 삭제 아님). 설정의 기본 저장소 · 기본 왼쪽/오른쪽 파일(시험 경로)을 비우고 데모 키 둘을 뺐다.
최근 목록에서 시험 경로 6줄만 뺐다(사용자가 실제로 비교한 18줄은 그대로). 이 상태를 새 기준본으로(`base-0930`, 옛 설정은 옆에 따로).
## G-6 고침. 담당 카드 · 프로그램 소개 문구 (2026-09-30, 사용자 지시 · 완료)
- 담당 카드를 **갈래 둘**로: `Platform` 최민영 대리 · `MergeHub` 김승준 사원. 이름표는 영문(사용자 선택). 사람은
`ProgramOverview.PlatformOwner` · `MergeHubOwner` 두 상수 — 설정의 "만든 사람" 은 정보 창의 회사 이름 받침이라 따로 둔다.
- **사내 문의처는 카드에서만 뺐다**(사용자 선택). [정보] 창의 문의처 줄과 G-5 구조(csproj `<SupportContact>` · 설정)는 그대로 — `g5-verify` 3건 통과.
- 소개 문구 `사내 Git Merge/Diff 도구` → **`사내 Merge/Diff + Git 도구`**(Merge 가 먼저 — 사용자 지시). 네 곳을 같은 말로:
csproj `<Description>` · `ProgramOverview.DefaultSubtitle` · 정보 창 받침 글 · 창 제목 `GetAppName()`(`MergeHub - 사내 Merge/Diff + Git`).
창 제목은 플랫폼이 왼쪽 위 글로만 쓰는 것을 읽어 확인했다(설정 폴더 등 열쇠로 안 쓴다).
- 시험: 하네스 `OverviewTest` 새 기대값으로 먼저 고쳐 **고치기 전 4건 실패** → 15건 통과, `AboutTest` 30건. 화면 `g6-verify` 실패 0
(두 담당자 위아래 · 같은 가운데, 설정에 만든 사람 · 문의처를 적어도 카드에 안 나옴, 창 제목) · 사진 확인. 끝에 기준본으로 되돌림.
## G-8. 메모 문법 강조 · 같은 글 칠하기 · 색 마킹 (2026-09-30, 사용자 요청 · 완료)
### 정한 것 (사용자 결정)
- 방식: 파일 비교처럼 **투명한 TextBox + 아래층**. 처음 지시의 `RichTextBox` 는 버렸다 — 칠하는 일이 되돌리기 기록에
섞이고, 줄 번호 · 캐럿 · 찾기 · 보관을 다시 짜야 해서.
- 강조 언어: 메모마다 콤보로 고른다. 기본은 파일이면 확장자, 이름 없는 메모는 C#. **보관하지 않는다.**
- 같은 글 칠하기: 한 줄 · 공백 아님 · 200자 이하 선택만. 낱말을 통째로 고르면 낱말 단위, 아니면 글자 단위.
대소문자를 가린다. 상한 5,000곳. 한 곳뿐이면(고른 그 자리) 칠하지 않는다.
- 마킹: 색 다섯, **Ctrl+1~5**(숫자판 포함) · 단추. 같은 글에 같은 색이면 풀고 다른 색이면 바꾼다. 고른 것이 없으면
캐럿 아래 낱말. 메모마다 따로, **보관하지 않는다.** 선택을 풀어도 남는다(글을 기억해 두었다가 그릴 때마다 찾아 칠한다).
- 오른쪽 위치 막대 12px: 같은 글은 왼쪽 반, 마킹은 오른쪽 반, 캐럿 줄은 가는 선. 누르면 그 줄로.
### 만든 것
- `Services/MemoHighlight.cs` — 셈하는 부분(`SmartTarget` · `Occurrences` · `LineStarts` · `LineOf` · `BarTicks` ·
`Tokenize`(여러 줄 주석 이어받기) · `SplitLines`), 줄 토큰 캐시 `MemoSyntaxCache`, 마킹 목록 `MemoMarkBook`.
로직을 먼저 쓰고 하네스를 붙였다 — **"고치기 전 실패" 단계는 없었다.**
- `Views/Controls/MemoPaintLayer.cs` — 아래층(보이는 줄만 · 배경 → 글자). 글자 자리는 스스로 셈하지 않고 편집기
`GetRectFromCharacterIndex` 에 묻고, 탭이 든 줄은 탭마다 끊어 다시 묻는다. `MemoMarkBar` — 위치 막대.
- `Memos.cs` — `Memo.Language`(고르지 않으면 기본값을 셈) · `Memo.Marks`.
- `MemoView.xaml(.cs)` — 콤보 · 마킹 단추 다섯 · [마킹 지우기] · 세 열 편집기. 다시 칠하기는 `Background` 우선순위로
한 번에 모은다(타이핑 한 번에 TextChanged · SelectionChanged 가 함께 온다). 글 · 언어가 그대로면 줄 토큰을 다시 셈하지 않는다.
세로 **또는 가로** 스크롤 · 크기 바뀜 · Ctrl+휠 뒤에 다시 그린다. 상태 줄 캐럿 글 끝에 `· 같은 글 N곳`.
### 화면에서 나온 것 (셋, 모두 값 · 사진으로 확인)
1. **아래층이 통째로 안 보였다.** 첫 `g8-verify` 에서 칠한 글자 잉크가 **0px**, 사진에는 편집기 선택 색만 있었다.
`같은 글 5곳` 은 나왔으니 칠할 값까지는 셈했다 — 파일 비교 `LocalEditor` 주석에 적힌 것과 같은 까닭:
플랫폼 암시적 TextBox 스타일이 템플릿 안에서 배경을 직접 칠해 `Background="Transparent"` 가 무시된다.
편집기에 `Style="{x:Null}"`. 줄 번호 칸은 "편집기와 같은 템플릿이라 같은 자로 잰다" 가 전제라 **함께** 뺐다
(줄 번호 7 과 7번째 줄 윗변 403px = 403px 로 확인). 선택 색은 LocalEditor 와 같은 값(Accent · 0.35)을 주었다 —
**선택 색은 사진으로 확인 못 함**(시험이 편집기에 포커스를 주지 않아 WPF 가 선택을 안 그렸다).
2. **강조 콤보에 `MergeHub.Services.LanguageProfile` 이 보였다**(사진). 플랫폼 콤보 템플릿은 선택 칸에 `DisplayMemberPath` 를
안 쓴다 — G-4 `SaveChoice` 와 같은 길로 `LanguageProfile.ToString() => Name`. 부르는 곳이 없는 것을 확인하고 넣었다.
3. `MemoMarkBar` 는 `FrameworkElement` 라 `Foreground` 가 없어 빌드 오류 — `TextElement.Foreground` 로.
### 어두운 테마에서 메모 탭 제목이 안 보인다 (사용자 보고, 같은 날)
`g8-tabs.ps1`(키 입력 없이 UIA + 사진)로 재현: 제목 `메모 1` 글 칸에서 바탕(#23262B)과 가장 다른 점의 차이 **116**(검정 글씨).
탭 줄의 `ItemContainerStyle` 이 플랫폼 `ListBoxItem` 스타일을 대신해서 글자색이 WPF 기본(검정)으로 떨어졌다.
`Foreground = Ubisam.Brush.ContentForeground` 를 그 스타일에 넣음 → 어두운 587 · 밝은 635.
파일 비교 문서 탭(`DocTabStrip`)도 같은 구조라 사용자 지시로 이어서 확인했다. `g8-doctabs.ps1`(폴더 비교에서 alpha · bravo 를
열어 탭 둘)로 재현 — 어두운 테마에서 `alpha.cs` 157 · `bravo.cs` 116. 같은 한 줄을 넣어 → 어두운 546 · 587, 밝은 673 · 635.
### 시험
- 빌드 경고 0 · 오류 0. 하네스 `MemoHlTest` 45건, `MemoTest` 69건(그대로 — `.csproj` 에 새로 필요한 소스 여섯 개를 넣음).
- 화면 `g8-verify.ps1 [-Dark]` — 편집기가 UIA 로 알려 주는 글자 칸(= 캐럿이 서는 자리)과 그 칸 안에 실제로 찍힌 잉크의
왼끝 · 오른끝을 px 로 비교한다(반 글자만 밀려도 칸 밖에 잉크가 걸린다).
① 겹침: `speed` 칸 95~148 · 잉크 96~146(왼 +1 · 오른 +2), 탭 든 줄 `speedLimit` · 뒤쪽 `speed` 도 +1~2 · 세로 칸 안.
② 색: 키워드 · 형 키워드 · 보통 이름 · 주석이 갈리고 여러 줄 주석 둘째 줄이 주석 색을 이어받는다. 콤보로 `평문` 을 고르면
`public` 이 보통 글 색, C# 으로 돌리면 돌아온다. ③ 낱말 통째 `같은 글 5곳`(speedLimit 안은 빼고) · 일부 `6곳` ·
`Speed` 는 표시 없음, 다른 자리 배경 칸 안 바탕 0%. ④ Ctrl+1 · 다시 누르면 풀림 · [마킹 2] · Ctrl+숫자판3 으로 색 바꿈 ·
[마킹 지우기] 1개 · 낱말 없는 자리는 안내만. ⑤ 노랑 눈금 y 81 · 95 · 111 · 1122 · 1138(기대 80 · 96 · 112 · 1123 · 1139),
막대를 누르면 71행. ⑥ **'7' 을 치고 마킹 · 선택을 바꾼 뒤 Ctrl+Z 한 번에 타이핑 전 글자 수로**(G-1 회귀 없음).
⑦ 가로로 굴린 뒤 71줄 끝 `speed` 도 +1~2. ⑧ 어두운 테마 같은 항목 모두.
**밝은 · 어두운 모두 실패 0**(사용자가 손을 뗀 동안 마지막으로 돌린 판).
- 흔들림: 사용자가 메시지를 보내던 동안 돌린 세 판은 판마다 다른 자리가 실패했다('7' 이 안 들어감 → Ctrl+Z 가 재료까지
되돌려 0자, 안내 글 항목 등). 흉내 낸 입력에 사용자 입력이 섞인 것으로 보이나 **값으로 확정하지 못했다.**
- 끝날 때마다 설정 네 파일 · 메모를 `base-0930` 과 해시가 같게 되돌림.
### 최종 회귀 (G 묶음 커밋 전, 사용자가 손을 뗀 동안)
`run-final.ps1` — 아홉 벌 + g1~g8(g8 셋은 밝은 · 어두운 두 번) 22벌: **통과 423 · 실패 4**, 끝에 설정 · 메모 해시 기준본과 같음 · 세션 보관함 0개.
실패 4건은 모두 `g8-verify -Dark` ④ 상태 글 — 읽힌 글이 `같은 글 · 마킹이 있는 자리. 누르면 그 줄로 갑니다`(위치 막대 **툴팁**)였다.
앞 벌(밝은)이 ⑤에서 막대를 누른 자리에 마우스가 남아 툴팁이 떴다. 같은 판의 배경 값(걸면 0% · 풀면 61% · 지우기 "1개")은 맞았다 →
**대본 잘못.** 상태 글에서 툴팁을 빼고 시작할 때 마우스를 치우게 고쳐 다시 돌림: 마킹 4건 통과.
그 판에서 ⑦ 마지막 항목("위로 돌아온 탭 줄") 한 번이 실패했다 — `speedLimit` 칸 130~237 안 잉크가 127~154 로 일부만.
실패할 때 사진을 남기게 하고 **다섯 번 더 돌렸으나 모두 실패 0** 이라 재현하지 못했다. 가로 스크롤을 되돌린 직후의 그림을 뜬 것으로
보이나 **값으로 확정하지 못했다**(사진이 없다). 다시 나오면 `g8-dark-7b.png` · `7c.png`(2초 뒤 다시 잰 것)가 남는다.
## G-7. `Alt+↓` 로 이동하다 보면 화면이 잘린다 (2026-09-30, 사용자 보고 · 완료)
### 먼저 재현 (값)
600줄 재료에 모양이 다른 차이 여덟 개(수정 한 줄 · 추가 5줄 · 삭제 10줄 · 400자 긴 줄 · 수정 · 50줄 큰 추가 ·
수정 · 마지막 줄)를 만들고 `Alt+↓` 를 차례로 누르며 칸마다 위치를 쟀다(`g7-probe.ps1`, 사진 `g7-00`~`g7-10`).
증상이 **둘** 나왔다.
| 언제 | 값 | 화면 |
|---|---|---|
| 앞으로 갈 때(2~7번째) | 차이 줄 위 **1,361px** · 칸 아래 1,394px · 줄 높이 약 35px | 가운데(842px)에 와야 할 차이 줄이 **칸 맨 아래에 걸려 잘림** |
| 마지막 → 첫 차이로 돌 때(9번째) | 두 칸의 위가 289 → **832px**(+543) | 두 칸이 통째로 반 칸 밀려 **위는 비고 아래는 잘림** |
### 원인 (기록으로 확인)
`MERGEHUB_G7_TRACE` 로 켜는 임시 기록을 `OnScrollToRowRequested` · `AlignDecorationToEditor` 에 심었다(끝나고 뺐다).
```
GOTO row=29 edOff=8950.5 listOff=8948.5 shift=-6.5
ALIGN row=29 ... wanted=-5.0 edOff=436.7 listOff=436.7 ← 편집기가 캐럿을 들이려고 먼저 구름, 목록도 따라옴
ALIGN row=29 ... wanted=357.2 edOff=74.4 listOff=436.7 ← 9ms 뒤 가운데 맞추기로 편집기만 74.4, 목록은 그대로
```
1. 차이로 가면 캐럿을 옮기는데, 그 순간 편집기가 **"캐럿을 화면에 들이려고" 스스로 한 번 구른다**(WPF 기본).
이 스크롤은 다른 칸을 따라오게 하면서 메아리 막기(`HoldSync`)를 건다 — 이 막기는 `Input` 우선순위에서 풀린다.
2. 곧이어 `CenterCharInView` 가 그 줄을 가운데로 올리려고 편집기를 **한 번 더** 굴린다. 이것은 `Loaded` 우선순위라
`Input` 보다 **먼저** 돈다 → 두 번째 스크롤은 메아리로 여겨져 **버려진다.**
3. 그래서 목록 칸들은 첫 스크롤 자리(= 차이 줄이 칸 맨 아래)에 남았다. 뒤로 크게 돌 때는 편집기와 목록이 반 칸
어긋난 채로 표시층 맞추기(`AlignDecorationToEditor`)가 그 차이를 **이동값 357**(화면 배율 1.5 → 약 543px)로
메워, 네 칸에 함께 걸린 이동값 때문에 두 칸이 통째로 밀렸다.
### 고침
`CenterCharInView` 가 편집기를 굴린 뒤 **다른 칸을 직접 맞춘다** — `HoldSync()` · `Scroll(LocalLines, y)` ·
`SyncPanesFrom(Local, y, 뷰포트)`. 편집기 ScrollChanged 가 하던 일과 같은 호출이라 동작이 갈리지 않고,
메아리 막기에 걸릴 일이 없다. 굴릴 값도 `Math.Max(0, …)` 에서 `Clamp(…, ScrollableHeight)` 로 위아래를 함께 막았다.
### 시험
- 화면 `g7-verify.ps1` — `Alt+↓` 9번(한 바퀴) · `Alt+↑` 3번(뒤로 돌기) · [처음] · [마지막] · [처음] 마다
① 두 칸이 제자리(위 ±12px) ② 차이 줄이 칸 안에 보임 ③ 파일 끝이 아니면 **칸 가운데 근처(칸 높이 ±25%)**.
**고치기 전 10건 실패**(맨 아래 걸림 8 · 칸 밀림 2) → **고친 뒤 실패 0**: 차이 줄 위 837px · 칸 가운데 842px,
칸 위 289px 그대로, 표시층 이동값 −5~−7 (예전 357 없음). 파일 끝 차이(8번)에서 칸이 2~3px 움직이는 것은
원래 있던 미세 맞춤(이동값 −5 → −6.5)이라 이번 증상과 별개다.
- 빌드 경고 0 · 오류 0. 회귀는 아래 G-6 과 함께 적는다.
## G-6. 개발자 정보 (설정 안 메뉴) (2026-09-30, 사용자 요청 · 완료)
### 자리가 바뀌었다 (사용자 지시)
처음에는 하단 탭 맨 끝의 `개요` 로 만들었는데, 사용자가 **"탭으로 만들지 말고 설정에 메뉴 하나 만들어서 개발자
정보라는 이름으로"** 옮겼다. 플랫폼은 건드리지 않고(사용자: "이제 웬만하면 플랫폼은 건들지 않을 거야") 플랫폼이
이미 주는 `AddSub<OverviewView, OverviewViewModel>(TabManager.SettingsTabKey, "개발자 정보")` 로 붙였다 — 플랫폼의
UI Setting · 백업 · 로그와 같은 길이다. 설정 하위 메뉴 차례는 `MergeHub · 개발자 정보 · UI Setting · 설정값 · 백업 ·
클리너 · 로그`, 하단 탭은 예전 그대로(`폴더 비교 · 파일 비교 · Git · 메모 · 설정`).
### 무엇을 만들었나
읽기 전용 안내 화면. 사용자가 준 참고 화면(가운데 제목 + 버전 배지 ·
소개 한 줄 · 목적 문장 · 카드)의 모양을 따랐다. 참고 화면의 이름들은 다른 서비스의 예시라 가져오지 않았다.
| 자리 | 값 | 어디서 |
|---|---|---|
| 제목 · 배지 | `MergeHub` · `v0.12` (배지에 올리면 빌드 날짜) | 실행 파일 (`ProgramInfo`) |
| 소개 한 줄 | `사내 Git Merge/Diff 도구` | csproj `<Description>` |
| 목적 문장 | `MergeHub 는 폴더 비교 · 파일 비교(2-Way · 3-Way 머지) · Git 커밋을 한 곳에서 하는 UbiSam 사내 머지 도구입니다.` | 코드(`ProgramOverview.PurposeText`) |
| 카드 `담당` | 만든 사람 · 사내 문의처 | 설정 → 실행 파일 받침값(G-5) — **정보 창과 같은 길** |
| 아래 | `빌드 2026-09-30 14:14` | 실행 파일 |
사용자 결정(추천안 그대로): ① (처음) 맨 뒤 탭 → 뒤에 설정 메뉴로 옮김 ② 카드 한 장(`담당`) — 이 프로그램에는 Backend/Frontend 같은 갈래가 없다
③ 목적 문장은 코드에, 사람 이름은 기존 설정 · csproj 받침값을 다시 씀 ④ [정보] 창은 그대로 둔다.
### 짠 방식
- `Services/ProgramOverview.cs` — 화면에 올릴 값을 셈하는 순수 모델(`From(ProgramInfo, description)`).
버전을 못 읽으면 `v알 수 없음` 이 되지 않게 `v` 를 붙이지 않는다.
- `OverviewViewModel` — 정보 창과 같은 `ProgramInfoReader.For(어셈블리, 설정 만든 사람, 설정 문의처)`.
설정에서 이름을 고칠 수 있으므로 **화면이 보일 때마다 다시 읽는다**(`IsVisibleChanged`).
- `OverviewView.xaml` — 가운데 정렬 · 카드는 `WrapPanel`(늘어나면 가로로, 좁으면 아래로). 색은 기존 토큰만 썼다
(`ContentBackground` · 카드 `Input.Background`/`Input.Border` · 배지 `Tag.Accent*`).
- `AppSetup.RegisterSettings` — `AddSub<OverviewView, OverviewViewModel>(TabManager.SettingsTabKey, "개발자 정보")`(처음의 `AddMain` · `ViewIds.Overview` 는 뺐다).
### 화면을 보고 고친 것
처음에는 카드 한 줄에 `이름표 값` 을 나란히 두었는데, 사진으로 보니 문의처 안내 글(`적혀 있지 않습니다 (설정 >
MergeHub 에서 적습니다)`)이 카드 폭에서 **`에 / 서` 로 낱말 가운데가 꺾였다.** 또 `Run` 사이 줄바꿈이 공백으로
들어가 이름표와 값 사이가 4칸이 됐다. → **이름표를 위에 작게, 값을 아래에** 두었다. 안내 글 높이 26px = 짧은 값
`UbiSam` 26px(한 줄), 폭 483px.
### 시험
- 하네스 `OverviewTest` **14건 새로** — 고치기 전에는 모델 파일이 없어 컴파일 실패. 제목 · 배지(`v0.12`, 못 읽으면
`v` 없음) · 소개(없으면 기본) · 목적 · 빌드 줄 · 카드 한 장 · 두 줄 · **문의처가 비면 정보 창과 같은 안내 글**.
- 빌드 경고 0 · 오류 0.
- 화면 `g6-verify.ps1` **22건 · 실패 0**(설정 메뉴로 옮긴 뒤):
하단 탭에 개요가 없음(`폴더 비교,파일 비교,Git,메모,설정,…`) · 설정 → 개발자 정보가 열림 · 글 값 전부 · 머리 가운데 1,271 / 목적 1,280 /
카드 1,281 (px, ±20 안) · 설정에 적은 만든 사람 · 문의처가 카드에 나옴 · 파일 비교 · 메모 탭 회귀 · 오류 글 없음.
시험 대본의 헛짚음 둘: 하단 탭 이름은 항목 Name 이 아니라 템플릿 안 TextBlock 이라 **Raw 보기**로 읽어야 했다.
### 값으로 확정하지 못한 것
- **설정 화면에서 이름을 고치고 돌아왔을 때 바로 바뀌는가**(`IsVisibleChanged` 로 다시 읽기) — 시험은 설정 파일을
고치고 **다시 켜서** 확인했다. 켜 둔 채 설정 화면을 흉내 내 고치는 길은 만들지 않았다.
- 시험 뒤 **메모 한 장에 `TL` 두 글자가 들어가 있었다.** 대본은 키를 보내지 않는다. 같은 탭 이동(개요 → 파일 비교 →
메모)만 따로 돌려 보니 메모는 0바이트 그대로라 재현되지 않았다 — 앱에 초점이 간 사이 밖에서 들어온 키 입력으로
보이나 확정하지 못했다. 메모 보관함은 기준본으로 되돌렸다.
## G-5. 사내 문의처가 배포본에 따라가지 않는다 (2026-09-30, 사용자 요청 · 구조 완료 · 문구 대기)
### 무엇이 문제였나
정보 창(12차 B-2)의 만든 사람은 설정이 비면 `csproj` 의 `<Company>` 로 받쳐지지만, 문의처는
`Prefer(contact, null)` — **받칠 것이 없었다.** 설정(`D:\UbisamConfig\Settings\MergeHubSettings.json`)은
PC 마다 따로라, 배포본을 새로 깐 PC 에서는 늘 `적혀 있지 않습니다` 가 보였다.
### 고친 방식 (사용자 결정: csproj 에 받침값)
- `csproj` 에 `<SupportContact></SupportContact>` 를 두고, 빌드할 때 `AssemblyMetadata("SupportContact", …)` 로
실행 파일에 박는다 — 이미 있는 `BuildDate` 와 같은 길이다. **비어 있어도 키는 박는다**(읽는 쪽이 "자리는 있다" 를 확인).
- `ProgramInfoReader.StampedContact` 가 그것을 읽고, `Contact = Prefer(설정, 박힌 값)`. 순서: 설정 → 박힌 값 → 안내 글.
`StampedBuildDate` 와 같은 읽기를 `Stamped(assembly, key)` 하나로 모았다.
- 설정 화면 글: `사내 문의처 (담당자 · 연락처 한 줄, 비우면 실행 파일에 박힌 값)`.
- **문구는 아직 받지 않았다**(사용자 결정: 구조만 먼저). 지금은 비어 있어 화면은 예전과 같다.
나중에 `csproj` 의 `<SupportContact>` 한 줄만 채우면 된다.
### 시험
- 하네스 `AboutTest`(옛 세션 scratchpad 에서 이번 도구 자리로 옮김) 28 → **30건**(+3, 기존 "안내 글" 1건을 바꿈).
고치기 전에는 `StampedContact` 가 없어 **컴파일부터 실패**.
`ABOUT_DLL` · `ABOUT_CONTACT` 로 dll 을 갈아 끼울 수 있게 해 **두 빌드를 모두 읽었다** —
원래 빌드(비어 있음) 30건 · 문구를 채운 시험 빌드(`-p:SupportContact=G5 시험 문의처 (내선 0000)`, scratchpad 로 출력) 30건 실패 0.
- 빌드 경고 0 · 오류 0.
- 화면 `g5-verify.ps1` — 두 빌드에서 각각 3건 · 실패 0:
| 빌드 | 설정 비움 | 설정에 `G5 설정 문의처` |
|---|---|---|
| 원래(비어 있음) | `적혀 있지 않습니다 (설정 > MergeHub 에서 적습니다)` | `G5 설정 문의처` |
| 시험(문구 채움) | **`G5 시험 문의처 (내선 0000)`** | `G5 설정 문의처` (설정이 앞선다) |
만든 사람은 두 경우 모두 `UbiSam` 그대로. 시험 빌드는 같은 자리(`bin\Debug`)에 빌드해 띄운 뒤 **원래대로 다시 빌드**했고,
되돌린 dll 이 안내 글을 내는 것을 하네스로 확인했다.
## G-4. 저장 형식 칸의 `원본 유지` 콤보 둘이 구별되지 않는다 (2026-09-30, 사용자 요청 · 완료)
### 무엇이 문제였나 (값)
저장 형식 칸에 콤보 둘이 나란히 붙어 있고, 기본값이 **둘 다 `원본 유지`** 였다 —
`TextFile.SaveEncodings[0]` 과 `TextFile.SaveNewLines[0]` 이 같은 상수 `KeepOriginal` 이다.
툴팁을 올려 봐야 어느 쪽이 인코딩인지 알 수 있었다.
### 그냥 문자열을 바꾸면 안 되는 까닭
`KeepOriginal` 은 **보이는 글이면서 저장되는 값**이다. `HomeViewModel.SaveEncoding`·`SaveNewLine` 의 값이
그대로 문서 탭 → 세션 보관본(`목록.txt` 의 7·8번째 칸)에 적힌다. 문자열을 바꾸면 이미 보관된 세션의 값
`원본 유지` 가 목록에 없는 값이 되어 되살릴 때 콤보가 빈 채로 뜬다. 또 `TextFile.Encode` 는 모르는 값을
원본 유지로 보므로 동작은 같겠지만, 화면과 값이 어긋난다.
### 고친 방식 — 보이는 글과 값을 가른다 (인수인계의 1안)
- `TextFile.SaveChoice(Value, Label)` 를 새로 두고, 목록을 `SaveEncodingChoices` · `SaveNewLineChoices` 로 바꿨다.
첫 칸만 글이 다르다 — `(원본 유지, "원본 인코딩 유지")` · `(원본 유지, "원본 줄바꿈 유지")`. 나머지는 글 = 값.
- 콤보는 `DisplayMemberPath="Label"` · `SelectedValuePath="Value"` · `SelectedValue="{Binding SaveEncoding}"`.
**ViewModel · 저장 · 세션 보관은 한 줄도 안 바뀌었다** — 여전히 문자열 값을 주고받는다.
- `SaveChoice.ToString()` 이 `Label` 을 돌려준다 — UIA 가 콤보 칸의 이름으로 이것을 읽는다.
- 화면 시험이 집을 수 있게 콤보에 `SaveEncodingBox` · `SaveNewLineBox` 이름을 달았다.
### 시험
- 하네스를 먼저 넣었다. `EncTest` 에 G-4 8건 — 고치기 전에는 새 목록이 없어 **컴파일부터 실패**, 고친 뒤
**56건 · 실패 0**(두 콤보 글이 겹치지 않음 · 저장값은 그대로 `원본 유지` · 원본 유지로 쓰면 원본 줄끝을 따름).
`SessionTest` 에 3건 — 보관본에 `원본 유지` 가 값으로 적히고 그대로 되읽힌다. 고치기 전에도 통과했다
(보관은 값을 글자 그대로 옮길 뿐이라는 뜻 = **값만 안 바꾸면 옛 보관본이 산다**), 고친 뒤 **68건 · 실패 0**.
- 빌드 경고 0 · 오류 0.
- 화면 `g4-verify.ps1` **25건 · 실패 0**:
| 확인 | 값 |
|---|---|
| 연 직후 콤보 글 | `원본 인코딩 유지` · `원본 줄바꿈 유지` |
| 콤보 칸들 | `원본 인코딩 유지,UTF-8,UTF-8 (BOM),CP949` · `원본 줄바꿈 유지,CRLF,LF` |
| 콤보 폭 | 330px (글이 잘리지 않음) |
| LF · UTF-8 (BOM) 골라 오른쪽 저장 | CRLF 로 만든 파일이 **CR 0개** · BOM 붙음 — 고른 값이 저장까지 간다 |
| 원본 유지로 돌리고 강제로 끔 | 보관본 7·8칸 = `원본 유지 \| 원본 유지` (옛 값 그대로) |
| 다시 켬 | 고친 글이 되살아나고 콤보 = `원본 인코딩 유지` · `원본 줄바꿈 유지` |
| CP949 · CRLF 로 같은 시험 | 보관본 `CP949 \| CRLF` → 되살린 콤보 `CP949` · `CRLF` |
- 회귀 `e1-verify`(세션 되살리기) — **첫 번은 12건 실패, 같은 빌드로 다시 돌리니 29건 · 실패 0.**
첫 번은 1단계에서 폴더 비교 줄을 **진짜 마우스로 더블클릭**해 둘째 짝을 여는 것이 안 먹어 탭이 하나뿐이었고,
뒤 단계가 줄줄이 따라 실패했다. 코드가 같은데 결과가 갈렸으므로 G-4 와는 무관하다. 다만 **더블클릭이 왜
빗나갔는지는 값으로 확정하지 못했다**(창이 가려졌거나 앞에 오지 않았던 것으로 보이나 찍어 두지 않았다).
## G-3. "덩어리" 라는 말이 어색하다 (2026-09-30, 사용자 요청 · 완료)
### 무엇을 바꿨나
사용자 결정대로 화면에 보이는 "덩어리" 를 **"구간"** 으로 바꿨다. 툴팁은 원래 "차이가 이어지는 구간" 이라
적고 있었다 — 단추 글만 다른 말을 쓰고 있던 셈이다.
| 자리 | 전 | 후 |
|---|---|---|
| 단추 (`AcceptBlockButton`) | `이 덩어리 반영` | **`구간 반영`** |
| 단추 (`AcceptBlockToBaseButton`) | `이 덩어리 왼쪽으로` | **`구간 왼쪽으로`** |
| 단축키 설명 `Alt+Shift+→` | `이 덩어리 반영 - 차이가 이어지는 구간을 한 번에` | `구간 반영 - 차이가 이어지는 구간을 한 번에` |
| 단축키 설명 `Alt+Shift+←` | `이 덩어리를 왼쪽 파일로 - 이어지는 구간을 한 번에 (2-Way)` | `구간 왼쪽으로 - 차이가 이어지는 구간을 한 번에 왼쪽 파일로 (2-Way)` |
| 로그 (Git 탭 아래) | `5~7행 덩어리 BASE(…) 반영 (3줄)` | `5~7행 구간 BASE(…) 반영 (3줄)` |
| 로그 (왼쪽으로) | `5~7행 덩어리 왼쪽에 반영 (3줄)` | `5~7행 구간 왼쪽에 반영 (3줄)` |
토스트(`3줄 반영` · `3줄 왼쪽에 반영`)는 원래 "덩어리" 가 없어 그대로다. 옆의 `이 메서드 반영` 도 그대로다.
**바꾸지 않은 것**: 코드 안 이름(`AcceptBlock` · `BlockAround` 등)과 주석의 "덩어리"(개념을 가리키는 말이라
화면에 안 나온다). 단추 이름을 그대로 인용하던 주석 두 곳만 맞췄다. **웹 문서(`doc/web/index.html`)에 옛 문구가
네 곳 남아 있다** — 웹 문서는 차수 끝에 한 번에 고친다.
### 시험
- 하네스 `ShortcutTest` 101 → **104건**(+3): 두 설명의 첫머리가 `구간 반영` · `구간 왼쪽으로` 인가,
단축키 표 **전체**에 "덩어리" 가 남지 않았는가. **고치기 전에 돌려 새 3건이 실패하는 것을 먼저 확인했다**
(`통과 101 · 실패 3`) → 고친 뒤 `통과 104 · 실패 0`.
- 빌드 경고 0 · 오류 0.
- 화면 `g3-verify.ps1` **18건 · 실패 0** — 단추 글(UIA Name)이 `구간 반영` · `구간 왼쪽으로`, 두 단추 폭 270px
(잘리지 않음), 누르기 전후 화면 어디에도 "덩어리" 없음, 두 방향 모두 세 줄이 한 번에 가고 한 번에 돌아옴,
**로그 칸(Git 탭, ValuePattern)** 에 `5~7행 구간 왼쪽에 반영 (3줄)` · `5~7행 구간 BASE(block.cs) 반영 (3줄)`.
- 회귀 `g2-verify` 23건 실패 0(단추는 AutomationId 로 찾아 문구와 무관).
- `f2-verify`·`f2-probe` 는 단추를 **글로** 찾아 새 문구로 맞췄다. 다만 `f2-verify` 는 이번 변경보다 앞 단계에서
`'저장' 안 보임` 으로 멈춘다 — `저장` 단추는 12차 B-1 에서 `◀ 왼쪽 저장` · `오른쪽 저장 ▶` 로 나뉘어 이미 없고,
이 대본은 회귀 몰이(`run-all-merge`)에도 없다. **원래 낡은 대본이라 G-3 과 무관**하다. 멈춘 채 뒤처리를 못 해
설정 두 파일 · 세션 보관함이 흐트러졌고, 이번 세션 시작 때 떠 둔 기준본으로 바이트까지 되돌렸다.
## G-2. base 칸으로 머지하는 단축키 (2026-09-30, 사용자 요청 · 완료)
### 무엇이 없었나
12차 B-1 로 **왼쪽 파일로 반영**하는 길이 생겼는데(`[<]` 단추 · `TakeLineToBase`) **단축키가 없었다.**
덩어리를 왼쪽으로 보내는 것은 명령 자체가 없었다 — `AcceptBlock`(`Alt+Shift+→`)은 오른쪽으로만 간다.
### 키를 어디에 둘지
사용자가 "`Alt+←` 도 비어 있지 않냐" 고 물었다. **비어 있지 않았다** — `CopyRightToLeft` 가 쓰고 있었는데,
2-Way 에서 그것이 하는 일은 이름·화살표와 달리 **"이 줄 되돌리기"** 였다. 그런데 이름과 화살표가
가리키는 "왼쪽으로 반영" 이 B-1 로 **실제로 생겼다.** 그래서 감각이 맞는 쪽으로 넘겼다(사용자 결정).
| 키 | 전 | 후 |
|---|---|---|
| `Alt+→` | 한 줄 오른쪽으로 | 그대로 |
| `Alt+Shift+→` | 덩어리 오른쪽으로 | 그대로 |
| `Alt+←` | **이 줄 되돌리기**(3-Way: Take Theirs) | **한 줄 왼쪽 파일로**(3-Way 는 Take Theirs 그대로) |
| `Alt+Shift+←` | 비어 있음 | **덩어리 왼쪽 파일로**(새로 만듦) |
| 이 줄 되돌리기 | `Alt+←` | **단축키 없음 — `Ctrl+Z` 만**(사용자 결정) |
`Alt+→` 가 이미 2-Way/3-Way 로 갈라져 있으므로 `Alt+←` 도 같은 식으로 갈랐다. 3-Way 는 안 건드렸다.
### 만든 것
- `AcceptBlockToBase` · `ApplyGroupToBase` — `AcceptBlock`/`ApplyGroup` 의 거울.
주석은 덩어리 첫 줄 위 한 줄, 되돌리기는 한 번에(11차 F-2 와 같은 규칙).
덩어리를 잡는 `BlockAround` 는 2-Way 에서 이미 `CanTakeBase` 로 잡으므로 **그대로 썼다** —
"가져올 수 있는 줄" 과 "보낼 수 있는 줄" 이 같은 줄이고 방향만 반대다.
- **[이 덩어리 왼쪽으로] 단추** — `[이 덩어리 반영]` 옆에. 두 파일 비교(`BothWays`)에서만 뜬다.
단축키만 두면 찾을 길이 없다.
### 겪은 것 둘
**`RaiseCanExecuteChanged` 에 새 명령을 안 넣어 단추가 꺼진 채였다.** CommunityToolkit 의 커맨드는
`CommandManager` 를 구독하지 않아 직접 알려야 한다(파일에 이미 그 까닭이 주석으로 있었다).
`OnCaretRowChanged` 에도 넣어야 줄을 옮길 때 켜지고 꺼진다.
**덩어리 명령은 고른 줄이 아니라 `CaretRow`(편집기 캐럿)를 본다.** 시험이 왼쪽 목록을 고르기만 해서
한 줄만 갔다 — `Alt+↓`(다음 차이)로 캐럿을 놓아야 했다.
### 화면으로 확인 (값)
`g2-verify.ps1` **21건 · 실패 0**. 연속 세 줄이 다른 재료(`speed` · `power` · `delay`)를 따로 만들어 썼다.
| 확인 | 값 |
|---|---|
| `Alt+←` 한 줄만 | `speed = 10 → 11`, `power`·`delay` 는 그대로 |
| `Ctrl+Z` | 되돌아간다 |
| 덩어리 왼쪽으로 | 셋이 **한 번에** 간다 |
| 덩어리 되돌리기 | 셋이 **한 번에** 돌아온다 |
| 회귀 | `Alt+→` · `[이 덩어리 반영]` 그대로 · `g1`·`e2`·`e3`·`e4` 실패 0 |
하네스 `ShortcutTest` 88 → **101건**(G-2 +13): 키가 겹치지 않음 · 화살표 넷의 모양이 맞음
(Alt = 한 줄, Alt+Shift = 덩어리) · 충돌 이동(`Alt+Shift+↑↓`)을 안 빼앗음 ·
**`Alt+←` 설명이 "되돌리기" 라고 거짓말하지 않음**.
### 값으로 확정하지 못한 것
**`Alt+Shift+←` 와 `Alt+Shift+→` 를 흉내 낸 키로는 못 눌렀다.** 윈도우가 `Alt+Shift` 를
입력 언어 전환으로 먼저 채간다 — `SendKeys` 로는 아예 안 닿고, `keybd_event` 로는 `Shift` 가 빠져
`Alt+←` 처럼 동작했다. **기존 `Alt+Shift+→` 로 견줘 봐도 똑같았으므로 시험 도구 한계다**(C-3 의
`DragMove` 와 같은 부류). 그래서 덩어리는 **같은 명령을 부르는 단추로** 확인했다.
사람 손으로 `Alt+Shift+←` 를 한 번 눌러 보는 것이 남았다.
## G-1. undo 가 계속 이어진다 (2026-09-30, 사용자 보고 · 완료)
### 먼저 재현 (화면, 값)
짝 A(`alpha.cs`)를 보다 짝 B(`bravo.cs`)로 옮기고 `Ctrl+Z` 를 누르면 **끝나지 않는다.**
```
5회: 228자 10회: 236자 15회: 228자 20회: 236자 … 40회: 236자
```
**글자를 하나도 안 쳐도 난다** — 짝만 바꾸면 A 의 글(234자)과 B 의 글(228자)을 영원히 오간다.
### 무엇이었나
`Ctrl+Z` 는 우리 목록(`_undo` · `_textUndo`)이 비면 **키를 먹지 않는다**(`HomeView.xaml.cs` 의
`RunShortcut` — *"되돌릴 머지 작업이 없으면 키를 먹지 않는다, 그래야 편집기의 글자 되돌리기가 살아 있다"*).
그러면 그 키가 `TextBox` 자체 되돌리기로 넘어간다. WPF 의 그것은 **한도가 없고, 프로그램이 코드로
넣은 글까지 기록한다.** 짝을 바꿔 새 파일을 실으면 그 글도 기록에 쌓이므로, `Ctrl+Z` 가
**앞 파일 글을 편집기에 되살렸다.**
거기서 끝이 아니다. 기록을 심어 고리를 떴다.
```
Sync 끝났다 편집기=228 ← B 를 편집기에 실었다
편집알림 편집기=234 모델=216 ← TextBox 되돌리기가 A 의 글을 되살렸다
Reflow 모델=216 편집기=221 같나=False ← 그걸 "사용자가 친 것" 으로 보고 모델을 A 로 되돌린다
StepText 목표=216 … 편집기=234 ← 우리 undo 가 다시 B 로
… 끝없이 되풀이
```
`221 = 234 − 13`(줄 13개의 CRLF↔LF 차이)이라 편집기가 쥔 것이 A 의 글임을 값으로 확인했다.
`Reflow` 는 다시하기(redo)까지 지우므로 빠져나갈 길도 없었다.
### 고친 것 — 한 줄
`HomeView.xaml` 의 `LocalEditor` 에 **`IsUndoEnabled="False"`**. 되돌리기는 우리 목록만 쓴다 —
줄 단위(`_undo`)와 글 단위(`_textUndo`, 타이핑이 멈출 때마다 한 걸음).
### 헛짚은 것 둘 (기록으로 남긴다)
| 짚었던 것 | 어떻게 아니라고 알았나 |
|---|---|
| "실린 문서가 바뀔 때 `TextBox` 기록을 비우면 된다"(`EditorUndoGate` 를 만들어 하네스 16건까지 붙였다) | 앞 파일로 넘어가는 것은 막았지만 **오가는 것은 그대로**였다 — 버렸다 |
| "타이핑 때 건 재정렬 타이머가 되돌린 뒤 뒤늦게 터진다" | `SyncEditorFromLines` 에서 타이머를 꺼도 **그대로**였다 — 되돌렸다 |
둘 다 만들어 시험까지 했지만 증상이 안 없어져 되돌렸다. **검증 안 된 동작 변경은 남기지 않는다.**
특히 `IsUndoEnabled="False"` 를 효과 없이 남기면 글자 단위 되돌리기만 잃는 꼴이 된다.
### 화면으로 확인 (값)
`g1-verify.ps1` **14건 · 실패 0**
| 확인 | 값 |
|---|---|
| **끝없이 오가지 않는다** | 길이 자취 `228 → 228 → 228 → 228 → 228 → 228`(30번 눌러도) |
| **앞 파일로 안 돌아간다** | A 는 234자 — 자취에 한 번도 안 나온다 |
| 친 뒤에도 같다 | 자취 `228 → …`, 친 글은 되돌려졌다 |
| 회귀 — 되돌리기는 살아 있다 | 친 글이 `Ctrl+Z` 로 되돌아간다 |
| 회귀 — 편집 관련 다섯 벌 | `e1` · `e2` · `e2-reg` · `e3` · `e4` 전부 실패 0 |
### 남은 것
글자 **한 자 단위** 되돌리기는 없어졌다. 이제 한 걸음은 "타이핑이 멈춘 자리" 다.
쓰다가 너무 굵게 느껴지면 `_textUndo` 를 더 잘게 쌓는 것으로 따로 잡는다.
## A-1. 폴더 줄의 크기가 늘 0B (2026-09-27, 완료)
### 무엇이 문제였나
크기를 **파일 줄에만** 채웠다(`FolderComparer` 의 파일 루프에서 `FileInfo.Length`). 폴더 줄은 `LeftSize` ·
`RightSize` 가 0 인 채로 화면에 내려가 어느 폴더든 `0 B` 로 보였다. 어느 쪽이 더 큰지, 어느 폴더가 무거운지를
목록에서 알 수 없었다.
### 어떻게 고쳤나
`Services/FolderSizeText.cs` 를 새로 두었다. 화면 없이 검사할 수 있게 계산만 떼어 낸 것이다.
| 하는 일 | 함수 |
|---|---|
| 사람이 읽는 크기로 적기 | `Of(long bytes)` — 1KB 미만은 바이트 그대로(`999 B`), 그 위는 소수 한 자리(`1.5KB` · `2.4MB` · `3GB`) |
| 폴더 줄에 합계 채우기 | `FillFolderSizes(items)` — 아래에서 위로, 폴더는 자기 아래 것들의 합 |
- 부르는 자리는 `FolderComparer.Compare` 의 **`ClassifyAll` 다음, `Tally` 앞**이다. 파일 갈래를 정하는 판단은
파일 크기를 보고 하므로(크기가 다르면 열지 않는다) 그보다 **먼저 채우면 판단이 달라진다** — 그래서 뒤에 둔다.
- 걸러 낸 것은 합계에서도 빠진다. 포함 · 제외 규칙에 걸린 파일은 애초에 목록에 없으므로 따로 뺄 일이 없다.
- 한쪽에만 있는 폴더는 그쪽만 합계고 반대쪽은 0 이다(화면에는 `-`).
- 화면 쪽은 `FolderViewModel.Describe` 가 `{크기} · {수정일}` 을 만들 때 `FolderSizeText.Of` 를 쓰게 바꿨다.
예전에는 `{바이트:N0} B` 라 300,000 B 처럼 읽기 어려웠다.
### 값으로 확인한 것
시험 재료는 만들어 썼다(`%TEMP%\mergehub-a1`, 실제 업무 저장소는 읽지 않는다).
| 폴더 | 화면 | 탐색기 속성(파일 크기 합) |
|---|---|---|
| `Docs` | 2.4MB · 2.4MB | 2,500,000 · 2,500,000 |
| `Legacy`(왼쪽만) | 50KB · `-` | 51,200 · 없음 |
| `NewOnly`(오른쪽만) | `-` · 40KB | 없음 · 40,960 |
| `Src` | 490.2KB · 491.7KB | 502,000 · 503,500 (기본 제외 규칙에 걸린 `Src\bin` 9MB 는 빠짐) |
| `Src\Core` | 472.7KB · 474.1KB | 484,000 · 485,500 |
| `Src\Core\Motion` | 179.7KB · 181.2KB | 184,000 · 185,500 |
| `Src\Ui` | 17.6KB · 17.6KB | 18,000 · 18,000 |
- 파일 줄도 함께 읽기 쉬워졌다 — `Unit0.cs 293KB` · `Home.cs 62.5KB / 64KB` · `manual.txt 2.4MB`.
- **비교 시간**: 4,608 줄(136MB · 파일 8,922개) 짜리 묶음에서 합계 채우기 **0ms**(빠른 비교 203ms 대비 0.0%).
디스크를 다시 읽지 않고 이미 만들어 둔 목록을 한 번 훑을 뿐이라 늘어날 여지가 없다.
### 하네스
`FolderTest` **88 → 111건(+23)** · 실패 0.
- 글자 8건: 1KB 미만 · 1024 · 소수 한 자리 · 딱 떨어질 때 · MB · GB · TB 를 넘을 때 · 음수
- 합계 9건: 여러 겹 · 양쪽 따로 · 가운데 폴더 · 맨 안쪽 · 한쪽에만 있는 폴더 · 파일 줄은 그대로 ·
탐색기 속성과 같은 값(양쪽)
- 걸러 내기 3건: 제외한 폴더 · 제외해도 다른 줄은 그대로 · 포함 규칙에 안 맞는 파일
- 판단이 안 바뀌는지 4건: 크기가 다른 이진 파일 · 같은 파일 · 한쪽에만 있는 폴더 · 하위 폴더를 안 볼 때
### 남긴 것
- 폴더 줄의 **수정일**은 그대로 비워 둔다. 폴더의 수정 시각은 안의 파일과 무관하게 바뀌어(윈도가 하위 항목을
더하거나 지울 때만 갱신) 사람이 기대하는 값과 다르다 — 크기만 적는 편이 오해가 적다.
## A-2. 좌우로 나눠 보이기 (2026-09-27, 완료)
### 무엇이 문제였나
한 줄에 `이름 | 상태 | 왼쪽(크기·수정일) | 오른쪽(크기·수정일)` 이 늘어서 있었다. 이름이 한 번만 나오니
"이 줄이 어느 쪽 것인가"를 상태 글자를 읽어야 알 수 있었고, 한쪽에만 있는 항목도 `-` 한 글자로만 표시됐다.
### 어떻게 고쳤나
한 줄을 **왼쪽 목록 · 가운데 상태 · 오른쪽 목록** 세 칸으로 나눴다(11차 F-10 파일 비교와 같은 모양).
- 칸마다 **자기 쪽 이름과 크기**를 적는다. 그쪽에 없으면 그 칸을 통째로 비운다 — 빈칸이 "여기엔 없다"를
상태 글자보다 먼저 말해 준다. 없는 쪽 크기를 `-` 로 적던 것도 뺐다.
- 가운데 칸이 두 목록을 가르는 선이다(같음 · 다름 · 왼쪽만 · 오른쪽만).
**줄마다 칸이 어긋나는 문제**가 하나 있었다. TreeView 는 깊이만큼 <b>줄 전체</b>를 오른쪽으로 밀기 때문에,
칸 폭을 고정해도 깊은 줄일수록 가운데 · 오른쪽 칸이 오른쪽으로 밀려 두 목록으로 읽히지 않는다.
그래서 `TreeDepthMarginConverter`(새로 둠)로
1. 줄 바깥틀을 민 만큼 **되돌리고**(`Negative=True`, 깊이 × 19 DIP 음수 여백) — 칸 경계와 가운데 구분선이 줄마다 같은 자리에 온다,
2. 이름에만 **다시 같은 값을 들여쓴다** — 계층은 그대로 보이고, 왼쪽 · 오른쪽 이름이 같은 높이 · 같은 들여쓰기로 맞는다.
트리 자체는 그대로 쓴다(목록 둘을 따로 두고 스크롤 · 펼침을 맞추는 길은 택하지 않았다) — 접기 · 펼치기,
선택, 복사 · 삭제, 걸러 보기, 더블클릭으로 파일 비교 열기가 손대지 않은 채로 남는다.
### 화면으로 확인한 것
| 판정 기준 | 결과 |
|---|---|
| 같은 이름이 같은 높이에 온다 | `Src > Core > Motion > Axis.cs` 까지 네 겹 모두 양쪽 같은 줄 · 같은 들여쓰기 |
| 한쪽만 있는 항목이 어느 쪽인지 한눈에 | `Legacy`(왼쪽만)는 오른쪽 칸이 빈칸, `NewOnly`(오른쪽만)는 왼쪽 칸이 빈칸 |
| 접기 · 펼치기 | 모두 펼침 · 모두 접기 모두 그대로. 칸 경계는 깊이가 달라도 한 자리 |
| 걸러 보기 | 왼쪽에만 2줄 · 오른쪽에만 2줄 · 같은 것만 2줄 · 다른 것만 6줄 |
| 복사 단추 | `Legacy` 고르고 [→ 오른쪽으로 복사] → 확인 창(본 창 가운데) → 복사됨. 다시 비교하니 `왼쪽만 1 · 같음 4` 가 `왼쪽만 0 · 같음 5` 로 |
| 좁은 창 | 1750×1100(1166 DIP) 창모드에서도 세 칸(420+170+420 DIP) 이 다 보인다 |
### 하네스
`FolderTest` **111 → 118건(+7)** · 실패 0 — 뿌리 여백 0 · 한 겹 19 · 세 겹 57 · 되돌리는 쪽은 음수 ·
되돌린 뒤 다시 들여쓰면 제자리 · 위아래 여백은 안 건드림 · 깊이가 아니거나 음수면 0.
### 남긴 것
- 칸 폭은 고정(420 · 170 · 420 DIP)이다. 트리가 깊이마다 폭을 다르게 잡아서 비율(`*`) 폭을 쓰면 줄마다 칸이
달라진다 — 화면이 아주 좁으면 오른쪽 목록이 잘린다(1166 DIP 까지는 확인했다).
## B-1. 2-Way 에서 양쪽으로 머지 (2026-09-27, 완료)
지시서에 남겨 둔 "정할 것"을 물어 정했다 — **두 파일 비교에서만 양방향**(사용자 결정 2026-09-27).
Git 모드에서 왼쪽은 지난 커밋이라 고쳐 쓸 파일이 없고, 네 칸(3-Way)의 BASE 는 공통 조상이라 거기에 쓰는 것이 뜻이 없다.
### 무엇이 문제였나
반영이 BASE → LOCAL 한 방향뿐이고 저장도 오른쪽 파일에만 됐다. 두 파일을 견주다 **왼쪽이 맞는 경우**
(오른쪽에서 잘못 지웠거나, 왼쪽 파일도 같이 맞춰야 하는 경우)에는 도구 밖에서 손으로 고쳐야 했다.
### 어떻게 고쳤나
| 무엇 | 어디 |
|---|---|
| 줄 반영 `[<]` | BASE 칸 여백(왼쪽에 있는 줄) · LOCAL 칸 여백(왼쪽에 **없는** 줄 - BASE 칸에는 그 줄 자체가 없어 달 자리가 없다) |
| 칸마다 저장 | ACTIONS 의 `◀ 왼쪽 저장` · `오른쪽 저장 ▶`. 고친 것이 없는 쪽은 꺼진다 |
| 반영 규칙 | `Services/PaneText.cs`(새로 둠) — 두 방향이 **같은 규칙**을 쓴다 |
| 되돌리기 | 편집 기록(`LineEdit`)에 `Side` 를 더해 어느 칸을 고친 편집인지 들고 다닌다. 한 스택에 쌓여 누른 차례대로 풀린다 |
| 저장 기준점 | 왼쪽 · 오른쪽을 따로 센다(`_leftBaseline`) — 한쪽을 저장해도 다른 쪽의 "저장할 것 있음"은 그대로 |
반영 규칙은 어느 쪽으로든 같다. **반대쪽에 줄이 있으면 그 글을 가져오고(없던 줄이면 생긴다), 반대쪽에 없으면
이 칸에서도 지운다.** 머지 주석(설정 "머지 주석 넣기")도 양쪽 모두 남는데, 왼쪽에 넣는 주석은
`LOCAL(파일이름)` 을 출처로 적고 **왼쪽에만 있는 줄**로 들어간다 — 오른쪽 파일에는 섞이지 않는다.
`MergeLine.HasBase` · `BaseNumber` 가 이제 바뀔 수 있는 값이다(예전에는 정렬 결과라 고정이었다).
BASE 칸 목록도 그 변화를 따라가게 했다(`PaneView` 의 live filtering) — 이게 없으면 왼쪽에 넣은 줄이 안 보이고
왼쪽에서 지운 줄이 빈 칸으로 남는다(화면 시험에서 실제로 걸려 고쳤다).
### 화면으로 확인한 것
시험 재료는 만들어 썼다(`%TEMP%\mergehub-b1`, left.cs · right.cs).
| 판정 기준 | 결과 |
|---|---|
| 왼쪽 파일에 반영 · 저장된다 | 값이 다른 줄 · 왼쪽에만 있는 줄 · 오른쪽에만 있는 줄 셋을 `[<]` 로 반영 → `◀ 왼쪽 저장` → 왼쪽 파일이 오른쪽 내용 + 머지 주석 2줄 |
| 오른쪽은 그대로다 | 오른쪽 파일 해시 `07f16198…` 저장 전후 같음. `오른쪽 저장 ▶` 은 고친 것이 없어 꺼져 있었다 |
| 되돌리기(11차 F-8) | Undo 세 번에 BASE 칸이 처음 모습으로(값 100 · 왼쪽에만 있던 줄 · 주석 사라짐), Redo 켜짐. 저장한 뒤라 `◀ 왼쪽 저장` 은 다시 켜진다(저장한 글과 달라졌으므로 - F-8 의 뜻 그대로) |
| 저장할 때 백업(11차 F-9) | `{설정}\Backup\MergeHub\2026-09-27\…\left.20260927-191108.cs` |
| 두 파일 비교에서만 | git mergetool 네 칸(`--merge --base --local --remote --merged`)에서 `[<]`(새 단추) 0개 · `◀ 왼쪽 저장` 안 보임. 네 칸의 기존 `<`(Take Mine · Take Theirs)는 그대로 |
### 하네스
`TwoWayTest` **29건 새로** · 실패 0 — 칸의 글 만들기 3 · 반영 규칙 7(양방향 · 한쪽에만 있는 줄 · 같은 줄 ·
멱등) · 왼쪽으로 반영한 뒤 5 · 오른쪽으로 반영한 뒤 3(예전 길 그대로) · 양쪽 저장 기준점 5 · 로그 문구 4 · 기타 2.
### 남긴 것
- 왼쪽 칸은 **타이핑으로는 못 고친다**. 편집기는 오른쪽에만 겹쳐 있다 - 왼쪽은 `[<]` 로 반영해서 고친다
(지시서가 정한 것: "칸마다 저장 단추 · 왼쪽 칸에도 반영 단추 [&lt;]").
- 왼쪽으로 반영한 줄에는 "머지 반영함" 색을 칠하지 않는다. 반영하고 나면 양쪽이 같아져 색이 필요 없다.
---
## B-2. 프로그램 정보 창 (2026-09-27, 완료)
지시서의 "정할 것"을 물어 정했다(사용자 결정 2026-09-27).
- **넣을 것**: 기본 4가지(프로그램 이름 · 버전 · 빌드 날짜 · 만든 사람) + **사내 문의처**.
- **버전 출처**: `MergeHub.csproj` 의 `<Version>` 을 실행 파일에서 읽는다. 12차 = `0.12.0`.
### 어떻게 만들었나
| 값 | 어디서 |
|---|---|
| 이름 · 만든 곳 | 어셈블리의 `Product`(MergeHub) · `Company`(UbiSam) — csproj 에 적는다 |
| 버전 | 어셈블리 버전. 뒤가 0 인 자리는 떼고 적는다(`0.12.0.0` → `0.12`) |
| 빌드 날짜 | **csproj 가 빌드할 때 박아 둔 값**(`AssemblyMetadata "BuildDate"`) |
| 만든 사람 · 문의처 | 설정 `프로그램 정보 > 만든 사람 · 사내 문의처`. 비우면 만든 사람은 회사 이름, 문의처는 "적혀 있지 않습니다" |
빌드 날짜를 어셈블리에 박는 까닭 — 실행 파일(PE) 머리의 시각 자리를 읽어 보니 **요즘 SDK 는 결정적 빌드라
그 자리에 내용 해시**가 들어간다(날짜로 읽으면 2100년 같은 값). 그래서 csproj 에서 빌드 시각을 박고,
못 찾을 때만 PE 시각 → 파일 수정 시각 순으로 물러난다(`ProgramInfoReader.ChooseBuildDate`).
파일 수정 시각을 첫째로 두지 않는 까닭은 복사 · 압축 풀기로 바뀌기 때문이다.
창은 [단축키] 옆 **[정보]** 단추로 연다. 단축키 창과 같은 규칙 — 이미 떠 있으면 새로 띄우지 않고 앞으로 가져온다.
[정보 복사] 로 네 줄을 그대로 클립보드에 담는다(결함을 알릴 때 첫 질문이 늘 "어느 버전이냐" 라서).
### 화면으로 확인한 것
| 판정 기준 | 결과 |
|---|---|
| 버전이 실제 실행 파일 값과 같다 | 창 `0.12` ↔ `MergeHub.dll` 어셈블리 버전 `0.12.0.0` |
| 빌드 날짜가 실제 값과 같다 | 창 `2026-09-27 19:51` ↔ 실행 파일에 박힌 `BuildDate` `2026-09-27 19:51` |
| 창을 닫아도 화면 상태가 그대로다 | 닫기 전후 화면 같음(BASE · LOCAL 칸 · 선택 줄 · ACTIONS 상태 그대로) |
| 설정에서 받은 값 | 만든 사람 · 문의처를 적으니 그대로 보이고, 비우면 회사 이름 · 안내 글 |
### 하네스
`AboutTest` **28건 새로** · 실패 0 — 버전 글 5 · PE 시각 읽기 9 · 빌드 날짜 고르기 4 ·
적혀 있지 않은 칸 4 · 실제 실행 파일 대조 5(버전 · 박힌 날짜 · 창에 적을 값 한 벌 · 문의처 빈칸)
### 남긴 것
- **만든 사람 · 사내 문의처는 비워 두었다.** 사람 이름 · 연락처는 지어낼 값이 아니다 -
설정 > MergeHub > 프로그램 정보 에서 적으면 그때부터 창에 나온다.
- 버전은 차수에 맞춰 손으로 올린다(12차 = 0.12.0). 올리는 자리는 csproj 한 곳이다.
## B-3. 칸 아래에 무엇이 달라졌는지 (2026-09-27, 완료)
### 무엇이 문제였나
요약이 창 맨 아래 상태 줄 한 곳에만 있었다("총 7줄 · 남은 차이 3줄"). 칸이 둘 · 넷이 되면서
**이 칸에 몇 줄이 있고 저쪽과 무엇이 다른가**는 눈으로 세야 했다.
### 어떻게 고쳤나
- **칸마다 한 줄**을 칸 아래에 붙였다 — `6줄 · 추가 1 · 삭제 1 · 수정 1`.
다른 것이 없으면 `6줄 · 같음`, 0 인 칸은 아예 적지 않는다(0 을 늘어놓으면 읽을 것만 는다).
- **가운데(칸 사이 틈) 아래에 진행** — 위에 `3 / 7`(몇 번째 차이를 보고 있나), 아래에 `남은 2`(아직 다른 줄).
틈이 48 DIP 라 두 줄로 짧게 적는다. 남은 것이 없으면 아랫줄은 비운다.
- 세는 규칙은 `Services/PaneSummary.cs`(새로 둠)에 모았다.
**칸을 기준으로 읽는다** — `추가` 는 *이 칸에만* 있는 줄, `삭제` 는 *저쪽에는 있는데 이 칸에 없는* 줄,
`수정` 은 양쪽에 있고 글이 다른 줄이다. 그래서 두 칸의 추가 · 삭제는 서로 거울이 된다.
무시 규칙에 걸린 줄과 우리가 넣은 머지 주석은 차이로 세지 않는다(화면 색 · 차이 이동과 같은 기준) —
다만 **줄 수에는 들어간다**(파일에 있는 줄이므로).
3분할에서는 네 칸 모두 **BASE(공통 조상)와** 견준다 — "누가 무엇을 고쳤나" 가 그 화면에서 읽어야 할 것이라서다.
BASE 칸 자신은 견줄 것이 없어 `4줄 · 기준` 이라고만 적는다.
### 화면으로 확인한 것
| 판정 기준 | 결과 |
|---|---|
| 반영 · 되돌리기 · 저장마다 숫자가 따라간다 | 반영 전 `6줄 · 추가 1 · 삭제 1 · 수정 1`(양쪽) · `남은 3` → `[>]` 하나 → 오른쪽 `8줄 · 추가 1 · 수정 1` · 왼쪽 `6줄 · 삭제 1 · 수정 1` · `남은 2` → Undo → 처음 값 그대로 |
| 3분할에서도 칸마다 맞는 숫자 | LOCAL(Mine) `4줄 · 수정 1` · BASE `4줄 · 기준` · REMOTE(Theirs) `4줄 · 수정 1` · MERGED `4줄 · 수정 1` · 가운데 `- / 1` · `남은 1` |
| 긴 파일에서 느려지지 않는다 | 5만 줄 × 두 칸 × 네 번 = **22ms**(한 번에 3ms 남짓). 목록을 한 번 훑을 뿐 파일을 다시 읽지 않는다 |
### 하네스
`SummaryTest` **25건 새로** · 실패 0 — 두 칸 요약 7 · 다른 것이 없을 때 4 · 세지 않는 줄 3 ·
세 칸(BASE 기준) 5 · 가운데 진행 5 · 긴 파일 1.
### 남긴 것
- 맨 아래 상태 줄의 "파일 전체" 요약은 그대로 둔다 — 칸 요약과 다른 값이다(전체 줄 수 · 남은 차이 · 편집 건수).
- 가운데 진행은 두 줄뿐이다. 틈을 넓히면 더 적을 수 있지만, 틈은 잇는 선(11차 F-10)의 자리라 좁게 두었다.
## C-2. 플랫폼 탭 줄 크기 줄이기 (2026-09-28, 완료 · 배포됨)
플랫폼(`D:\Sorce\ubsrepo`) 쪽이라 **모든 사내 프로그램에 함께 간다.** 지시서 원칙대로 코드와 값은 다 만들고
**배포는 사용자 확인 뒤**에 한다 — 아래 "배포" 참고.
### 무엇을 얼마나 줄였나
`Themes/ShellStyles.xaml` 의 `Ubisam.Style.SidebarItem.Horizontal`(하단 탭 줄 항목) 한 곳이다.
| 값 | 전 | 후 |
|---|---|---|
| 위아래 여백 | 13 / 14 | **9 / 10** |
| 아이콘 | 22×22 | **20×20** |
| 아이콘 아래 틈 | 7 | **5** |
| 글자 | 13.5 | **12.5** |
| 항목 높이(DIP) | 74 | **61** |
### 값으로 잰 것
2560×1600 · 배율 150% · 셸 배율 1.2 에서 실제 화면 픽셀로 쟀다.
| | 전 | 후 |
|---|---|---|
| 탭 줄 높이 | **133px** | **109px** (−24px, −18%) |
| 파일 비교 코드 칸 높이 | 1,081px | **1,105px** |
| 코드 줄 수(줄 높이 23px) | 47줄 | **48줄** (+1) |
배율 1.2 기준으로는 89 → 73px 이다(사용자가 말한 "95px" 과 같은 자리 - 재는 방법에 따라 테두리 · 밑줄이 붙는다).
글자 · 아이콘이 작아졌지만 한글 라벨(폴더 비교 · 파일 비교 · 화면 잠금 · 바탕화면 · 종료)은 잘리지 않고,
선택 밑줄 · 탭 접기 띠 · 오른쪽 동작 항목도 그대로다(화면 확인).
### 배포
이 항목만 빌드하면 `D:\UbisamPlatform\bin` 에 바로 반영된다. 처음에는 배포본을 백업해 두고 잠깐 바꿔치기해서
시험하고 되돌렸는데, **그 뒤 새 플랫폼(`D:\Sorce\ubsrepo`)을 빌드하면서 함께 나갔다.**
- 영향: 이 플랫폼으로 만든 **모든 프로그램의 하단 탭 줄**이 함께 작아진다. 기능 · 배치는 그대로고 높이만 준다.
- **배포 확인 (2026-09-28)**: 배포본 `UbisamBase.Core.dll` 이 지금 소스의 빌드 결과와 **해시가 같고**(BA27EE48…, 738,304바이트, 04:09),
실제로 켜서 잰 하단 탭 줄이 **111px · 항목 109px**(배율 150% 기준 72.7 DIP)로 "후" 값과 맞았다.
앞서 적었던 "지금 배포 폴더에는 예전 것이 그대로" 는 그 뒤 빌드로 낡은 글이 되어 고쳤다.
## C-3. 플랫폼 헤더 줄도 접을 수 있게 (2026-09-28, 완료 · 배포됨)
### 먼저 재현 (화면, 값)
| | |
|---|---|
| 헤더가 먹던 자리 | 모듈 화면이 **144px** 부터 시작(시계 줄 아래 끝 88px) |
| 접는 길 | **설정 화면 체크박스뿐** — `UiSettings.TopBarCollapsed` 는 있었지만 그것을 켜고 끄는 자리가 설정 화면 하나였다 |
| 접으면 | 되살릴 **띠도 단축키도 없다** — 다시 펴려면 설정 화면으로 들어가야 했다 |
### 고친 것
아래 탭 줄 접기(11차 F-4)와 **같은 방식**으로 맞췄다. 두 곳이 다르게 움직이면 배우는 것이 둘이 된다.
- **접은 띠**(`TopBarExpandStrip`) — 창 맨 위에 16 DIP 짜리 얇은 띠. `⌄ 헤더 펴기 · Ctrl+F11`.
사용자 결정대로 **화살표와 안내 글만** 남기고 시계 · 이름은 넣지 않았다.
- **Ctrl+F11** 로 접고 편다(사용자 결정 — 아래 탭 줄 F11 과 짝이 맞는다).
- 하단 동작 줄에 **[헤더 접기]** 를 [탭 접기] 옆에 두었다 — "접는 것들" 을 한 곳에서 찾게.
- 접은 상태는 **그 항목만** 파일에 남긴다(`UiSettingsFile.WithTopBarCollapsed`) —
설정 화면에서 바꾸고 아직 [저장] 하지 않은 테마 · 배율까지 함께 써지면 안 된다(F-4 와 같은 까닭).
- 설정 화면 체크박스로 바꿔도 띠가 따라온다(`OnUiSettingsChanged`).
**띠는 단추가 아니라 `Border` 다.** 한 자리에서 **끌기와 누르기를 둘 다** 받아야 하는데, 단추에 `DragMove` 를
붙이면 마우스를 놓는 순간 `Click` 이 오지 않아 펴지지 않는다. 그래서 끌린 거리를 보고 가른다 —
움직이지 않았으면 누른 것이다. 전체화면 · 최대화에서는 끌 것이 없으므로 바로 편다.
### 하네스
`PlatformCfgTest` 29 → **42건** 통과(C-3 +13) — 항목 없는 옛 파일은 펼침 · 망가진 글도 펼침 ·
다른 값(테마 · 배율 · 위치 · 알림)을 건드리지 않음 · **탭 줄 접힘과 한 파일에 나란히 살고 서로 덮지 않음** ·
모르는 항목도 지우지 않음. 하네스가 옛 플랫폼 소스를 가리키고 있던 것도 새 저장소로 옮겼다.
### 화면으로 확인 (값)
| 확인 | 값 |
|---|---|
| **접으면 코드 칸이 높아진다** | 모듈 시작 **144px → 51px** (93px · 배율 150% 기준 **62 DIP** 벌었다) |
| 남는 띠 | 51px(34 DIP)만 — 화살표와 안내 글뿐 |
| 다시 켜도 | **접힌 채**(`"TopBarCollapsed": true` 가 남는다) · 코드 칸도 그대로 높다 |
| 띠 누르기 | 펴진다 · 자리도 144px 로 돌아온다 |
| Ctrl+F11 | 접고 펴기 둘 다 |
| 회귀 | **F11 은 그대로 아래 탭 줄**만 접고 편다 · 헤더는 건드리지 않는다 |
| 최대화 | 그대로 2560×1600 |
### 값으로 확정하지 못한 것
**접은 띠를 끌면 창이 따라오는가** — 흉내 낸 마우스로는 WPF 의 `DragMove` 가 움직이지 않는다.
예전부터 되던 **기존 헤더 끌기로 견줘 봐도 똑같이 안 움직였다**(`c3-drag.ps1`) — 시험 도구 한계다.
띠는 헤더와 **같은 `DragMove()`** 를 부르고, 끌리지 않았을 때 펴지는 쪽은 값으로 확인했다.
사람 손으로 한 번 끌어 보는 것이 남았다.
### 배포
사용자 확인 뒤 **빌드해서 바로 반영**했다(`D:\UbisamPlatform\bin`). 빌드 오류 0 ·
경고 4(전부 기존 `ChartTheme.cs` 것). 되돌릴 자리로 배포본을 `D:\UbisamPlatform\bin-backup-c3` 에 떠 두었다.
**이 플랫폼으로 만든 모든 사내 프로그램의 헤더 줄에 함께 간다** — 기본은 지금까지와 같은 "펴짐" 이라
아무것도 하지 않으면 달라지는 것이 없다.
### 배포 뒤 전체 화면 회귀 (2026-09-30)
플랫폼을 바꿔 배포했으니 **그 위에 얹혀 도는 12차 화면 시험을 전부 다시 돌렸다**.
플랫폼만 달라졌고 MergeHub 쪽은 손대지 않았다.
| 벌 | 보는 것 | 통과 |
|---|---|---|
| `f1-verify` | 메모장 탭 | 38 |
| `d1-verify` | 폴더 비교 위쪽 한 줄 | 39 |
| `e1-verify` | 껐다 켜도 남는 편집 | 24 |
| `e2-verify` | 문서 탭 | 34 |
| `e2-reg` | 문서 탭 회귀 | 18 |
| `e2-start` | 시작 때 기본 짝 탭 | 8 |
| `e3-verify` | 왼쪽 줄 번호 | 16 |
| `e4-verify` | `[<]` 줄 밀림 | 6 |
| `c3-verify` | 헤더 줄 접기(배포 직후에 돌렸다) | 25 |
**합 208건 · 실패 0 · 예외 0.** 한 벌에 들어가기 전 앞 벌이 남긴 창과 보관함
(`D:\UbisamConfig\Memo` · `Session\MergeHub`)을 치워 서로 섞이지 않게 했다(`run-all-c3.ps1`).
끝나고 `MergeHubSettings.json` · `UiSettingsService.json` · `MergeHub.Recent.txt` ·
`ShellWindowPlacement.json` 을 시작 기준본과 **바이트까지 같게** 되돌렸다.
### 공용 플랫폼에 합친 뒤 다시 (2026-09-30)
우리가 따로 쓰던 플랫폼(`ubsrepo`)의 두 커밋을 **공용 플랫폼**(`D:\Sorce\UbisamCommonPlatform`)에
합쳐 배포했다. MergeHub 가 얹히는 바닥이 또 달라졌으니 아홉 벌을 다시 돌렸다.
MergeHub 코드는 이번에도 손대지 않았다.
| 벌 | 통과 | | 벌 | 통과 |
|---|---|---|---|---|
| `c3-verify` | 25 | | `e2-verify` | 34 |
| `f1-verify` | 38 | | `e2-reg` | 18 |
| `d1-verify` | 39 | | `e2-start` | 8 |
| `e1-verify` | 24 | | `e3-verify` | 16 |
| | | | `e4-verify` | 6 |
**합 208건 · 실패 0 · 예외 0** (09:48 → 09:58, `run-all-merge.ps1`).
MergeHub 를 새 플랫폼으로 다시 빌드해도 **경고 0 · 오류 0** — 플랫폼 API 는 깨지지 않았다.
**값 하나가 흔들렸다** — 이 회귀에서 C-3 의 모듈 시작 자리가 `144px → 51px` 이 아니라
`154px → 61px` 로 나왔다. 벌어드린 높이는 **93px · 62 DIP 로 똑같다**. 뒤에 `c3-verify` 를
따로 돌리니 다시 `144px → 51px` 이 나와 문서 값이 맞는 것은 확인했다.
| 아니었던 것 | 어떻게 알았나 |
|---|---|
| 공용 플랫폼의 네 커밋 | 셸 배치에 닿는 것이 없다 — 더한 암시적 스타일은 `ContextMenu` · `MenuItem` 뿐 |
| 뜨는 모드(전체화면 ↔ 최대화) | `mode-probe.ps1` 로 둘 다 재 보니 **똑같이 144px** |
**무엇이 10px 을 밀었는지는 확정하지 못했다.** 몰이 대본 안에서 한 번 나왔고 따로 돌리면
나오지 않는다 — 시험 환경 쪽 변덕으로 보이지만 끝까지 캐지 않았다. 다음에 또 나오면
그때 잡는다. 통과 건수와 다른 값은 모두 그대로다.
## F-1. 하단 탭에 메모장 자리 (2026-09-28, 완료)
사용자가 정한 것 — **메모 + 파일 열기 둘 다**, **저장 단추 없이 계속 남는다**,
이름은 **`메모 1` · `메모 2` 번호만**, 편집기는 줄 번호 · 글자 크기 · 줄바꿈 **+ 찾기/바꾸기**.
### 무엇을 만들었나
- `Services/Memos.cs` — `Memo`(한 장) · `MemoBook`(번호 · 이름 · 넘기기 · 닫기, 계산만) ·
`MemoStore`(보관, `{설정 폴더}\Memo\`).
- `ViewModels/MemoViewModel.cs` — 새 메모 · 파일 열기 · 저장 · 다른 이름으로 · 닫기 · 자동 보관 · 되살리기.
- `Views/MemoView.xaml(.cs)` — 동작 한 줄 · 메모 탭 줄 · 찾기/바꾸기 줄 · 줄 번호 편집기 · 아래 상태 줄.
- `AppSetup` — 하단 탭 **맨 뒤**에 넣었다. 앞 세 탭의 차례가 바뀌면 손버릇도 바뀐다.
문서 탭(12차 E-2)과 **같은 모양**을 쓴다 — 탭 줄 · 저장 안 한 가운뎃점 · Ctrl+Tab · 가운데 단추로 닫기.
두 화면이 서로 다르게 움직이면 배우는 것이 둘이 된다.
메모는 문서 탭과 달리 **글을 스스로 들고 있다** — 메모는 원래 작은 글이고, 이름 없는 메모에는
다시 읽어 올 파일 자체가 없다.
### 하네스
`MemoTest` **69건** 통과 — 번호 매기기(빈 번호 다시 쓰기) · 파일 이름 겹칠 때 폴더 덧붙이기 ·
같은 파일 두 장 금지 · 이름 붙이기 · 넘기기 · 닫기 자리 · 떠 있는 장 표시 ·
글로 옮기고 되읽기(빈칸 · 한글 · UNC) · 파일로 쓰고 되읽기 · 옛 글 파일 치우기 · 되살려 담기.
### 화면으로 확인 (값)
| 확인 | 값 |
|---|---|
| 세 장 만들어 각각 쓰기 | `메모 1` `메모 2` `메모 3` · 가운뎃점 3개 · 글이 섞이지 않음 |
| 손 멈춘 뒤 | 보관본 `01.txt` `02.txt` `03.txt` `목록.txt` |
| **강제 종료 → 다시 켜기** | **세 장이 차례 · 글 그대로**(저장 단추를 누른 적이 없다) |
| 찾기/바꾸기 | Ctrl+F 로 펴짐 · `사과`→`포도` 모두 바꾸기 3곳 |
| 닫기 | 고친 메모는 **세 갈래로 물음** · [취소] 면 그대로 3장 · [저장하지 않고 닫기] 면 2장 · **빈 메모는 묻지 않음** |
| 줄 번호 | 40줄에서 번호 40개, 글과 한 줄씩 맞음 |
| 다른 탭 | 폴더 비교 · 파일 비교 그대로 · 탭을 오가도 메모 글 40줄 그대로 |
하네스 29벌 **1,934건** 통과 · 실패 0, 화면 여덟 벌 모두 통과(빌드 경고 0 · 오류 0).
### 화면을 보고 고친 세 가지 (사용자 보고)
**(가) 다른 탭에 갔다 오면 메모 글이 비어 보였다.** 탭을 오가면 화면이 다시 만들어지는데 그때 편집기가
잠깐 비어 있다. 그 틈에 글을 거두면 빈 글로 메모를 덮어썼다. 편집기가 **지금 어느 장을 싣고 있는지**(`_shown`)를
두고, 맞지 않으면 거두지 않게 했다.
**(나) 줄 번호와 글이 어긋났다.** 줄 번호를 `TextBlock` 으로 그렸더니 줄 높이가 조금씩 달라 아래로 갈수록
벌어졌다. **편집기와 같은 종류(TextBox)** 로 바꾸고 글꼴 · 크기 · 여백을 같은 값으로 두니 같은 자로 줄을 잰다.
**(다) 글이 칸 한가운데에서 시작했다.** 플랫폼 입력 칸 스타일이 글 칸(PART_ContentHost)을 세로 가운데에
붙박아 둔다 — 한 줄짜리 칸을 겨냥한 값이다. `VerticalContentAlignment` 로는 안 풀려(템플릿 안에 박힌 값)
그 칸을 직접 늘려 **맨 위부터** 쓰게 했다.
### 시험 도구
시험 스크립트가 변수 이름을 `$keep` 로 써서 **라이브러리의 `$Keep`(되돌릴 설정 파일 목록)을 덮어쓰고** 있었다 —
그래서 시험이 끝나도 설정이 안 돌아왔다(두 번 겪고 나서야 찾았다). 이름을 갈랐다.
묻는 창을 찾는 도구도 단추 이름을 `저장하지 않고 계속` 로 못 박아 두어, 말이 다른 창(`저장하지 않고 닫기`)을
못 알아봤다 — 앞부분만 맞춰 보게 했다.
## D-1. 폴더 비교 위쪽을 한 줄로 · 옵션은 서랍으로 (2026-09-28, 완료)
### 먼저 재현 (화면, 값)
| | |
|---|---|
| 위쪽이 먹던 높이 | **305px (배율 150% 기준 203 DIP)** · 네 줄 |
| 무엇이 있었나 | ① 경로 두 개 + 최근 ② 빠른 비교 · 하위 폴더 · 보기 · 포함 · 제외 · [비교] · [취소] ③ 폴더 안 찾기 한 벌 ④ 색 구분 범례 |
비교하기 <b>전에도</b> 네 줄이 그대로 있었고, 자주 쓰는 [비교] 는 옵션 줄 끝에 묻혀 있었다.
### 고친 것
- **위 한 줄**: `[…] 왼쪽 경로 ⇆ […] 오른쪽 경로 [최근▾] 켠 옵션 [비교] [취소] [옵션 ▾]`
- 폴더 고르기는 글자 대신 `…` 로. 경로 칸 <b>바로 앞</b>에 두어 어느 쪽 것인지 헷갈리지 않는다.
- **좌우 바꾸기(⇆)** 를 새로 넣었다(사용자 결정) - 반대로 견주려고 두 번 고르지 않아도 된다.
- 최근 목록은 화살표만 남겼다.
- **옵션 서랍**: 빠른 비교 · 하위 폴더 포함 · 보기 · 포함 · 제외를 `[옵션 ▾]` 안으로. `[폴더 안 찾기]` 도 여기에 뒀다.
- **켠 옵션 표시**: 서랍에 넣으면 "내가 뭘 켜 뒀더라" 를 알 수 없게 된다 - 특히 제외 규칙은 켠 줄 모르고
"왜 이 파일이 안 보이지" 로 이어진다. <b>기본과 다른 것만</b> 한 줄에 짧게 적는다(`FolderOptionText`).
기본 그대로면 아무것도 적지 않는다 - 늘 떠 있는 글은 눈에 띄지 않는다.
- **찾기**: 늘 한 줄을 먹던 것을 **Ctrl+F 로 부를 때만** 편다. Esc · [찾기 접기] 로 접는다.
- **색 범례**: 위에서 빼고 <b>아래 상태 줄</b>로 내렸다 - 색은 목록을 보는 동안 필요한 것이라 결과 요약 옆이 제자리다.
### 화면으로 확인 (값)
| 확인 | 값 |
|---|---|
| **위쪽 높이** | **305px → 77px** (203 → **51 DIP**, −75%) |
| 한 줄에 있는 것 | […] ⇆ […] · 최근 · [비교] [취소] [옵션 ▾] · 켠 옵션 |
| 좌우 바꾸기 | 누르면 두 경로가 맞바뀌고, 다시 누르면 되돌아온다 |
| 옵션 서랍 | 빠른 비교 · 하위 폴더 · 보기 · 포함 · 제외 · [폴더 안 찾기] 모두 있음 |
| 켠 옵션 표시 | 빠른 비교를 켜면 줄에 "빠른 비교", 처음 제외 규칙이 있어 "규칙 1" |
| 찾기 | Ctrl+F 로 77 → 158px, Esc 로 다시 77px |
| 비교 | 그대로 동작(`전체 2개 · 다름 2 · 왼쪽만 0 · 오른쪽만 0 · 같음 0`) |
| 없어지지 않은 것 | 결과 내보내기 · 줄 단위 비교 · 양쪽 복사 · 양쪽 삭제 · 색 구분(아래로) |
| 좁은 창(1920px = 1280 DIP) | 여전히 **77px 한 줄**, [비교] · [옵션] 다 보임 |
하네스 `FolderTest` 118 → **131건**(D-1 +13), 전체 28벌 **1,865건** 통과 · 실패 0(빌드 경고 0 · 오류 0).
### 화면을 보고 고친 것
`[옵션 ▾]` 를 `ToggleButton` 으로 뒀더니 플랫폼 기본 스타일이 <b>스위치 모양</b>이라 같은 줄의 다른 단추와
따로 놀았다. 여느 단추로 바꾸고 누를 때마다 서랍을 여닫게 했다. 최근 목록도 폭을 못 박지 않아 넓게 벌어지던 것을 고쳤다.
## E-1. 꺼도 작업 내용이 남아 있게 (2026-09-28, 완료)
E-2 가 만든 **보관함**(탭이 사라져도 고친 글이 짝 이름으로 남는 자리)을 그대로 파일로 옮겨 적는 일이다.
새로 정할 것은 **언제 쓰고 언제 되살리는가**뿐이었다.
### 어디에 · 어떤 모양으로
`{설정 폴더}\Session\MergeHub\` — 11차 F-9 백업과 **다른 자리**다. 백업은 "저장하기 전 파일",
이것은 "저장하지 않은 편집" 이라 뜻도 수명도 다르다.
```
목록.txt 차례 · 짝 경로 · 캐럿 · 스크롤 · 저장 형식 · 보고 있던 탭 · 그때 파일의 자취
01-오른쪽.txt 저장하지 않은 오른쪽 글(있을 때만)
01-왼쪽.txt 저장하지 않은 왼쪽 글(12차 B-1, 있을 때만)
```
- **JSON 을 쓰지 않는다**(최근 목록과 같은 까닭). 사람이 열어 보고 손으로 고칠 수 있고, 무엇보다
**글 안에 무엇이 들었든**(따옴표 · 줄바꿈 · 제어 문자) 본문을 따로 파일로 두면 탈이 없다.
- **원본 글은 담지 않는다.** 파일에서 다시 읽으면 되고, 담아 두면 그 사이 파일이 바뀌었을 때 낡은 것을 보여 준다.
대신 그때 파일의 **길이 · 마지막으로 쓴 때**(`FileStamp`)만 적어 두고, 다를 때 알린다.
- 목록은 **맨 나중에** 쓴다 — 목록이 있다는 것이 곧 글 파일이 다 있다는 뜻이다.
- 못 읽거나 못 써도 **조용히 넘어간다**. 보관 때문에 프로그램이 안 켜지거나 저장이 막히면 안 된다.
### 언제 쓰나
| 때 | 어떻게 |
|---|---|
| 글자가 멈춘 뒤 | **2초**에 한 번(`DispatcherTimer`). 글자마다 쓰면 큰 파일에서 걸린다 |
| 탭을 옮길 때 · 닫을 때 · 새 짝을 열 때 | **그 자리에서 바로** — 그 순간이 곧 끄기 직전일 수 있다 |
| 고친 것이 생기거나 없어질 때 | 2초 뒤에 한 번(저장하면 보관본이 빠져야 한다) |
쓰는 일은 **다른 실로 넘긴다**(`Task.Run`). 지금 화면의 상태를 뜰 때 `ParkCurrentTab` 을 쓰지 않는다 -
그쪽은 되돌리기 목록을 글 단위로 눌러 담는데(`FlattenHistory`), 2초마다 이력을 주무를 까닭이 없다.
보관할 것은 **글과 자리**뿐이고 되돌리기 이력은 되살리지 않기로 정했다(사용자 결정).
### 언제 되살리나
켤 때 **묻지 않고 그대로**(사용자 결정). 다만 **사용자가 지금 열라고 한 것이 먼저**다 -
충돌 파일 열기 · `git mergetool` · 폴더 비교에서 고른 파일이 있으면 그것을 연다.
설정의 기본 저장소 · 기본 짝보다는 **앞선다**.
되살리는 것은 탭 짝 · 저장하지 않은 글 · 캐럿 · 스크롤 · 어느 탭을 보고 있었는지다.
원본 글은 파일에서 다시 읽고 그 위에 편집을 얹는다(E-2 의 `ResumePendingTab` 이 하던 그대로).
두 파일이 다 없어진 짝은 되살릴 자리가 없으므로 버린다.
### 하네스
`SessionTest` **65건** 통과 — 글로 옮기고 되읽기(빈칸 · 한글 · UNC 경로, 알아볼 수 없는 줄 건너뛰기) ·
파일로 쓰고 되읽기 · 보관할 것이 없으면 치우기 · 고친 것 없는 줄 빼고 번호 다시 매기기 ·
옛 글 파일 치우기 · 글 파일이 없어져도 넘어가기 · `FileStamp` · 보관함을 내주고 되받기.
### 화면으로 확인 (값)
두 짝을 열어 둘 다 고치고 **작업 관리자로 끄듯 강제로** 끝낸 뒤 다시 켰다.
| 확인 | 값 |
|---|---|
| 손을 멈춘 뒤 | 보관본 `01-오른쪽.txt` · `02-오른쪽.txt` · `목록.txt` 생김 |
| 강제 종료 뒤 | 보관본 그대로 남음 |
| 다시 켜면 | **묻지 않고** 탭 2개 되살아남(`alpha.cs` · `bravo.cs`), 이름 · 차례 그대로 |
| 보고 있던 탭 | `bravo` 의 고친 글 그대로 · 가운뎃점 2개 |
| 다른 탭 | `alpha` 의 고친 글도 그대로 · 서로 섞이지 않음 |
| 한쪽을 저장 | 가운뎃점 2 → 1 · 파일에 써짐 |
| 둘 다 저장 | 가운뎃점 0 · **보관본이 치워짐** |
| 다시 켜면 | 되살릴 것 없음 · 설정의 기본 짝이 열림 |
| 원본이 그 사이 바뀌면 | 고친 글 되살아나고 **"그 사이 파일이 바뀌었습니다 … 저장하면 바뀐 파일을 덮어씁니다"** 알림 |
하네스 28벌 **1,852건** 통과 · 실패 0, 화면 여섯 벌 모두 통과(빌드 경고 0 · 오류 0).
### 빠르기에 대해 말할 수 있는 것과 없는 것
5,003줄(113KB) 짝에서 글자를 보내 보니 **글자당 31ms 로 4줄짜리와 같았다** - 앱이 입력에 밀리지 않는다.
보관은 손을 멈춘 뒤 한 번, 다른 실에서 107KB 를 적었다.
다만 **"글자를 친 뒤 화면에 보이기까지" 는 값으로 확정하지 못했다.** 그것을 재려면 편집기 글을
UIA 로 읽어야 하는데, 5,003줄 창에서는 그 읽기가 타이핑보다 훨씬 오래 걸려(한 번에 수 초) 값이 통째로 오염된다.
재는 법을 따로 마련해야 한다.
### 시험 도구도 함께 고쳤다
E-1 이 도니 **지난 시험이 남긴 "저장하지 않은 편집" 이 다음 시험에서 되살아나** 화면이 달라졌다
(`e3-verify` 가 왼쪽 7줄 대신 14줄을 봤다). 시험마다 보관 자리를 치우고 시작해 끝에 되돌리도록
`Backup-Cfg` · `Restore-Cfg` 를 고쳤다 - 기능이 제대로 도는 증거이기도 하다.
## B-1 을 쓰다 나온 것 (2) - [<] 단추가 있는 줄만 글이 밀린다 (2026-09-28, 사용자 보고)
사용자가 그림으로 짚어 준 것이 <b>이것</b>이었다. 아래 "왼쪽 줄 번호" 건은 이것을 쫓다 따로 찾은 다른 흠이다.
### 먼저 재현 (화면, 값)
`[<]` 단추가 보이는 줄만 코드가 오른쪽으로 밀려, <b>같은 칸 안에서도 줄마다 글 시작 자리가 달랐다.</b>
```
BASE LOCAL
01 class Axis 01 class Axis
02 { 02 {
< 03 int speed = 100; 03 > int speed = 200; ← 밀림
< 04 // left only line 04 void Home() { }
05 void Home() { } 05 > < void Stop() { } ← 더 밀림
06 } 06 }
```
오른쪽 칸은 단추가 하나 늘 때마다 한 칸씩 더 밀렸다.
### 까닭
단추가 든 칸의 폭이 `Auto` 였다. 단추가 안 보이는 줄에서는 0 이 되고 보이는 줄에서는 단추 폭만큼 벌어지니,
그 줄만 뒤 칸이 통째로 오른쪽으로 밀린다. `Visibility="Collapsed"` 는 <b>자리까지 없앤다</b> -
자리를 지키려면 칸 폭이 고정이어야 한다.
- BASE 는 `[<]` 가 맨 앞 칸(`Auto`)이라 **번호까지 함께** 밀렸다.
- LOCAL 은 `[<]` 가 번호 뒤 칸(`Auto`)이라 **코드만** 밀렸다.
### 고친 것
`GutterBothWaysWidth` 를 두고 두 칸이 같은 값을 쓴다. 화면이 모드에 따라 0 ↔ 단추 폭으로 갈아 끼운다(`ApplyGutterReserve`).
- **두 파일 비교**: 단추 폭만큼 늘 잡아 둔다 - 단추가 없는 줄도 같은 자리에서 글이 시작한다.
- **Git 모드 · 네 칸**: `[<]` 가 아예 없으므로 0 - 여백이 예전 그대로라 코드 칸이 좁아지지 않는다.
- **편집기 여백**(`EditorGutterMargin`)도 같은 값만큼 민다. 투명한 편집기와 아래 표시층이 어긋나면
캐럿이 글자와 다른 자리에 선다.
- 글자 크기(11차 F-5)를 바꾸면 단추 폭도 따라 바뀌므로, 잡아 두는 자리도 `CodeFontMetrics` 한 벌에서 나온다.
### 화면으로 확인 (값)
| 확인 | 값 |
|---|---|
| 두 파일 비교 BASE | 여섯 줄 모두 `x=137` (전에는 단추 줄만 달랐다) |
| 두 파일 비교 LOCAL | 여섯 줄 모두 `x=1283` |
| 글자 크기 키운 뒤 | 모두 한 x(`147`) |
| Git 모드(회귀) | 모두 한 x(`459`) · 여백 예전 그대로 |
### 남겨 둔 것
BASE 줄 <b>끝</b>의 `[>]`(LOCAL 에서 지운 줄 되살리기, 11차 F-10)는 아직 `Auto` 다. 이것은 왼쪽 가장자리를
밀지 않고 오른쪽에서 코드 칸을 조금 좁힐 뿐이라 눈에 띄지 않는다. 워드랩을 켜면 그 줄만 접히는 자리가
달라질 수 있다 - 워드랩은 기본이 꺼짐이고, 손대면 네 칸 폭을 다시 맞춰야 해서 이번에는 두었다.
## B-1 을 쓰다 나온 것 - 왼쪽 줄 번호가 밀린다 (2026-09-28, 사용자 보고)
### 먼저 재현 (화면, 값)
왼쪽 6줄 · 오른쪽 8줄(오른쪽에만 있는 줄 둘)로 열고, 오른쪽에만 있는 줄을 `[<]` 로 왼쪽에 넣었다.
| | 왼쪽 칸 번호 |
|---|---|
| 연 직후 | `01 02 03 04 05 06` |
| 한 줄 넣은 뒤 | `01 02 · 03 04 05 06` |
| 두 줄 넣은 뒤 | `01 02 · 03 04 05 · 06` |
`·` 는 번호가 **비어 있는 줄**이다. 왼쪽 파일은 8줄이 됐는데 번호는 6까지만 있고, 새로 들어간 줄에는
번호가 없다. 저장하면 8줄로 써지므로 "몇 번째 줄을 고쳤나" 를 번호로 짚을 수가 없다.
### 까닭
8차 B-3 에서 만든 `RenumberMerged` 는 **오른쪽(MERGED) 칸만** 다시 번호를 맨다. 그때는 BASE 가 얼어 있었다.
12차 B-1 로 **왼쪽에도 반영**할 수 있게 되면서 왼쪽 줄이 늘고 줄게 됐는데, 왼쪽은 정렬기가 처음 준 번호를
그대로 달고 있었다. `MergeLine.BaseNumber` 의 설명에는 이미 "이제 두 파일 비교에서는 … 번호도 따라 바뀐다" 고
적혀 있었지만 실제로 다시 매기는 자리가 없었다 - 적어 둔 뜻과 코드가 어긋나 있었다.
### 고친 것
`RenumberBase` 를 `RenumberMerged` 옆에 두고, 그릴 때마다(`UpdateSummaries`) 왼쪽에 있는 줄만 1부터 다시 맨다.
- **두 파일 비교에서만**(`BothWays`). Git 모드 · 네 칸의 BASE 는 지난 커밋 · 공통 조상이라 고칠 수 없고,
그때 번호는 그 파일의 번호이므로 건드리면 안 된다.
- **"머지 반영함" 표시의 열쇠를 함께 옮긴다.** 그 표시는 BASE 줄 번호로 붙들려 있어서(`_appliedBaseNumbers`),
번호만 바꾸면 규칙을 한 번 건드리는 순간 엉뚱한 줄이 반영함으로 칠해진다. 번호가 실제로 달라질 때만
옛 번호 → 새 번호로 옮겨 담는다(안 바뀌면 손대지 않는다 - 연 직후가 그렇다).
### 화면으로 확인 (값)
왼쪽 7줄(왼쪽에만 있는 줄 하나) · 오른쪽 8줄로 다시 잡고 봤다.
| 확인 | 값 |
|---|---|
| 연 직후 | `01 … 07` · 왼쪽 줄 수 = 파일 줄 수 7 |
| 오른쪽 줄 둘을 왼쪽에 넣음 | `01 … 09` 로 이어짐 · 왼쪽 9줄 |
| 왼쪽에만 있는 줄을 반영 | 번호가 이어짐 · 번호 빠진 줄 0 |
| 되돌리기 | 번호가 이어짐 · 번호 빠진 줄 0 |
| 왼쪽 저장 | 저장한 파일 9줄 = 칸의 끝 번호 09 · 넣은 줄이 파일에 있음 |
| Git 모드(회귀) | BASE 번호 그대로 이어짐 |
하네스 27벌 **1,787건** 통과 · 실패 0, E-2 화면 세 벌도 그대로 통과(빌드 경고 0 · 오류 0).
### 아직 확인하지 못한 것
"왼쪽에만 있는 줄을 `[<]` 로 빼면 왼쪽 줄 <b>수</b>가 하나 준다" 는 것은 **값으로 확정하지 못했다.**
따로 열어 한 번만 누르면 잘 빠지는데(3줄 → 2줄 확인), 다른 반영을 몇 번 한 뒤에 같은 것을 하면
시험 도구가 읽은 줄 칸이 낡아 옆 줄 단추를 누르는 일이 생겨 값이 흔들렸다.
번호 쪽 불변식(늘 1부터 이어지고 빠진 번호가 없다)은 어느 경우에도 지켜졌다.
**빼는 길 자체가 제대로 도는지는 따로 봐야 한다.**
## E-2. 여러 짝을 동시에 (문서 탭) (2026-09-28, 완료)
### 먼저 재현 (화면, 값)
| 단계 | 값 |
|---|---|
| 짝 A 열기 | 오른쪽 15줄 · `public int Version = 2;` |
| 고치고 저장하지 않음 | 16줄 · 마지막 줄 `// E2-MARK-ALPHA` · `[오른쪽 저장 ▶]` 켜짐 · 아래 줄 "남은 차이 3줄 · **편집 1건**" |
| 다른 짝 열기 | 모달 "**a-right.cs 에 저장하지 않은 편집이 있습니다**" · [취소] [저장하지 않고 계속] [저장하고 계속] |
| [저장하지 않고 계속] | 화면이 15줄로 돌아감 — **고친 글이 사라졌다**(원본 파일은 안 바뀜) |
한 번에 한 짝만 들기 때문이다. 짝을 바꾸는 순간 앞 짝의 편집 글이 갈 곳이 없어서 묻고, 버리면 그대로 사라진다.
### 고친 방식
**탭마다 `HomeViewModel` 을 따로 만들지 않았다.** 이 화면은 `GitWorkspace` 의 여러 사건과 <b>한 자리짜리 약속</b>
(`ConfirmDiscardEdits` · `ConfirmLeaveCommit`)에 물려 있다 — 여럿을 만들면 같은 사건에 여러 화면이 동시에 반응하고
약속은 마지막 것만 남는다. 그래서 **화면은 하나로 두고 탭이 편집만 맡는다.**
- `Services/DocTabs.cs` (새로 만듦) — 계산만 한다(화면 없이 검사할 수 있다).
- `DocTab` : 짝 하나 · 이름(`Title`) · 저장 안 한 표시(`IsDirty`) · 떠 있는지(`IsCurrent`) · 맡아 둔 편집(`Parked`)
- `DocTabState` : **파일에 없는 것만** 담는다 — 고친 글 · 왼쪽 글(B-1) · 되돌리기/다시하기 이력 · 두 기준점 ·
캐럿 자리 · 스크롤 행 · 저장 형식. 파일에서 다시 읽을 수 있는 것(원본 글 · 줄 정렬)은 담지 않는다 —
담아 두면 파일이 그 사이 바뀌었을 때 낡은 것을 보여 준다.
- **이름 짓기** : 파일 이름, 양쪽이 다르면 `왼 ↔ 오`(최근 목록 `RecentPair.Label` 과 같은 규칙).
같은 이름이 겹치면 **갈릴 때까지 위 폴더를 한 겹씩** 덧붙인다(`Unit0.cs` 가 여러 폴더에 있는 사내 소스 때문).
- **보관함** : 탭을 닫거나 짝을 바꿔 탭 자체가 사라져도 고친 글은 짝 이름으로 남는다(50개).
◀ ▶ 로 훑다가 앞 파일로 돌아왔을 때 고친 것이 사라지면 그것이 곧 E-2 가 없애려던 일이다.
**E-1(껐다 켜도 남는 편집)은 이 보관함을 파일로 옮겨 적으면 된다.**
- **상한 20** : 넘으면 저장할 것 없는 탭 가운데 가장 오래 안 본 것을 닫는다. **고친 탭은 절대 닫지 않는다**(넘겨서라도 연다).
- `ViewModels/HomeViewModel.Tabs.cs` (새로 만듦) — 화면과 이어 주는 자리.
`ParkCurrentTab`(맡기기) · `ResumePendingTab`(되살리기) · `OpenPairInTab` · `EnsureTabForCurrentPair` ·
`GoToTab` · 명령 네 개(ActivateTab · CloseTab · NextTab · PreviousTab).
- `HomeViewModel.cs` 세 군데만 고쳤다.
- `OnPlainCompareRequested` : 두 파일 비교끼리 짝을 바꿀 때 **묻지 않는다**. Git · 네 칸에서 넘어올 때는 그대로 묻는다.
- `MoveFile`(◀ ▶) : 묻지 않고 **지금 탭의 짝만 바꾼다**(사용자 결정 — 탭이 늘지 않아 목록을 훑기 좋다).
- `LoadPlainFilesAsync` : 맨 앞에서 `EnsureTabForCurrentPair`, 맨 끝(파일을 다 읽은 뒤)에서 `ResumePendingTab`.
- `HomeView.xaml` : 맨 위에 문서 탭 줄. **ItemsControl 이 아니라 ListBox** 다 — ItemsControl 은 UIA 에 "컨트롤" 로
보이지 않아 화면 시험이 탭 줄을 아예 못 읽고, 키로 옮기는 것도 공짜로 얻을 수 없다. 고르기가 곧 탭 옮기기다.
탭이 둘 이상일 때만 보인다(한 짝만 볼 때 줄을 낭비하지 않는다).
- 단축키 : **Ctrl+Tab · Ctrl+Shift+Tab · Ctrl+W**(notepad++ · 브라우저와 같은 손버릇). 가운데 단추로도 닫힌다.
- 닫을 때 · 옮길 때 **묻지 않는다** — 글이 보관함에 남기 때문이다.
### 원본 파일이 그 사이 바뀌었으면
**되살리고 알린다.** 사용자가 아까운 것은 자기가 고친 글이다. "파일을 연 시점"은 **새로 읽은 글**로 두어
[전체 되돌리기]가 지금 파일로 가고, 고친 글은 편집으로 남는다 — 그래야 어느 쪽도 잃지 않는다.
알림으로 "저장하면 바뀐 파일을 덮어씁니다" 를 말해 준다.
### 화면을 켜 놓고 보다 고친 두 가지 (2026-09-28)
만들어 놓고 실제 설정으로 켜서 눈으로 보다 찾았다. 둘 다 사용자가 고르게 했다.
**(가) 켜자마자 가운뎃점이 붙었다.** 설정에 C# 중괄호 맞춤이 켜져 있으면 `.cs` 를 <b>여는 것만으로</b>
"저장할 것 있음" 이 되어, 손대지도 않은 탭에 점이 붙었다. 늘 붙어 있는 점은 뜻이 없다.
"저장하지 않은 편집이 있습니다" 를 묻는 자리(`WillAskBeforeDiscard`)는 이미 스타일 맞춤만 바뀐 것을 빼고 세고 있었다 —
**점도 같은 기준**(`HasTypedEdits`)을 쓰게 했다. 맡길 때도 같다(스타일로만 바뀐 글은 맡기지 않는다 — 다시 열면 똑같이 만들어진다).
**(나) 긴 짝 이름이 탭 줄을 많이 먹었다.** `WeldController_기준.cs ↔ WeldController_내작업.cs` 하나가 **421px(280 DIP)**,
탭 줄의 1/9 를 차지하고 뒤가 잘려 `WeldController_내작...` 으로 보였다. 사내 소스에는 이름이 살짝만 다른 짝이 흔하다.
**양쪽이 같은 부분은 한 번만** 적게 했다(사용자 결정).
| | 전 | 후 |
|---|---|---|
| 탭 이름 | `WeldController_기준.cs ↔ WeldController_내작...` | `WeldController_기준 ↔ 내작업.cs` |
| 탭 폭 | 421px | **288px** (−32%, 잘림 없음) |
뒤쪽은 **확장자일 때만** 접는다. 글자 단위로 같은 꼬리를 세면 낱말 가운데서 잘린다 —
`a-left.cs ↔ a-right.cs` 에서 `t.cs` 가 같다고 `a-lef ↔ right.cs` 가 되어 읽을 수 없었다(하네스에서 걸렸다).
앞이 한 글자만 같은 것은 접지 않는다(`abc.cs ↔ axy.cs` 는 그대로).
### 하네스
`DocTabTest` **106건** 통과 — 이름 짓기(겹칠 때 폴더 덧붙이기 · 한쪽만 있는 짝) · 저장 표시 · 같은 짝 두 탭 금지 ·
Replace · 닫기 자리 이동 · Ctrl+Tab 돌기 · 상한 · 보관함(짝 바꿔도 · 닫아도 · 모두 닫아도 · 치워도 되살아남 · 50개 한도) ·
**같은 부분 접기**(사내 소스 짝 · 앞뒤가 다 겹치는 짝 · 한 글자만 같으면 접지 않음 · 폴더까지 겹치는 경우).
### 화면으로 확인 (값)
| 확인 | 값 |
|---|---|
| 폴더 비교에서 두 번 누르기 | alpha.cs → 탭 1개(줄 안 보임) · bravo.cs → **탭 2개** `alpha.cs \| bravo.cs` |
| 짝을 바꿀 때 | **묻는 창이 뜨지 않는다** |
| 저장 안 한 표시 | alpha 만 고쳤을 때 가운뎃점 **1개** → 둘 다 고치면 **2개** |
| 탭을 오간 뒤 | alpha 15줄(`E2-MARK-ALPHA` 그대로) · bravo 15줄(`E2-MARK-BRAVO` 그대로) · **서로 섞이지 않음** |
| 되돌리기 | Undo 가 그 탭 것만 되돌리고 Redo 로 돌아옴 · 다른 탭은 그대로 |
| Ctrl+Tab | alpha 로 넘어가고 그 탭의 편집도 그대로 |
| 저장 | 그 탭의 가운뎃점만 사라짐(2 → 1) · 저장 단추 꺼짐 · 파일에 실제로 써짐 · 다른 파일은 안 바뀜 |
| 탭 닫기 | 묻지 않음 · 한 짝만 남자 탭 줄이 사라짐 · 오류 글 0 |
회귀로 함께 본 것.
- ◀ ▶ 로 훑기 : `1 / 2` → `2 / 2` → `1 / 2`, **탭이 늘지 않고**(0개 = 줄 안 보임) 돌아오니 고친 글 그대로(15줄).
- 켤 때 저절로 열리는 짝(설정의 기본 짝)에서 고친 것도 탭이 맡는다 — `EnsureTabForCurrentPair` 를 넣기 전에는
그 짝만 탭이 없어 편집이 사라졌다(화면에서 찾아 고쳤다).
- Git 모드로 넘어갈 때는 **여전히 묻는다**(탭이 맡을 수 없는 자리). 데모 전환은 예전부터 **두 번** 묻는다 —
화면이 한 번(`데모 폴더를 바꿉니다`), `GitWorkspace` 가 한 번(`Git 작업을 합니다: 데모 저장소 열기`). 이번 변경과 무관하다.
- 하네스 전체 **27벌 1,777건 통과 · 실패 0**(빌드 경고 0 · 오류 0).
## 시험 도구를 런처에 맞추기 (2026-09-28)
새 플랫폼부터 프로그램을 `UbisamBase.Launcher.exe` 로 켜고, 런처가 셸을 **같은 프로세스 안에** 올린다
(별도 프로세스로 띄우면 디버거가 안 붙어서 소비 프로젝트 중단점이 안 걸린다 — 런처 주석). 그래서 창을 든
프로세스 이름이 `UbisamBase.Shell` 에서 `UbisamBase.Launcher` 로 바뀌었고, 예전 이름만 찾던 화면 시험
도구가 전부 **"창이 안 뜬다"** 로 멈춰 있었다.
| | 전 | 후 |
|---|---|---|
| 켜는 파일 | `UbisamBase.Shell.exe` | `UbisamBase.Launcher.exe` (없으면 셸로 되돌아감) |
| 창 주인 프로세스 | `UbisamBase.Shell` 만 | `UbisamBase.Launcher` · `UbisamBase.Shell` 둘 다 |
- `a2lib.ps1` 에 `Shell-Procs` · `Shell-Proc` · `Stop-App` 을 두고 `Start-App` · `Get-Win` 이 그것을 쓰게 했다.
런처가 업데이트 진행 창(제목 없는 창)을 먼저 띄우는 경우를 대비해 **제목이 있는 창을 먼저** 고른다.
- 스크립트 **76개 · 156곳**을 함께 고쳤다(옛 이름만 찾던 `Get-Process`, 지금 빌드 자리를 가리키던 실행 경로).
고치기 전 파일은 스크래치패드 `_prelauncher\` 에 그대로 뒀다.
- 확인: 문법 검사 187개 통과, 끄기 → 켜기 → 창 찾기 → 네 탭 넘기기 → 단추 찾기 → 끄기까지 실제로 돌았고,
설정 · 창 자리 네 파일은 해시 그대로 되돌렸다.
- 시험 도구일 뿐 **제품 코드는 바뀌지 않았다.**
## 여기까지의 하네스 (2026-09-27)
A-1 · A-2 · B-1 · B-2 · B-3 을 넣은 뒤 **30벌 1,830건 통과 · 실패 0**(빌드 경고 0 · 오류 0).
- 새로 만든 것: `TwoWayTest` 29건(B-1) · `AboutTest` 28건(B-2) · `SummaryTest` 25건(B-3)
- 늘어난 것: `FolderTest` 88 → 118건(A-1 +23 · A-2 +7)
- 나머지 26벌은 그대로 통과 — `MergeLine.HasBase` · `BaseNumber` 를 바뀔 수 있는 값으로 바꾼 것이
정렬 · 이동 · 3-Way · 저장 길에 영향을 주지 않았다는 뜻이다(MergeSaveTest 350 · NoteTest 93 · MemberTest 83 포함).