Ubisam-MergeTool/doc/진행상황.md
ksj09406 451b6245cb D-1: 주석만 다른 파일을 다른 색으로
폴더 비교에 "주석만 다름" 상태를 새로 두었다. 코드는 같고 주석/공백만 다른
파일이다. 봐야 할 성격이 다르다. 주석만 손댄 파일은 눈으로 훑고 넘길 수
있는데 다른 것과 같은 색으로 섞여 있으면 전부 열어 봐야 한다.

판단은 주석 토큰을 걷어내고 공백을 지운 뒤 견주는 것이다. 주석 기호는 언어마다
다르므로 확장자로 고른 언어표를 따르고, 언어를 모르는 파일은 하지 않는다.
양쪽 확장자가 다르면 같은 언어로 볼 근거가 없으니 역시 하지 않는다.
줄 구분은 코드가 있는 줄에만 남긴다. 큰 파일은 건너뛴다.

곁들여 색 뜻풀이 줄을 목록 위에 붙이고, 보기 필터에 "코드가 다른 것만"을
더했다. 주석만 다른 것을 빼고 보는 쪽이 실제로 필요한 동작이다.

확인: 주석만 고친 .cs 는 회색 "주석만 다름", 값이 바뀐 .cs 와 평문 .txt 는
노란 "다름". "코드가 다른 것만"을 고르면 앞의 것이 빠진다.
FolderTest 4건 추가. 전체 428건 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 14:33:25 +09:00

35 KiB
Raw Blame History

MergeHub 진행 상황

작업 하나가 끝날 때마다 이 표를 고치고 로컬 커밋한다. 푸시는 하지 않는다 — 사내 Gitea 저장소가 만들어지면 그때 한 번에 올린다.

상태 표기: 대기 · 진행 · 완료


이번 주 목표 (2주차 · 폭 넓히기)

기획안(문서/사내_MergeTool_개발기획안) 기준으로 기능 덩어리를 100% 로 채우는 것이 목표다. 한 덩어리를 깊게 파는 것보다 여러 덩어리를 "있다"고 말할 수 있는 선까지 올린다.

④ Git 조작 — 완료 (2026-09-08)

서비스에는 이미 있는데 화면에 없던 것들이라, 다섯 가지가 하루가 아니라 반나절에 붙었다.

상태 항목 내용 확인
완료 커밋 이름 변경(amend) 커밋 수정 버튼. 고쳐 쓸 메시지를 다시 치지 않게 마지막 메시지 버튼도 함께 뒀다 확인: 6e6bb78 → 0d7401f 로 메시지가 바뀜
완료 stash 목록 콤보에서 골라 적용/삭제. 목록이 바뀌면 고른 항목을 다시 잡는다 - 하나 꺼내면 번호가 당겨지는데 옛 항목을 붙들고 있으면 다음 삭제가 엉뚱한 것을 지운다 확인: 저장 → 목록에 뜸 → 적용 → 워킹트리 복구
완료 두 커밋 사이 비교 이력에서 여기부터 로 시작점을 잡으면 그 뒤 고르는 커밋이 끝점이 된다. 안 잡으면 지금까지처럼 부모와 비교 확인: 헤더가 2766bc5 → 6e6bb78 (2개 파일)
완료 blame (줄별 작성자) Blame 탭 신설. 변경 목록에서 파일을 고르고 blame 을 누르면 경로가 넘어간다 확인: 31줄 전부 커밋·날짜·작성자 가 붙어 나옴
완료 변경분 파일 추출 GitService.ExportChangedFiles 신설. 저장소 안 경로 구조를 그대로 만들고, 바이트 그대로 옮긴다(텍스트로 다시 쓰면 줄바꿈·인코딩이 바뀐다). 지워진 파일은 뽑을 것이 없어 건너뛴다 확인: 2개 파일 → ...Temp (건너뜀 0개)

③ 3-Way 병합 — 완료 (2026-09-08)

"볼 수 있다"에서 "끝낼 수 있다"로.

상태 항목 내용 확인
완료 자동 병합 저쪽만 고친 줄(ThreeWayState.RemoteChanged)을 한 번에 가져온다. 충돌과 내가 고친 줄은 안 건드린다. git merge 가 자동으로 해 주는 일을 여기서도 해 주는 것 Theirs only 1 → 0, 자동 병합 버튼이 스스로 꺼짐
완료 충돌만 이동 이전 충돌 / 다음 충돌 (Alt+Shift+↑↓). 아직 안 정한 충돌만 딛는다 3개를 차례로 딛고 마지막에 버튼이 꺼짐
완료 병합 완료 판정 ACTIONS 위쪽에 남은 충돌 N개(빨강) → 충돌 정리 끝(초록) 3개 → 2 → 1 → 끝

