diff --git a/src/SheetMe.Data/Stores/PatientContextStore.cs b/src/SheetMe.Data/Stores/PatientContextStore.cs index eb500fd..cf080f1 100644 --- a/src/SheetMe.Data/Stores/PatientContextStore.cs +++ b/src/SheetMe.Data/Stores/PatientContextStore.cs @@ -94,6 +94,50 @@ public sealed class PatientContextStore return new PatientContext(comNum, adpDtm, patInf, comInf, codInf, coiInf, cowInf); } + /// + /// 특정 시점의 진료과 코드 — 레거시 GetCodInfDT(comNum, moment, 1) 의 첫 행 대응. + /// 입원과(접수 시점)·퇴원과(퇴원/현재 시점) 태그가 문맥의 적용일시와 다른 시점을 + /// 물을 때 쓴다. 원문 GetCodInfDT 는 ORDER BY 가 없어 임의 행이었다 — 문맥 로드와 + /// 같은 규칙(시작일시 최신 1행)으로 고정한다. moment 가 비면 서버 현재시각이다. + /// + public string DepartmentCodeAt(decimal comNum, string? moment) + { + using var connection = new OracleConnection(connectionString); + connection.Open(); + var at = (moment ?? string.Empty).Trim(); + var row = Row(connection, + "SELECT CodDepCod FROM (SELECT CodDepCod FROM P_CodInf WHERE CodComNum = :k AND CodMtiSeq = 0" + + (at.Length >= 12 + ? " AND :t BETWEEN CodStrDtm AND CodEndDtm" + : " AND TO_CHAR(SYSDATE, 'YYYYMMDDHH24MI') BETWEEN CodStrDtm AND CodEndDtm") + + " ORDER BY CodStrDtm DESC) WHERE ROWNUM = 1", + at.Length >= 12 + ? new (string, object)[] { ("k", comNum), ("t", at[..12]) } + : new (string, object)[] { ("k", comNum) }); + return PatientContext.Value(row, "CodDepCod"); + } + + /// + /// 퇴원과 조회(bzDataInterface.vb:6162-6231) — 내원의 퇴원일시(또는 접수일시)가 + /// 유효기간 안인 진료과 행. 원문이 P_ComInf 에 M_DepMst 를 날짜 조건만으로 조인하는 + /// 특이한 모양이라 그대로 옮겼다. anchorColumn 은 ComLevDtm 또는 ComAcpDtm. + /// + public IReadOnlyDictionary DischargeDepartment(decimal comNum, string depCod, string anchorColumn) + { + if (anchorColumn is not ("ComLevDtm" or "ComAcpDtm")) + { + throw new ArgumentOutOfRangeException(nameof(anchorColumn)); + } + using var connection = new OracleConnection(connectionString); + connection.Open(); + return Row(connection, + "SELECT * FROM (SELECT DepOfcNam, DepKorNam FROM P_ComInf" + + " INNER JOIN M_DepMst ON DepCod = :d" + + $" AND {anchorColumn} BETWEEN DepStrDte AND DepEndDte || '2359'" + + " WHERE ComNum = :k) WHERE ROWNUM <= 1", + ("d", depCod), ("k", comNum)); + } + /// /// 한 행을 컬럼명 → 값 사전으로. 없으면 빈 사전. /// 컬럼명은 대문자로 담는다 — 오라클이 대문자로 돌려주고, 호출부가 대소문자로 헷갈리면 diff --git a/src/SheetMe.Designer/Services/PatientIdentity.cs b/src/SheetMe.Designer/Services/PatientIdentity.cs index 569f364..bb7cfb2 100644 --- a/src/SheetMe.Designer/Services/PatientIdentity.cs +++ b/src/SheetMe.Designer/Services/PatientIdentity.cs @@ -185,6 +185,13 @@ public static class PatientIdentity return () => connection.Length == 0 ? null : new PatientExtraStore(connection); } + /// 문맥 저장소 공급자 — 입원과·퇴원과의 시점별 진료과 조회용 + public static Func ContextStoreSource() + { + var connection = ConfigService.Current.ConnectionString; + return () => connection.Length == 0 ? null : new PatientContextStore(connection); + } + /// 수술 일정(S_OprInf) 공급자 — 최신순 여부별 1회 public static Func> ScheduledSurgerySourceFor(PatientContext context) { diff --git a/src/SheetMe.Designer/Services/PatientSession.cs b/src/SheetMe.Designer/Services/PatientSession.cs index 616d463..6e7feed 100644 --- a/src/SheetMe.Designer/Services/PatientSession.cs +++ b/src/SheetMe.Designer/Services/PatientSession.cs @@ -75,7 +75,7 @@ public static class PatientSession PatientIdentity.VitalSourceFor(picked), PatientIdentity.ScheduledSurgerySourceFor(picked), PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource(), - PatientIdentity.ExtraStoreSource()) + PatientIdentity.ExtraStoreSource(), PatientIdentity.ContextStoreSource()) : session; return (tags, new MDataTableRunner(document, Variables())); } diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs index 581c32f..5472038 100644 --- a/src/SheetMe.Designer/Services/PatientTagResolver.cs +++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs @@ -231,6 +231,14 @@ public sealed class PatientTagResolver : ITagValueResolver private readonly Dictionary extraCache = new(StringComparer.Ordinal); + /// + /// 문맥 저장소 공급자 — 입원과·퇴원과처럼 문맥의 적용일시와 다른 시점을 + /// 물어야 하는 태그가 쓴다(접수 시점·퇴원 시점의 진료과). + /// + private readonly Func? contextStoreSource; + + private readonly Dictionary deptAtCache = new(StringComparer.Ordinal); + private readonly Dictionary>> reviewCache = new(); public PatientTagResolver(PatientContext context, ITagValueResolver next, @@ -246,7 +254,8 @@ public sealed class PatientTagResolver : ITagValueResolver Func>? scheduledSurgerySource = null, Func>? pathologyDoctorSource = null, Func>? labDoctorSource = null, - Func? extraStoreSource = null) + Func? extraStoreSource = null, + Func? contextStoreSource = null) { this.context = context; this.next = next; @@ -264,6 +273,7 @@ public sealed class PatientTagResolver : ITagValueResolver this.pathologyDoctorSource = pathologyDoctorSource; this.labDoctorSource = labDoctorSource; this.extraStoreSource = extraStoreSource; + this.contextStoreSource = contextStoreSource; } #endregion @@ -319,6 +329,10 @@ public sealed class PatientTagResolver : ITagValueResolver { return stayValue; } + if (FromDeptAt(tag) is { } deptAtValue) + { + return deptAtValue; + } if (Computed.TryGetValue(tag, out var compute)) { var made = compute(context); @@ -514,14 +528,28 @@ public sealed class PatientTagResolver : ITagValueResolver /// private TagValue? FromFacility(string tag) { + string Dep(string column) => department.TryGetValue(column, out var v) ? v.Trim() : string.Empty; + TagValue DeptColumn(string column, string reasonWhenEmpty) + { + if (department.Count == 0) + { + return new TagValue(false, "이 내원의 진료과를 찾지 못했습니다"); + } + var value = Dep(column); + return value.Length > 0 ? new TagValue(true, value) : new TagValue(false, reasonWhenEmpty); + } switch (tag) { case "OCM_진료과": { - string Dep(string column) => department.TryGetValue(column, out var v) ? v.Trim() : string.Empty; + // 이름 선택(bzDataInterface.vb:5474-5484): NH 는 무조건 DepOfcNam, + // 그 밖은 개원일 2022-07-01 이후 + DepOfcNam 있음 → DepOfcNam, 아니면 DepKorNam. + // 개원일은 요양기관 행의 HspStrDte 다 — 처음엔 없어서 DepKorNam 으로 고정했었다. var office = Dep("DepOfcNam"); var value = string.Equals(UserSession.Current.HspCod, "NH", StringComparison.OrdinalIgnoreCase) ? office + : string.CompareOrdinal(PatientContext.Value(hospital, "HspStrDte"), "20220701") >= 0 + && office.Length > 0 ? office : Dep("DepKorNam"); return value.Length > 0 ? new TagValue(true, value) @@ -529,6 +557,20 @@ public sealed class PatientTagResolver : ITagValueResolver ? "이 내원의 진료과를 찾지 못했습니다" : "진료과 마스터에 이름이 비어 있습니다"); } + // ── 진료과 파생 4종 — 같은 행(M_DepMst SELECT *)의 다른 컬럼(:5556-6007) ── + // 레거시의 시점(퇴원했으면 퇴원 시점, 재원 중이면 현재, 외래면 접수)은 문맥 + // 적용일시와 같은 판정이다 — 자격이 퇴원보다 먼저 끝난 희귀 케이스에서만 갈린다. + case "OCM_진료과_그룹코드": + // 행이 있으면 무조건 "이름(그룹코드)" 결합이다(:5593) — 조각이 비어도 그대로 + return department.Count == 0 + ? new TagValue(false, "이 내원의 진료과를 찾지 못했습니다") + : new TagValue(true, $"{Dep("DepKorNam")}({Dep("DepGrpCod")})"); + case "OCM_진료과_영어": + return DeptColumn("DepEngNam", "진료과 마스터에 영문 이름이 비어 있습니다"); + case "OCM_진료과대외명칭": + return DeptColumn("DepOfcNam", "진료과 마스터에 대외 명칭이 비어 있습니다"); + case "OCM_진료과약어명칭": + return DeptColumn("DepBrfNam", "진료과 마스터에 약어 명칭이 비어 있습니다"); // ── 요양기관 12종 — 같은 조인의 다른 컬럼(bzDataInterface.vb:15656-16709) ── // 접미·결합도 본문 그대로: 병원장 = 병원명+"장", 귀하 = +"장 귀하", // 전화_팩스 = "Tel : … FAX : …", 영문판은 "+82 - " 접두가 붙는다. @@ -895,6 +937,105 @@ public sealed class PatientTagResolver : ITagValueResolver : new TagValue(false, "수술일자 값이 형식에 맞지 않습니다"); } + /// + /// 입원과·퇴원과 4종(bzDataInterface.vb:6009-6318) — 문맥의 적용일시와 다른 시점의 + /// 진료과를 묻는다. 입원과는 접수 시점(전과했으면 처음 과), 퇴원과는 + /// 퇴원(ILV → ComLevDtm) 또는 현재 시점의 과다. + /// + /// 퇴원과 본체(:6194-6210)는 "낮병동은 퇴원일시가 빈다"는 주석과 달리 If/Else 가 + /// 똑같은 쿼리다(복붙 결함으로 보인다 — ComLevDtm 이 비면 0행 → 빈 값). + /// 한글명칭(:6265-6281)만 빈 퇴원일시에서 접수일시로 대신한다. 둘 다 그대로 옮겼다. + /// + private TagValue? FromDeptAt(string tag) + { + var admission = tag is "OCM_입원과" or "OCM_입원과_한글명칭"; + var discharge = tag is "OCM_퇴원과" or "OCM_퇴원과_한글명칭"; + if (!admission && !discharge) + { + return null; + } + if (deptAtCache.TryGetValue(tag, out var cached)) + { + return cached; + } + var value = Compute(); + deptAtCache[tag] = value; + return value; + + TagValue Compute() + { + if (contextStoreSource is null) + { + return new TagValue(false, "진료과 시점 조회가 연결되지 않았습니다"); + } + var comNum = PatientContext.Value(context.ComInf, "ComNum"); + if (!decimal.TryParse(comNum, out var m)) + { + return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); + } + try + { + var store = contextStoreSource(); + if (store is null) + { + return new TagValue(false, "진료과 시점 조회가 연결되지 않았습니다"); + } + var open = PatientContext.Value(hospital, "HspStrDte"); + string PickName(IReadOnlyDictionary row) + { + // 개원일 2022-07-01 이후 + 대외명칭 있음 → 대외명칭, 아니면 한글명(:6044-6048) + var office = PatientContext.Value(row, "DepOfcNam").Trim(); + return string.CompareOrdinal(open, "20220701") >= 0 && office.Length > 0 + ? office + : PatientContext.Value(row, "DepKorNam").Trim(); + } + if (admission) + { + var acp = PatientContext.Value(context.ComInf, "ComAcpDtm"); + if (acp.Length < 12) + { + return new TagValue(false, "이 내원의 접수일시를 읽지 못했습니다"); + } + var depCod = store.DepartmentCodeAt(m, acp[..12]); + if (depCod.Length == 0) + { + return new TagValue(false, "접수 시점의 진료 행(P_CodInf)이 없습니다"); + } + var row = store.Department(depCod, acp[..8]); + if (row.Count == 0) + { + return new TagValue(true, string.Empty); // 레거시도 마스터 0행이면 "" 다 + } + return new TagValue(true, tag == "OCM_입원과" + ? PickName(row) + : PatientContext.Value(row, "DepKorNam").Trim()); + } + var prg = PatientContext.Value(context.ComInf, "ComPrgStt"); + var lev = PatientContext.Value(context.ComInf, "ComLevDtm"); + var depAtLeave = store.DepartmentCodeAt(m, + prg == "ILV" && lev.Length >= 12 ? lev[..12] : null); + if (depAtLeave.Length == 0) + { + return new TagValue(true, string.Empty); // 레거시가 명시적으로 "" 를 돌려준다(:6189) + } + var anchor = tag == "OCM_퇴원과_한글명칭" && lev.Trim().Length == 0 + ? "ComAcpDtm" : "ComLevDtm"; + var deptRow = store.DischargeDepartment(m, depAtLeave, anchor); + if (deptRow.Count == 0) + { + return new TagValue(true, string.Empty); + } + return new TagValue(true, tag == "OCM_퇴원과" + ? PickName(deptRow) + : PatientContext.Value(deptRow, "DepKorNam").Trim()); + } + catch (Exception ex) + { + return new TagValue(false, $"진료과 시점 조회에 실패했습니다: {ex.GetType().Name}"); + } + } + } + /// /// 입·퇴원 일시와 재원일수 12종(bzDataInterface.vb:7152-7940, 전부 Case Else 갈래). /// @@ -1275,6 +1416,8 @@ public sealed class PatientTagResolver : ITagValueResolver "PAT_건보_급여_세대주명", "PAT_협력업체", "PAT_보호자연락처", "OCM_FollowUp", "OCM_외출외박신청일", "OCM_외출외박종료일", "OCM_최초내원일", "OCM_초진일_발병일", "OCM_신환_초진일", + "OCM_진료과_그룹코드", "OCM_진료과_영어", "OCM_진료과대외명칭", "OCM_진료과약어명칭", + "OCM_입원과", "OCM_입원과_한글명칭", "OCM_퇴원과", "OCM_퇴원과_한글명칭", "OCM_입원시간", "OCM_입원일시", "OCM_입원일시_영문", "OCM_입실시간", "OCM_의사퇴원예고일시", "OCM_퇴원예정일시", "OCM_퇴원시간", "OCM_퇴원일시", "OCM_퇴원예고일시", "OCM_재원일수", "OCM_퇴원예고재원일수", "OCM_입원일자_낮병동", diff --git a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs index 4067cc0..fdc0dcd 100644 --- a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs +++ b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs @@ -232,7 +232,7 @@ public partial class PreviewWindow : Window PatientIdentity.VitalSourceFor(picked), PatientIdentity.ScheduledSurgerySourceFor(picked), PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource(), - PatientIdentity.ExtraStoreSource()); + PatientIdentity.ExtraStoreSource(), PatientIdentity.ContextStoreSource()); // 러너를 새로 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아 // 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다. fields = new MDataTableRunner(designer.Document,