UbisamBase 플랫폼 위에서 도는 플러그인. 지금까지 만든 것을 그대로 담는다. - 폴더 비교 / 파일 비교(부분 수용·워드랩·찾기) / 3-Way 병합 / Git 조작 - 비교 엔진: histogram 정렬 · 이동 블록 감지 · 무시 규칙 · 13개 언어 토크나이저 - doc/ 에 작업 지시서와 진행 기록 빌드 산출물(bin/obj)은 추적하지 않는다 - 플랫폼 런타임이 빌드 때마다 D:\UbisamPlatform\bin 에서 복사되어 들어오므로 저장소에 담을 이유가 없다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
38 KiB
MergeHub 확장 — 머지툴 기본기 보강
작성 2026-09-07 · 상태: 착수 전 · 선행 확인 3건 있음(맨 아래)
새 세션에서 이어서 작업할 때는 이 문서를 읽고 시작하면 된다.
배경
MergeHub는 사내 Git 머지/비교 도구다. 지금은 Git 저장소 안의 파일만, C# 소스만, BASE ↔ LOCAL 2분할로만 비교할 수 있다. WinMerge와 비교하면 머지툴이면 당연히 있어야 할 조작(차이 탐색·무시 규칙·폴더 비교)이 빠져 있어 실무에서 쓰기 어렵다.
방향을 다시 잡는다: 머지툴이 본체이고 Git은 그 위에 얹는 기능이다. Git 저장소가 아니어도, 소스 코드가 아니어도 비교가 되어야 한다.
프로젝트 현황
| 항목 | 내용 |
|---|---|
| 소비 프로젝트 | D:\Sorce\MergeTool\MergeHub\src\MergeHub (net48, OutputType=Library) |
| 플랫폼 | UbisamBase 호스트+플러그인. D:\UbisamPlatform\bin 을 Reference+Private=false 로 참조, CopyPlatformRuntime 타겟이 런타임 복사 |
| 플랫폼 소스 | D:\Sorce\MergeTool\UbisamPlatform — 가이드는 doc\platform-guide.md |
| 실행 | bin\Release\net48\UbisamBase.Shell.exe |
| MVVM | CommunityToolkit.Mvvm 8.3.2 ([ObservableProperty], [RelayCommand]) |
| Git | LibGit2Sharp 0.32.0 |
관련 파일
Views/HomeView.xaml(.cs) 머지 화면. 현재 [커밋트리 | BASE | LOCAL | ACTIONS] 4열
ViewModels/HomeViewModel.cs ~900줄. 정렬·적용·Undo/Redo·전체편집
Views/GitView.xaml(.cs) Git 조작 탭
Services/LineAligner.cs LCS 줄 정렬. Align(baseLines, localLines) → AlignedRow 목록
Services/GitService.cs Git API 60여 개. GetThreeWayContent(path) 이미 있음
Services/GitWorkspace.cs DI 싱글턴. 저장소 상태·세마포어 게이트
Services/CodeSanityChecker.cs 코드 이상 검사
Services/MergeNote.cs 머지 주석 생성
Views/Controls/CSharpTokenizer.cs C# 전용 토크나이저. CodeTokenKind 15종
Views/Controls/CodeTextBlock.cs TextBlock 파생. Code/Words/InBlockComment/HighlightSyntax DP
Views/Controls/ChangeMiniMap.cs 미니맵
Models/MergeLine.cs 한 행. LineHighlight { None,Changed,Applied,Edited,Added,Removed }
MergeHubSettings.cs IgnoreWhitespace, WordLevelDiff, SyntaxHighlight 등
참고자료와 라이선스 — 먼저 읽을 것
WinMerge 소스 코드를 받거나 읽지 말 것. GPLv2+ 라 코드를 참고해 옮기면 MergeHub 전체가 GPL 의무에 걸린다. C++/MFC 라 어차피 C#/WPF 로 옮길 수도 없다.
대신 이것들을 쓴다.
| 목적 | 참고할 것 |
|---|---|
| 조작 감각·UI 관례·단축키 | WinMerge 설치본을 직접 써 볼 것 (UI 관례를 따르는 건 자유) |
| 기능 정의·필터 파일 형식 | https://manual.winmerge.org/en/ |
| 줄 정렬 알고리즘 | Histogram diff (git 기본 알고리즘) · Myers diff 논문 An O(ND) Difference Algorithm |
| 단어 단위 diff | DiffPlex (MIT) — 필요하면 참조 가능 |
작업 1. 머지툴 기본 조작 · UI
WinMerge 화면에 가깝게 만든다. 세 갈래다.
1-1. 파일 비교
-
차이 탐색 — 첫 / 이전 / 다음 / 마지막 차이로 점프. 단축키
Alt+↑Alt+↓Ctrl+HomeCtrl+End, 툴바 버튼도 같이. 현재 몇 번째 차이인지 상태바에차이 3 / 15로 표시. -
Go to — 줄 번호 또는 차이 번호로 이동 (
Ctrl+G). -
워드랩 토글 — 긴 줄을 접어서 표시. 현재는 가로 스크롤뿐. 접힌 줄은 여백에 화살표 표시. BASE/LOCAL 양쪽 줄 높이가 어긋나지 않아야 한다.
완료 (2026-09-07). 여백(줄번호·
[>])을 행 안쪽으로 옮겨 높이가 저절로 맞게 했고, 가로 스크롤 때는 글자만TranslateTransform으로 밀어 여백을 고정한다. 접히는 폭을 맞추려고 편집기 세로 스크롤바를 항상 띄우고 표시층에 같은 폭의 여백을 줬다.한 가지 더 필요했다 - 한쪽만 접힌 행에서 아래가 통째로 밀리는 문제.
MergeLine.RowHeight에 두 패널이 각자 글 높이를 알려 주고 큰 쪽을 함께 쓰도록 해서 맞췄다. -
상태바 — 줄/열 위치, 감지된 인코딩, 줄바꿈 형식(CRLF/LF) 표시.
-
북마크 — 여백 더블클릭으로 표시/해제,
F2로 다음 북마크. -
읽기 전용 토글 — LOCAL 편집 잠금.
1-1-A. 정렬 알고리즘 교체 — LCS → Histogram diff
현재 LineAligner 는 순수 LCS 다. LCS 는 {, }, 빈 줄처럼 짧고 자주 반복되는 줄을
엉뚱하게 짝지어 정렬이 지저분해진다. git 이 LCS 대신 histogram 을 기본으로 쓰는 이유가 그것이다.
- Histogram diff 로 교체한다. 원리: 양쪽에서 드물게 나오는 줄(빈도가 낮은 줄)을 먼저 앵커로 잡고, 그 앵커를 기준으로 앞뒤 구간을 재귀적으로 다시 정렬한다. 흔한 줄로 짝을 맞추지 않으므로 사람이 보기에 자연스러운 정렬이 나온다.
- 기존 최적화는 유지한다 — 앞뒤 공통 구간 잘라내기, 크기 상한(
MaxCells) 넘으면 포기. - 기존
AlignTest26건이 전부 통과해야 한다. 실패하면 정렬이 나빠진 것이므로 원인을 밝힌다. - 새 알고리즘 전용 테스트를 추가한다. 최소한 이 두 가지:
- 같은 코드 블록이 여러 번 반복되는 파일에서 올바른 쪽에 붙는지
- 중괄호·빈 줄만 있는 구간이 엉키지 않는지
1-1-B. 이동된 블록 감지
- 정렬 결과에서 삭제 구간과 추가 구간의 내용이 같으면
"삭제+추가" 가 아니라 "이동" 으로 표시한다 (
LineHighlight에Moved추가, 별도 색). - 메서드 순서만 바꾼 경우가 흔한데, 지금은 전부 삭제+추가로 보인다.
- 양쪽 블록을 선으로 잇거나 "→ 42행으로 이동" 같은 힌트를 준다.
- 미니맵과 범례에도 반영한다.
1-2. 무시 규칙
지금 MergeHubSettings.IgnoreWhitespace 는 GitService 의 patch 파싱에만 걸려 있고
홈 화면의 LineAligner 비교에는 적용되지 않는다. 이걸 바로잡고 확장한다.
- 공백 3단계 — 전부 비교 / 개수 변화만 무시 / 완전 무시
- 대소문자 무시
- 줄 필터 — 정규식에 맞는 줄은 차이로 치지 않는다.
타임스탬프·빌드번호·
$Id$같은 것. 규칙 여러 개를 이름 붙여 저장/재사용. - 무시 규칙은 툴바에서 즉시 토글되고, 바꾸면 화면이 바로 다시 계산되어야 한다. 현재 켜진 규칙을 상태바에 요약 표시.
- 무시로 처리된 줄은 "차이 없음" 과 구분되는 옅은 색으로 표시.
- 구현 위치 주의 — 무시 규칙은 정렬 전에 비교용 키를 만드는 단계에 넣는다. 화면에 보이는 원문은 그대로 두고, 비교할 때만 정규화한 문자열을 쓴다.
1-3. 폴더 비교
새 화면. 파일 하나가 아니라 폴더 두 개를 비교한다.
- 비교 방식: 전체 내용 / 빠른 내용(크기+수정일) 선택. 큰 파일은 자동으로 빠른 비교.
- 트리 뷰 (하위 폴더 재귀) + 목록 뷰
- 각 항목 상태: 같음 / 다름 / 왼쪽만 / 오른쪽만 / 이진파일
- 파일 필터 — 정규식 포함·제외. 규칙 세트 저장
- 항목 더블클릭 → 기존 파일 비교 화면으로
- 좌↔우 복사, 삭제. 파괴적 동작은 확인 대화상자 필수
- 결과 요약: 전체 N개 · 다름 N · 왼쪽만 N · 오른쪽만 N
작업 2. Git 유무에 따라 화면 바꾸기
- 연 경로에서
.git을 상위로 거슬러 탐색한다 (GitService.Discover이미 있음). - 있으면 — 지금처럼 커밋 이력 패널을 보여주고, BASE를 커밋에서 불러온다.
- 없으면 — 커밋 이력 패널을 숨기고, Git 탭도 숨기거나 비활성화한다. 대신 "파일 두 개를 직접 골라 비교" 모드로 동작한다 (왼쪽/오른쪽 파일 선택).
- 전환은 자동이며, 사용자에게 현재 어느 모드인지 명확히 보여야 한다.
- 폴더 비교도 마찬가지 — Git 없이 동작해야 한다.
판정 기준: Git이 아닌 아무 폴더의 텍스트 파일 두 개를 열어 비교·편집·저장까지 된다.
작업 3. 진짜 3-Way 비교
기획안에는 "3-Way Merge" 라고 적혀 있으나 실제는 2분할이다. 1번 칸은 커밋 트리이고 BASE ↔ LOCAL만 비교한다.
- 패널을 BASE(공통 조상) / LOCAL(내 작업) / REMOTE(상대 변경) 3분할로 만든다.
GitService.GetThreeWayContent(path)가 이미 세 버전을 돌려준다 — 이걸 쓴다.LineAligner를 3열 정렬로 확장한다. 지금은 2열이다. 권장 방식: BASE 를 축으로 BASE↔LOCAL, BASE↔REMOTE 를 각각 정렬한 뒤 BASE 행 기준으로 합친다. 세 쪽 모두에 대응되는 행, 두 쪽만 있는 행, 한 쪽만 있는 행이 한 행에 정렬되어야 한다.- 각 줄에서 LOCAL로 가져오기 / REMOTE로 가져오기 / 양쪽 다 를 고를 수 있어야 한다.
- 충돌 판정: BASE 대비 LOCAL과 REMOTE가 서로 다르게 바뀐 줄을 별도 색으로.
- 커밋 트리 패널은 접거나 폭을 줄일 수 있게 한다 — 3분할이면 가로가 부족하다.
- Git이 없거나 3-way 대상이 아니면 2분할로 자동 전환.
작업 4. 소스 아닌 파일 · 다른 언어
지금 구문 강조는 CSharpTokenizer 하나뿐이고, CodeSanityChecker.AppliesTo 도
.cs .cpp .c .h .hpp .java .js .ts 만 받는다.
- 토크나이저를 언어별로 갈아끼울 수 있는 구조로 바꾼다.
CSharpTokenizer를ITokenizer뒤로 넣고 확장자로 고른다. 기존Tokenize(line, inBlockComment, out stillInBlockComment)시그니처(줄 넘어가는 블록 주석 상태)는 유지한다. - 최소 지원: C 계열(C/C++/C#/Java/JS/TS) · XML/HTML · JSON · SQL · Python · INI/설정 · Markdown · 일반 텍스트(강조 없음)
- 일반 텍스트가 기본값이다. 모르는 확장자는 강조 없이 비교만 되면 된다.
.txt .md .csv .log .xml .json .yml등 문서 파일이 정상 동작해야 한다. - 이진 파일 감지 — NUL 바이트가 있으면 텍스트로 열지 않고 "이진 파일 · 같음/다름" 만 알린다. 열어서 깨뜨리면 안 된다.
CodeSanityChecker는 코드 파일에서만 돌린다. 문서에서는 조용해야 한다.- 인코딩 판별(BOM → UTF-8 엄격 → CP949)은 지금 로직을 그대로 쓴다.
- 기존
TokenTest63건은 C# 토크나이저용이다 — 그대로 통과해야 한다.
지켜야 할 것
- 주석은 한글로, 기존 코드와 같은 밀도로. "무엇을" 이 아니라 "왜" 를 적는다.
- 빌드 경고 0 · 오류 0 을 유지한다.
- 새 알고리즘(histogram 정렬, 3열 정렬, 이동 블록 감지, 무시 필터)은
스크래치패드에 테스트 하네스를 만들어 검증하고 결과 건수를 보고한다.
기존 하네스 방식(
AlignTest,SanityTest,TokenTest)을 따른다. - 기존 테스트를 깨뜨리지 말 것. 깨지면 원인을 밝히고 보고한다.
- PowerShell
Get-Content/Set-Content로 프로젝트 파일을 읽고 쓰지 말 것. PS 5.1이 UTF-8을 ANSI로 읽어 한글 주석을 손상시킨다. Read/Write/Edit 도구를 쓴다. - 플랫폼(
UbisamPlatform)을 고쳐야 하면 먼저 알리고, 고친 뒤에는 Core → Shell 빌드 →scripts\publish-platform.ps1→ MergeHub 재빌드 순서를 지키고doc\history\changelog.md에 기록한다. - 커밋·푸시하지 않는다. 필요하면 먼저 확인받는다.
하지 말 것
- WinMerge 소스 코드를 받거나 읽지 말 것 (GPLv2+ — 위 라이선스 항목 참조).
- WinMerge를 따라가되 우리 목적과 무관한 것은 넣지 않는다 — 이미지 비교, CSV 표 형식 비교, 압축파일 내부 비교, 플러그인 시스템(ActiveX).
진행 방식
권장 순서:
1-1-A 정렬 교체(histogram) ← 다른 모든 것의 품질이 여기에 달림
1-1 차이 탐색 · Go to · 워드랩 · 상태바 · 북마크
1-1-B 이동 블록 감지
1-2 무시 규칙
2 Git 유무 분기
4 다국어 · 문서 · 이진파일
3 3-Way
1-3 폴더 비교
정렬 교체를 맨 앞에 두는 이유 — 이동 블록 감지와 3-Way 정렬이 모두 그 위에 얹히므로 나중에 바꾸면 두 번 일한다.
각 항목을 끝낼 때마다 빌드하고 실제로 실행해 화면을 확인한 뒤 보고할 것. 전부 끝내고 한 번에 보고하지 말 것.
시작 전에 확인할 것 (미해결)
- 3-Way의 REMOTE는 무엇인가 — 병합 충돌 중일 때의
MERGE_HEAD인가, 아니면 사용자가 고른 임의의 브랜치·커밋인가? 후자면 브랜치 선택 UI가 필요하다. - 폴더 비교에서 Git 커밋 두 개 사이 비교도 필요한가, 아니면 파일시스템 폴더 두 개만인가?
- 가로 공간 — 3분할 + 커밋트리 + ACTIONS 를 한 화면에 넣을지, 커밋 트리를 접는 방식으로 갈지.
진행 기록
| 날짜 | 항목 | 결과 |
|---|---|---|
| 2026-09-07 | 문서 작성 | 착수 전 |
| 2026-09-07 | 패널 이름·머리 정렬 · C#/C++ 데모 · 탭 인디케이터 | 완료. ① 패널 이름을 모드와 상관없이 BASE / LOCAL 로 통일(기준 파일·내 파일 삭제). ② 경로 줄 높이를 맞췄다 — LOCAL 머리에만 있던 전체 반영·전체 되돌리기 를 ACTIONS 로, BASE 머리에만 있던 파일 목록 콤보를 상단 툴바로 옮겨 두 머리를 제목 + 부제 + 경로 로 똑같이 만들었다. 옮긴 버튼들은 줄 단위 반영과 성격이 같아 ACTIONS 가 원래 자리이기도 하다. ③ C# · C++ 데모 파일 을 모두 만들었다 — 파일 비교 기본값은 WeldController_기준.cs ↔ _내작업.cs, 같은 폴더에 .cpp 쌍도 있고, 폴더 비교 데모(folder-demo)에는 .cs·.cpp·.h·문서가 섞여 있다. Git 데모 저장소에는 WeldController.cpp 를 새 커밋으로 넣고 거기서 갈라지는 feature/cpp-tuning 을 만들어, C++ 파일로도 제대로 된 3-Way(merge base 가 그 파일을 갖고 있음)가 된다. ④ 탭 인디케이터 결함: 폴더 비교에서 파일을 더블클릭하면 내용과 강조는 파일 비교로 넘어가는데 아래 파란 막대만 폴더 비교에 남았다. 코드로 탭을 옮기면 그 탭 화면이 그 자리에서 만들어지느라 배치가 밀리는데, MoveTabIndicator 는 그때 막대를 0 으로 만들려 했고 진행 중인 애니메이션이 값을 붙들고 있어 그 지정이 무시됐다 — 결국 옛 자리 그대로였다. 배치가 끝난 뒤 한 번 다시 재도록 고치고(무한 재시도 방지 포함), 선택이 아예 없을 때는 애니메이션을 먼저 끊고 지우게 했다. 곁들여 고친 것: 파일 두 개를 직접 고른 모드에서 코드 검사가 늘 "검사 대상 아님" 이었다 — SelectedFile 은 Git 모드에서만 채워지는데 그걸 보고 있었다. CurrentFilePath 로 바꿔 .cs·.cpp 모두 검사된다(확인: 이상 없음). 전체 되돌리기 버튼 글자가 두 칸 배치에서 잘리던 것도 한 줄에 하나씩으로 폄. |
| 2026-09-07 | 폴더 비교 → 파일 비교 더블클릭이 안 먹던 것 | 완료. 기능은 이미 있었는데 탭 순서를 바꾸면서 깨졌다. 탭 내용은 그 탭을 처음 열 때 만들어진다. 폴더 비교가 첫 화면이 되면서, 파일을 처음 더블클릭하는 순간에는 파일 비교 화면이 아직 없어 요청 이벤트를 받을 사람이 없었다. 요청은 허공에 사라지고, 뒤이어 만들어진 화면은 기본 데모 파일을 열었다 - 겉보기에 "더블클릭이 안 먹는다". GitWorkspace 가 요청을 남겨 두고, 화면이 만들어질 때 스스로 집어 가도록 고쳤다(HasPendingPlainCompare / DeliverPendingPlainCompare). 곁들여 고친 것: ① 한쪽에만 있는 파일(왼쪽만·오른쪽만)은 열리지 않았다 — 오히려 그런 파일이야말로 "뭐가 들었길래 새로 생겼나"를 보고 싶은 대상이다. 없는 쪽을 빈 내용으로 열어 전부 추가(또는 삭제)로 보이게 했다. ② 열 수 없는 항목(폴더·이진 파일)을 눌러도 아무 일도 일어나지 않아 기능이 없는 줄 알게 됐다. 왜 못 여는지 토스트로 알린다. ③ Enter 로도 열린다. ④ 클릭한 자리에서 항목을 직접 집는다 — 펼침 화살표처럼 선택을 바꾸지 않는 자리를 두 번 누르면 엉뚱한 항목이 열렸다. ⑤ 안내 문구가 없어진 탭 이름("홈 화면")을 가리키고 있던 것을 고쳤다. 실행 확인: 프로그램을 새로 켠 뒤 첫 더블클릭부터 바로 열림 — 양쪽 다 있는 Weld.cs(총 5 라인·차이 2 라인), 왼쪽에만 있는 Legacy.cs(총 1 라인·차이 1 라인) 모두 정상. 폴더는 접기/펼치기만 된다. |
| 2026-09-07 | 탭 순서·용어 정리 · 데모 전환 · 모니터 2 확인 | 완료. ① 탭 순서를 실제 작업 순서로: 폴더 비교 → 파일 비교 → Git. 사용자가 짚은 대로 "폴더를 먼저 보고 고칠 파일을 골라 들어가는" 흐름이 맞다. 홈 이라는 이름도 하는 일과 맞지 않아 파일 비교 로 바꿨다. 폴더 비교에서 더블클릭하면 파일 비교 탭으로 넘어가므로 흐름이 이어진다. ② 용어를 머지툴 표준으로: LOCAL (Mine) · BASE · REMOTE (Theirs) · MERGED, 버튼은 Take Mine / Take Theirs, 범례는 Theirs only / Conflict. 한글 풀이(내 작업 원본·공통 조상·고른 브랜치·최종 결과)와 곁들인 설명 줄은 지웠다 — 이 용어는 Git 과 다른 머지툴에서 그대로 쓰이므로 여기서만 다르게 부르면 옮겨 다닐 때 오히려 헷갈린다. ③ 데모 전환 버튼(발표용): 상단의 데모 → .git 있음/없음 하나로 Git 저장소 화면과 파일 비교 화면을 즉시 오간다. 설정에 DemoGitPath·DemoPlainPath 를 넣었고 둘 다 실재할 때만 버튼이 뜬다. 여기서 결함 둘을 잡았다 — (1) 전환 중 3-Way 를 끄면서 부른 재로드가 작업 게이트를 먼저 잡아 뒤따르는 저장소 닫기·파일 비교가 "다른 작업 진행 중"으로 통째로 건너뛰어져 화면이 빈 채 남았다(_suspendReload 로 차단). (2) 늦게 끝난 Git 본문 로드가 이미 바뀐 화면을 덮어써 경로는 새 파일인데 내용은 옛 파일이 됐다(양쪽 로드의 UI 반영 직전에 모드가 그대로인지 확인). 실행 확인: 21줄 레시피 ↔ 34줄 StartWeld.cs 를 세 번 왕복해도 매번 정확. ④ 3-Way 워드랩 재검증: LOCAL 4줄 접힘 · REMOTE 3줄 접힘 · BASE 빈칸인데도 void StartWeld() 가 세 칸 정확히 같은 y. 아래 MERGED 는 폭이 넓어 같은 줄이 한 줄에 들어가고 빈칸이 안 생긴다. ⑤ 모니터 2 확인(1920×1080 @100%, 모니터 1 은 2560×1600 @150%): 4분할·워드랩·용어 모두 정상, 배율 때문에 깨지는 곳 없음. 상단 툴바만 폭에 맞춰 두 줄로 접힌다(정상 동작). |
| 2026-09-07 | 워드랩 4분할 대응 · 전체화면 점검 | 완료. ① 3-Way 로 패널이 넷이 되면서 행 높이 계산이 어긋나 있었다 — MergeLine.ReportTextHeight 가 BASE·LOCAL 둘만 보고 있어 REMOTE·LOCAL원본이 접혀도 반영되지 않았다. 더 중요한 것은 맞춰야 할 묶음이 하나가 아니라는 점이다. 3-Way 에서 위 원본 세 칸은 폭이 같아 서로 맞춰야 하지만, 아래 MERGED 는 폭이 가로 전체라 접히는 자리가 애초에 다르다. 같은 높이를 강요하면 MERGED 는 안 접힌 줄에 빈칸이 생기고 그 위에 겹친 편집기와도 어긋난다. 그래서 높이를 RowHeight(MERGED·편집기) 와 SourceRowHeight(원본 세 칸) 둘로 나눴다(2-Way 에서는 같은 값). ② 같은 이유로 글자 폭 보정(MatchTextWidths)도 모드를 타게 했다 — 2-Way 는 BASE 가 미니맵 폭만큼 물러나야 LOCAL 과 맞지만, 3-Way 에서 그러면 BASE 만 좁아져 위 세 칸의 접히는 자리가 어긋난다. 모드가 바뀌면 다시 재도록 IsThreeWay 변경을 구독한다. ③ 폴더 비교의 줄 배경이 글자 끝에서 끊겨 넓은 화면에서 색 띠가 중간에 잘려 보였다. TreeViewItem 기본 템플릿이 머리를 Auto 폭으로 잡아 HorizontalContentAlignment=Stretch 가 듣지 않는다 — 템플릿을 갈아끼우면 플랫폼 테마를 잃으므로 배경 띠에 트리 폭만큼 MinWidth 를 주고 넘치는 부분은 잘리게 뒀다. ④ 줄바꿈 끄기 버튼 글자가 칸을 넘어 잘리던 것을 줄바꿈 켬/끔 으로 맞춤(필터 켬/끔 과 같은 꼴). 실행 확인(2560×1600 최대화): 2-Way 는 BASE 의 빈 블록이 LOCAL 의 접힌 높이를 그대로 따라가 void StartWeld() 가 같은 눈높이. 3-Way 는 LOCAL 4줄 · REMOTE 3줄로 접힘이 다른데도 위 세 칸이 같은 높이로 맞물리고, 아래 MERGED 는 폭이 넓어 한 줄에 다 들어가며 빈칸이 안 생긴다. 접힌 줄에서 캐럿도 논리 행/열을 정확히 보고한다(End → 5행 67열, 시각 줄이 아니라 실제 줄 기준). 폴더 비교도 실제 프로젝트 폴더(Views ↔ Services, 27개)로 전체화면 확인. |
| 2026-09-07 | 1-3 폴더 비교 (별도 탭) | 완료. 새 파일 Services/FolderComparer.cs · ViewModels/FolderViewModel.cs · Views/FolderView.xaml · 테스트 27/27. 홈에 넣지 않고 탭을 따로 뒀다 — 홈은 3-Way 에서 이미 네 칸을 쓰고 있어 트리를 얹을 자리가 없다. 담은 것: 트리 뷰(하위 폴더 재귀) · 상태 5종(같음 / 다름 / 왼쪽만 / 오른쪽만 / 다름(이진)) · 전체 내용 ↔ 빠른 비교(크기+수정일) 전환 · 한도(기본 8MB)를 넘는 파일은 전체 비교를 골라도 자동으로 빠른 비교 · 정규식 포함·제외 필터(기본 제외에 .git · bin · obj · .dll · .pdb) · 같은 파일 숨기기 · 요약(전체 N개 · 다름 N · 왼쪽만 N · 오른쪽만 N · 같음 N) · 좌↔우 복사와 삭제(원본·대상 경로를 그대로 보여 주는 확인 대화상자 필수, 기본 선택은 취소) · 항목 더블클릭 → 홈 탭에서 줄 단위 비교. 비용이 드는 것은 파일을 여는 일뿐이라 열기 전에 거를 수 있는 것을 먼저 거른다 — 크기가 다르면 내용을 안 보고, 내용 비교도 64KB 씩 읽다가 다른 곳을 만나면 즉시 멈춘다. 실수로 C:\ 를 고른 경우를 대비해 5만 항목에서 끊고 요약에 그렇다고 적는다. 실행 확인: 데모 폴더 두 개 → 절차서.txt·Weld.cs 다름, Legacy.cs 왼쪽만, New.cs 오른쪽만, bin/tool.dll 은 기본 제외 규칙으로 빠짐(전체 5개 · 다름 2 · 왼쪽만 1 · 오른쪽만 1 · 같음 1). Weld.cs 를 더블클릭하니 홈 탭으로 넘어가 두 파일이 줄 단위로 열렸다(1800→2000, Emit→EmitFast 단어 단위 강조). 곁들여 고친 플랫폼 결함: 코드로 탭을 옮기면(새 API TabManager.Activate) 내용만 바뀌고 네비게이션 강조는 그대로 남아 두 곳이 어긋났고, 그 상태에서 강조된 탭을 눌러도 이미 선택된 항목이라 아무 일도 일어나지 않았다. |
| 2026-09-07 | 3-Way 를 4분할로 재구성 · 캐럿 가로 어긋남 | 완료. ① 사용자 지적대로 **일반적인 3-Way 도구 구성(원본 3 + 결과 1)**으로 바꿨다. 위에 LOCAL(내 작업 원본) · BASE(공통 조상) · REMOTE(고른 브랜치) 를 읽기 전용으로 나란히, 아래에 MERGED(최종 결과) 를 폭 전체로 두고 여기서만 고친다. 원본을 넷째 칸으로 붙이지 않은 이유 — 한 칸이 40자도 안 돼 코드를 읽을 수 없다. 세로를 쓰면 위는 68자, 아래는 200자 넘게 나온다. 2-Way 는 아래 두 줄이 높이 0 이라 지금까지의 좌우 배치 그대로다. MergeLine.OriginalLocalText 를 새로 붙들어 둔다 — 이게 없으면 REMOTE 를 한 번 가져오는 순간 내 원래 내용이 화면에서 사라져 정작 충돌을 판단할 근거를 잃는다. LOCAL 패널의 [<] 와 ACTIONS 의 내 것으로 가 Accept Mine, REMOTE 쪽 [<] 와 저쪽 것으로 가 Accept Theirs 다. 위 세 칸은 3-Way 에서 머리를 74px 로 줄이고 바닥을 감춰(같은 정보가 아래 상태바에 있다) 코드를 더 보여 준다. ② 캐럿이 글자 가운데에 박히던 것: LOCAL 표시층의 본문 테두리(문제 줄 표시용 2px)가 레이아웃 폭을 먹어 BASE·REMOTE 및 그 위에 겹친 편집기보다 글자가 2 DIP 오른쪽에서 시작하고 있었다. 테두리와 안여백의 합을 세 패널 모두 2 로 맞춰 해결. ③ 상태바 열 번호가 틀리던 것: TextBox.GetLineIndexFromCharacterIndex·GetCharacterIndexFromLineIndex 는 "화면에 보이는 줄" 기준이라 글을 갈아 끼운 직후 CaretIndex 와 어긋난다(11글자 줄에서 Home 을 눌렀는데 10열). 글에서 직접 세도록 바꿔 Home=1열·End=길이+1 로 정확해졌다. |
| 2026-09-07 | 작업 4 다른 언어·문서 지원 | 완료. 새 파일 Services/LanguageProfile.cs(언어 표 + LanguageCatalog) · Services/CodeTokenizer.cs(표를 보고 도는 범용 토크나이저) · 테스트 40/40, 기존 하네스 전부 유지(Align 37 · Move 11 · Editor 14 · Rules 21 · Sanity 20 · Token 15 · Note 16 · ThreeWay 20). 언어마다 토크나이저를 만들지 않았다 — 색칠에 필요한 것은 "주석은 어디서 시작하나 · 문자열은 무엇으로 묶나 · 예약어는 무엇인가" 셋뿐이고 나머지는 어느 언어나 같아서, 표 하나에 적고 토크나이저 하나가 그 표를 본다. C# 만 전용 토크나이저를 그대로 쓴다(축자·보간 문자열 @"" $"" 과 접근제한자 구분은 표로 담기지 않고 이미 테스트로 굳어 있다). 담은 것: C# · C/C++ · Java · JS/TS · Python · SQL(대소문자 무시) · 셸/배치(rem 포함) · XML/HTML/XAML · JSON · YAML · INI/설정 · Markdown · 평문(기본값). 모르는 확장자는 평문이라 색을 잘못 입히는 일이 없다. CodeSanityChecker.AppliesTo 의 확장자 switch 를 걷어내고 언어 표의 HasBraces 하나로 판단하게 바꿔, 괄호로 블록을 여닫지 않는 형식(YAML·INI·평문)에는 검사를 걸지 않는다. 상태바에 UTF-8 · LF · Python 처럼 고른 언어를 띄운다. 이진 파일은 앞 8KB 에 NUL 이 있으면 이진으로 보고(UTF-16 BOM 은 예외) 비교하지 않고 그렇다고 알린다 — 깨진 글자로 화면을 채우지 않기 위해서다. 실행 확인: .py 두 개를 비교하니 # 주석·def/if·독스트링·숫자가 파이썬 규칙대로 칠해지고, 상태바가 Python, 코드 검사는 검사 대상 아님(파이썬은 중괄호를 쓰지 않는다)으로 나왔다. |
| 2026-09-07 | 작업 3 3-Way 비교 | 완료. REMOTE 는 사용자가 고르는 브랜치다(상단 3-Way 상대 드롭다운). 고르면 BASE 가 merge-base(HEAD, 고른 브랜치), LOCAL 은 워킹트리, REMOTE 는 그 브랜치의 파일이 된다 — BASE 를 HEAD 로 두면 "저쪽이 고친 것"과 "내가 고친 것"을 가를 수 없어서 반드시 공통 조상이어야 한다. 병합 중이면 브랜치를 고를 것도 없이 Git 이 들고 있는 세 벌(GetThreeWayContent)을 그대로 쓴다. 새 파일 Services/ThreeWayAligner.cs · 테스트 20/20. 세 글을 한꺼번에 정렬하지 않고 BASE↔LOCAL · BASE↔REMOTE 두 번 정렬한 뒤 BASE 줄 번호로 맞물린다 — 검증된 2-way histogram 정렬기를 그대로 쓰기 위해서다. 양쪽이 같은 자리에 넣은 줄은 한 번 더 정렬해 짝지어 한 행으로 합친다(안 합치면 "둘 다 같은 줄 추가"가 충돌로 보인다). 판정은 MergeLine.ThreeWay — 같음 / 나만 고침 / 저쪽만 고침 / 둘 다 같게 / 충돌. 색은 저쪽만 고침=남색, 충돌=자홍(가장 눈에 띄게, 이동·단순 차이보다 앞세움), 범례에도 3-Way 일 때만 두 항목이 붙는다. REMOTE 줄의 [<] 버튼과 ACTIONS 의 저쪽 것으로 로 그 줄을 가져오고, 되돌리기 스택은 BASE 반영과 같은 것을 쓴다. 3-Way 를 안 쓰면 REMOTE 칸은 폭 0 으로 접힌다(Visibility 만 숨기면 star 열이 자리를 그대로 차지한다). 실행 확인: 데모 저장소에 공통 조상에서 갈라진 feature/cooling 을 만들어 붙였더니 laser.Power·Wait() 두 줄이 충돌 2, fan.Speed = 70; 이 저쪽만 고침 1 로 잡히고, [<] 로 가져오니 머지 주석까지 붙어 LOCAL 에 들어왔다(편집 1건). 세 패널의 같은 행이 정확히 같은 눈높이. 곁들여 고친 것: LoadFileAsync 도 RunAsync 실패 시 화면을 갈아엎지 않게 막았다(파일 비교에서 겪은 것과 같은 결함). |
| 2026-09-07 | 캐럿 어긋남 · 입력 지연 · 화면 배율 · 데모 기본값 | 완료. ① 캐럿이 글자에 안 맞던 원인: 편집기와 표시층을 맞출 때 기준이 서로 다른 두 좌표를 견줬다. 별도 WPF 프로브(scratchpad/LineProbe)로 직접 재 보니 Consolas 12·줄높이 20 에서 TextBox.GetRectFromCharacterIndex(0).Top = 4.67(줄상자 위 여백을 지난 글자 상자 위쪽), TextBlock 의 (0,0) = 0(줄상자 위쪽). 이 여백만큼 표시층이 아래로 밀려 선택 영역이 글자보다 반 줄 위에 있는 것처럼 보였다. 여백을 BaselineOffset − FontFamily.Baseline×FontSize 로 글꼴에서 직접 구해 빼도록 고침(상수를 안 써서 배율이 바뀌어도 따라간다). 덧붙여 BASE 패널에도 같은 이동값을 걸었다 — LOCAL 만 밀면 두 패널의 줄이 6px 어긋난다(UIA 로 확인: 어긋남 0). ② 입력 지연의 남은 절반: 화면 글자는 편집기가 아니라 표시층이 그리는데(편집기 글자는 투명) 표시층을 600ms 재정렬 타이머에만 맡겨 친 글자가 그때까지 안 보였다. ShowTypedLineNow 로 캐럿이 있는 줄만 즉시 반영(줄 수가 달라지면 재정렬에 맡김). 실행 확인: 타이핑 120ms 뒤 촬영해 -RC7 이 이미 보이고 캐럿이 글자 끝에 정확히 붙음. ③ 화면 배율: 지금까지 상·하단 바만 커지고 정작 작업 화면은 그대로였다. 플랫폼 ShellWindow 의 콘텐츠에 LayoutTransform 을 걸고 UiSettingsService.ContentScale(= ChromeScale ÷ 기준 1.2, 즉 "100% = 지금 그대로") 추가. 실행 확인: 80% 에서 코드 행 높이 30px → 24px(UIA 측정), 같은 화면에 더 많은 줄. ④ 데모를 .git 없는 폴더로: plain-compare 의 용접 레시피 두 개(WeldRecipe_기준.txt ↔ WeldRecipe_내작업.txt)를 기본으로 열게 함(DefaultLeftFile/DefaultRightFile 설정 추가). 곁들여 고친 실제 결함: 저장소가 아닌 폴더에 자동 열기를 걸면 실패할 뿐 아니라 작업 게이트를 쥐고 있어 뒤따르는 파일 비교가 통째로 취소돼 화면이 차이 없음 으로 비었다. GitWorkspace.LooksLikeRepository 로 저장소일 때만 자동 열기를 시도하고, LoadPlainFilesAsync 도 RunAsync 실패 시 화면을 갈아엎지 않게 함. |
| 2026-09-07 | 작업 2 Git 유무 분기 | 완료. .git 이 없으면 커밋 이력 칸을 접고(폭 0) Git 상태·새로고침·파일 목록·커밋 상세·커밋 메시지를 숨긴다. 대신 상단에 기준 파일 / 내 파일 고르기가 나오고, 패널 제목이 기준 파일 / 내 파일 로 바뀌며(Git 용어 안 씀), 저장 버튼이 Save & Commit → 저장(커밋 없이 오른쪽 파일에 쓰기)이 된다. 모드는 상단 배지로 항상 보인다. 새 파일 Services/TextFile.cs — 인코딩·줄바꿈 판별 규칙을 GitService 와 동일하게 맞춰, 같은 파일을 어느 경로로 열든 결과가 같고 저장할 때 형식이 바뀌지 않는다. 실행 확인: Git 아닌 폴더의 left.txt↔right.txt 비교 → 다름 1 · 추가 1, 전체 반영 후 저장하니 파일이 정확히 갱신됨(머지 주석 포함). Git 모드 복귀도 이상 없음. |
| 2026-09-07 | 설정 반영·패널 높이·ACTIONS 잘림 수정 | 플랫폼 SettingsService 인스턴스 분리 버그를 고쳐(아래 플랫폼 변경 이력) 설정 → 저장 → 홈 복귀만으로 재비교되게 함(RecompareIfSettingsChanged, 홈이 다시 보일 때 설정 서명이 바뀌었으면 한 번만). BASE/LOCAL 하단 요약 칸을 공통 높이(PaneFooterHeight 54)로 맞춰 두 패널의 코드 영역 크기를 같게 함 — LOCAL 쪽 3줄을 2줄로 합침. ACTIONS 패널에 스크롤을 주고 폭을 196 → 214 로 넓혀 잘리지 않게 함. |
| 2026-09-07 | 타이핑 지연 해소 · 버튼 재배치 · 검사 팝업 | 완료. ① 지연의 진짜 원인은 겹친 레이어가 아니라 글자마다 도는 두 가지였다 — 문서 전체를 문자열로 복사(TextBox.Text 양방향 바인딩)와 전체 줄을 훑는 차이 판정(무시 규칙이 켜져 있으면 양쪽 줄을 정규화). 15,005줄 파일 기준 각각 0.12ms / 2.19ms 한 타. 바인딩을 걷어내 편집기가 글을 직접 들고(ViewModel 은 필요할 때만 읽음), 차이 덩어리 목록을 캐시하고, 캐럿이 같은 줄 안에서 움직이면 명령 상태 갱신을 건너뛰게 했다. 캐시 조회는 0.000ms. ② 차이 탐색·비교 규칙 버튼을 코드 패널 위에서 ACTIONS 의 커밋 메시지 아래로 옮김(읽는 자리와 조작하는 자리를 분리). 머리 높이도 150 → 124 로 복귀. ③ 코드 검사 요약을 눌러 팝업으로 문제 목록을 보고, 항목을 누르면 그 줄로 이동. 곁들여: 화살표 글리프(⤒↑↓⤓)가 이 서체에 없어 빈 버튼으로 나오던 것을 글자로 교체, 파일 로드 시 차이 캐시가 갱신되지 않아 차이 없음 으로 잘못 나오던 것 수정. |
| 2026-09-07 | 1-2 무시 규칙 | 완료. Services/CompareRules.cs · 테스트 21/21, 기존 정렬 37/37 유지. 공백 3단계 · 대소문자 · 정규식 줄 필터. 툴바에서 즉시 토글되고 상태바에 무시: 공백 개수 · 대소문자 · 줄필터 1 로 요약된다. 구현 갈래가 둘이다 — 공백·대소문자는 정렬 단계의 비교용 키(LineAligner.Align 에 keys 인자 추가, 원문은 보존), 줄 필터는 정렬 뒤 행 표시(MergeLine.IsIgnored). 필터를 정렬 키에 섞으면 걸린 줄끼리 같은 것으로 묶여 엉뚱하게 짝지어진다. 곁들여 고친 것: ① MergeLine.State 가 원문끼리만 비교해 규칙이 안 먹던 문제 → 정규화 함수를 넘겨 해결 ② 무시된 줄이 차이 탐색에 걸리던 문제. 실행 확인: 다름 3 → (공백) 1 → (대소문자) 0, 추가 3 → (줄필터) 1 + 무시 2, 차이 - / 2 → - / 1. |
| 2026-09-07 | 1-1-B 이동 블록 감지 | 완료. Services/MoveDetector.cs · 테스트 11/11. 지워진 구간과 추가된 구간에서 줄이 이어지는 최장 공통 부분을 찾아 이동으로 묶는다(구간 전체를 견주면 정렬기가 }·빈 줄을 한쪽에 붙여 놓아 어긋난다). 2줄 미만이거나 중괄호·빈 줄만인 구간은 제외. 색은 청록(삭제/추가와 계열 구분), 범례에 자리 옮김 추가, 툴팁에 24행으로 옮김. 실행 확인: 데모 파일의 Cooldown() 을 옮겨 두니 범례가 자리 옮김 6 으로 잡힘. |
| 2026-09-07 | LOCAL 행 구조 개편 (워드랩 준비) | 여백(줄번호·[>])을 별도 컨트롤에서 행 안쪽으로 옮겨 높이가 저절로 맞게 함. 가로 스크롤 때는 글자만 TranslateTransform 으로 밀고 열에서 클리핑. BASE 여백도 54로 맞추고, 세 곳의 글자 폭을 스크롤바·미니맵 폭만큼 보정해 접히는 자리를 일치시킴. 워드랩 토글 버튼 추가. 화면 확인 완료 — 가로 스크롤 시 여백 고정, 줄 번호가 두 패널에서 정확히 같은 눈높이. 워드랩도 양쪽이 같은 자리에서 접히고 행이 맞물림(MergeLine.RowHeight 로 큰 쪽 높이를 공유). |
| 2026-09-07 | 1-1 파일 비교 (워드랩 제외) | 완료. 차이 탐색(첫/이전/다음/마지막 · Alt+↑↓ Ctrl+Home/End, 끝에서 순환) · 상태바(12행 5열 · 차이 2 / 3 · 인코딩 · 줄바꿈) · Ctrl+G 줄 이동 · 북마크(여백 더블클릭, F2) · 편집 잠금. 차이는 덩어리 단위로 센다(12줄 삭제 = 차이 1개). 인코딩·줄바꿈 판별은 GitService.DescribeWorkingFile 로 노출. 워드랩은 남김 — 아래 참고. |
| 2026-09-07 | LOCAL 패널을 상시 편집기로 전환 | 완료. "전체 편집" 모드 버튼 삭제. LOCAL 칸이 항상 편집 가능(여러 줄 선택·붙여넣기·삭제). 화면은 세 겹 — 왼쪽 여백(줄번호·[>]) / 색·문법 표시층 / 투명 편집기. BASE 와 눈높이를 맞추려고 지워진 줄 자리에 빈 줄(필러)을 넣고, 저장·재정렬 전에 EditorLayout.StripFillers 로 걷어낸다(테스트 14/14). 타이핑이 멈추면(600ms) 다시 정렬. 곁들여 고친 것: 편집 후 원래부터 BASE 와 달랐던 줄까지 전부 "직접 수정"으로 칠해지던 문제 → 파일을 연 시점 LOCAL 과 견줘 실제로 손댄 줄만 표시. |
| 2026-09-07 | 1-1-A 정렬 교체 (LCS → Histogram) | 완료. AlignTest 26건 유지 + 신규 11건 = 37/37 통과. 빌드 경고 0. 데모 파일에서 중복된 fan.Off(); 가 엉뚱한 짝에 붙던 문제가 실제로 해소됨. 안전선을 표 크기(MaxCells 400만 칸)에서 줄 수(MaxRegionLines 2만 줄)로 바꿈 — 히스토그램은 표를 만들지 않으므로 예전보다 훨씬 큰 구간도 정확히 정렬한다. |