diff --git a/src/SheetMe.Core/Catalog/LegacyTagUsageCounts.cs b/src/SheetMe.Core/Catalog/LegacyTagUsageCounts.cs new file mode 100644 index 0000000..643ae33 --- /dev/null +++ b/src/SheetMe.Core/Catalog/LegacyTagUsageCounts.cs @@ -0,0 +1,240 @@ +namespace SheetMe.Core.Catalog; + +/// +/// 태그별 운영 사용 건수 — 피커가 '이게 표준인지 희귀한지'를 보여 주기 위한 실측값. +/// +/// 출처: 진단 --db-tags. 운영 디자인 1,271건의 DataInterfaceTag·DataActionTag 8,032건을 집계했다. +/// 카탈로그에는 있으나 운영에 한 번도 안 쓰인 태그는 여기 없다(건수 0으로 본다) — +/// 그것도 정보다. 설계자가 처음 보는 태그를 고를 때 '아무도 안 쓰는 것'임을 알 수 있어야 한다. +/// +/// 수치는 스냅샷이라 시간이 지나면 어긋난다. 순서와 규모의 근거로만 쓰고, 정확한 통계로 쓰지 않는다. +/// +public static class LegacyTagUsageCounts +{ + #region Member Fields + private static readonly Dictionary counts = new(StringComparer.Ordinal) + { + ["RadCheckControl"] = 852, + ["DtrLicList"] = 155, + ["ChoiceData"] = 135, + ["ComOkdList"] = 77, + ["ImgeList"] = 59, + ["OkdInfList"] = 37, + ["VisibleYon"] = 35, + ["CodeFInd"] = 32, + ["OprInfList"] = 25, + ["DataCopy_Chk"] = 24, + ["YondoList"] = 24, + ["UmlsMst"] = 21, + ["OkdInfData"] = 20, + ["MinusToPlus"] = 17, + ["ReturnUidNam"] = 15, + ["AllCheck"] = 15, + ["EmrSelectData"] = 13, + ["TPRList"] = 10, + ["PanelVisible"] = 9, + ["ComPidList"] = 9, + ["TPR"] = 7, + ["ReturnLabData"] = 6, + ["EmgTotalEmr"] = 6, + ["DataCopy_MtM"] = 6, + ["LabOdrList"] = 5, + ["ReturnEmrData"] = 5, + ["ButtonData"] = 5, + ["UnAllCheck"] = 5, + ["PanelUnVisible"] = 4, + ["DataTableRefresh"] = 4, + ["CodList"] = 3, + ["ComAcpLev_O_Ver2"] = 3, + ["ZipFinder"] = 3, + ["BldOutList"] = 2, + ["PrgRcdList"] = 2, + ["SMSWebLink"] = 2, + ["NdsMst"] = 2, + ["ComAcpLev_O_Ver3"] = 2, + ["ComAcpLev_I_Ver3"] = 2, + ["ComAcpLev_I_Ver2"] = 2, + ["ComAcpLev_I"] = 2, + ["ComAcpLev_O"] = 2, + ["ComAcpLev"] = 2, + ["SetVisibleStatus"] = 2, + ["ReturnAllEmrData"] = 2, + ["HiraSearch"] = 1, + ["EmgOkdInfData"] = 1, + ["PdeOdrList"] = 1, + ["ReturnLevOrderList"] = 1, + ["EmgLevTim"] = 1, + ["EndoOdrList"] = 1, + ["PathologyInfo"] = 1, + ["EmgKTAS"] = 1, + ["MakeDataList"] = 1, + ["EmgDetList"] = 1, + ["DateVisibleYon"] = 1, + ["PAT_이름"] = 669, + ["PAT_차트번호"] = 463, + ["ETC_요양기관명칭_병원명"] = 303, + ["ETC_현재일자"] = 282, + ["PAT_성별"] = 277, + ["OCM_진단서의사명"] = 228, + ["PAT_생년월일"] = 216, + ["PAT_나이"] = 204, + ["OCM_진료과"] = 193, + ["OCM_진단서의사싸인"] = 174, + ["PAT_주민번호_Dash"] = 169, + ["ETC_로그인_사용자명"] = 163, + ["PAT_휴대전화번호"] = 156, + ["OCM_진단서의사면허번호"] = 154, + ["ETC_요양기관기호"] = 149, + ["OCM_담당의사"] = 144, + ["PAT_도로명주소"] = 126, + ["ETC_현재일시"] = 118, + ["OCM_병실"] = 117, + ["ETC_요양기관명칭_병원주소"] = 109, + ["ETC_요양기관명칭_병원전화번호"] = 95, + ["OCM_진단서전문과목"] = 84, + ["ETC_요양기관명칭_병원직인"] = 82, + ["OCM_진단서의사전문의번호"] = 78, + ["PAT_자택전화번호"] = 76, + ["OCM_주상병명"] = 74, + ["PAT_연락처"] = 65, + ["PAT_지번주소"] = 61, + ["OCM_외래내원일자"] = 55, + ["PAT_나이_세"] = 46, + ["OCM_입원내원일시"] = 44, + ["OCM_입원과"] = 43, + ["PAT_주민번호"] = 41, + ["OCM_병동"] = 37, + ["OCM_주상병코드"] = 36, + ["PAT_주민번호_SexTyp_Blind"] = 36, + ["OCM_담당의사면허번호"] = 35, + ["OCM_진단서의사싸인_Refer"] = 34, + ["OCM_상병코드_상병명"] = 31, + ["OCM_입원일시"] = 30, + ["ETC_요양기관명칭_병원팩스번호"] = 29, + ["PAT_성별_나이"] = 28, + ["OCM_상병명_세로_Refer"] = 26, + ["OCM_몸무게"] = 24, + ["OCM_보험유형"] = 24, + ["OCM_진료과대외명칭"] = 24, + ["PAT_주민번호_Blind"] = 23, + ["ETC_요양기관명칭_사업자등록번호"] = 23, + ["OCM_상병코드_세로_Refer"] = 23, + ["ETC_요양기관명칭_병원로고"] = 22, + ["OCM_담당의사싸인"] = 21, + ["PAT_한글성별"] = 20, + ["OCM_키"] = 19, + ["OCM_주상병일자"] = 17, + ["OCM_입원내원일자"] = 17, + ["OCM_혈압"] = 16, + ["ETC_요양기관명칭_병원명_영문"] = 16, + ["OCM_상병명_가로"] = 16, + ["ETC_요양기관명칭_병원대표자"] = 16, + ["OCM_주상병코드_Refer"] = 16, + ["OCM_체온"] = 15, + ["OCM_주상병명_Refer"] = 15, + ["OCM_호흡"] = 14, + ["OCM_맥박"] = 14, + ["OCM_퇴원일시"] = 14, + ["ETC_현재일시_MaskBox"] = 14, + ["ETC_건강보험증번호"] = 14, + ["OCM_진료기간"] = 14, + ["OCM_키_LAST"] = 13, + ["OCM_상병명_세로"] = 13, + ["OCM_외래내원일시"] = 12, + ["ETC_요양기관명칭_병원주소_영문"] = 12, + ["OCM_진료과_Refer"] = 12, + ["OCM_몸무게_LAST"] = 11, + ["PAT_우편번호"] = 10, + ["OCM_로그인_사용자싸인"] = 9, + ["ETC_요양기관명칭_병원장귀하"] = 9, + ["OCM_혈액형"] = 8, + ["OCM_진단서의사면허번호_Refer"] = 8, + ["OCM_병동_병실"] = 7, + ["OCM_전과일자"] = 7, + ["OCM_진료과_전화번호"] = 7, + ["OCM_초진일_발병일"] = 7, + ["OCM_상병코드_세로"] = 6, + ["OCM_진단서의사명_Refer"] = 6, + ["OCM_입통원구분_통원"] = 5, + ["OCM_입통원구분_입원"] = 5, + ["ETC_요양기관명칭_병원장"] = 5, + ["ETC_요양기관명칭_영문_병원전화_팩스번호"] = 5, + ["OCM_퇴원약"] = 4, + ["OCM_적용진료의사"] = 4, + ["OCM_상병코드_상병명_주상병"] = 4, + ["OCM_맥박_LAST"] = 4, + ["PAT_생년월일_영문"] = 4, + ["OCM_수술진단명"] = 4, + ["PAT_보호자연락처"] = 4, + ["OCM_체온_LAST"] = 3, + ["PAT_건보_급여_세대주명"] = 3, + ["OCM_혈압_LAST"] = 3, + ["OCM_SPO2"] = 3, + ["ETC_수술간호사목록"] = 3, + ["OCM_진단서의사_영문"] = 3, + ["OCM_상병코드_가로"] = 3, + ["PAT_여권번호"] = 3, + ["OCM_진단서의사전문의번호_Refer"] = 3, + ["OCM_퇴원의사"] = 2, + ["OCM_입원의사"] = 2, + ["OCM_전과"] = 2, + ["OCM_상병코드_상병명_부상병"] = 2, + ["OCM_호흡_LAST"] = 2, + ["ETC_현재년도"] = 2, + ["OCM_진단서전문그룹과목"] = 2, + ["OCM_수술일자"] = 2, + ["OCM_SPO2_LAST"] = 2, + ["OCM_적용진료과"] = 2, + ["OCM_입통원구분_입원_Refer"] = 2, + ["OCM_입통원구분_통원_Refer"] = 2, + ["ETC_요양기관명칭_병원전화_팩스번호"] = 2, + ["OCM_상병명_가로_Refer"] = 2, + ["OCM_진료기간_Refer"] = 2, + ["OCM_상병코드_상병명_Refer"] = 2, + ["OCM_외래실진료일"] = 2, + ["ETC_건강보험증번호_Refer"] = 2, + ["OCM_진단서전문과목_Refer"] = 2, + ["ETC_로그인_근무부서"] = 2, + ["OCM_진단구분_상병코드_상병명"] = 1, + ["OCM_퇴원과"] = 1, + ["OCM_심사_부상병코드"] = 1, + ["OCM_심사_부상병명"] = 1, + ["OCM_심사_주상병명"] = 1, + ["OCM_심사_주상병코드"] = 1, + ["OCM_임신주기"] = 1, + ["OCM_전과내역"] = 1, + ["OCM_계산유형"] = 1, + ["PAT_휴대전화번호_뒷4자리"] = 1, + ["OCM_부상병명"] = 1, + ["OCM_응급증상여부_NO"] = 1, + ["OCM_응급증상여부_YES"] = 1, + ["OCM_BST"] = 1, + ["OCM_담당의사전문의번호"] = 1, + ["OCM_재원일수"] = 1, + ["OCM_수술명칭"] = 1, + ["ETC_로그인사용자_병동_수간호사"] = 1, + ["OCM_진단서전문그룹과목_Refer"] = 1, + ["OCM_퇴원일시_Refer"] = 1, + ["OCM_입원일시_Refer"] = 1, + ["OCM_User진료의사명"] = 1, + ["OCM_상병코드_상병명_주상병_Refer"] = 1, + ["OCM_담당의사_영문_Refer"] = 1, + ["OCM_진단서의사_영문_Refer"] = 1, + ["OCM_User진료과"] = 1, + ["PAT_주민번호_SexTyp_BlindStar"] = 1, + ["PAT_국적"] = 1, + ["PAT_건보세대주명"] = 1, + ["OCM_병실_Refer"] = 1, + ["OCM_비급여처방"] = 1, + ["ETC_로그인_근무부서_사용자명"] = 1, + }; + #endregion + + #region Methods + /// 운영 사용 건수 — 기록이 없으면 0 + public static int Of(string tag) => counts.TryGetValue(tag, out var n) ? n : 0; + + /// 운영에서 한 번이라도 쓰인 적이 있는지 + public static bool IsUsed(string tag) => counts.ContainsKey(tag); + #endregion +} diff --git a/src/SheetMe.Core/Catalog/TagSearch.cs b/src/SheetMe.Core/Catalog/TagSearch.cs new file mode 100644 index 0000000..5caeaf7 --- /dev/null +++ b/src/SheetMe.Core/Catalog/TagSearch.cs @@ -0,0 +1,126 @@ +using System.Text; + +namespace SheetMe.Core.Catalog; + +/// +/// 태그 검색·분류 — 피커가 "찾기"와 "둘러보기"를 둘 다 되게 하는 규칙. +/// +/// 태그 이름은 PAT_주민번호_Dash 처럼 접두어와 밑줄로 이미 구조가 있다. +/// 그 구조를 화면에 드러내지 않고 384줄을 한 덩어리로 늘어놓으면, +/// 무엇이 있는지 알 수 없어 결국 아는 이름만 반복해 쓰게 된다. +/// +public static class TagSearch +{ + #region Member Fields + // 초성 표 — 한글 음절 U+AC00~U+D7A3 은 (초성×21×28) 규칙으로 합성된다 + private const string Choseong = "ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ"; + private const char HangulBase = '가'; + private const char HangulLast = '힣'; + #endregion + + #region Methods + /// 그 어느 접두어 규약에도 안 맞는 태그가 모이는 곳 + public const string OtherCategory = "그 밖"; + + /// + /// 태그가 속한 분류 — 접두어로 가른다. + /// 레거시 bzDataInterface 의 메서드 이름 규약이 그대로 분류가 된다. + /// + /// 아는 접두어만 분류로 올린다. 나머지는 전부 '그 밖'으로 묶는다 — + /// 레거시에는 규약에서 벗어난 이름이 섞여 있어서(오타 ECT_로그인사용자싸인, + /// 접두어가 아예 없는 GetNurseShtCod), 접두어를 그대로 분류로 쓰면 + /// 1~3개짜리 분류가 목록을 채워 '무엇이 있는지' 를 오히려 가린다. + /// 액션 태그는 접두어 규약 자체가 없어 통째로 여기로 온다. + /// + public static string CategoryOf(string tag) + { + var underscore = tag.IndexOf('_'); + var prefix = underscore > 0 ? tag[..underscore] : tag; + return prefix switch + { + "PAT" => "환자", + "OCM" => "진료·오더", + "ETC" => "기관·문서", + "USR" => "사용자", + _ => OtherCategory, + }; + } + + /// + /// 분류 안의 소분류 — 두 번째 토큰. PAT_주민번호_Dash → "주민번호". + /// 같은 뿌리에서 갈라진 변형(_Dash, _Blind, _Refer)이 흩어지지 않고 붙어 보이게 한다. + /// + public static string GroupOf(string tag) + { + var parts = tag.Split('_'); + return parts.Length >= 2 ? parts[1] : tag; + } + + /// + /// 한글 문자열의 초성만 뽑는다 — 한글이 아니면 그대로 둔다. + /// "PAT_차트번호" → "PAT_ㅊㅌㅂㅎ" + /// + public static string ToChoseong(string text) + { + var builder = new StringBuilder(text.Length); + foreach (var ch in text) + { + if (ch is >= HangulBase and <= HangulLast) + { + builder.Append(Choseong[(ch - HangulBase) / (21 * 28)]); + } + else + { + builder.Append(ch); + } + } + return builder.ToString(); + } + + /// 입력이 초성만으로 이루어졌는지 — 그럴 때만 초성 대조를 시도한다 + public static bool IsChoseongQuery(string query) + { + if (query.Length == 0) + { + return false; + } + foreach (var ch in query) + { + if (Choseong.IndexOf(ch) < 0) + { + return false; + } + } + return true; + } + + /// + /// 한 토큰이 태그에 걸리는지 — 부분 일치, 그리고 초성 입력이면 초성 대조. + /// + /// 초성을 받는 이유는 한글 태그 이름이 길어서다. "ㅊㅌㅂㅎ" 네 글자로 + /// PAT_차트번호 에 닿을 수 있으면 손이 훨씬 덜 간다. + /// + public static bool Matches(string tag, string token) + { + if (tag.Contains(token, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + return IsChoseongQuery(token) + && ToChoseong(tag).Contains(token, StringComparison.Ordinal); + } + + /// 공백으로 나눈 여러 토큰을 전부 만족해야 통과(AND) + public static bool MatchesAll(string tag, IReadOnlyList tokens) + { + foreach (var token in tokens) + { + if (!Matches(tag, token)) + { + return false; + } + } + return true; + } + #endregion +} diff --git a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs index 821c804..c8efdcd 100644 --- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs @@ -719,6 +719,9 @@ public static class DbSmoke var unknown = ranked.Where(p => !catalog.Contains(p.Key)).ToList(); lines.Add($" 카탈로그에 없는 값: {unknown.Count}종 / {unknown.Sum(p => p.Value)}건"); lines.AddRange(unknown.Take(10).Select(p => $" {p.Key} ×{p.Value}")); + // 카탈로그 생성용 전량 덤프 — 피커가 건수를 보여 주려면 상위 30종으로는 부족하다 + lines.Add($" ══ 전량({key}) ══"); + lines.AddRange(ranked.Where(p => catalog.Contains(p.Key)).Select(p => $" §{p.Key}\t{p.Value}")); } if (perSheet.Count > 0) diff --git a/src/SheetMe.Designer/Diagnostics/DialogShots.cs b/src/SheetMe.Designer/Diagnostics/DialogShots.cs index d38e5ab..22c0b89 100644 --- a/src/SheetMe.Designer/Diagnostics/DialogShots.cs +++ b/src/SheetMe.Designer/Diagnostics/DialogShots.cs @@ -115,7 +115,11 @@ public static class DialogShots ("02-sheet-history", () => new Views.SheetHistoryDialogView("S999", "표본 서식", SampleVersions())), ("03-query-editor", () => new Views.QueryEditorWindow("데이터소스", "SELECT ChtNum, PatNam FROM P_PatMst\nWHERE ChtNum = <>")), - ("04-tag-picker", () => new Views.TagPickerDialogView("동작 태그", SampleTags(), SampleTags()[1])), + // 실제 카탈로그(384종)와 실제 제안 순위로 찍는다 — 표본 몇 개로는 분류·건수가 안 보인다 + ("04-tag-picker", () => new Views.TagPickerDialogView( + "데이터 태그 선택 — 자동 채움 원천(bzDataInterface)", + Core.Catalog.LegacyTagCatalog.DataInterfaceTags, null, + Core.Catalog.LegacyTagUsageCatalog.SuggestFor("DataInterfaceTag", "TextBox"))), ("05-mask-picker", () => new Views.MaskPickerDialogView("0000년 90월 90일")), // 데이터소스 배선 — 운영 다수파인 Rows 형 값을 넣어 되읽기·형태 유지가 보이게 찍는다 ("05b-datatable-field", () => new Views.DataTableFieldDialogView( diff --git a/src/SheetMe.Designer/Views/TagPickerDialogView.xaml b/src/SheetMe.Designer/Views/TagPickerDialogView.xaml index 28fc220..8e5aea6 100644 --- a/src/SheetMe.Designer/Views/TagPickerDialogView.xaml +++ b/src/SheetMe.Designer/Views/TagPickerDialogView.xaml @@ -1,24 +1,88 @@ - + + + + + + + + + + + + + + + + + + + + + + + TextWrapping="Wrap" Margin="0,0,0,10"/> - + + + + + - - + +