From e62e43d176412f29a54f7fa0c930d240e24f0921 Mon Sep 17 00:00:00 2001 From: Msystech Date: Wed, 19 Aug 2026 14:54:18 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A7=84=EB=A3=8C=EA=B3=BC=C2=B7=EB=B3=91?= =?UTF-8?q?=EB=8F=99=C2=B7=EB=B3=91=EC=8B=A4=C2=B7=EC=9A=94=EC=96=91?= =?UTF-8?q?=EA=B8=B0=EA=B4=80=EB=AA=85=EC=9D=84=20=EC=98=AE=EA=B8=B4?= =?UTF-8?q?=EB=8B=A4=20=E2=80=94=2026=EC=A2=85=20=E2=86=92=2030=EC=A2=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자가 서식에서 빈칸으로 확인한 자리들이다(B·C 배치). ## 본문에서 확인한 것 - OCM_병동 / OCM_병실 (bzDataInterface.vb:6718, 6801) 문맥의 CowInf 행에서 코드값 그대로다(CowWadCod·CowRomCod). 마스터로 이름을 찾지 않는다 — 레거시가 코드를 그대로 찍는다. 외래는 CowInf 가 0행이라 자연히 빈 값(레거시도 외래·비낮병동은 ""). - OCM_진료과 (:5437-5495) M_DepMst 를 CodDepCod + 접수일자 BETWEEN 으로 찾는다. 유효기간 일자는 입원·외래 무관하게 접수일자다(:5445). 이름 선택: NH 는 DepOfcNam, 그 밖은 개원일 2022-07-01 이후 + DepOfcNam 존재 시 DepOfcNam, 아니면 DepKorNam. 개원일은 세션에 없어 빈 값 결정(기존과 동일)이라 그 갈래는 DepKorNam 으로 간다. - ETC_요양기관명칭_병원명 (:15792-15860) M_DepMst ⨝ M_HspMst(DepHspCod=HspCod) 에서 HspInsNam.Trim. 진료과 코드로 병원을 찾는 구조다(다부지 구성). 판정 일자가 진료과와 다르다 — ILV 는 퇴원일시·재원 중은 지금·외래는 접수일시. 문맥의 적용일시가 정확히 그 판정이라 AdpDtm 을 그대로 쓴다. 원본 마스터 조회에 ORDER BY 가 없어 담당의 때와 같은 규칙(최신순 1행)으로 고정했다. ## 왕복은 환자당 두 번 추가 진료과·요양기관명을 환자를 붙일 때 한 번씩 읽는다(주민번호·주소·담당의와 같은 정책). 미리보기 창과 Ctrl+P 인쇄(PatientSession.ResolversFor) 둘 다 같은 재료를 받는다. ## 실DB 확인 --db-patient ㉓·㉔ 신규: 진료과 코드 '0506' → 44열·한글명 5자, 요양기관명 6자. 코드가 이름이 되는 것까지 확인해야 태그가 값을 낸다. ## 게이트 - dotnet test 338/338 · --edit-smoke 실패 0(이름 검사 30종) - --db-patient ①~㉔ 전건 통과 - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 Co-Authored-By: Claude Opus 5 --- .../Stores/PatientContextStore.cs | 47 ++++++++++++++++ src/SheetMe.Designer/Diagnostics/DbSmoke.cs | 19 +++++++ .../Services/PatientIdentity.cs | 48 ++++++++++++++++ .../Services/PatientSession.cs | 3 +- .../Services/PatientTagResolver.cs | 56 ++++++++++++++++++- .../Views/PreviewWindow.xaml.cs | 3 +- 6 files changed, 173 insertions(+), 3 deletions(-) diff --git a/src/SheetMe.Data/Stores/PatientContextStore.cs b/src/SheetMe.Data/Stores/PatientContextStore.cs index ae30734..fcdf861 100644 --- a/src/SheetMe.Data/Stores/PatientContextStore.cs +++ b/src/SheetMe.Data/Stores/PatientContextStore.cs @@ -173,6 +173,53 @@ public sealed class PatientContextStore ("c", code)); } + /// + /// 진료과 마스터 한 행 — OCM_진료과(bzDataInterface.vb:5437-5495)의 조회. + /// WHERE DepCod = :d AND :day BETWEEN DepStrDte AND DepEndDte. + /// + /// 유효기간 판정 일자는 레거시가 입원·외래 무관하게 접수일자를 쓴다(:5445 sAcpDte). + /// 원본에는 ORDER BY 가 없고 Rows(0) 을 집는다 — 담당의 때와 같은 이유로 + /// 최신순 1행으로 고정한다(기간이 겹친 이력에서 비결정적이 되는 것을 막는다). + /// + public IReadOnlyDictionary Department(string? depCod, string? day) + { + var code = (depCod ?? string.Empty).Trim(); + var date = (day ?? string.Empty).Trim(); + if (code.Length == 0 || date.Length < 8) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + using var connection = new OracleConnection(connectionString); + connection.Open(); + return Row(connection, + "SELECT * FROM (SELECT * FROM M_DepMst WHERE TRIM(DepCod) = :d" + + " AND :y BETWEEN DepStrDte AND DepEndDte ORDER BY DepStrDte DESC) WHERE ROWNUM = 1", + ("d", code), ("y", date[..8])); + } + + /// + /// 요양기관명 — ETC_요양기관명칭_병원명(bzDataInterface.vb:15792-15860)의 조회. + /// M_DepMst ⨝ M_HspMst(DepHspCod=HspCod) 에서 HspInsNam 하나를 뽑아 Trim. + /// 진료과 코드로 병원을 찾는 구조다 — 다부지(병원 코드가 다른 진료과) 구성 때문으로 보인다. + /// + public string HospitalName(string? depCod, string? day) + { + var code = (depCod ?? string.Empty).Trim(); + var date = (day ?? string.Empty).Trim(); + if (code.Length == 0 || date.Length < 8) + { + return string.Empty; + } + using var connection = new OracleConnection(connectionString); + connection.Open(); + var row = Row(connection, + "SELECT HspInsNam FROM (SELECT HspInsNam FROM M_DepMst" + + " INNER JOIN M_HspMst ON DepHspCod = HspCod AND :y BETWEEN HspStrDte AND HspEndDte" + + " WHERE TRIM(DepCod) = :d AND :y BETWEEN DepStrDte AND DepEndDte) WHERE ROWNUM = 1", + ("d", code), ("y", date[..8])); + return PatientContext.Value(row, "HspInsNam"); + } + /// /// 건물관리번호로 주소 — 레거시 GetZipInfoByBuildingNumber(clsCommonLib.vb:16600). /// diff --git a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs index fe6f2bb..4c6d5cb 100644 --- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs @@ -2186,6 +2186,25 @@ public static class DbSmoke + $" 이름 {uidNam.Length}자 (값은 기록하지 않는다)"); Check("㉒ 담당의 조회가 예외 없이 돌고 행을 돌려준다", doctor.Count > 0, "0열 — 적용일시 BETWEEN 조건이나 CodDtrCod 를 의심할 것"); + + // ㉓ 진료과·요양기관명 — 같은 내원의 CodDepCod 로. + // 코드가 이름이 되는 것까지 확인해야 태그가 값을 낸다. + if (ownerCtx is { } octx) + { + var depCod = PatientContext.Value(octx.CodInf, "CodDepCod"); + var acp = PatientContext.Value(octx.ComInf, "ComAcpDtm"); + var dep = contexts.Department(depCod, acp); + var korNam = PatientContext.Value(dep, "DepKorNam"); + lines.Add($" 진료과 조회: 코드 '{depCod}' → {dep.Count}열," + + $" 한글명 {korNam.Length}자"); + Check("㉓ 진료과 마스터가 이름을 돌려준다", korNam.Trim().Length > 0, + $"코드 '{depCod}' 로 M_DepMst 에서 이름을 못 찾았다"); + + var hosp = contexts.HospitalName(depCod, octx.AdpDtm); + lines.Add($" 요양기관명: {hosp.Length}자"); + Check("㉔ 요양기관명이 나온다", hosp.Trim().Length > 0, + "M_DepMst⨝M_HspMst 조인이 0행 — 유효기간이나 DepHspCod 를 의심할 것"); + } } } } diff --git a/src/SheetMe.Designer/Services/PatientIdentity.cs b/src/SheetMe.Designer/Services/PatientIdentity.cs index fa8214b..b1994f4 100644 --- a/src/SheetMe.Designer/Services/PatientIdentity.cs +++ b/src/SheetMe.Designer/Services/PatientIdentity.cs @@ -94,6 +94,54 @@ public static class PatientIdentity } } + /// + /// 이 내원의 진료과 마스터 행 — 못 읽으면 빈 사전. + /// 진료과 코드는 문맥의 CodInf(적용일시로 고른 행)에서, 유효기간 일자는 + /// 레거시대로 접수일자다(bzDataInterface.vb:5445 — 입원·외래 무관). + /// + public static IReadOnlyDictionary DepartmentOf(PatientContext context) + { + var connection = ConfigService.Current.ConnectionString; + var empty = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (connection.Length == 0) + { + return empty; + } + try + { + return new PatientContextStore(connection).Department( + PatientContext.Value(context.CodInf, "CodDepCod"), + PatientContext.Value(context.ComInf, "ComAcpDtm")); + } + catch + { + return empty; + } + } + + /// + /// 요양기관명 — 못 읽으면 빈 문자열. + /// 판정 일자가 진료과와 다르다(bzDataInterface.vb:15815-15825): + /// 입원·퇴원(ILV)은 퇴원일시, 입원 중은 지금, 외래는 접수일시 — 문맥의 적용일시가 그 판정과 같다. + /// + public static string HospitalNameOf(PatientContext context) + { + var connection = ConfigService.Current.ConnectionString; + if (connection.Length == 0) + { + return string.Empty; + } + try + { + return new PatientContextStore(connection).HospitalName( + PatientContext.Value(context.CodInf, "CodDepCod"), context.AdpDtm); + } + catch + { + return string.Empty; + } + } + /// /// 이 환자의 주소 조각들(구역번호·도로명·지번·영문) — 못 읽으면 빈 사전. /// diff --git a/src/SheetMe.Designer/Services/PatientSession.cs b/src/SheetMe.Designer/Services/PatientSession.cs index 734c4a3..e8a3692 100644 --- a/src/SheetMe.Designer/Services/PatientSession.cs +++ b/src/SheetMe.Designer/Services/PatientSession.cs @@ -66,7 +66,8 @@ public static class PatientSession SheetMe.Core.Catalog.ITagValueResolver tags = Current is { } picked ? new PatientTagResolver(picked, session, PatientIdentity.ResidentNumberOf(picked), PatientIdentity.AddressOf(picked), - PatientIdentity.DoctorOf(picked)) + PatientIdentity.DoctorOf(picked), PatientIdentity.DepartmentOf(picked), + PatientIdentity.HospitalNameOf(picked)) : session; return (tags, new MDataTableRunner(document, Variables())); } diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs index 136694f..e548087 100644 --- a/src/SheetMe.Designer/Services/PatientTagResolver.cs +++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs @@ -81,6 +81,12 @@ public sealed class PatientTagResolver : ITagValueResolver ["OCM_외래내원일자"] = c => Hyphenate(Head(Com(c, "ComAcpDtm"), 8)), ["OCM_입원전환일자"] = c => Hyphenate(Head(Com(c, "ComTrsDtm"), 8)), ["OCM_입원전환일시"] = c => Stamp(Com(c, "ComTrsDtm")), + + // 병동·병실 — 문맥의 CowInf 행에서 코드값 그대로다(bzDataInterface.vb:6734, 6817). + // 마스터로 이름을 찾지 않는다 — 레거시가 CowWadCod/CowRomCod 를 그대로 찍는다. + // 외래는 CowInf 가 0행이라 자연히 빈 값이 된다(레거시도 외래·비낮병동은 "" 다). + ["OCM_병동"] = c => PatientContext.Value(c.CowInf, "CowWadCod"), + ["OCM_병실"] = c => PatientContext.Value(c.CowInf, "CowRomCod"), }; private static string Pat(PatientContext c, string column) => PatientContext.Value(c.PatInf, column); @@ -134,15 +140,24 @@ public sealed class PatientTagResolver : ITagValueResolver /// 이 내원의 담당의 행(M_UidMst) — 없으면 빈 사전 private readonly IReadOnlyDictionary doctor; + /// 이 내원의 진료과 마스터 행(M_DepMst) — 없으면 빈 사전 + private readonly IReadOnlyDictionary department; + + /// 요양기관명(HspInsNam) — 못 읽었으면 빈 문자열 + private readonly string hospitalName; + public PatientTagResolver(PatientContext context, ITagValueResolver next, string residentNumber = "", IReadOnlyDictionary? address = null, - IReadOnlyDictionary? doctor = null) + IReadOnlyDictionary? doctor = null, + IReadOnlyDictionary? department = null, string hospitalName = "") { this.context = context; this.next = next; this.residentNumber = residentNumber; this.address = address ?? new Dictionary(StringComparer.OrdinalIgnoreCase); this.doctor = doctor ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + this.department = department ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + this.hospitalName = hospitalName; } #endregion @@ -162,6 +177,10 @@ public sealed class PatientTagResolver : ITagValueResolver { return doctorValue; } + if (FromFacility(tag) is { } facilityValue) + { + return facilityValue; + } if (Computed.TryGetValue(tag, out var compute)) { var made = compute(context); @@ -295,6 +314,40 @@ public sealed class PatientTagResolver : ITagValueResolver : "담당의에게 그 값이 없습니다"); } + /// + /// 진료과·요양기관명 — 해당 태그가 아니면 null. + /// + /// OCM_진료과 의 이름 선택(bzDataInterface.vb:5474-5484): + /// NH 는 무조건 DepOfcNam, 그 밖은 개원일이 2022-07-01 이후이고 DepOfcNam 이 있으면 + /// DepOfcNam, 아니면 DepKorNam. 개원일(HspStrDte)은 세션에 없어 빈 값으로 두므로 + /// (FormDesignDataBusiness.HospitalStartDate 와 같은 결정) 그 갈래는 DepKorNam 으로 간다. + /// + private TagValue? FromFacility(string tag) + { + switch (tag) + { + case "OCM_진료과": + { + string Dep(string column) => department.TryGetValue(column, out var v) ? v.Trim() : string.Empty; + var office = Dep("DepOfcNam"); + var value = string.Equals(UserSession.Current.HspCod, "NH", StringComparison.OrdinalIgnoreCase) + ? office + : Dep("DepKorNam"); + return value.Length > 0 + ? new TagValue(true, value) + : new TagValue(false, department.Count == 0 + ? "이 내원의 진료과를 찾지 못했습니다" + : "진료과 마스터에 이름이 비어 있습니다"); + } + case "ETC_요양기관명칭_병원명": + return hospitalName.Trim().Length > 0 + ? new TagValue(true, hospitalName.Trim()) + : new TagValue(false, "요양기관명을 찾지 못했습니다(M_DepMst·M_HspMst 조인)"); + default: + return null; + } + } + /// 주민번호에서 나온 값 — 비면 비었는지 말한다 private TagValue Wrap(string value) => value.Length > 0 ? new TagValue(true, value) @@ -309,6 +362,7 @@ public sealed class PatientTagResolver : ITagValueResolver "PAT_주민번호", "PAT_주민번호_Dash", "PAT_성별", "PAT_한글성별", "PAT_우편번호", "PAT_도로명주소", "PAT_지번주소", "PAT_영문도로명주소", "OCM_담당의사", "OCM_담당의사_담당의사코드", "OCM_담당의사_연락처", + "OCM_진료과", "ETC_요양기관명칭_병원명", }; /// diff --git a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs index 595a9d1..65913e2 100644 --- a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs +++ b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs @@ -223,7 +223,8 @@ public partial class PreviewWindow : Window ? session : new PatientTagResolver(picked, session, PatientIdentity.ResidentNumberOf(picked), PatientIdentity.AddressOf(picked), - PatientIdentity.DoctorOf(picked)); + PatientIdentity.DoctorOf(picked), PatientIdentity.DepartmentOf(picked), + PatientIdentity.HospitalNameOf(picked)); // 러너를 새로 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아 // 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다. fields = new MDataTableRunner(designer.Document,