태그 행에서 모달 없이 태그를 고른다 — 인라인 완성

4단계 계속. 주 경로에서 1080x700 모달이 사라진다.

칩 상위 8종만으로 운영 사용의 41.7%, 상위 16종으로 61.5% 를 덮는다.
나머지는 이름을 정확히 16자 외워 치거나 모달을 열어야 했고, 자유 입력은 열려 있는데
제안이 없어서 <b>오타가 검증 없이 저장되고 EMR 에서 조용히 빈 칸이 됐다</b>.
같은 인스펙터의 '속성 추가' 행에는 이미 부분일치 제안이 있었다 —
패턴은 있는데 운영 8,032건이 걸린 칸에만 없었다.

<b>새 패턴을 발명하지 않았다.</b> QueryEditorWindow 의 CompletionPopup 에서 배운 것을 그대로 쓴다:
StaysOpen=False 로 두면 팝업이 마우스를 캡처해 항목 클릭을 '바깥 클릭'으로 먹어 버린다
(눌러도 입력이 안 되고 사라진다 — 그 함정을 이미 한 번 겪었다).
AllowsTransparency 도 켜지 않는다 — 레이어드 창이 되면 ClearType 이 꺼져 13px 한글이 뭉개진다
(진단 cleartype 의 D 표본). 대신 안쪽에 ClearTypeHint 를 건다.

<b>칩이 사라지는 기준을 바꿨다.</b> 전에는 ValueText.Length == 0 이라 값이 정해지면 칩이 사라졌다.
그래서 잘못 고른 태그를 바꿀 때 도움이 끊겼다 — 태그를 고치는 일은 처음 고르는 일만큼 잦은데
고치는 쪽만 맨손이었다. 이제 기준은 '값이 있는지'가 아니라 <b>'타이핑 중인지'</b>다.
타이핑을 시작하면 아래 완성 목록이 같은 일을 더 정확하게 하므로 칩을 접는다.
그 옛 규칙을 고정하던 검사 하나를 새 규칙으로 고쳤다 — 의도한 변경이다.

완성 목록은 입력 중인 글자를 <b>인자로</b> 받는다. 태그 행의 Text 바인딩이
UpdateSourceTrigger=LostFocus 라 타이핑 중에는 ValueText 가 아직 옛 값이고,
그것으로 목록을 만들면 한 글자 뒤처진다.
순위는 이 타입에서 자주 쓰이는 순서 → 운영 사용 건수 → 이름이다.
알파벳 순으로 두면 상위 10종(전체 사용의 47.5%)이 목록 아래로 흩어진다.
검색은 TagSearch 를 그대로 쓴다 — 별칭·초성이 여기서도 듣는다('환자명'으로도 뜬다).

<b>Popup.IsOpen 은 기본이 TwoWay 다.</b> 읽기 전용 속성에 바인딩하면 XamlParseException 이고,
그 실패가 --dialog-shots 를 통째로 죽였다(종료코드 3). Mode=OneWay 를 명시한다.
게이트가 아니면 인스펙터를 띄우는 화면에서만 터졌을 것이다.

실측 14건: 값이 있어도 칩 유지 · 타이핑 전 미표시 · 타이핑 시 칩 접힘 · 별칭 적용 ·
자주 쓰는 것이 위 · 10줄 상한 · ↑↓ 순환 · Enter 확정과 커밋 · 확정 후 닫힘/칩 복귀 ·
정확히 하나면 안 가림 · Esc 가 값을 보존.

