From 128aedc34f3ab569219dedfec52561e53653cca3 Mon Sep 17 00:00:00 2001 From: Msystech Date: Wed, 19 Aug 2026 10:56:42 +0900 Subject: [PATCH] =?UTF-8?q?=ED=99=98=EC=9E=90=20=ED=83=9C=EA=B7=B8=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=EC=9D=84=20=EB=A0=88=EA=B1=B0=EC=8B=9C=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EB=B3=B8=EB=AC=B8=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=EC=84=B8=EC=9A=B4=EB=8B=A4=20=E2=80=94=20?= =?UTF-8?q?=EC=85=8B=EC=9D=80=20=EC=A1=B4=EC=9E=AC=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EC=9D=B4=EB=A6=84=EC=9D=B4=EC=97=88?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "환자 태그이지만 아직 옮기지 않았습니다 엄청 많은데?" — 맞다. 그리고 옮겼다고 적어 둔 것도 절반이 거짓이었다. ## 지어낸 이름 셋 PAT_휴대전화 · PAT_전화번호 · PAT_주소 는 실존하지 않는 태그다. 컬럼 이름만 보고 태그 이름을 추측해 표에 넣었고, 이름이 안 맞으면 매칭이 안 되므로 값도 안 나오고 오류도 안 난다 — 화면에는 "아직 안 옮겼습니다"로 보인다. 실제 이름은 PAT_휴대전화번호 · PAT_자택전화번호 이고 PAT_주소 는 없다. 그래서 진단에 이름 검사를 넣었다 — 옮긴 이름이 전부 LegacyTagCatalog.DataInterfaceTags 에 있는지 본다. 대조군(없는 이름이 실제로 걸리는지)도 함께 둔다. ## PAT_주민번호 는 빼야 했다 레거시는 PatResNum_DEC(복호화 컬럼)를 읽는다(bzDataInterface.vb:87). P_PatInf 의 PatResNum 을 그대로 쓰면 암호화 병원에서 암호문이 종이에 찍힌다. 빈칸보다 나쁜 값이라 표에서 뺐다. ## 실제로 맞게 돌던 것은 2종이었다 PAT_이름 · PAT_차트번호. 앞서 "6종"이라고 보고한 것은 틀렸다. ## 이번에는 함수 본문을 읽고 넣었다 기계적으로 옮길 수 있는 것은 본문이 컬럼 하나를 그대로 돌려주는 5종뿐이다 (추출해서 셌다). 거기에 형식이 하나 붙는 생년월일을 더해 7종: PAT_이름 · PAT_영문이름 · PAT_휴대전화번호 · PAT_자택전화번호 · PAT_여권번호 PAT_차트번호(내원의 ComChtNum — moPatInfoBiz.ChtNum 이 그것이다, :70) PAT_생년월일(PatBthDay → YYYY-MM-DD, :188-198) 차트번호를 PatInf 에서 ComInf 로 고쳤고, 진단의 가상 문맥도 같이 고쳤다 — 가상 문맥이 옛 매핑을 쓰고 있으면 실제와 다른 것을 검사한다. ## 왜 나머지가 많이 남는가 (노력 부족이 아니다) - PAT_성별/한글성별 : CoiCalCod 가 "24" 로 시작하면 PatSexTyp, 아니면 GetSexAge(PatResNum_DEC) 로 주민번호에서 뽑는다(:283-316) — 복호화가 선행 조건이다. - PAT_나이 계열 : 병원 코드로 갈라지고 CGCH 는 자기 SQL 을 들고 있다(:373-). - 그 밖 238종 : 함수 본문에 자기 SQL 이 있다. 즉 다음 관문은 태그를 더 옮기는 것이 아니라 주민번호 복호화다. 그것이 풀리면 성별 계열이 따라 열린다. 짐작으로 채우면 종이에 그럴듯한 값이 찍히고, 그건 아무도 의심하지 않는다. ## 게이트 - dotnet test 323/323 - --edit-smoke 실패 0 (이름 검사 + 대조군 2건 신규) - --db-patient ①~⑱ 전건 통과 - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 Co-Authored-By: Claude Opus 5 --- src/SheetMe.Designer/Diagnostics/EditSmoke.cs | 22 ++++++- .../Services/PatientTagResolver.cs | 62 +++++++++++++++---- 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs index bfe3eb8..24e419a 100644 --- a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs @@ -530,12 +530,17 @@ public static class EditSmoke { var patInf = new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["PATCHTNUM"] = chart, ["PATNAM"] = name, }; + // 차트번호는 내원이 들고 있다 — 레거시 moPatInfoBiz.ChtNum 이 그것이다 + // (bzDataInterface.vb:70). 여기를 PatInf 로 두면 실제와 다른 것을 검사한다. + var comInf = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["COMCHTNUM"] = chart, + }; var blank = new Dictionary(StringComparer.OrdinalIgnoreCase); return new SheetMe.Data.Stores.PatientContext( - 90001m, "202608181500", patInf, blank, blank, blank, blank); + 90001m, "202608181500", patInf, comInf, blank, blank, blank); } var patientDoc = new FormDocument(); @@ -571,6 +576,19 @@ public static class EditSmoke return CollectText(visual); } + // ── 옮긴 태그 이름이 실존하는가 ── + // 이 검사가 없어서 지어낸 이름 셋(PAT_휴대전화·PAT_전화번호·PAT_주소)이 + // 표에 앉아 조용히 아무것도 안 하고 있었다. 이름이 틀리면 매칭이 안 되므로 + // 값도 안 나오고 오류도 안 난다 — 화면에는 "아직 안 옮겼습니다"로 보인다. + var bogus = Services.PatientTagResolver.MappedTags + .Where(t => !SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags.Contains(t)) + .ToList(); + Check("환자 태그: 옮긴 이름이 모두 실제 태그다", bogus.Count == 0, + $"카탈로그에 없는 이름: {string.Join(", ", bogus)}"); + // 대조군 — 없는 이름이 실제로 걸리는지. 통과만 보면 검사가 도는지 알 수 없다. + Check("환자 태그(대조군): 없는 이름은 걸러진다", + !SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags.Contains("PAT_주소")); + // 대조군 — 환자 없이. 이 줄이 없으면 아래 판정은 "원래 그랬던 것"과 구분되지 않는다. var withoutPatient = PatientPaper(new StubTagResolver()); Check("환자 태그(대조군): 환자가 없으면 값이 아니라 사유다", diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs index f4adb6e..4498a99 100644 --- a/src/SheetMe.Designer/Services/PatientTagResolver.cs +++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs @@ -19,18 +19,45 @@ namespace SheetMe.Designer.Services; public sealed class PatientTagResolver : ITagValueResolver { #region Member Fields - /// 태그 → (문맥 행, 컬럼). 전부 조회 SQL 에서 컬럼을 확인한 것이다. - private static readonly Dictionary Direct = + /// + /// 태그 → (문맥 행, 컬럼, 형식). 전부 레거시 함수 본문을 읽어 확인한 것이다 + /// ([001]Common\[012]DataTag\M.CMM.DataTag\bzDataInterface.vb). + /// + /// 처음에 컬럼 이름만 보고 여섯 개를 넣었는데 셋은 태그 이름 자체가 없었다 — + /// PAT_휴대전화·PAT_전화번호·PAT_주소 는 내가 지어낸 이름이라 아무것도 매칭되지 않았다. + /// 실제 이름은 PAT_휴대전화번호·PAT_자택전화번호 이고 PAT_주소 는 존재하지 않는다. + /// 그래서 이름은 에 있는지 진단이 검사한다. + /// + private static readonly Dictionary? Format)> Direct = new(StringComparer.Ordinal) { - // P_PatInf — dtPatVisitList.vb:51 SELECT 목록에서 확인 - ["PAT_이름"] = ("PatInf", "PatNam"), - ["PAT_차트번호"] = ("PatInf", "PatChtNum"), - ["PAT_주민번호"] = ("PatInf", "PatResNum"), - ["PAT_휴대전화"] = ("PatInf", "PatMblPhn"), - ["PAT_전화번호"] = ("PatInf", "PatHomPhn"), - ["PAT_주소"] = ("PatInf", "PatAdrDtl"), + // 본문이 컬럼 하나를 그대로 돌려주는 것들(bzDataInterface.vb:74, 79, 그리고 동형 3개) + ["PAT_이름"] = ("PatInf", "PatNam", null), + ["PAT_영문이름"] = ("PatInf", "PatEngNam", null), + ["PAT_휴대전화번호"] = ("PatInf", "PatMblPhn", null), + ["PAT_자택전화번호"] = ("PatInf", "PatHomPhn", null), + ["PAT_여권번호"] = ("PatInf", "PatPspNum", null), + + // moPatInfoBiz.ChtNum — 내원의 차트번호(:70) + ["PAT_차트번호"] = ("ComInf", "ComChtNum", null), + + // PatBthDay 를 YYYY-MM-DD 로(:188-198). 형식이 붙으므로 컬럼만으로는 안 된다 + ["PAT_생년월일"] = ("PatInf", "PatBthDay", Hyphenate), + + // ── 넣지 않은 것과 그 이유 ── + // PAT_주민번호 : 레거시는 PatResNum_DEC(복호화 컬럼)를 읽는다(:87). + // P_PatInf 의 PatResNum 을 그대로 쓰면 암호화 병원에서 암호문이 종이에 찍힌다. + // 처음에 그렇게 넣어 뒀다 — 빈칸보다 나쁜 값이었다. + // PAT_성별 / PAT_한글성별 : CoiCalCod 가 "24" 로 시작하면 PatSexTyp, 아니면 + // GetSexAge(PatResNum_DEC) 로 주민번호에서 뽑는다(:283-316). 복호화가 먼저다. + // PAT_나이 계열 : 병원 코드로 갈라지고(CGCH 는 자기 SQL) 계산식이 제각각이다(:373-). }; + #endregion + + #region Member Fields + /// YYYYMMDD → YYYY-MM-DD. 8자리가 아니면 손대지 않는다 — 잘라서 그럴듯하게 만들지 않는다 + private static string Hyphenate(string value) + => value.Length == 8 ? $"{value[..4]}-{value[4..6]}-{value[6..8]}" : value; private readonly ITagValueResolver next; private readonly PatientContext context; @@ -67,13 +94,22 @@ public sealed class PatientTagResolver : ITagValueResolver "CoiInf" => context.CoiInf, _ => context.CowInf, }; - var value = PatientContext.Value(row, map.Column); - return value.Length > 0 - ? new TagValue(true, value) - : new TagValue(false, $"이 환자에게는 {map.Column} 값이 비어 있습니다"); + var raw = PatientContext.Value(row, map.Column); + if (raw.Length == 0) + { + return new TagValue(false, $"이 환자에게는 {map.Column} 값이 비어 있습니다"); + } + var value = map.Format is null ? raw : map.Format(raw); + return new TagValue(true, value); } /// 지금 옮겨진 환자 태그 수 — 화면이 커버리지를 정직하게 말할 때 쓴다 public static int MappedCount => Direct.Count; + + /// + /// 옮긴 태그 이름들 — 진단이 실존하는 태그인지 검사한다. + /// 이 검사가 없어서 지어낸 이름 셋이 조용히 아무것도 안 하고 있었다. + /// + public static IReadOnlyCollection MappedTags => Direct.Keys; #endregion }