왼쪽 줄 번호가 밀리던 것 - 왼쪽도 다시 번호를 맨다

사용자 보고. 12차 B-1 로 왼쪽에도 반영할 수 있게 되면서 왼쪽 줄이 늘고 주는데,
번호는 정렬기가 처음 준 것 그대로였다. 오른쪽에만 있던 줄을 왼쪽에 넣으면
그 줄은 번호가 비고 뒤 줄은 옛 번호를 달아 01 02 (빈) 03 04 05 (빈) 06 으로 보였다.
왼쪽 파일은 8줄이 됐는데 번호는 6까지라 어느 줄을 고쳤는지 짚을 수 없었다.

MergeLine.BaseNumber 의 설명에는 이미 "이제 두 파일 비교에서는 번호도 따라 바뀐다" 고
적혀 있었지만 실제로 다시 매기는 자리가 없었다 - 적어 둔 뜻과 코드가 어긋나 있었다.

- RenumberBase 를 RenumberMerged(8차 B-3) 옆에 두고, 그릴 때마다 왼쪽에 있는 줄만 1부터 다시 맨다
- 두 파일 비교(BothWays)에서만. Git 모드 · 네 칸의 BASE 는 지난 커밋 · 공통 조상이라
  고칠 수 없고, 그때 번호는 그 파일의 번호이므로 건드리지 않는다
- "머지 반영함" 표시는 BASE 줄 번호를 열쇠로 붙들고 있다(_appliedBaseNumbers) -
  번호가 실제로 달라질 때만 그 열쇠도 옛 번호에서 새 번호로 옮겨 담는다.
  안 그러면 규칙을 한 번 건드리는 순간 엉뚱한 줄이 반영함으로 칠해진다

화면으로 확인: 연 직후 01..07, 두 줄 넣으면 01..09 로 이어짐, 되돌려도 이어짐,
왼쪽 저장하면 파일 9줄 = 칸의 끝 번호 09, Git 모드 회귀 그대로.
하네스 27벌 1,787건 · 실패 0. 빌드 경고 0 · 오류 0.

아직 확정하지 못한 것: 왼쪽에만 있는 줄을 [<] 로 뺄 때 왼쪽 줄 수가 하나 주는지.
따로 열어 한 번 누르면 잘 빠지는데(3줄 -> 2줄), 다른 반영을 몇 번 한 뒤에는
시험 도구가 읽은 줄 칸이 낡아 옆 줄 단추를 눌러 값이 흔들렸다.
번호 불변식(늘 1부터 이어지고 빠진 번호가 없다)은 어느 경우에도 지켜졌다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
김승준 2026-09-28 13:58:46 +09:00
parent 13988d1f0a
commit 62126e5e0e
4 changed files with 121 additions and 0 deletions

View File

@ -285,6 +285,61 @@ BASE 칸 자신은 견줄 것이 없어 `4줄 · 기준` 이라고만 적는다.
실제로 켜서 잰 하단 탭 줄이 **111px · 항목 109px**(배율 150% 기준 72.7 DIP)로 "후" 값과 맞았다.
앞서 적었던 "지금 배포 폴더에는 예전 것이 그대로" 는 그 뒤 빌드로 낡은 글이 되어 고쳤다.
## 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, 완료)
### 먼저 재현 (화면, 값)

View File

