diff --git a/src/SheetMe.Data/Stores/PatientContextStore.cs b/src/SheetMe.Data/Stores/PatientContextStore.cs index 3f69d62..ae30734 100644 --- a/src/SheetMe.Data/Stores/PatientContextStore.cs +++ b/src/SheetMe.Data/Stores/PatientContextStore.cs @@ -125,6 +125,54 @@ public sealed class PatientContextStore private static string Day(string stamp) => stamp.Length >= 8 ? stamp[..8] : stamp; + /// + /// 이 내원의 담당의 — OCM_담당의사 계열이 쓴다. 없으면 빈 사전. + /// + /// 레거시와 의도적으로 다른 곳이 한 군데 있다. + /// 원본 GetCodInfDT(..., With_Join)CodComNum + CodMtiSeq='0' 으로만 거르고 + /// ORDER BY 가 없는데 호출부는 Rows(0) 을 집는다. + /// 이 DB 에서 세어 보니 그런 내원이 25,488건이고 최대 41행이다 — + /// 즉 레거시는 그 내원들의 담당의를 확정적으로 찍지 못한다. 재현할 대상이 없으므로 + /// "그대로 옮긴다"가 성립하지 않는다. + /// + /// 그래서 적용일시로 거르고 최신순 1행을 집는다 — 문맥의 CodInf 와 같은 규칙이다. + /// 레거시도 이 시각을 넘기기는 한다(마스터 유효기간 조인에만 쓸 뿐이다). + /// 근거는 docs/TAG-PORTABLE-48.md. + /// + /// 담당의 조회는 레거시 GetUidMst(clsCommonLib.vb:10615)를 그대로 옮긴다 — + /// 유효기간으로 먼저 찾고, 0행이면 코드만으로 다시 찾는다. + /// 그 폴백을 빼면 유효기간이 지난 의사가 통째로 빈칸이 된다. + /// + /// 진료과 행을 고를 적용일시 12자리 + /// 담당의 유효기간을 볼 날짜 8자리. 비면 의 앞 8자리 + public IReadOnlyDictionary Doctor(decimal comNum, string moment, string? doctorDay = null) + { + using var connection = new OracleConnection(connectionString); + connection.Open(); + + var cod = Row(connection, + "SELECT * FROM (SELECT * FROM P_CodInf WHERE CodComNum = :k AND CodMtiSeq = 0" + + " AND :t BETWEEN CodStrDtm AND CodEndDtm ORDER BY CodStrDtm DESC) WHERE ROWNUM = 1", + ("k", comNum), ("t", moment)); + var code = PatientContext.Value(cod, "CodDtrCod"); + if (code.Length == 0) + { + return cod; + } + // 레거시는 입원이면 CodEndDtm 의 날짜로, 외래면 접수일자로 본다 — 호출부가 정해 넘긴다 + var day = Day((doctorDay ?? string.Empty).Trim().Length > 0 ? doctorDay!.Trim() : moment); + + var uid = Row(connection, + "SELECT * FROM (SELECT * FROM M_UidMst WHERE UidCod = :c" + + " AND :d BETWEEN UidStrDte AND UidEndDte) WHERE ROWNUM = 1", + ("c", code), ("d", day)); + return uid.Count > 0 + ? uid + // 폴백 — 유효기간 밖이어도 그 코드의 행을 쓴다(레거시 :10641-10644) + : Row(connection, "SELECT * FROM (SELECT * FROM M_UidMst WHERE UidCod = :c) WHERE ROWNUM = 1", + ("c", code)); + } + /// /// 건물관리번호로 주소 — 레거시 GetZipInfoByBuildingNumber(clsCommonLib.vb:16600). /// diff --git a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs index 4675296..fe6f2bb 100644 --- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs @@ -2168,6 +2168,25 @@ public static class DbSmoke { lines.Add($" [조사] CodMtiSeq=0 이 2행 이상인 내원: {line}"); } + + // ㉒ 담당의 조회가 실제로 행을 돌려주는가. + // 적용일시 BETWEEN + 최신순 1행으로 골라야 결정적이다(위 조사 근거). + // 0행이면 그 조건이 어긋난 것이라 태그 셋이 통째로 빈다. + { + // 시험 내원에 진료과 행이 아예 없으면 0열이 나오는데 + // 그건 쿼리가 아니라 데이터다. P_CodInf 에 행을 가진 내원을 찾아 건다 — + // 앞서 이 함정을 밟은 적이 있다(내원 0건인 환자로 판정하려 했다). + var owner = FirstComNum(conn, "P_CodInf", "CodComNum", "CodStrDtm") ?? target; + var ownerCtx = owner == target ? ctx : contexts.Load(owner); + var doctor = ownerCtx is null + ? new Dictionary() + : (IReadOnlyDictionary)contexts.Doctor(owner, ownerCtx.AdpDtm); + var uidNam = PatientContext.Value(doctor, "UidNam"); + lines.Add($" 담당의 조회: 내원 {owner:F0} → {doctor.Count}열," + + $" 이름 {uidNam.Length}자 (값은 기록하지 않는다)"); + Check("㉒ 담당의 조회가 예외 없이 돌고 행을 돌려준다", doctor.Count > 0, + "0열 — 적용일시 BETWEEN 조건이나 CodDtrCod 를 의심할 것"); + } } } } diff --git a/src/SheetMe.Designer/Services/PatientIdentity.cs b/src/SheetMe.Designer/Services/PatientIdentity.cs index a086ef0..fa8214b 100644 --- a/src/SheetMe.Designer/Services/PatientIdentity.cs +++ b/src/SheetMe.Designer/Services/PatientIdentity.cs @@ -55,6 +55,45 @@ public static class PatientIdentity } } + /// + /// 이 내원의 담당의 행 — 못 읽으면 빈 사전. + /// + /// 어느 시각의 진료과를 볼지는 레거시 OCM_담당의사 가 세 갈래로 정한다 + /// (bzDataInterface.vb): + /// · 입원이고 ComPrgStt='ILV'(퇴원) → 퇴원일시 + /// · 입원이고 그 밖 → 지금(= 문맥이 정한 적용일시) + /// · 외래 → 접수일시 + /// 담당의 유효기간을 볼 날짜도 갈린다 — 입원은 진료과 행의 CodEndDtm, 외래는 접수일자. + /// + public static IReadOnlyDictionary DoctorOf(PatientContext context) + { + var connection = ConfigService.Current.ConnectionString; + var empty = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (connection.Length == 0) + { + return empty; + } + var inpatient = PatientContext.Value(context.ComInf, "ComPatTyp") == "I"; + var accepted = PatientContext.Value(context.ComInf, "ComAcpDtm"); + var left = PatientContext.Value(context.ComInf, "ComLevDtm"); + var discharged = PatientContext.Value(context.ComInf, "ComPrgStt") == "ILV"; + + var moment = inpatient + ? (discharged && left.Length >= 12 ? left[..12] : context.AdpDtm) + : (accepted.Length >= 12 ? accepted[..12] : context.AdpDtm); + // 입원은 진료과 행의 종료일시로 담당의 유효기간을 본다 — 그 값은 저장소가 행을 읽은 뒤에야 안다. + // 여기서는 외래만 날짜를 지정하고, 입원은 저장소가 적용일시에서 뽑게 둔다. + var doctorDay = inpatient ? null : (accepted.Length >= 8 ? accepted[..8] : null); + try + { + return new PatientContextStore(connection).Doctor(context.ComNum, moment, doctorDay); + } + catch + { + return empty; + } + } + /// /// 이 환자의 주소 조각들(구역번호·도로명·지번·영문) — 못 읽으면 빈 사전. /// diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs index caa9c40..136694f 100644 --- a/src/SheetMe.Designer/Services/PatientTagResolver.cs +++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs @@ -131,13 +131,18 @@ public sealed class PatientTagResolver : ITagValueResolver /// M_ZipMst 에서 만든 주소 조각들 — 없으면 상세주소만 쓴다 private readonly IReadOnlyDictionary address; + /// 이 내원의 담당의 행(M_UidMst) — 없으면 빈 사전 + private readonly IReadOnlyDictionary doctor; + public PatientTagResolver(PatientContext context, ITagValueResolver next, - string residentNumber = "", IReadOnlyDictionary? address = null) + string residentNumber = "", IReadOnlyDictionary? address = null, + IReadOnlyDictionary? doctor = null) { this.context = context; this.next = next; this.residentNumber = residentNumber; this.address = address ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + this.doctor = doctor ?? new Dictionary(StringComparer.OrdinalIgnoreCase); } #endregion @@ -153,6 +158,10 @@ public sealed class PatientTagResolver : ITagValueResolver { return located; } + if (FromDoctor(tag) is { } doctorValue) + { + return doctorValue; + } if (Computed.TryGetValue(tag, out var compute)) { var made = compute(context); @@ -260,6 +269,32 @@ public sealed class PatientTagResolver : ITagValueResolver : new TagValue(false, "이 환자에게는 주소 정보가 없습니다"); } + /// + /// 담당의 3종 — 해당 태그가 아니면 null. + /// 세 태그의 차이는 마지막 한 줄뿐이다(이름 / 이름(코드) / 휴대전화). + /// + private TagValue? FromDoctor(string tag) + { + if (tag is not ("OCM_담당의사" or "OCM_담당의사_담당의사코드" or "OCM_담당의사_연락처")) + { + return null; + } + string Field(string column) => doctor.TryGetValue(column, out var v) ? v.Trim() : string.Empty; + + var name = Field("UidNam"); + var value = tag switch + { + "OCM_담당의사" => name, + "OCM_담당의사_담당의사코드" => name.Length > 0 ? $"{name}({Field("UidCod")})" : string.Empty, + _ => Field("UidMblPhn"), + }; + return value.Length > 0 + ? new TagValue(true, value) + : new TagValue(false, doctor.Count == 0 + ? "이 내원의 담당의를 찾지 못했습니다" + : "담당의에게 그 값이 없습니다"); + } + /// 주민번호에서 나온 값 — 비면 비었는지 말한다 private TagValue Wrap(string value) => value.Length > 0 ? new TagValue(true, value) @@ -273,6 +308,7 @@ public sealed class PatientTagResolver : ITagValueResolver { "PAT_주민번호", "PAT_주민번호_Dash", "PAT_성별", "PAT_한글성별", "PAT_우편번호", "PAT_도로명주소", "PAT_지번주소", "PAT_영문도로명주소", + "OCM_담당의사", "OCM_담당의사_담당의사코드", "OCM_담당의사_연락처", }; /// diff --git a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs index cd4e203..39f0c15 100644 --- a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs +++ b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs @@ -222,7 +222,8 @@ public partial class PreviewWindow : Window tags = picked is null ? session : new PatientTagResolver(picked, session, - PatientIdentity.ResidentNumberOf(picked), PatientIdentity.AddressOf(picked)); + PatientIdentity.ResidentNumberOf(picked), PatientIdentity.AddressOf(picked), + PatientIdentity.DoctorOf(picked)); // 러너를 새로 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아 // 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다. fields = new MDataTableRunner(designer.Document,