실제 소스에 XAML 이 400개 42,477줄인데 여태 주석과 따옴표만 알아봐서 사실상 평문이었다. 마크업 전용 토크나이저를 따로 뒀다. 기존 토크나이저에 못 얹은 까닭은 규칙이 다르기 때문이다. 그쪽은 낱말을 읽어 예약어 표와 맞춰 보는데, 마크업은 같은 Content 가 꺾쇠 안에서는 속성 이름이고 밖에서는 그냥 글자다. 낱말이 무엇이냐가 아니라 어디에 있느냐로 갈린다. 줄 넘김 상태를 bool 하나에서 깃발 묶음(CarryOver)으로 넓혔다. XAML 은 여는 태그가 여러 줄에 걸치는 것이 보통이라 - 시험에 쓴 실제 파일은 첫 태그 하나가 14줄이다 - "지금 꺾쇠 안이다"를 안 넘기면 이어지는 줄의 속성이 그냥 글자로 보인다. 부르는 자리 다섯을 모두 바꿨다. 색은 새로 만들지 않고 이미 있는 갈래에 얹었다. 태그는 예약어 색, 속성 이름은 메서드 색, 값은 문자열 색, 앞가지는 타입 색이다. 태그 밖의 본문은 일부러 안 칠한다. 붙이는 중에 파일 비교 화면이 통째로 비어서 뜨는 일이 있었다. 의존 속성 타입을 bool 에서 CarryOver 로 바꾸면서 기본값 false 를 같이 안 고쳤다. 빌드도 통과하고 로그에도 안 남는다. 같은 주의를 코드 옆에 적어 뒀다. 실제 저장소 XAML 사본(638줄)으로 화면 확인. C# 색칠은 그대로. MarkupTest 65건. 전체 688건 통과(하네스 20벌). 경고 0 · 오류 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
252 lines
14 KiB
Markdown
252 lines
14 KiB
Markdown
# 5차 — 실제 소스에 맞추기 (2026-09-11 ~)
|
|
|
|
지시서: [작업지시/5차.md](../작업지시/5차.md)
|
|
|
|
---
|
|
|
|
## A-1. CP949 파일 67개를 잘못 읽으면 한글이 깨진다 — **완료** (2026-09-11)
|
|
|
|
지시서가 **먼저 확인만 하고 틀리면 고치라**고 했다. 확인부터 했다.
|
|
|
|
### 잰 것 — 실제 저장소 2,091개 전부
|
|
|
|
`TextFile.Read` 로 실제 파일을 한 개씩 읽어 보는 도구를 만들어 돌렸다(원본은 읽기만 하고,
|
|
저장 왕복은 사본에만 했다).
|
|
|
|
| 실제 인코딩 | 개수 |
|
|
|---|---|
|
|
| UTF-8 (BOM) | 1,441 |
|
|
| UTF-8 (BOM 없음) | 353 |
|
|
| **CP949** | **67** |
|
|
| ASCII 만 | 230 |
|
|
|
|
| MergeHub 로 읽었을 때 | 결과 |
|
|
|---|---|
|
|
| 한글이 깨진 파일 | **0개** |
|
|
| 사본에 그대로 저장했을 때 바이트가 달라진 파일 (한글 파일 1,861개) | **0개** |
|
|
|
|
**판정 기준을 이미 만족하고 있었다.** 판별 규칙(BOM → 엄격한 UTF-8 → 실패하면 시스템 ANSI)이
|
|
실제 소스에서 맞게 돈다. 고칠 것이 없었다.
|
|
|
|
### 그래도 고친 것 둘
|
|
|
|
**① 판별 코드가 두 군데에 복사돼 있었다.**
|
|
`TextFile.Detect` 와 `GitService.DetectEncoding` 이 글자 하나까지 같은 코드였다.
|
|
`TextFile` 주석에는 "판별 규칙은 GitService 와 같다 - 같은 파일을 어느 경로로 열든 결과가 같아야 한다"
|
|
고 적혀 있었는데, **지키는 방법이 복사뿐이었다.** 한쪽만 고치면 조용히 어긋나고,
|
|
그러면 <b>같은 파일이 워킹트리에서는 멀쩡한데 커밋에서 열면 깨져</b> 보인다.
|
|
어느 쪽이 맞는지 알 방법도 없다. `TextFile.DetectEncoding` 하나로 모으고 `GitService` 는 그것을 부른다.
|
|
|
|
**② 같은 인코딩을 두 이름으로 부르고 있었다.**
|
|
상태바는 `KS_C_5601-1987`(코드페이지 949 의 공식 이름), 저장 형식 목록은 `CP949`.
|
|
실제 CP949 파일을 열어 보다가 눈에 띄었다 - "지금 이 파일이 그 CP949 라는 건가" 를 되묻게 된다.
|
|
`FriendlyName` 으로 949 는 언제나 `CP949` 라고 적는다. 두 자리 모두 고쳤다.
|
|
|
|
### 확인한 것
|
|
|
|
| 확인 | 결과 |
|
|
|---|---|
|
|
| 실제 CP949 파일(`AXA.cs` 사본)을 열었을 때 | `//========== 보드 및 모듈 정보 확인 함수 ...` 그대로 읽힌다 |
|
|
| 깨진 글자(U+FFFD) | 없다 |
|
|
| 상태바 인코딩 | `CP949` (전에는 `KS_C_5601-1987`) |
|
|
| 고친 뒤 실제 저장소 2,091개 재확인 | 깨짐 0개 · 왕복 어긋남 0개 (고치기 전과 같다) |
|
|
|
|
**검사를 새로 만들었다**(`EncTest` 16건). 실제 저장소가 없어도 도는 것이 요점이다 -
|
|
BOM · BOM 없는 UTF-8 · CP949 · ASCII 를 만들어 놓고 읽기 · 왕복 저장 · 형식 바꿔 저장까지 본다.
|
|
규칙을 여기 못박아 두면 다음에 누가 고쳐도 바로 걸린다.
|
|
|
|
전체 **587건 통과**(하네스 18벌) · 빌드 경고 0 · 오류 0.
|
|
**실제 저장소는 건드리지 않았다** - 작업 전후 모두 변경 1건(7월부터 있던 것)으로 같다.
|
|
|
|
---
|
|
|
|
## A-2. 커밋·체크아웃 뒤에 저장 안 한 편집이 조용히 사라진다 (2026-09-11, 완료)
|
|
|
|
### 어디서 사라지고 있었나
|
|
|
|
4차 A-1 에서 편집이 말없이 사라지던 길 여덟 군데를 막았지만, **Git 탭은 그대로 두었다.**
|
|
`RefreshCommits` → `LoadCommitAsync` 는 프로그램이 목록을 다시 세우는 한가운데라
|
|
거기서 확인 창을 띄우면 무엇에 답하는 창인지 알 수 없고, 취소해도 목록은 이미 새로 세워진 뒤였다.
|
|
|
|
그래서 **막는 자리를 옮겼다.** 목록을 다시 세우는 자리가 아니라 **Git 작업을 시작하는 자리**에서 묻는다.
|
|
그 순간에는 아직 아무것도 바뀌지 않았으므로 취소가 뜻을 가진다.
|
|
|
|
`GitWorkspace.RunAndRefreshAsync` 는 Git 탭의 작업 서른 가지가 모두 지나가는 문이다.
|
|
여기에 `ConfirmDiscardEdits` 갈고리를 달고, 파일 비교 화면이 그 갈고리를 채운다.
|
|
|
|
```csharp
|
|
public async Task<bool> RunAndRefreshAsync(string operation, Action action)
|
|
{
|
|
if (GitOperationKinds.TouchesFiles(operation) && ConfirmDiscardEdits is { } ask && !ask(operation)) return false;
|
|
var ok = await RunAsync(operation, action);
|
|
if (ok) await RefreshAsync();
|
|
return ok;
|
|
}
|
|
```
|
|
|
|
### 처음 고른 답이 틀렸다 — 가려서 묻기
|
|
|
|
처음에는 **작업을 가려서** 물으려 했다. fetch · 태그 생성 · 전체 스테이징처럼
|
|
**파일 내용을 안 건드리는 열두 가지**는 묻지 않는 쪽이 낫다고 봤다.
|
|
쓸데없이 뜨는 창만큼 사람을 지치게 하는 것이 없기 때문이다.
|
|
|
|
첫 시도는 호출 자리마다 `touchesFiles: false` 를 붙이는 것이었는데,
|
|
줄을 훑어 고치는 스크립트가 **엉뚱한 닫는 괄호**(예: `throw new InvalidOperationException(...)`)에 붙였다.
|
|
되돌리고, 판단을 **이름표 하나**(`GitOperationKinds`)로 모았다. 부르는 자리는 손대지 않는다.
|
|
|
|
그런데 **화면에서 해 보니 안 묻고 지나간 뒤에도 편집이 사라졌다.**
|
|
파일은 그대로인데도 저장소 상태를 다시 읽으면서 본문을 새로 그리기 때문이다.
|
|
|
|
다시 그리지 않게 막는 쪽으로도 해 봤다.
|
|
|
|
```csharp
|
|
if (HasChanges && !threeWay && string.Equals(localText, _originalLocalText, StringComparison.Ordinal))
|
|
return;
|
|
```
|
|
|
|
**이것도 안 들었다.** 다시 읽는 김에 고른 파일까지 바뀌는 경우가 있어
|
|
"안 바뀌었다" 를 정확히 가릴 조건을 세울 수가 없었다. 이 코드는 지웠다.
|
|
|
|
### 골라잡은 답 — 전부 묻는다
|
|
|
|
**잃지 않는 쪽으로 갔다.** `TouchesFiles` 는 이제 언제나 `true` 다.
|
|
|
|
- 확인 창은 **고치던 것이 있을 때만** 뜬다. 평소에는 한 번도 보이지 않는다.
|
|
- 틀리더라도 **한 번 더 묻는 것**으로 끝나지, 작업을 잃지는 않는다.
|
|
가려서 묻다 틀리면 사람이 고친 것을 잃는다. 두 실수의 값이 다르다.
|
|
|
|
다만 **무엇이 안전한지 알아낸 것은 버리지 않았다.** 열두 가지 이름을
|
|
`GitOperationKinds.KnownSafeCandidates` 에 남겨 두고, 왜 지금 쓰지 않는지를 클래스 주석에 적었다.
|
|
"다시 그려도 편집이 살아남는" 구조가 되면 거기서부터 다시 시작한다.
|
|
|
|
### 덤으로 잡은 것 — 4차 A-1 이 새던 자리 셋
|
|
|
|
전부 묻게 만들고 나니 **Git 탭을 그냥 열기만 해도 확인 창이 떴다.**
|
|
사람이 한 일이 아닌데 4차 A-1 의 확인 창이 불려 나오고 있었다. 셋을 잡았다.
|
|
|
|
| 새던 자리 | 왜 떴나 | 어떻게 막았나 |
|
|
|---|---|---|
|
|
| `OnSelectedNodeChanged` | 커밋 목록을 다시 세우면 잠깐 `null` 이 밀려 들어온다 | `value is not null && !sameCommit` 일 때만 묻는다 |
|
|
| `OnRemoteBranchChanged` | `RefreshRemoteBranches` 가 목록을 갈아 끼우며 값을 바꾼다 | `_syncingBranches` 동안은 묻지 않는다 |
|
|
| `OnSelectedFileChanged` | `Files.Clear()` 가 가드 밖에 있었다 | `_suspendGuard` 안으로 옮겼다 |
|
|
|
|
**목록을 다시 세우면 화면이 값을 되밀어 넣는다.** 그것은 사람이 고른 것이 아니다.
|
|
4차 A-1 을 넣을 때 놓친 것인데, A-2 를 화면에서 시험하다 드러났다.
|
|
|
|
### 확인한 것 (실제 화면, 데모 저장소)
|
|
|
|
| 확인 | 결과 |
|
|
|---|---|
|
|
| LOCAL 을 고친 줄이 들어간다 | OK |
|
|
| 전체 스테이징이 묻는다 | OK |
|
|
| 취소하면 스테이징도 안 한다 (`git diff --cached` 비었다) | OK |
|
|
| 취소하면 고친 줄이 그대로다 | OK |
|
|
| 체크아웃이 묻는다 | OK |
|
|
| 취소하면 브랜치가 그대로다 (`master`) | OK |
|
|
| 체크아웃을 취소해도 고친 줄이 그대로다 | OK |
|
|
| "저장하고 계속" 을 고르면 고친 내용이 파일에 남는다 | OK |
|
|
| 고친 것이 없으면 안 묻는다 | OK |
|
|
|
|
**열 가지 전부 통과.** 화면 시험은 UI Automation 으로 몰았다(`a2screen.ps1`).
|
|
확인 창이 **따로 뜨는 창이 아니라 본 창 안에 덮어 그리는 판**이라, 바깥 창 목록을 뒤지면 못 찾는다.
|
|
처음 두 번은 이것 때문에 "안 묻는다" 고 잘못 읽었다 — **시험 도구가 틀려서 제품을 의심했다.**
|
|
|
|
검사를 `GitOpTest` 36건으로 고쳤다. 전체 **623건 통과**(하네스 19벌) · 빌드 경고 0 · 오류 0.
|
|
**실제 저장소는 건드리지 않았다** — 데모 저장소(`mergehub-demo`) 사본으로만 했다.
|
|
|
|
|
|
---
|
|
|
|
## B-1. XAML 색칠이 사실상 없다 (2026-09-11, 완료)
|
|
|
|
### 무엇이 문제였나
|
|
|
|
실제 소스에 XAML 이 400개 · 42,477줄이다. C# 다음으로 많다.
|
|
그런데 지금까지 XAML 은 `LanguageProfile.Markup` 으로 열렸고, 그것이 아는 것은 **주석과 따옴표뿐**이었다.
|
|
태그 이름 · 속성 이름 · 속성 값이 전부 같은 색이라 사실상 평문이었다.
|
|
|
|
### 왜 기존 토크나이저에 못 얹었나
|
|
|
|
`CodeTokenizer` 는 **낱말을 읽어 예약어 표와 맞춰 보는** 방식이다. 마크업은 규칙이 다르다.
|
|
같은 `Content` 가 <b>꺾쇠 안에서는 속성 이름이고 밖에서는 그냥 글자</b>다.
|
|
낱말이 무엇이냐가 아니라 **어디에 있느냐**로 갈린다. 그래서 마크업만 따로 뒀다(`MarkupTokenizer`).
|
|
|
|
### 줄을 넘어가는 상태를 넓혀야 했다
|
|
|
|
토크나이저는 한 줄씩 따로 훑는다. 앞 줄에서 넘어오는 것은 여태 `bool inBlockComment` 하나였다.
|
|
그런데 XAML 은 **여는 태그가 여러 줄에 걸치는 것이 예외가 아니라 보통**이다.
|
|
시험에 쓴 실제 파일(`UCManualRCCOperation.xaml`)은 **첫 태그 하나가 14줄**이다.
|
|
|
|
```xml
|
|
<Button Grid.Row="1"
|
|
Content="비교"
|
|
Click="OnCompare" />
|
|
```
|
|
|
|
둘째·셋째 줄은 `<` 로 시작하지 않는다. "지금 꺾쇠 안이다" 를 넘겨주지 않으면
|
|
**속성 이름이 그냥 글자로 보인다** - XAML 은 이런 줄이 대부분이라 색칠이 없는 것과 같다.
|
|
|
|
그래서 `bool` 을 깃발 묶음(`CarryOver`)으로 넓혔다. `None` · `BlockComment` · `Tag` 셋이다.
|
|
부르는 자리 다섯(검사기 · 폴더 비교 · 화면 VM · 화면 컨트롤 · 행 모델)을 모두 바꿨다.
|
|
|
|
### 무엇을 무슨 색으로 보는가
|
|
|
|
| 무엇 | 색 | 까닭 |
|
|
|---|---|---|
|
|
| 태그 이름 | 예약어 (파랑) | 구조를 이루는 것이라 가장 먼저 눈에 들어와야 한다 |
|
|
| 속성 이름 | 메서드 (황토/연노랑) | 태그와 값 사이에서 따로 보여야 한다 |
|
|
| 속성 값 | 문자열 (빨강/주황) | 다른 언어의 문자열과 같은 색 - 같은 것이다 |
|
|
| 앞가지(`x:` · `local:`) | 타입 예약어 (청록) | XAML 에서 "어느 네임스페이스냐"는 정보다. 이름에 묻으면 `x:Name` 과 `Name` 이 같아 보인다 |
|
|
| `< > / :` | 구두점 (회색) | 뒤로 물러나야 한다 |
|
|
| `=` | 연산자 (회색) | 위와 같다 |
|
|
| `<?xml ?>` · `<!DOCTYPE>` · `<![CDATA[` | 지시문 (회색) | 파일마다 한두 줄이고 고칠 일이 없다. 안을 더 쪼개지 않는다 |
|
|
| 태그 밖의 본문 | **칠하지 않는다** | 물려받은 색 그대로 두어야 본문이 눈에 띈다 |
|
|
|
|
색을 새로 만들지 않고 **이미 있는 갈래에 얹었다.** 팔레트를 늘리면 밝은/어두운 두 벌을 다 손봐야 하고,
|
|
같은 화면에서 C# 과 XAML 이 서로 다른 색 체계를 쓰면 오히려 읽기 어렵다.
|
|
|
|
### 못 하는 것 (일부러)
|
|
|
|
속성 값이 줄을 넘어가면(닫는 따옴표가 다음 줄에 있으면) 그 줄까지만 문자열로 보고
|
|
다음 줄은 속성 자리로 돌아간다. XAML 에서 드물고, 틀려도 **색만 어긋난다.**
|
|
깃발을 하나 더 늘릴 만한 값이 아니라고 봤다.
|
|
|
|
### 하루를 날릴 뻔한 것 - 빈 화면
|
|
|
|
붙이고 나니 **파일 비교 화면이 통째로 비어서 떴다.** 단추 하나 없이 하얀 판이었다.
|
|
빌드는 경고 0 · 오류 0 이고 로그에도 아무것도 안 남았다.
|
|
|
|
까닭은 한 줄이었다. 의존 속성의 **기본값 타입이 속성 타입과 달랐다.**
|
|
|
|
```csharp
|
|
DependencyProperty.Register(nameof(InBlockComment), typeof(CarryOver), typeof(CodeTextBlock),
|
|
new FrameworkPropertyMetadata(false, OnVisualInputChanged)); // ← false 는 bool 이다
|
|
```
|
|
|
|
타입을 `bool` 에서 `CarryOver` 로 바꾸면서 **기본값을 같이 안 고쳤다.**
|
|
이러면 이 컨트롤을 처음 쓰는 순간 정적 초기화에서 터지고, 화면 전체가 안 뜬다.
|
|
빌드는 통과한다 - `object` 를 받는 자리라 컴파일러가 잡을 수 없다.
|
|
|
|
찾은 방법은 **고치기 전 상태로 되돌려 보는 것**이었다(`git stash`). 되돌리니 화면이 나왔고,
|
|
그래서 내 변경이 원인인 줄 알았다. 그 다음에 파일을 바이트로 들여다보다 `false` 를 발견했다.
|
|
같은 주의를 코드 옆에 적어 두었다.
|
|
|
|
### 확인한 것
|
|
|
|
**실제 저장소의 XAML 사본**(`UCManualRCCOperation.xaml` · 638줄)을 열어 눈으로 봤다.
|
|
|
|
| 확인 | 결과 |
|
|
|---|---|
|
|
| 14줄짜리 여는 태그의 **이어지는 줄** 속성(`d:DesignHeight` · `x:Name`) | 속성 색으로 나온다 |
|
|
| 태그 이름(`Setter` · `ControlTemplate` · `Border`) | 파랑 |
|
|
| 앞가지(`view:` · `x:` · `d:`) | 청록 |
|
|
| 속성 값(`"White"` · `"#1A22..."`) | 빨강 |
|
|
| 주석 줄(`<!--<Setter .../>-->`) | 초록 한 덩어리 |
|
|
| C# 파일 색칠 | 달라지지 않았다 (문서 주석 초록 · `public` 굵은 자홍 · 타입 청록) |
|
|
|
|
**검사를 새로 만들었다**(`MarkupTest` 65건). 여러 줄 태그 · 주석 이어짐 · 앞가지 · CDATA 와 함께,
|
|
**C# · C++ · JSON 색칠이 안 망가졌는지**도 같이 본다. 토큰이 겹치거나 거꾸로 가지 않는지도 본다.
|
|
|
|
전체 **688건 통과**(하네스 20벌) · 빌드 경고 0 · 오류 0.
|
|
**실제 저장소는 읽기만 했다** - 사본을 떠서 시험했다.
|