@ -8,6 +8,7 @@
| 날짜 | 항목 | 커밋 |
|---|---|---|
| 2026-09-28 | **왼쪽 줄 번호가 밀리던 것**(사용자 보고) — 12차 B-1 로 왼쪽에도 반영할 수 있게 되면서 왼쪽 줄이 늘고 주는데, 번호는 정렬기가 처음 준 것 그대로였다. 오른쪽에만 있던 줄을 왼쪽에 넣으면 그 줄은 번호가 비고 뒤 줄은 옛 번호를 달아 `01 02 · 03 04 05 · 06` 으로 보였다 — 왼쪽 파일은 8줄인데 번호는 6까지라 어느 줄을 고쳤는지 짚을 수 없었다. `RenumberBase` 를 `RenumberMerged`(8차 B-3) 옆에 두고 두 파일 비교에서만 왼쪽 줄을 1부터 다시 맨다. Git 모드 · 네 칸의 BASE 는 고칠 수 없는 쪽이라 건드리지 않는다. "머지 반영함" 표시가 BASE 줄 번호를 열쇠로 붙들고 있어 번호가 실제로 달라질 때만 그 열쇠도 옛 번호 → 새 번호로 옮긴다. 화면으로 넣기 · 빼기 · 되돌리기 · 저장 · Git 회귀 확인, 하네스 27벌 1,787건 · 실패 0. **왼쪽에만 있는 줄을 뺄 때 줄 수가 주는지는 값으로 확정하지 못했다**(시험 도구가 낡은 줄 칸을 집는 한계) | 아래 |
| 2026-09-28 | **12차 E-2** — 파일 비교가 짝 여러 개를 동시에 든다(문서 탭). 지금까지는 한 짝만 들어서 짝을 바꾸면 "저장하지 않은 편집이 있습니다" 를 묻고 버리면 고친 글이 사라졌다(재현: 16줄 → 15줄). 이제 떠 있던 탭이 그 글을 맡아 두므로 **묻지 않는다**. 탭마다 `HomeViewModel` 을 따로 만들지 않고(GitWorkspace 의 한 자리짜리 약속 때문) 화면은 하나로 두고 탭이 **파일에 없는 것만** 맡는다 — 고친 글 · 왼쪽 글(B-1) · 되돌리기 이력 · 두 기준점 · 캐럿 · 스크롤 · 저장 형식. 탭 이름은 파일 이름(다르면 `왼 ↔ 오`, 겹치면 위 폴더를 갈릴 때까지 덧붙임), 저장 안 한 탭은 `•`. 탭 줄은 둘 이상일 때만 보인다. `◀ 이전 · 다음 ▶` 은 지금 탭의 짝만 바꾸고(탭이 늘지 않음), 폴더 비교에서 두 번 누르면 새 탭(사용자 결정). Ctrl+Tab · Ctrl+Shift+Tab · Ctrl+W · 가운데 단추 닫기. 탭이 사라져도 고친 글은 **보관함**(짝 50개)에 남아 다시 열면 되살아난다 — E-1 은 이 보관함을 파일로 옮겨 적으면 된다. 원본이 그 사이 바뀌면 되살리고 알린다("연 시점"은 새로 읽은 글로 둔다). 탭 줄은 ItemsControl 이 아니라 ListBox 다(ItemsControl 은 UIA 에 컨트롤로 안 보여 화면 시험이 못 읽는다). 켤 때 저절로 열리는 짝만 탭이 없어 편집이 사라지던 것을 화면에서 찾아 `EnsureTabForCurrentPair` 로 고쳤다. 켜 놓고 눈으로 보다 두 가지를 더 고쳤다(사용자가 고름) — 중괄호 맞춤으로만 바뀐 것에는 점을 붙이지 않고(묻는 자리와 같은 기준), 탭 이름은 양쪽이 같은 부분을 한 번만 적는다(`WeldController_기준.cs ↔ WeldController_내작...` → `WeldController_기준 ↔ 내작업.cs`, 421 → 288px). 뒤쪽은 확장자일 때만 접는다 — 글자 단위로 세면 `a-lef ↔ right.cs` 처럼 낱말 가운데서 잘린다(하네스에서 걸렸다). 하네스 `DocTabTest` 106건, 화면 34건 · 회귀 19건 통과, 전체 27벌 1,787건 · 실패 0. **커밋 안 함**(사용자 지시) | 아래 |
| 2026-09-28 | **시험 도구를 런처에 맞춤** — 새 플랫폼은 `UbisamBase.Launcher.exe` 가 셸을 같은 프로세스에 올려 창 주인 프로세스 이름이 바뀌었고, 예전 이름만 찾던 화면 시험 도구가 전부 "창이 안 뜬다" 로 멈춰 있었다. `a2lib.ps1` 에 `Shell-Procs` · `Shell-Proc` · `Stop-App` 을 두고 `Start-App` · `Get-Win` 이 그것을 쓰게 했다(런처가 제목 없는 진행 창을 먼저 띄우는 경우를 대비해 제목 있는 창을 먼저 고름). 스크립트 76개 · 156곳을 함께 고침. 문법 187개 통과, 끄기 → 켜기 → 창 찾기 → 네 탭 → 단추 → 끄기까지 실제로 돌았고 설정 · 창 자리 네 파일 해시 그대로 되돌림. **제품 코드 변경 없음** | 없음 |
| 2026-09-28 | **12차 C-2** — 플랫폼 하단 탭 줄을 한 단계 작게(항목 74 → 61 DIP: 여백 13/14→9/10 · 아이콘 22→20 · 아이콘 아래 틈 7→5 · 글자 13.5→12.5). 실측 133 → 109px(−18%)이고 그만큼 콘텐츠가 늘어 파일 비교 코드 칸이 47 → 48줄. 라벨 잘림 · 선택 밑줄 · 탭 접기 띠 · 오른쪽 동작 항목 모두 그대로. 플랫폼 쪽이라 모든 사내 프로그램에 함께 간다. 처음에는 배포본을 백업해 잠깐 바꿔치기해 시험하고 되돌렸으나, 그 뒤 **새 플랫폼을 빌드하면서 함께 배포됐다** — 2026-09-28 배포본 `UbisamBase.Core.dll` 해시 일치 · 화면 실측 111px(항목 109px)로 확인. **커밋 안 함**(사용자 지시) | 없음 |