여기서 판단이 하나 필요했다 — 충돌인지 아닌지는 BASE·MINE·REMOTE 를 견주면 저절로 나오지만, "내 것으로 간다"고 정한 줄은 내용이 그대로라 여전히 충돌로 보인다. 그래서 NeedsDecision (충돌이면서 아직 반영·수정 표시가 없는 줄)을 따로 두었다. 그 결정은 글을 바꾸지 않으므로 편집 뒤 다시 정렬하면 사라지는데, 얼어 있는 BASE 줄 번호(없으면 MINE 줄 번호)로 붙들어 되살린다.

곁들여 고친 것: Take Mine · Take Theirs 버튼이 줄을 옮겨도 켜짐/꺼짐이 그대로였다. CommunityToolkit 커맨드는 CommandManager 를 구독하지 않는데 이 둘만 갱신 목록에서 빠져 있었다. 키보드(Alt+←/→)로는 되고 버튼만 안 되던 이유가 이것이다.

① 폴더 비교 — 완료 (2026-09-08)

상태 항목 내용 확인
완료 폴더째 복사·삭제 파일 하나씩만 되던 것을 폴더에도 열었다. 확인 창에 안에 든 파일 수를 적는다 - "폴더 하나"라고만 물으면 몇 개가 덮이는지 모르는 채 확인을 누르게 된다 sub(파일 2개) 폴더째 복사 → bsubdeep wo.txt 까지 따라옴
완료 상태별 보기 필터 다른 것만 / 전체 / 왼쪽에만 / 오른쪽에만 / 같은 것만. 다시 비교하지 않고 목록만 다시 그린다 15개 → "왼쪽에만" 5개, 요약 줄은 그대로
완료 비교 취소 FolderComparer.Compare 에 CancellationToken. 항목 하나를 볼 때마다 확인하므로 누르는 즉시 멈춘다. 앞서 본 결과는 그대로 남는다 테스트 3건

설계 하나: 파일을 실제로 옮기고 지우는 부분을 Services/FileOps.cs 로 뺐다. 되돌릴 수 없는 일이라 화면 없이 검사할 수 있어야 하기 때문이다 - 겹치는 파일은 덮고, 대상에만 있던 파일은 남기고, 두 번 복사해도 같은지를 테스트로 못 박았다.

② 파일 비교 — 완료 (2026-09-08)

상태 항목 내용 확인
완료 바꾸기 (Ctrl+H) 찾기 칸 아래에 바꿀 말 줄이 붙는다(Ctrl+F 로 열면 안 뜬다 - 찾기만 할 때 자리를 뺏지 않는다). 바꾸기 는 고른 자리가 찾는 말일 때만 바꾸고 다음으로 넘어간다 - 엉뚱한 자리를 덮어쓰는 것보다 한 번 더 누르게 하는 편이 낫다 PulseUs → PulseMicros 2개 바꿈
완료 덩어리 단위 반영 이 덩어리 반영(Alt+Shift+→). 차이로 잡힌 줄이 이어지는 구간을 한 번에 마지막 차이에서 2줄 한 번에 반영
완료 인코딩·줄바꿈 변환 저장 ACTIONS 의 저장 형식 두 칸. 기본 "원본 유지"라 지금까지와 똑같이 나간다. UTF-8 / UTF-8(BOM) / CP949 · CRLF / LF 화면 확인
완료 바이너리·이미지 비교 "줄 단위로 비교할 수 없습니다" 한 줄로 끝나던 것을 답이 있는 화면으로. 크기·SHA-1·처음 갈리는 바이트 위치를 알려 주고, 그림이면 나란히 띄우고 아니면 앞 4KB 를 16진으로 늘어놓는다(다른 줄만 색) .bin 두 개: "왼쪽 17 B · 오른쪽 19 B · 처음 갈리는 곳 0x10" · .png 두 개 나란히

⑤ 밖에서 부르기 — 완료 (2026-09-08)

상태 항목 내용 확인
완료 명령줄 인자 수신 --merge/--diff --base/--local/--remote/--merged 와 그냥 넘어온 경로. 이것이 다리의 나머지 반쪽이다 - git config 에 등록하는 쪽은 전부터 있었는데 받는 쪽이 없었다 ArgsTest 26건 · 실제 실행 2건
완료 탐색기에서 부르기 우클릭 → 보내기 → MergeHub 로 비교. Git 탭 버튼 하나로 등록/해제하고 버튼 이름이 지금 상태를 말한다 실제 메뉴에서 확인

