태그 피커 재설계 — 찾기(초성·건수)와 둘러보기(분류)를 함께
칩으로 자주 쓰는 것을 앞에 깔았지만, 피커 자체는 여전히 384줄짜리 평면 목록이었다.
목록에 구조가 없으면 무엇이 있는지 알 길이 없어 결국 아는 이름만 반복해 쓰게 된다.
■ 분류 — '무엇이 있는지'
태그 이름에는 이미 구조가 있다(PAT_ 환자 · OCM_ 진료·오더 · ETC_ 기관·문서).
그 구조를 왼쪽 분류 목록으로 드러냈다. 건수와 함께 보이므로 규모가 잡힌다:
환자 40 · 진료·오더 260 · 기관·문서 75 · 그 밖 9.
가상 분류 셋을 위에 뒀다:
· 자주 쓰는 것 — 이 컨트롤 타입의 실사용 상위(타입마다 다르다)
· 운영에서 쓰임 157 — 384종 중 실제로 한 번이라도 쓰인 것만
· 전체 384
아는 접두어만 분류로 올리고 나머지는 '그 밖'으로 묶는다. 레거시에는 규약에서 벗어난 이름이
섞여 있어서(오타 ECT_로그인사용자싸인 — bzDataInterface.vb:17850 의 실제 오타다,
접두어가 아예 없는 GetNurseShtCod) 접두어를 그대로 분류로 쓰면 1~3개짜리 분류가
목록을 채워 '무엇이 있는지'를 오히려 가린다.
■ 사용 건수 — '이게 표준인가'
이름만으로는 PAT_주민번호 와 PAT_주민번호_Dash 중 무엇이 표준인지 알 수 없다.
운영 실측(--db-tags)을 줄마다 배지로 띄운다(PAT_이름 669, PAT_차트번호 463 …).
한 번도 안 쓰인 태그는 '미사용'으로 표시한다 — 그것도 정보다.
정렬은 자주 쓰는 순 → 운영 건수 → 이름. 가나다순은 운영 절반을 차지하는 태그와
한 번도 안 쓰인 태그를 같은 무게로 보여 준다.
■ 초성 검색 — 손이 덜 가게
한글 태그 이름이 길어 전체를 칠 이유가 없다. "ㅊㅌㅂㅎ" 네 글자로 PAT_차트번호 에 닿는다.
입력이 초성만으로 이뤄졌을 때만 초성 대조를 시도한다 — 일반 검색어를 초성으로 오인하면 안 된다.
공백으로 나눈 여러 토큰은 모두 만족해야 하고(AND), 일반어와 초성을 섞어도 된다("OCM ㅈㄷㅅ").
■ 그 밖
· 검색칸에서 ↑↓·PageUp/Down 으로 바로 목록을 움직인다 — 검색하고 Tab 으로 옮겨 가는 왕복을 없앴다.
· 현재 값이 있으면 그 값이 속한 분류로 열고 그 줄을 선택해 둔다.
· 검색으로 지금 분류가 비면 결과가 있는 분류로 옮겨 간다(빈 화면을 보여 주지 않는다).
· 창을 720×620 으로 키우고 크기 조절을 열었다. 선택된 값을 하단에 항상 띄운다.
· AutomationProperties.Name 을 붙였다(사내 UI 가이드 접근성 항목).
사내 UI 가이드의 판단 기준을 따랐다 — '빈도 높은 동작에 모달을 쓰지 않는다'.
자주 쓰는 태그는 인스펙터 행의 칩으로 이미 빠져 있어 이 창을 열 필요가 없고,
이 창은 '둘러보기' 전용으로 남긴다(그 용도에는 집중 화면이 맞다).
단위 테스트 20건 추가(분류 매핑·규약 밖 이름 접기·초성 추출·초성 판정·부분 일치·
다중 토큰 AND·변형 묶기·건수 스냅샷).
회귀: 테스트 162/162, 편집 스모크 210건 실패 0,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
de01012b7e
commit
146d808aac
@@ -0,0 +1,116 @@
|
||||
using SheetMe.Core.Catalog;
|
||||
|
||||
namespace SheetMe.Core.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 태그 검색·분류 규칙 테스트.
|
||||
///
|
||||
/// 태그는 384종이라 검색이 곧 사용성이다. 한글 이름이 길어 초성으로 닿을 수 있어야 하고,
|
||||
/// 분류는 레거시의 규약 밖 이름(오타·접두어 없음)에 흔들리지 않아야 한다.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public sealed class TagSearchTests
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DataRow("PAT_이름", "환자")]
|
||||
[DataRow("PAT_주민번호_Dash", "환자")]
|
||||
[DataRow("OCM_진단서의사명", "진료·오더")]
|
||||
[DataRow("ETC_요양기관명칭_병원명", "기관·문서")]
|
||||
public void CategoryOf_MapsKnownPrefixes(string tag, string expected)
|
||||
=> Assert.AreEqual(expected, TagSearch.CategoryOf(tag));
|
||||
|
||||
/// <summary>
|
||||
/// 규약 밖 이름은 전부 '그 밖'으로 묶인다.
|
||||
/// ECT_ 는 레거시 bzDataInterface.vb:17850 의 실제 오타이고(ETC 가 아니다),
|
||||
/// GetNurseShtCod 는 접두어가 아예 없다. 접두어를 그대로 분류로 쓰면
|
||||
/// 1~3개짜리 분류가 목록을 채워 '무엇이 있는지'를 오히려 가린다.
|
||||
/// </summary>
|
||||
[DataTestMethod]
|
||||
[DataRow("ECT_로그인사용자싸인")]
|
||||
[DataRow("GetNurseShtCod")]
|
||||
[DataRow("GetOprInfDT_OKDMain")]
|
||||
[DataRow("RadCheckControl")]
|
||||
public void CategoryOf_FoldsIrregularNames(string tag)
|
||||
=> Assert.AreEqual(TagSearch.OtherCategory, TagSearch.CategoryOf(tag));
|
||||
|
||||
[TestMethod]
|
||||
public void ToChoseong_ExtractsKoreanInitials()
|
||||
{
|
||||
Assert.AreEqual("PAT_ㅊㅌㅂㅎ", TagSearch.ToChoseong("PAT_차트번호"));
|
||||
Assert.AreEqual("ㅈㄷㅅㅇㅅㅁ", TagSearch.ToChoseong("진단서의사명"));
|
||||
}
|
||||
|
||||
/// <summary>초성만 입력했을 때만 초성 대조를 시도한다 — 일반 검색어를 초성으로 오인하면 안 된다</summary>
|
||||
[DataTestMethod]
|
||||
[DataRow("ㅊㅌㅂㅎ", true)]
|
||||
[DataRow("ㅇㅁ", true)]
|
||||
[DataRow("차트", false)]
|
||||
[DataRow("PAT", false)]
|
||||
[DataRow("", false)]
|
||||
public void IsChoseongQuery_OnlyForPureInitials(string query, bool expected)
|
||||
=> Assert.AreEqual(expected, TagSearch.IsChoseongQuery(query));
|
||||
|
||||
/// <summary>초성 네 글자로 긴 한글 태그에 닿는다 — 손이 덜 가는 것이 요점이다</summary>
|
||||
[TestMethod]
|
||||
public void Matches_FindsByChoseong()
|
||||
{
|
||||
Assert.IsTrue(TagSearch.Matches("PAT_차트번호", "ㅊㅌㅂㅎ"));
|
||||
Assert.IsTrue(TagSearch.Matches("OCM_진단서의사명", "ㅈㄷㅅ"));
|
||||
Assert.IsFalse(TagSearch.Matches("PAT_이름", "ㅊㅌㅂㅎ"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Matches_StillDoesPlainSubstring()
|
||||
{
|
||||
Assert.IsTrue(TagSearch.Matches("PAT_차트번호", "차트"));
|
||||
Assert.IsTrue(TagSearch.Matches("PAT_차트번호", "pat"), "대소문자를 가리지 않아야 한다");
|
||||
}
|
||||
|
||||
/// <summary>공백으로 나눈 토큰은 모두 만족해야 한다(AND)</summary>
|
||||
[TestMethod]
|
||||
public void MatchesAll_RequiresEveryToken()
|
||||
{
|
||||
var tokens = new[] { "OCM", "의사" };
|
||||
Assert.IsTrue(TagSearch.MatchesAll("OCM_진단서의사명", tokens));
|
||||
Assert.IsFalse(TagSearch.MatchesAll("PAT_이름", tokens));
|
||||
// 일반어와 초성을 섞어도 된다
|
||||
Assert.IsTrue(TagSearch.MatchesAll("OCM_진단서의사명", new[] { "OCM", "ㅈㄷㅅ" }));
|
||||
}
|
||||
|
||||
/// <summary>변형이 흩어지지 않게 같은 뿌리로 묶는다</summary>
|
||||
[TestMethod]
|
||||
public void GroupOf_UsesSecondToken()
|
||||
{
|
||||
Assert.AreEqual("주민번호", TagSearch.GroupOf("PAT_주민번호"));
|
||||
Assert.AreEqual("주민번호", TagSearch.GroupOf("PAT_주민번호_Dash"));
|
||||
Assert.AreEqual("주민번호", TagSearch.GroupOf("PAT_주민번호_Blind"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 사용 건수는 실측 스냅샷이다 — 상위 태그가 비어 있으면 피커의 정렬 근거가 사라진다.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void UsageCounts_CarryTheMeasuredTopTags()
|
||||
{
|
||||
Assert.AreEqual(669, LegacyTagUsageCounts.Of("PAT_이름"));
|
||||
Assert.AreEqual(852, LegacyTagUsageCounts.Of("RadCheckControl"));
|
||||
Assert.IsTrue(LegacyTagUsageCounts.IsUsed("OCM_진단서의사싸인"));
|
||||
// 기록에 없는 태그는 0 — '미사용'으로 표시되어야 한다
|
||||
Assert.AreEqual(0, LegacyTagUsageCounts.Of("존재하지_않는_태그"));
|
||||
Assert.IsFalse(LegacyTagUsageCounts.IsUsed("존재하지_않는_태그"));
|
||||
}
|
||||
|
||||
/// <summary>집계된 태그는 전부 실제 카탈로그에 있어야 한다 — 없는 태그를 상위로 올리면 안 된다</summary>
|
||||
[TestMethod]
|
||||
public void UsageCounts_OnlyContainCatalogTags()
|
||||
{
|
||||
var known = LegacyTagCatalog.DataInterfaceTags
|
||||
.Concat(LegacyTagCatalog.DataActionTags)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
foreach (var tag in new[] { "PAT_이름", "RadCheckControl", "OCM_진단서의사싸인", "ETC_현재일자" })
|
||||
{
|
||||
Assert.IsTrue(known.Contains(tag), $"카탈로그에 없는 태그가 집계에 있습니다: {tag}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user