View File

@ -426,6 +426,13 @@
| **완료** | E-2 여러 짝을 동시에(문서 탭) — 파일 비교 맨 위에 문서 탭 줄(둘 이상일 때만 보임). 탭 이름은 파일 이름 · 다르면 `왼 ↔ 오` · 겹치면 위 폴더를 덧붙임 · 저장 안 한 탭은 `•`. 짝을 바꿀 때 **묻지 않는다**(탭이 편집을 맡는다). 탭이 사라져도 고친 글은 보관함에 남아 다시 열면 되살아남(◀ ▶ 로 훑다 돌아와도). Ctrl+Tab · Ctrl+Shift+Tab · Ctrl+W · 가운데 단추 닫기. 탭마다 되돌리기 · 캐럿 · 스크롤 · 저장 형식. 상한 20(고친 탭은 안 닫음). 탭 이름은 **양쪽이 같은 부분을 한 번만** 적는다(`WeldController_기준 ↔ 내작업.cs` — 421 → 288px). 점은 **사람이 손댄 것만**(스타일 맞춤으로만 바뀐 것은 빼고 센다 — 묻는 자리와 같은 기준). 하네스 `DocTabTest` 106건, 화면 34건 · 회귀 19건 확인 |
| 할 일 | E-1 꺼도 남는 편집(notepad++ 식) — E-2 의 보관함을 `{설정 폴더}\Session\` 에 옮겨 적고 켤 때 되살린다 |
### 쓰다 나온 것
| 상태 | 항목 |
|---|---|
| **완료** | 왼쪽 줄 번호가 밀린다(사용자 보고) — 12차 B-1 로 왼쪽에도 반영할 수 있게 됐는데 왼쪽 번호를 다시 매기는 자리가 없었다. 줄을 넣으면 그 줄은 번호가 비고 뒤 줄은 옛 번호(`01 02 · 03 04 05 · 06`), 저장하면 8줄인데 번호는 6까지. `RenumberBase` 를 두어 두 파일 비교에서만 왼쪽 줄을 1부터 다시 맨다. "머지 반영함" 표시가 BASE 번호를 열쇠로 쓰므로 그 열쇠도 함께 옮긴다. Git · 네 칸은 손대지 않는다 |
| 볼 것 | 왼쪽에만 있는 줄을 `[<]` 로 뺄 때 줄 수가 실제로 주는지 — 따로 열면 되는데 다른 반영 뒤에는 시험 도구가 낡은 줄 칸을 집어 값이 흔들렸다. 번호 불변식은 지켜짐 |
### 시험 도구
| 상태 | 항목 |

View File

@ -4104,6 +4104,7 @@ namespace MergeHub.ViewModels
{
// 줄을 고르거나 머지 주석이 끼어들면 결과 줄 수가 바뀐다. 그릴 때마다 번호를 다시 매긴다(8차 B-3).
RenumberMerged(Lines);
RenumberBase(Lines); // 왼쪽도 마찬가지다(12차 B-1 로 왼쪽이 바뀔 수 있게 됐다)
var total = Lines.Count;
var diff = Lines.Count(l => l.IsDifferent);
@ -4628,6 +4629,63 @@ namespace MergeHub.ViewModels
for (var i = 0; i < rows.Count; i++) rows[i].LocalNumber = numbers[i];
}
/// <summary>
/// 왼쪽(BASE) 칸 번호를 <b>왼쪽 파일 번호</b>로 다시 매긴다(12차 B-1 을 쓰다 나온 것, 2026-09-28).
///
/// <b>무엇이 잘못됐었나</b> - 12차 B-1 로 왼쪽에도 반영할 수 있게 되면서 왼쪽 줄이 늘고 줄게 됐는데,
/// 번호는 정렬기가 처음 준 것 그대로였다. 오른쪽에만 있던 줄을 왼쪽에 넣으면 그 줄은 <b>번호가 비고</b>
/// 뒤 줄은 옛 번호를 그대로 달고 있어, 왼쪽 파일이 8줄인데 칸에는 <c>01 02 (빈) 03 04 05 (빈) 06</c> 으로 보였다.
/// 저장하면 8줄이 되는데 번호는 6까지만 있으니 어느 줄을 고쳤는지 짚을 수가 없다.
///
/// <para>
/// Git 모드 · 네 칸(3-Way)의 BASE 는 지난 커밋 · 공통 조상이라 고칠 수 없다 - 그때 번호는 그 파일의 번호이므로
/// 건드리지 않는다. 두 파일 비교(<see cref="BothWays"/>)에서만 다시 맨다.
/// </para>
///
/// <para>
/// "머지 반영함" 표시는 <b>BASE 줄 번호를 열쇠로</b> 붙들고 있다(<see cref="_appliedBaseNumbers"/>) -
/// 번호를 바꾸면 그 열쇠도 같은 줄을 가리키도록 함께 옮긴다. 안 그러면 규칙을 한 번 건드리는 순간
/// 엉뚱한 줄이 "반영함"으로 칠해진다.
/// </para>
/// </summary>
private void RenumberBase(IList<MergeLine> rows)
{
if (!BothWays || rows is null || rows.Count == 0) return;
var inLeft = new bool[rows.Count];
for (var i = 0; i < rows.Count; i++) inLeft[i] = rows[i].HasBase;
var numbers = MergedNumbering.Assign(inLeft);
// 바뀐 것이 없으면(연 직후가 그렇다) 표시도 건드리지 않는다.
var changed = false;
for (var i = 0; i < rows.Count; i++)
{
if (rows[i].BaseNumber == numbers[i]) continue;
changed = true;
break;
}
if (!changed) return;
var moved = _appliedBaseNumbers.Count > 0 ? new HashSet<int>() : null;
for (var i = 0; i < rows.Count; i++)
{
if (moved is not null && rows[i].BaseNumber is int was && numbers[i] is int now &&
_appliedBaseNumbers.Contains(was))
{
moved.Add(now);
}
rows[i].BaseNumber = numbers[i];
}
if (moved is null) return;
_appliedBaseNumbers.Clear();
foreach (var number in moved) _appliedBaseNumbers.Add(number);
}
private string BuildLocalText() => PaneText.Build(Lines, MergeLine.TextPane.Local);
}
}