우클릭 메뉴에 바로 넣으려다 보내기로 돌아섰다. 처음에는 레지스트리에 정적 verb 를 넣었다 (HKCU\Software\Classes\AllFilesystemObjects\shell\...). 키는 제대로 들어갔고 HKEY_CLASSES_ROOT 에도 병합돼 보였는데 탐색기 메뉴에는 뜨지 않았다. 확인해 보니 이 PC 에서 우클릭 메뉴에 나오는 다른 프로그램(Notepad++ · Git Bash · WinMerge)은 전부 HKLM(전체 사용자)에 등록돼 있었고, HKCU 에 넣은 것은 - 우리 것이든 실험용으로 넣은 메모장 항목이든 - 하나도 그려지지 않았다. HKLM 은 관리자 권한이 필요해 쓸 수 없다.

그래서 보내기(SendTo) 로 갔다. %APPDATA%\Microsoft\Windows\SendTo 에 바로가기 파일 하나를 두는 것이 전부라 레지스트리도 권한도 필요 없고 윈도우 판을 타지 않는다. 한 단계 더 들어가는 것이 유일한 단점인데, 대신 여러 개를 골라 한 번에 보낼 수 있다 - 미뤄 두었던 "파일 두 개 골라 비교"가 덤으로 따라왔다.

셸 확장 DLL 은 쓰지 않았다 - DLL 은 탐색기 프로세스 안에서 돌아 잘못되면 탐색기가 통째로 죽는다.

고른 것이 하나뿐이면 왼쪽만 채우고 오른쪽은 비워 둔 채 화면을 연다 - 사용자가 그 자리에서 상대를 고르면 되고, 아무것도 안 열리는 것보다 낫다.


3차 — 버그 정리 + 기능 확장 (2026-09-09 ~)

지시서: doc/작업지시-3차.md. 실제로 써 보고 나온 문제부터 순서대로 잡는다.

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 차이로 이동할 때 코드가 바닥 글 위에 겹쳐 그려짐

D-1. 주석만 다른 파일을 다른 색으로 — 완료 (2026-09-09)

폴더 비교 결과에 주석만 다름 상태를 새로 두었다. 코드는 같고 주석·공백만 다른 파일이다.

왜 나누나 - 봐야 할 성격이 다르다. 주석만 손댄 파일은 눈으로 훑고 넘길 수 있는데 다른 것과 같은 색으로 섞여 있으면 전부 열어 봐야 한다.

판단하는 법: 주석 토큰을 걷어내고 공백을 지운 뒤 견준다. 주석 기호는 언어마다 다르므로 확장자로 고른 언어표를 따르고, 언어를 모르는 파일(평문·표)은 이 판단을 하지 않는다 - 주석이라는 것이 없다. 양쪽 확장자가 다르면 같은 언어로 볼 근거가 없으니 역시 하지 않는다.

줄 구분은 코드가 있는 줄에만 남긴다. 그래야 주석 한 줄을 통째로 지운 경우도 같다고 보고, 반대로 코드를 다른 줄로 옮긴 것은 다르다고 본다.

큰 파일은 이 판단을 건너뛴다. 전체 비교에서 이미 쓰는 한도(8MB)를 그대로 따른다 - 폴더 하나에 수천 개가 있을 때 이 판단만으로 몇 분이 더 들면 안 된다.

곁들여 둘

  • 색 뜻풀이 줄을 목록 위에 붙였다. 색만 칠해 두면 무슨 뜻인지 아무도 모른다 - 파일 비교 화면의 "색 구분" 줄과 같은 성격이다.
  • 보기 필터에 코드가 다른 것만을 더했다. 주석만 다른 것을 빼고 보는 쪽이 실제로 필요한 동작이다.

확인: 주석만 고친 .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 를 두어 C 계열에서만 켠다. 자바·자바스크립트는 # 이 지시문이 아니다.

검사: LangTest 12건 추가(40 → 52건).

D-5. 덩어리 단위 반영

2-Way 는 이미 됐다(이 덩어리 반영 · Alt+Shift+→). 확인했다.

3-Way 가 잘못 동작하고 있었다. 덩어리를 BASE 에서 가져오고 있었는데, 3-Way 에서 BASE 는 공통 조상이다. 저쪽(REMOTE)을 가져오는 것이 Take Theirs 이고 덩어리 반영은 그것을 줄 묶음으로 하는 것인데, BASE 를 가져오면 조상으로 되돌리는 셈이라 뜻이 전혀 달라진다. 덩어리를 묶는 기준도 같이 바꿨다 - 2-Way 는 "BASE 를 가져올 수 있는 줄", 3-Way 는 "저쪽을 가져올 수 있는 줄"이 이어진 구간이다.

