diff --git a/src/SheetMe.Core/Catalog/TagSearch.cs b/src/SheetMe.Core/Catalog/TagSearch.cs
index 581a6bc..08e75c8 100644
--- a/src/SheetMe.Core/Catalog/TagSearch.cs
+++ b/src/SheetMe.Core/Catalog/TagSearch.cs
@@ -128,10 +128,83 @@ public static class TagSearch
{
return true;
}
+ // 사람이 치는 말을 카탈로그 표기로 옮겨 한 번 더 본다.
+ // 이것 없이는 '환자명'·'날짜'·'사인' 같은 흔한 말이 전부 0건이었다(§Aliases).
+ foreach (var alias in AliasesFor(token))
+ {
+ if (tag.Contains(alias, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
return IsChoseongQuery(token)
&& ToChoseong(tag).Contains(token, StringComparison.Ordinal);
}
+ ///
+ /// 사용자가 칠 말 → 카탈로그의 실제 표기.
+ ///
+ /// 왜 필요한가. 검색은 부분 문자열과 초성만 봤고, 카탈로그 표기는 레거시가 정한 것이다.
+ /// 실제로 세어 본 결과 흔한 말이 전부 0건이었다 —
+ /// 환자명(표기는 이름) · 날짜(일자 21건) · 사인(싸인 16건) ·
+ /// 주민등록번호(주민번호 5건) · 오늘(현재 11건) · 도장(직인) ·
+ /// 신장(키) · 남녀(성별) · 성명(이름).
+ /// 서명 태그 16종이 전부 레거시 오표기 싸인 이라, 의료진이 '사인'을 치면
+ /// "결과 없음"만 나오고 회복 경로가 없었다. 사용자가 못 찾은 이유는 접두어가 아니라 어휘였다.
+ ///
+ /// 태그마다 설명을 지어내는 것이 아니다. 여기 있는 것은 입력어 번역표이고
+ /// 오른쪽 값은 전부 카탈로그에 실재하는 표기다. 없는 내용을 만들지 않는다.
+ ///
+ /// 한 방향으로만 적어도 양방향으로 듣는다 — '이름'을 쳐도 원래 부분 일치로 걸린다.
+ ///
+ private static readonly Dictionary Aliases = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["환자명"] = new[] { "이름" },
+ ["환자이름"] = new[] { "이름" },
+ ["성명"] = new[] { "이름" },
+ ["이름"] = new[] { "성명" },
+ ["주민등록번호"] = new[] { "주민번호" },
+ ["주번"] = new[] { "주민번호" },
+ ["등록번호"] = new[] { "차트번호" },
+ ["차번"] = new[] { "차트번호" },
+ ["날짜"] = new[] { "일자" },
+ ["날자"] = new[] { "일자" },
+ ["오늘"] = new[] { "현재" },
+ ["금일"] = new[] { "현재" },
+ ["지금"] = new[] { "현재" },
+ ["시각"] = new[] { "시간" },
+ ["사인"] = new[] { "싸인" },
+ ["서명"] = new[] { "싸인" },
+ ["싸인"] = new[] { "서명" },
+ ["도장"] = new[] { "직인" },
+ ["인감"] = new[] { "직인" },
+ ["신장"] = new[] { "키" },
+ ["남녀"] = new[] { "성별" },
+ ["성"] = new[] { "성별" },
+ ["전화"] = new[] { "연락처", "전화" },
+ ["폰"] = new[] { "연락처", "전화" },
+ ["핸드폰"] = new[] { "휴대", "연락처" },
+ ["휴대폰"] = new[] { "휴대", "연락처" },
+ ["번호"] = new[] { "차트번호", "주민번호" },
+ ["병원"] = new[] { "요양기관", "병원" },
+ ["의사"] = new[] { "의사", "담당" },
+ ["과"] = new[] { "진료과" },
+ ["주소"] = new[] { "주소", "도로명" },
+ ["나이"] = new[] { "나이", "연령" },
+ ["연령"] = new[] { "나이" },
+ ["생일"] = new[] { "생년월일" },
+ };
+
+ private static string[] AliasesFor(string token)
+ => Aliases.TryGetValue(token, out var found) ? found : Array.Empty();
+
+ /// 이 말이 카탈로그 표기와 다른가 — 화면이 "'사인'은 '싸인'으로 찾습니다"를 알릴 때 쓴다
+ public static string? AliasHintFor(string query)
+ {
+ var token = query.Trim();
+ return Aliases.TryGetValue(token, out var found) && found.Length > 0 ? found[0] : null;
+ }
+
/// 공백으로 나눈 여러 토큰을 전부 만족해야 통과(AND)
public static bool MatchesAll(string tag, IReadOnlyList tokens)
{
diff --git a/src/SheetMe.Designer/Views/TagPickerDialogView.xaml.cs b/src/SheetMe.Designer/Views/TagPickerDialogView.xaml.cs
index 1c87431..2d24402 100644
--- a/src/SheetMe.Designer/Views/TagPickerDialogView.xaml.cs
+++ b/src/SheetMe.Designer/Views/TagPickerDialogView.xaml.cs
@@ -206,9 +206,16 @@ public partial class TagPickerDialogView : Window
.ToList();
TagList.ItemsSource = items;
+ // 별칭으로 찾아 준 경우에는 그 사실을 말해 준다.
+ // 레거시 카탈로그는 '사인'을 '싸인'으로, '날짜'를 '일자'로 적어 두었다.
+ // 결과만 내놓으면 사용자는 다음에도 자기 말이 통했다고 오해하고,
+ // 결과가 비었을 때 왜 비었는지도 모른다.
+ var alias = TagSearch.AliasHintFor(SearchBox.Text);
ListCaption.Text = items.Count == 0
- ? "결과 없음"
- : $"{items.Count}개 — 자주 쓰는 것과 많이 쓰인 것이 위에";
+ ? alias is null ? "결과 없음" : $"결과 없음 — '{alias}' 로도 찾아봤습니다"
+ : alias is null
+ ? $"{items.Count}개 — 자주 쓰는 것과 많이 쓰인 것이 위에"
+ : $"{items.Count}개 — 이 카탈로그는 '{alias}' 로 적혀 있습니다";
CountText.Text = $"{Searched().Count}/{allTags.Count}건";
if (selectValue is { Length: > 0 })
diff --git a/tests/SheetMe.Core.Tests/TagAliasTests.cs b/tests/SheetMe.Core.Tests/TagAliasTests.cs
new file mode 100644
index 0000000..b8b3e67
--- /dev/null
+++ b/tests/SheetMe.Core.Tests/TagAliasTests.cs
@@ -0,0 +1,96 @@
+using SheetMe.Core.Catalog;
+
+namespace SheetMe.Core.Tests;
+
+///
+/// 태그 검색 별칭 고정 — 실제 카탈로그로 잰다.
+///
+/// 별칭 표만 단독으로 검사하면 "환자명 → 이름" 이라는 매핑이 옳다는 것만 확인되고,
+/// 그 표기가 카탈로그에 실재하는지는 확인되지 않는다. 없는 표기로 옮기면 여전히 0건이다.
+/// 그래서 전체를 대상으로 몇 건이 걸리는지를 단정한다.
+///
+/// 아래 말들은 고치기 전에 전부 0건이었다. 사용자가 태그를 못 찾은 이유는
+/// 접두어(PAT_·ETC_)가 아니라 어휘였다.
+///
+[TestClass]
+public sealed class TagAliasTests
+{
+ #region Methods
+ private static int Hits(string query)
+ {
+ var tokens = new[] { query };
+ return LegacyTagCatalog.DataInterfaceTags.Count(tag => TagSearch.MatchesAll(tag, tokens));
+ }
+
+ [TestMethod]
+ public void 고치기_전에_0건이던_말들이_이제_걸린다()
+ {
+ // 각 말 오른쪽은 카탈로그의 실제 표기다. 숫자가 아니라 "0보다 크다"를 단정한다 —
+ // 카탈로그가 늘거나 줄면 건수는 바뀌지만 '찾힌다'는 사실은 바뀌면 안 된다.
+ foreach (var word in new[]
+ {
+ "환자명", "성명", "주민등록번호", "날짜", "오늘", "사인", "서명", "도장", "남녀", "생일",
+ })
+ {
+ Assert.IsTrue(Hits(word) > 0, $"'{word}' 가 0건이다 — 별칭이 카탈로그 표기에 닿지 않는다");
+ }
+ }
+
+ [TestMethod]
+ public void 서명_태그_16종이_사인으로_찾힌다()
+ {
+ // 카탈로그가 전부 레거시 오표기 '싸인' 이라, 이 한 건이 고치기 전 가장 큰 구멍이었다.
+ //
+ // 질의끼리 건수를 비교하면 안 된다. 별칭은 대칭이 아니다 —
+ // '싸인' 질의는 별칭 '서명' 까지 타서 17건, '사인' 질의는 '싸인' 만 타서 16건이다.
+ // 처음에 두 질의가 같은 수라고 단정했다가 그 차이에 걸렸다.
+ // 재야 할 것은 '싸인' 이라고 적힌 태그를 '사인' 으로 다 찾는가 이므로 원문으로 센다.
+ var literal = LegacyTagCatalog.DataInterfaceTags
+ .Where(tag => tag.Contains("싸인", StringComparison.Ordinal)).ToList();
+ Assert.IsTrue(literal.Count >= 10, $"카탈로그의 '싸인' 태그가 {literal.Count}건뿐이다");
+
+ var tokens = new[] { "사인" };
+ var missed = literal.Where(tag => !TagSearch.MatchesAll(tag, tokens)).ToList();
+ Assert.AreEqual(0, missed.Count,
+ $"'사인' 으로 못 찾는 서명 태그가 있다: {string.Join(", ", missed)}");
+ }
+
+ [TestMethod]
+ public void 날짜는_일자를_찾는다()
+ {
+ Assert.AreEqual(Hits("일자"), Hits("날짜"));
+ Assert.IsTrue(Hits("일자") >= 10, $"일자 태그가 {Hits("일자")}건뿐이다 — 카탈로그를 잘못 보고 있다");
+ }
+
+ [TestMethod]
+ public void 원래_되던_것이_계속_된다()
+ {
+ // 별칭을 넣다가 기존 경로를 깨면, 잘 되던 검색이 조용히 달라진다.
+ Assert.IsTrue(Hits("이름") > 0);
+ Assert.IsTrue(Hits("PAT_") > 0);
+ Assert.IsTrue(Hits("ㅊㅌㅂㅎ") > 0, "초성 검색이 죽었다");
+ }
+
+ [TestMethod]
+ public void 모르는_말은_별칭을_만들지_않는다()
+ {
+ Assert.IsNull(TagSearch.AliasHintFor("존재하지않는말"));
+ Assert.AreEqual("이름", TagSearch.AliasHintFor("환자명"));
+ Assert.AreEqual("싸인", TagSearch.AliasHintFor("사인"));
+ }
+
+ [TestMethod]
+ public void 별칭이_가리키는_표기는_카탈로그에_실재한다()
+ {
+ // 이 검사가 없으면 "오늘 → 없는말" 같은 표를 적어도 통과한다(0건인 채로).
+ foreach (var word in new[] { "환자명", "날짜", "사인", "주민등록번호", "오늘", "도장", "신장", "남녀" })
+ {
+ var target = TagSearch.AliasHintFor(word);
+ Assert.IsNotNull(target, $"'{word}' 에 별칭이 없다");
+ Assert.IsTrue(
+ LegacyTagCatalog.DataInterfaceTags.Any(tag => tag.Contains(target!, StringComparison.Ordinal)),
+ $"'{word}' → '{target}' 인데 그 표기를 가진 태그가 카탈로그에 없다");
+ }
+ }
+ #endregion
+}