게이트: 테스트 291/291, --edit-smoke 0실패(신규 14), --dialog-shots 글자 2,465개 넘침 0 대조군 4/4,
--modal-check 0실패, --cleartype 11/11, --maxrect 0실패, --scale-budget 5/5, 빌드 경고 0,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-14 17:43:53 +09:00
co-authored by Claude Opus 5
parent a869e5a5bc
commit 9b2ae0ea67
5 changed files with 380 additions and 8 deletions
+71 -2
View File
@@ -151,6 +151,67 @@ public static class EditSmoke
Check("Enter: 글자면 편집을 시작한다", asked == 1, $"요청 {asked}회");
}
// 태그 행 인라인 완성 — 이 행에서 모달 없이 태그를 고르는 주 경로.
{
var tagRow = new ViewModels.Inspector.TagPickerRowViewModel(
"데이터 태그", "태그 선택",
SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags,
SheetMe.Core.Catalog.LegacyTagUsageCatalog.SuggestFor("DataInterfaceTag", "TextBox"));
var committed = new List<string>();
tagRow.Commit = value => committed.Add(value);
Check("완성: 값이 있어도 칩이 남는다", tagRow.ShowSuggestions,
"잘못 고른 태그를 바꿀 때 도움이 끊기지 않아야 한다");
// 타이핑 전에는 열리지 않는다
tagRow.RefreshCompletions("환자");
Check("완성: 타이핑 전에는 안 뜬다", !tagRow.ShowCompletions);
tagRow.BeginTyping();
Check("완성: 타이핑하면 칩이 접힌다", !tagRow.ShowSuggestions);
// 별칭이 여기서도 들어야 한다 — 카탈로그 표기는 '이름' 이다
tagRow.RefreshCompletions("환자명");
Check("완성: 별칭이 인스펙터에서도 듣는다", tagRow.ShowCompletions && tagRow.Completions.Count > 0,
$"목록 {tagRow.Completions.Count}개");
// 순위 — 이 타입에서 자주 쓰이는 것이 위로
tagRow.RefreshCompletions("PAT_");
var top = tagRow.Completions.FirstOrDefault();
Check("완성: 자주 쓰는 것이 위에", top == "PAT_이름", $"1번째 {top}");
Check("완성: 10줄을 넘기지 않는다", tagRow.Completions.Count <= 10,
$"{tagRow.Completions.Count}줄");
// ↑↓ 순환
tagRow.MoveCompletion(-1);
Check("완성: ↑ 로 끝으로 돈다", tagRow.CompletionIndex == tagRow.Completions.Count - 1,
$"index {tagRow.CompletionIndex}");
tagRow.MoveCompletion(1);
Check("완성: ↓ 로 처음으로 돈다", tagRow.CompletionIndex == 0);
// Enter 확정 — 값이 되고 목록이 닫히고 칩이 돌아온다
var picked = tagRow.Completions[0];
Check("완성: Enter 로 확정된다", tagRow.CommitCompletion());
Check("완성: 확정한 값이 커밋된다", tagRow.ValueText == picked && committed.Contains(picked),
$"값 {tagRow.ValueText}, 커밋 {committed.Count}회");
Check("완성: 확정하면 목록이 닫힌다", !tagRow.ShowCompletions);
Check("완성: 확정하면 칩이 돌아온다", tagRow.ShowSuggestions);
// 정확히 그것만 남으면 목록을 띄우지 않는다 — 다 골랐는데 가리는 셈이다
tagRow.BeginTyping();
tagRow.RefreshCompletions("PAT_이름");
var exact = tagRow.Completions.Count == 1 && tagRow.Completions[0] == "PAT_이름";
Check("완성: 정확히 하나 남으면 안 가린다", !exact || !tagRow.ShowCompletions,
$"목록 {tagRow.Completions.Count}개, 열림 {tagRow.ShowCompletions}");
// Esc 는 값을 바꾸지 않는다
var before = tagRow.ValueText;
tagRow.BeginTyping();
tagRow.RefreshCompletions("ETC_");
tagRow.EndTyping();
Check("완성: Esc 는 값을 바꾸지 않는다", tagRow.ValueText == before && !tagRow.ShowCompletions);
}
// 미리보기가 인쇄 필터를 <b>실제로 쓰는가</b>.
// PrintFilter 단위 테스트는 판정이 옳다는 것만 말한다 — 렌더 경로가 그 판정을 부르는지는
// 다른 문제이고, 이 앱에서 "코드는 맞아 보이는데 화면은 다른" 일이 여러 번 있었다.
@@ -1089,12 +1150,20 @@ public static class EditSmoke
}
}
// 값이 정해지면 칩을 감춘다 — 좁은 인스펙터가 길어지지 않도록
// 칩을 감추는 기준이 바뀌었다.
//
// 전에는 "값이 정해지면 감춘다"였고 이 검사가 그것을 고정하고 있었다.
// 그 규칙 때문에 <b>잘못 고른 태그를 바꿀 때 도움이 끊겼다</b> —
// 태그를 고치는 일은 처음 고르는 일만큼 잦은데(운영 8,032건) 고치는 쪽만 맨손이었다.
// 이제 기준은 '값이 있는지'가 아니라 <b>'타이핑 중인지'</b>다.
// 타이핑을 시작하면 아래 완성 목록이 같은 일을 더 정확하게 하므로 칩을 접는다.
var tagRow = new ViewModels.Inspector.TagPickerRowViewModel("데이터 태그", "제목",
SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags, pictureSuggest);
Check("태그 제안: 값이 비면 칩을 보여 준다", tagRow.ShowSuggestions);
tagRow.ValueText = "PAT_이름";
Check("태그 제안: 값이 정해지면 칩을 감춘다", !tagRow.ShowSuggestions);
Check("태그 제안: 값이 있어도 칩이 남는다", tagRow.ShowSuggestions);
tagRow.BeginTyping();
Check("태그 제안: 타이핑 중에는 칩을 접는다", !tagRow.ShowSuggestions);
}
// 20-5d) 태그 카탈로그 — 레거시에서 주석 처리된 태그를 고를 수 있으면 안 된다