확인: 충돌 6개짜리 C++ 파일에서 다음 충돌 → 이 덩어리 반영 한 번에 네 줄이 REMOTE 값으로 바뀌고 Conflict 6 → 2, 머지 반영함 4. 단축키(Alt+Shift+→)도 같은 명령을 부르므로 함께 고쳐졌다.

C. 잔손질 — 완료 (2026-09-09)

C-1. 코드 위 툴팁 제거

코드 위에 마우스를 올리면 그 줄 내용이 그대로 툴팁으로 다시 떴다. 글이 이미 보이는데 같은 글이 상자로 겹쳐 뜨니 가리기만 한다. 원본 세 칸에서 뺐다. 버튼·상태 아이콘 툴팁은 그대로 둔다 - 그쪽은 화면에 없는 정보를 알려 준다.

C-2. 글자가 잘리던 곳

Git 탭의 목록들 — 커밋 제목이 ... 없이 글자 중간에서 뚝 끊겼다 ("A-5 다시: 폴더 고르는 창을 윈도우 폴더 선택 ‸"). 줄임표 설정은 이미 있었는데 듣지 않았다. 목록의 가로 스크롤이 켜져 있으면 항목이 제 글 길이만큼 넓게 잡히고, 그러면 잘릴 일이 없으니 줄임표도 나오지 않는다(칸 밖으로 나간 부분이 안 보일 뿐이다). 가로 스크롤을 끄면 항목이 칸 너비에 갇히고 그때 비로소 줄임표가 나온다. 커밋·변경 파일·브랜치·충돌·태그·서브모듈 여섯 목록에 같이 걸었다.

설정 화면의 항목 이름 — 이름 칸이 200 폭으로 고정이라 긴 이름이 잘렸다 ("공백 무시 단계 (0=전부 비교, ‸"). 무슨 설정인지 알 수 없어진다. 접히게 바꿨다. 플랫폼 변경이라 모든 소비 프로그램에 적용된다.

못 찾은 것: "작업 취소" 버튼과 Git 탭 입력 칸이 잘리는 것은 재현하지 못했다. 2560×1600 과 1920×1080 에서, 화면 배율 100%·120% 로 여섯 하위 탭을 모두 봤지만 그 자리는 멀쩡했다. 다시 보이면 그때 화면을 받아 고친다.

C-3. 폴더 복사 확인 창을 플랫폼 것으로

윈도우 기본 대화상자를 쓰고 있었다. 글꼴도 색도 배치도 혼자 달라 "이 프로그램이 묻는 것"으로 읽히지 않는다. 플랫폼이 이미 가지고 있는 MessageUtil.ShowYesNo 로 바꿨다.

묻는 것 자체는 없애지 않았다. 복사·삭제는 되돌릴 수 없다. 지시서에도 그렇게 적었다. 완료 알림은 전부터 토스트였고 그대로 둔다.

E. 추가 요청 세 가지 — 완료 (2026-09-09)

E-1. 원본 세 칸에 세로 스크롤 막대

가로(B-1)만 붙이고 세로가 빠져 있었다. 세 칸에 VerticalScrollBarVisibility="Visible" 을 켰다. 가로와 달리 세로는 이 칸들이 실제로 굴러가므로 따로 막대를 만들 필요 없이 ScrollViewer 가 주는 것을 그대로 쓴다. 대신 막대가 자리를 차지하는 만큼 여백 보정을 걷어냈다 - 전에는 편집기 막대 폭만큼 오른쪽 여백을 줘서 글자 폭을 맞췄는데, 이제 진짜 막대가 그 자리를 차지하므로 여백까지 주면 글자 폭이 좁아져 접히는 자리가 어긋난다.

E-2. 칸 폭을 줄였다 늘리면 행 높이가 돌아오지 않던 문제

화면을 좁혔다 되돌리면 접혔던 행이 넓은 자리를 그대로 차지한 채 남았다.

행 높이는 각 칸이 "내 글이 이만큼 높다"고 알려 온 값 중 가장 큰 것을 쓴다. 워드랩에서 한쪽만 접힌 행이 있으면 그쪽에 맞춰야 옆 칸과 줄이 맞물리기 때문이다. 그런데 글자를 그리는 칸이 세로로 늘어나 있어서(WPF 기본값 Stretch) 행이 한 번 커지면 그 다음부터는 "행 높이만큼 높다"고 되돌려 보냈다. 자기가 만든 값을 자기가 다시 읽는 셈이라 높이가 올라가기만 하고 내려오지 않았다.

글자 칸을 위로 붙여(VerticalAlignment="Top") 자기 글 높이만 알리도록 고쳤다.

E-3. 경로 줄과 코드 사이의 빈 공간

