태그 선택 — 타입별로 자주 쓰는 것을 그 자리에 깐다

태그를 하나 걸려면 매번 모달을 열어 500여 개(액션 160여 개) 목록에서 찾아야 했다.
한 서식이 쓰는 서로 다른 태그는 중앙값 6개, 최대 27개다(--db-tags) — 서식 하나 만들 때마다
그 짓을 예닐곱 번 한다.

■ 실측이 설계를 정했다

--db-tags 진단을 신설해 운영 디자인 1,271건의 태그 사용 8,032건을 컨트롤 타입별로 집계했다.
후보가 타입마다 거의 완전히 갈린다:

  이미지 · 데이터 태그        343건 / 7종 — 상위 16종이 100%  (서명·직인·로고가 전부)
  라디오 · 액션 태그          736건 / 7종 — 100%             (RadCheckControl 이 580건)
  체크박스 · 액션 태그        357건 / 7종 — 100%
  텍스트박스 · 액션 태그      200건 / 6종 — 100%
  마스크입력 · 데이터 태그     57건 / 13종 — 100%
  버튼 · 액션 태그            393건 / 46종 — 상위 16종이 83%
  텍스트박스 · 데이터 태그   5,927건 / 144종 — 상위 16종이 65%  (여기만 꼬리가 길다)

즉 일곱 조합 중 다섯은 **짧은 목록이 운영 전체를 덮는다**. 이미지 하나 고르려고
500개를 뒤지고 있었다는 뜻이다.

■ 바꾼 것

· 인스펙터 행 아래에 그 타입에서 자주 쓰는 태그를 빈도순 칩으로 깐다(최대 8개). 누르면 바로 값이 된다.
  칩은 **값이 비어 있을 때만** 뜬다 — 이미 정해진 행 아래에도 깔면 좁은 패널이 금방 길어진다.
· 피커를 열면 자주 쓰는 것이 맨 위로 올라온다(그 안에서는 빈도순, 나머지는 원래 가나다순 유지).
  건수 표시에 '자주 쓰는 것 N건이 위에' 를 덧붙였다.
· 전체 카탈로그와 자유 입력은 그대로다. 제안은 제안이지 제한이 아니다 —
  타입별 자료가 없는 조합의 액션 태그는 아예 비워 둔다. 추측으로 권하면
  그 칸은 배포된 뒤에야 빈칸으로 드러난다.

순서를 가나다순이 아니라 빈도순으로 둔 것이 핵심이다. 가나다순은 운영의 절반을 차지하는
RadCheckControl 과 한 번도 안 쓰인 태그를 같은 무게로 보여 준다.

■ 곁다리로 확인한 것

카탈로그에 없는 값이 데이터 태그 1종(PAT_전체주소 ×11), 액션 태그 1종(OprEmrList ×5) 나왔다.
둘 다 레거시 bzDataInterface / EN_DataActionTyp 에 존재하지 않는다 —
우리 카탈로그가 맞고, 저 값들은 운영 서식에 이미 박힌 죽은 태그다(런타임이 해석하지 못한다).
우리가 만든 것이 아니라 손대지 않았다.

편집 스모크 17건 추가(타입별 좁힘 / 빈도 순서 / 자료 없는 타입은 빈 목록 /
제안이 전부 카탈로그에 있는지 12조합 검사 / 값이 정해지면 칩 감춤).
회귀: 테스트 142/142, 편집 스모크 210건 실패 0,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-13 13:52:54 +09:00
co-authored by Claude Opus 5
parent ddf2bdaee8
commit de01012b7e
9 changed files with 432 additions and 22 deletions
@@ -886,6 +886,49 @@ public static class EditSmoke
ViewModels.MainViewModel.FindOpenSheet(new[] { Tab(string.Empty) }, string.Empty) is null);
}
// 20-5i) 태그 제안 — 후보는 컨트롤 타입마다 거의 완전히 갈린다.
// 이미지의 데이터 태그는 운영 전체가 6종(서명·직인·로고)이라 500여 개 목록을 뒤질 이유가 없다.
{
var pictureSuggest = SheetMe.Core.Catalog.LegacyTagUsageCatalog
.SuggestFor("DataInterfaceTag", "PictureBox");
Check("태그 제안: 이미지는 서명·직인으로 좁혀진다",
pictureSuggest.Count is > 0 and <= 8
&& pictureSuggest[0] == "OCM_진단서의사싸인"
&& pictureSuggest.All(t => t.Contains("싸인") || t.Contains("직인") || t.Contains("로고")),
string.Join(",", pictureSuggest));
var textSuggest = SheetMe.Core.Catalog.LegacyTagUsageCatalog
.SuggestFor("DataInterfaceTag", "TextBox");
Check("태그 제안: 텍스트박스는 환자 기본정보가 앞에", textSuggest[0] == "PAT_이름", textSuggest[0]);
Check("태그 제안: 타입마다 후보가 다르다", !textSuggest.SequenceEqual(pictureSuggest));
// 액션 태그는 타입 자료가 없으면 추측하지 않는다 — 잘못 권하면 런타임이 조용히 어긋난다
Check("태그 제안: 자료 없는 타입의 액션 태그는 빈 목록",
SheetMe.Core.Catalog.LegacyTagUsageCatalog.SuggestFor("DataActionTag", "Panel").Count == 0);
// 제안은 전부 실제 카탈로그에 있어야 한다 — 없는 태그를 권하면 그 칸은 배포 후 빈칸이 된다
foreach (var (key, catalog) in new[]
{
("DataInterfaceTag", (IReadOnlyList<string>)SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags),
("DataActionTag", SheetMe.Core.Catalog.LegacyTagCatalog.DataActionTags),
})
{
foreach (var type in new[] { "TextBox", "PictureBox", "MaskedTextBox", "CheckBox", "RadioButton", "Button" })
{
var bad = SheetMe.Core.Catalog.LegacyTagUsageCatalog.SuggestFor(key, type)
.Where(t => !catalog.Contains(t)).ToList();
Check($"태그 제안: {key}|{type} 은 전부 카탈로그에 있다", bad.Count == 0, string.Join(",", bad));
}
}
// 값이 정해지면 칩을 감춘다 — 좁은 인스펙터가 길어지지 않도록
var tagRow = new ViewModels.Inspector.TagPickerRowViewModel("데이터 태그", "제목",
SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags, pictureSuggest);
Check("태그 제안: 값이 비면 칩을 보여 준다", tagRow.ShowSuggestions);
tagRow.ValueText = "PAT_이름";
Check("태그 제안: 값이 정해지면 칩을 감춘다", !tagRow.ShowSuggestions);
}
// 20-5d) 태그 카탈로그 — 레거시에서 주석 처리된 태그를 고를 수 있으면 안 된다
Check("태그 카탈로그: 미구현 3종 제외",
!SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags.Contains("PAT_BMI")