패널 머리 높이를 상수로 못 박아 두었다(124 / 74). 실제 내용보다 훨씬 커서 경로 줄 아래가 휑하게 비었고 그만큼 코드가 덜 보였다. 상수를 없애고 내용에 맞춰 줄어들게 한 뒤, 나란히 놓인 칸끼리는 SharedSizeGroup 으로 가장 큰 것에 맞추게 했다 (REMOTE 만 "Conflict N" 줄이 하나 더 있어서 그렇게 해야 첫 줄이 같은 눈높이에 온다). 2-Way 에서는 MERGED 머리도 같은 무리에 넣고, 3-Way 에서는 뺀다 - 3-Way 의 MERGED 는 아래 줄로 내려가 맞출 상대가 없는데, 넣어 두면 그쪽 경로 줄 때문에 위 세 칸까지 덩달아 커진다.

확인: 2-Way 에서 한 화면에 보이는 코드가 29줄에서 34줄로 늘었다. BASE·LOCAL 의 01행은 같은 높이 그대로다.

E-4. 2-Way 로 돌아오면 LOCAL 옆에 빈 칸이 남던 문제

3-Way 에서 구분선을 끌어 칸 폭을 바꾼 뒤 2-Way 로 돌아오면 LOCAL 오른쪽에 칸 하나만 한 빈 자리가 남았다. 구분선을 건드리지 않으면 멀쩡했다.

GridSplitter 는 끌고 나면 양옆 칸을 비율(star)이 아니라 고정 픽셀로 바꿔 놓는다. 그러면 두 가지가 어긋난다. 창 크기가 바뀌어도 칸이 따라 늘지 않고, 2-Way 로 돌아와 REMOTE 칸이 접히면서 생긴 자리를 아무도 채우지 않는다(늘어날 star 칸이 없으니까).

  • 구분선을 놓는 순간 고정 픽셀을 같은 비율의 star 로 되돌린다. 사용자가 잡아 놓은 폭은 그대로 유지되면서 창 크기를 따라가게 된다.
  • 2-Way ↔ 3-Way 를 오갈 때는 세 칸의 폭 지정을 아예 놓아준다. 배치가 통째로 다르기도 하고, 특히 REMOTE 칸은 폭을 Style 이 정하는데 끌어서 생긴 값이 남아 있으면 2-Way 에서 접히지 않는다.

확인: 3-Way 에서 구분선을 끌고 2-Way 로 돌아와도 두 칸이 폭을 꽉 채운다. 2-Way 에서 끌어 놓은 비율은 다른 모니터를 다녀와도 그대로다.

E-5. 반영한 줄이 규칙을 바꾸면 "직접 수정" 색이 되던 문제

줄을 반영(Accept)한 뒤 비교 규칙을 하나 건드리면 그 줄이 초록(머지 반영함)에서 보라(직접 수정)로 바뀌었다.

반영 표시가 되돌리기 스택에만 있었다. 규칙을 바꾸면 화면을 통째로 다시 만들면서 그 스택을 비운다. 그러면 표시가 사라지고 남는 단서는 "글이 원본과 다르다"뿐이라 직접 수정으로 칠해진다. B-3 에서 Take Mine 이 충돌을 못 지우던 것과 같은 뿌리다.

  • 반영할 때 줄 번호로도 남긴다. BASE 줄 번호, 없으면 MINE 줄 번호 - 둘 다 얼어 있는 값이라 다시 정렬해도 살아남는다.
  • 다시 만들 때 반영 여부를 먼저 본다. 반영한 줄은 직접 수정 조건에도 걸리므로 순서가 중요하다.
  • 되돌리면 그 기록도 함께 지운다.

곁들여 머지 주석 줄도 같은 이유로 보라색이 됐다. 주석 줄이라는 표시 역시 줄 객체에만 있어서 다시 만들면 사라진다. 글을 보고 알아보게 했다(MergeNote.LooksLikeNote). 사람이 우연히 같은 모양으로 쓴 주석을 잘못 볼 수 있지만, 그래도 색이 달라질 뿐이고 반대로 못 알아보면 반영할 때마다 "직접 수정"이 하나씩 늘어난다.

확인: 반영 → 공백 규칙 → Aa 규칙 순으로 눌러도 머지 반영함 2 · 직접 수정 0 그대로. NoteTest 13건 추가(16 → 29건).

E-6. 차이로 이동하면 코드가 바닥 글 위에 겹쳐 그려지던 문제

Alt+↑/↓ 로 차이를 옮겨 다니면 칸 아래 상태바 위에 코드가 겹쳐 그려졌다.

표시층은 편집기 글자와 눈높이를 맞추려고 RenderTransform 으로 살짝 밀어 둔다. 그런데 RenderTransform 은 배치가 아니라 그리기라 칸 밖으로 나가도 아무도 막지 않는다. 격자(Grid)의 행도 자기 칸 밖으로 나가는 자식을 자르지 않는다.

코드가 놓이는 자리를 자르는 상자로 감쌌다(ClipToBounds). 네 칸 모두.

확인: Alt+↓ 8회 · Alt+↑ 6회 눌러도 마지막 줄이 칸 경계에서 깨끗이 잘린다.

세 가지 모두 확인: 세로 막대를 끌면 세 칸이 함께 움직인다. 2560×1600 ↔ 1920×1080 을 오가도, 가운데 구분선을 끌었다 되돌려도 처음과 같은 화면이 된다. 테스트 399건 그대로 통과.

B. 3-Way 화면 — 완료 (2026-09-09)

B-1. 원본 세 칸에 가로 스크롤 막대가 없었다

원본 칸은 ScrollViewer 를 가로로 굴리지 않는다. 굴리면 줄 번호까지 화면 밖으로 밀려 나가고 세 칸이 서로 어긋난다. 대신 글자만 SourceShiftX 로 민다. 그래서 ScrollViewer 가 만들어 주는 막대는 움직이지 않는 막대가 되어 감춰 두고 있었고, 결과적으로 긴 줄이 있다는 사실 자체를 알 수 없었다(가로 휠을 굴려 봐야 알 수 있었다).

막대를 따로 놓고 SourceShiftX 와 서로 값을 맞추게 했다. 세 칸이 늘 같이 움직이므로 세 막대의 손잡이도 같은 자리에 있다. 워드랩을 켜면 밀 곳이 없으므로 사라진다.

확인: 손잡이를 끌면 세 칸이 함께 움직이고, 가로 휠을 굴리면 손잡이가 따라온다. 워드랩을 켜면 세 막대가 사라지고 끄면 돌아온다.

B-2. 워드랩을 켠 3-Way 에서 세로 스크롤이 아예 안 됐다

증상은 "행 수가 다르다"였는데, 재 보니 스크롤이 한 자리에 눌러앉아 있었다. 스크롤 값을 350 으로 바꿔도 곧바로 80 으로 되돌아왔다.

원인이 둘이었다.

하나 — 메아리를 막는 자물쇠가 너무 일찍 풀렸다. 편집기가 굴러가면 원본 칸을 따라 굴리는데, 목록(ListBox)의 스크롤 알림은 배치가 끝난 다음에 온다. 자물쇠를 try/finally 로 그 자리에서 풀어 버리니 그때는 이미 열려 있었고, 그 메아리가 다시 편집기를 굴렸다. 워드랩이 꺼져 있으면 환산이 1:1 이라 같은 값이 되돌아와 조용히 끝나지만, 켜면 조금씩 어긋나 서로 밀어내다 멈춘다. 자물쇠를 그 일로 밀려서 오는 알림이 다 지나간 뒤(DispatcherPriority.Input)에 풀도록 바꿨다.

둘 — 두 좌표계를 픽셀로 환산하고 있었다. 위 세 칸과 아래 MERGED 는 폭이 달라 접히는 횟수가 다르므로, 픽셀로 맞추려면 행마다 실제 높이를 알아야 한다. 그런데 목록은 가상화라 화면에 그려진 행만 자기 높이를 알려 준다. 아직 안 그려진 행은 "한 줄"로 잡혀 환산이 어긋났다. 재지 않고 세는 쪽으로 바꿨다 - 워드랩일 때는 원본 칸을 행 단위로 굴려 "몇 번째 행이 맨 위인가"만 주고받는다. 높이를 몰라도 정확히 맞는다.

곁들여, 편집기 화면 맨 위의 행을 알아낼 때 스크롤 값을 줄 높이로 나누던 것을 좌표로 물어보는 방식으로 바꿨다. 실제로 재 보니 스크롤 160 에서 편집기가 말하는 첫 줄은 11 인데 화면 맨 위에 있는 것은 9행이었다. TextBox 의 줄 번호 계산은 그려진 것과 맞지 않는다.

확인: 워드랩을 켠 3-Way 에서 네 칸이 같은 행을 맨 위에 놓는다(원본 09 / BASE 05 / REMOTE 05 = 같은 줄, MERGED 09). 워드랩을 끈 경우도 그대로다(16 / 12 / 12 / 16).

B-3. Take Mine 이 충돌을 정리하지 못했다

Take Theirs 는 되는데 Take Mine 을 눌러도 "남은 충돌" 이 줄지 않았다.

"내 것으로 간다"는 결정은 글을 바꾸지 않는다(이미 내 원본과 같은 줄이다). 그래서 되돌리기 스택에 남지 않는데, 편집 뒤 정리 단계(RefreshAppliedFlags)가 되돌리기 스택만 보고 반영 표시를 다시 칠하면서 방금 세운 표시를 지워 버렸다. 줄 번호로 따로 기억해 둔 결정(_resolvedBaseNumbers · _resolvedMineNumbers)을 같이 보도록 고쳤다. 같은 판단이 두 군데 흩어져 있던 것도 IsDecidedAsMine 하나로 합쳤다.

확인: 충돌 6개짜리 C++ 파일에서 Take Mine · Take Theirs 를 번갈아 눌러 6 → 5 → 4 → … → 충돌 정리 끝 까지 갔다.

검사: 전체 399건 그대로 통과.

A-5. 폴더 고르는 창에서 상위 폴더를 고를 수 없던 문제 — 완료 (2026-09-09)

처음에 요구를 잘못 읽었다. "최상위 단에서 봐야 하는데 가장 깊은 곳에서 폴더 열기밖에 안 된다"를 Git 저장소 이야기로 읽고 .git 찾아 올라가기를 만들었는데, 실제로는 폴더 비교 탭의 폴더 고르는 창 이야기였다. 상위 폴더를 골라 놓고 하위까지 한꺼번에 비교하고 싶은데 그 상위 폴더를 고를 방법이 없다는 것이었다.

진짜 원인: 폴더 고르기를 파일 열기 창으로 돌려쓰고 있었다(net48 WPF·WinForms 에는 쓸 만한 폴더 선택 창이 없다). 이 방식은 "지금 서 있는 폴더"를 돌려주기 때문에, 목록에 보이는 폴더를 고르려면 반드시 그 안으로 들어가야 한다. 그래서 늘 가장 깊은 폴더만 골라졌다. 게다가 윈도우가 기억해 둔 "이 프로그램이 마지막으로 본 폴더"에서 열려서 엉뚱한 곳(지난번 Git 저장소를 고르러 들어갔던 자리)에서 시작하기도 했다.

고친 것

  • 윈도우가 원래 가지고 있는 폴더 선택 창을 셸 COM(IFileDialog + FOS_PICKFOLDERS)으로 직접 부른다. 목록에서 폴더를 한 번 눌러 고를 수 있고, 주소 칸도 있고, 경로를 쳐 넣을 수도 있다. COM 이 안 되는 환경이 있을 수 있어 그때는 전에 쓰던 파일 열기 창으로 물러선다.
  • SetDefaultFolder 가 아니라 SetFolder 를 쓴다 — 전자는 윈도우가 기억한 폴더를 우선하기 때문에 지금 경로 칸에 적힌 폴더에서 열리지 않는다.
  • 네 자리(폴더 비교 왼쪽·오른쪽, Git 저장소 경로, 변경분 추출 대상)가 이 창 하나를 쓴다.

곁들여: Git 탭은 하위 폴더를 골랐더라도 위로 거슬러 올라가 .git 이 있는 최상위를 찾아 연다 (PathUtil.FindRepositoryRoot). 워크트리·서브모듈은 .git 이 파일이라 둘 다 보고, 안쪽에 저장소가 또 있으면 가장 가까운 것을 잡는다. 시작할 때의 자동 열기도 같은 규칙을 쓴다.

확인: 폴더 고르기 창을 열어 위로 한 번 올라간 뒤 목록의 right 폴더를 한 번 눌러 골랐고, 경로 칸에 그대로 들어왔다. 그 상태로 비교하니 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)

세 가지가 아니라 하나였다.

증상은 셋으로 보였다. 화면 이동을 눌렀는데 창이 최소화되거나, 잠금 창이 뜨거나, 파일 비교 화면에서는 옮겨졌다가 원래 모니터로 되돌아왔다. 셋 다 같은 자리에서 나온다.

스택을 찍어 보니 이렇게 흘렀다.

ListBox.OnMouseMove
  → ItemsControl.DoAutoScroll
    → ListBox.FocusItem
      → ListBox.MakeSingleSelection      ← 여기서 옆 항목이 선택된다
        → NavList_SelectionChanged        ← 그 항목의 동작이 실행된다

하단 탭 바는 ListBox 다. ListBox 는 누르는 순간 항목을 고르고 마우스를 잡는다 (끌어서 여러 개 고르기 위해서다). 손을 떼기 전에 창이 다른 모니터로 넘어가면 커서는 목록 밖에 남게 되고, ListBox 는 그것을 "끌어서 화면 밖으로 나갔다"고 보고 옆 항목을 대신 고른다. 하단 탭 바의 오른쪽 끝은 화면 잠금 · 화면 이동 · 바탕화면 · 종료 순으로 붙어 있어서, 옆 항목이 무엇이냐에 따라 잠금 창이 뜨거나(A-4) 최소화되거나(A-2) 방금 옮긴 창이 도로 넘어온다(A-3). 어느 쪽으로 튈지 그때그때 달랐던 이유가 이것이다.

전에 한 번 손을 댄 자리였는데, 그때는 실행을 DispatcherPriority.Background 로 미루는 방식이었다. 그것으로는 막히지 않는다 — 사람이 버튼을 누르고 있는 시간(보통 100ms 이상)이 그 지연보다 길기 때문이다. 누르고 있는 동안 창이 움직이면 똑같이 재현된다.

고친 것: 동작형 항목(화면 이동·바탕화면·화면 잠금·종료)은 탭이 아니라 버튼이다. 마우스로 누를 때는 ListBox 의 선택 경로를 아예 타지 않게 PreviewMouseLeftButtonDown 에서 끊고, 실행은 손을 뗄 때 한다. 창이 움직이는 순간에 눌린 버튼이 없으니 끌어서 스크롤이 일어날 여지가 없다. 누른 뒤 밖으로 끌고 나가 떼면 실행되지 않는 것도 버튼과 같은 규칙이다. 키보드로 항목을 옮겨 고르는 경로는 그대로 두었다.

확인: 두 모니터(2560×1600@150% ↔ 1920×1080@100%) 사이를 21회 연속 왕복. 버튼을 누르고 있는 시간을 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)이 그대로 해석하고, 슬래시가 섞이면 거부하며 예외를 던진다. 그 예외가 대입할 때가 아니라 ShowDialog() 안쪽에서 올라오기 때문에 대입 줄만 try 로 감싸 둔 곳(파일 비교 쪽)도 똑같이 죽게 돼 있었다. 오른쪽 폴더 칸은 비어 있어서 멀쩡했던 것이고, 사실상 "경로를 한 번이라도 채워 둔 채 다시 고르려 하면 죽는" 상태였다.

고친 것

  • Services/PathUtil.cs 신설. 셸에 넘길 경로는 전부 여기를 거친다. 구분자를 역슬래시로 맞추고, 상대 경로를 펴고, 확인할 수 없는 경로는 빈 문자열로 돌려준다. 빈 문자열이면 부르는 쪽이 InitialDirectory 를 아예 지정하지 않는다.
  • 폴더 고르기·파일 고르기 둘 다 ShowDialog() 를 감쌌다. 끊긴 네트워크 드라이브처럼 정규화로 걸러지지 않는 경우가 남기 때문에, 그럴 때는 시작 폴더 없이 한 번 더 연다. 폴더 고르기 자체를 못 쓰게 되는 것보다 낫다.
  • 플랫폼(UbisamBase.Shell/App.xaml.cs)에 전역 예외 그물을 놓았다. 화면 스레드에서 예외가 새어 나오면 WPF 는 프로그램을 그대로 끝내 버린다. 현장 장비에서 버튼 하나 잘못 눌렀다고 프로그램이 사라지면 무슨 일이 있었는지도 남지 않는다. 이제 로그에 남기고 알림창을 띄운 뒤 계속 돈다. 조용히 삼키지는 않는다 - 알리지 않으면 "눌렀는데 아무 일도 안 일어난다"가 되어 더 나쁘다. 이 저장소만이 아니라 플랫폼을 쓰는 모든 프로그램에 적용된다.

검사: PathTest 31건 신설(슬래시 섞인 경로 · 따옴표 · 못 쓰는 글자 · 없는 드라이브 · 아주 긴 경로 · 드라이브 뿌리). 전체 356 → 387건.


3~4주차로 미룬 것

디테일이라 지금 붙잡을 일이 아니다.

  • 파일·폴더 드래그&드롭, 최근 파일 목록
  • 로그인·권한
  • 비교 결과 보고서 내보내기
  • 대용량 파일 성능 (수만 줄) — 실제 소스 크기를 확인한 뒤 판단

완료 기록

날짜 항목 커밋
2026-09-08 저장소 개설 (MergeHub · UbisamPlatform 각각) 7719094 / 36ee902
2026-09-08 ④ Git 조작 5가지 — amend · stash 목록 · 커밋 간 비교 · blame · 변경분 추출 9956567
2026-09-08 ③ 3-Way — 자동 병합 · 충돌만 이동 · 병합 완료 판정 85c5e9e
2026-09-08 ① 폴더 비교 — 폴더째 복사·삭제 · 보기 필터 · 비교 취소 524a6a7
2026-09-08 ② 파일 비교 — 바꾸기 · 덩어리 반영 · 저장 형식 · 이진/그림 비교 de1a43c 55d9deb
2026-09-08 ⑤ 밖에서 부르기 — 명령줄 인자 · 탐색기 우클릭 아래