From f2f4cf81d3ad3bd55ae3c8ce321d07f37ec93045 Mon Sep 17 00:00:00 2001 From: Msystech Date: Wed, 19 Aug 2026 17:08:58 +0900 Subject: [PATCH] =?UTF-8?q?=ED=98=91=EC=A7=84=EA=B3=BC=C2=B7=EC=9E=85?= =?UTF-8?q?=ED=87=B4=EC=9B=90=EC=9D=98=EC=82=AC=204=EC=A2=85=20=E2=80=94?= =?UTF-8?q?=20152=EC=A2=85=20=E2=86=92=20156=EC=A2=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OCM_협진과(bzDataInterface.vb:6360 Case Else — 회신 완료 CstSttFlg='G' 협진의 회신 과, DISTINCT + 시작일 순 앞 2건을 ", " 결합. GNBEDRO 의 이중 DISTINCT 분기는 미이식), OCM_협진과_협진의(:6437 — M_UidMst 조인 + 앞 3건을 "과[의사]" 결합), OCM_입원의사(:6499 — 접수 시점 담당의 코드 → 접수일자 기준 UidNam), OCM_퇴원의사(:6583 — 퇴원일시가 비면 바로 빈 값, ILV 면 퇴원 시점·아니면 현재 시점 담당의 → 퇴원일자 기준 이름. 원문의 "퇴원일시 빈 값" If 갈래는 위의 조기 반환 탓에 도달 불가한 데드코드라 옮기지 않았다). CodInfAt(진료과+담당의 코드 행)·UserNameAt·Consults 를 신설. 이 시험 DB 에 O_CstInf 가 15,224행 있어 ㉞(회신 완료 협진이 있는 내원 → 협진과 실값)를 신설했다 — 2건 결합 확인. - dotnet test 338/338 · --edit-smoke 실패 0 (이름 검사 156종) - --db-patient ①~㉞ 전건 통과 - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 Co-Authored-By: Claude Fable 5 --- .../Stores/PatientContextStore.cs | 63 ++++++++++++++++++- src/SheetMe.Designer/Diagnostics/DbSmoke.cs | 25 ++++++++ .../Services/PatientTagResolver.cs | 53 +++++++++++++--- 3 files changed, 129 insertions(+), 12 deletions(-) diff --git a/src/SheetMe.Data/Stores/PatientContextStore.cs b/src/SheetMe.Data/Stores/PatientContextStore.cs index cf080f1..85203ba 100644 --- a/src/SheetMe.Data/Stores/PatientContextStore.cs +++ b/src/SheetMe.Data/Stores/PatientContextStore.cs @@ -101,12 +101,17 @@ public sealed class PatientContextStore /// 같은 규칙(시작일시 최신 1행)으로 고정한다. moment 가 비면 서버 현재시각이다. /// public string DepartmentCodeAt(decimal comNum, string? moment) + => PatientContext.Value(CodInfAt(comNum, moment), "CodDepCod"); + + /// 특정 시점의 진료 행(진료과 코드 + 담당의 코드) — 의 원형 + public IReadOnlyDictionary CodInfAt(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" + return Row(connection, + "SELECT CodDepCod, CodDtrCod FROM (SELECT CodDepCod, CodDtrCod 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") @@ -114,7 +119,59 @@ public sealed class PatientContextStore at.Length >= 12 ? new (string, object)[] { ("k", comNum), ("t", at[..12]) } : new (string, object)[] { ("k", comNum) }); - return PatientContext.Value(row, "CodDepCod"); + } + + /// + /// 특정 일자에 유효한 사용자 이름(M_UidMst.UidNam) — 입원의사·퇴원의사가 쓴다 + /// (bzDataInterface.vb:6520-6527). 유효기간이 겹치는 이중 행은 정상 데이터가 아니지만 + /// 원문의 Rows(0) 대신 한 행으로 고정한다. + /// + public string UserNameAt(string uidCod, string day) + { + if (uidCod.Trim().Length == 0 || day.Trim().Length < 8) + { + return string.Empty; + } + using var connection = new OracleConnection(connectionString); + connection.Open(); + var row = Row(connection, + "SELECT UidNam FROM (SELECT UidNam FROM M_UidMst WHERE UidCod = :u" + + " AND :d BETWEEN UidStrDte AND UidEndDte ORDER BY UidStrDte DESC) WHERE ROWNUM = 1", + ("u", uidCod.Trim()), ("d", day.Trim()[..8])); + return PatientContext.Value(row, "UidNam").Trim(); + } + + /// + /// 협진 목록(bzDataInterface.vb:6360-6494) — 회신 완료(CstSttFlg='G')된 협진의 + /// 회신 과(+의사). 원문 그대로: DISTINCT + 시작일 순 + 앞에서 과는 2건, 과·의사는 3건. + /// + public IReadOnlyList> Consults(decimal comNum, bool withDoctor) + { + using var connection = new OracleConnection(connectionString); + connection.Open(); + using var command = connection.CreateCommand(); + command.BindByName = true; + command.CommandText = withDoctor + ? "SELECT * FROM (SELECT DISTINCT UidCod, UidNam, DepCod, DepKorNam, CstStrDte FROM O_CstInf" + + " INNER JOIN M_DepMst ON CstRplDep = DepCod AND CstStrDte BETWEEN DepStrDte AND DepEndDte" + + " INNER JOIN M_UidMst ON CstRplDtr = UidCod AND CstStrDte BETWEEN UidStrDte AND UidEndDte" + + " WHERE CstComNum = :m AND CstSttFlg = 'G' ORDER BY CstStrDte) WHERE ROWNUM <= 3" + : "SELECT * FROM (SELECT DISTINCT DepCod, DepKorNam, CstStrDte FROM O_CstInf" + + " INNER JOIN M_DepMst ON CstRplDep = DepCod AND CstStrDte BETWEEN DepStrDte AND DepEndDte" + + " WHERE CstComNum = :m AND CstSttFlg = 'G' ORDER BY CstStrDte) WHERE ROWNUM <= 2"; + command.Parameters.Add(new OracleParameter("m", comNum)); + var rows = new List>(); + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + var row = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < reader.FieldCount; i++) + { + row[reader.GetName(i)] = reader.IsDBNull(i) ? string.Empty : reader.GetValue(i).ToString()!.Trim(); + } + rows.Add(row); + } + return rows; } /// diff --git a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs index b21e16a..91a09e9 100644 --- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs @@ -2331,6 +2331,12 @@ public static class DbSmoke + " GROUP BY PatEtcChtNum)"); lines.Add($" [조사] P_pdsoInf 패딩 행={padded}," + $" P_PatEtcInf 차트당 최대 행수={multiEtc}"); + // 협진(O_CstInf)은 O_ 계열이라 이 시험 DB 에 없을 수 있다 — + // 빈 값이면 ORA-00942(운영 DB 전용 검증 대상)라는 뜻이다 + var cstCount = FirstText(conn, "SELECT COUNT(*) FROM O_CstInf"); + lines.Add(cstCount.Length == 0 + ? " [조사] O_CstInf 없음 — 협진과 태그는 운영 DB 에서만 검증 가능" + : $" [조사] O_CstInf {cstCount}행 — 협진과 조회 검증 가능"); if (extraOwner.Length == 0) { lines.Add("SKIP ㉛ 부가정보 — P_PatEtcInf 가 비어 있어 확인 불가"); @@ -2389,6 +2395,25 @@ public static class DbSmoke extra.FirstVisit(revisitOwner, "999912312359").Length >= 8, "RPAD 매칭이나 P_CodInf 조인을 의심할 것"); } + + // 협진도 같은 방식 — 회신 완료 협진이 있는 내원으로 물으면 + // 과 이름이 반드시 나와야 한다(조인 회귀를 잡는다). + var consultOwner = FirstText(conn, + "SELECT CstComNum FROM (SELECT CstComNum FROM O_CstInf" + + " INNER JOIN M_DepMst ON CstRplDep = DepCod" + + " AND CstStrDte BETWEEN DepStrDte AND DepEndDte" + + " WHERE CstSttFlg = 'G') WHERE ROWNUM = 1"); + if (consultOwner.Length == 0 || !decimal.TryParse(consultOwner, out var cstCom)) + { + lines.Add("SKIP ㉞ 협진과 값 — 회신 완료 협진이 없어 판정 불가"); + } + else + { + var consults = new PatientContextStore(conn).Consults(cstCom, false); + lines.Add($" 협진: 내원 {cstCom:F0} → {consults.Count}건 (값은 기록하지 않는다)"); + Check("㉞ 회신 완료 협진이 있는 내원에서는 협진과가 나온다", + consults.Count > 0, "O_CstInf 조인이나 CstSttFlg 를 의심할 것"); + } } } catch (Exception ex) diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs index 5472038..b7741da 100644 --- a/src/SheetMe.Designer/Services/PatientTagResolver.cs +++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs @@ -948,9 +948,10 @@ public sealed class PatientTagResolver : ITagValueResolver /// private TagValue? FromDeptAt(string tag) { - var admission = tag is "OCM_입원과" or "OCM_입원과_한글명칭"; - var discharge = tag is "OCM_퇴원과" or "OCM_퇴원과_한글명칭"; - if (!admission && !discharge) + var admission = tag is "OCM_입원과" or "OCM_입원과_한글명칭" or "OCM_입원의사"; + var discharge = tag is "OCM_퇴원과" or "OCM_퇴원과_한글명칭" or "OCM_퇴원의사"; + var consult = tag is "OCM_협진과" or "OCM_협진과_협진의"; + if (!admission && !discharge && !consult) { return null; } @@ -989,6 +990,19 @@ public sealed class PatientTagResolver : ITagValueResolver ? office : PatientContext.Value(row, "DepKorNam").Trim(); } + if (consult) + { + // 회신 완료된 협진의 과(2건)·과[의사](3건)를 ", " 로 잇는다(:6410-6480) + var rows = store.Consults(m, withDoctor: tag == "OCM_협진과_협진의"); + if (rows.Count == 0) + { + return new TagValue(false, "회신 완료(CstSttFlg='G')된 협진이 없습니다"); + } + var joined = string.Join(", ", rows.Select(r => tag == "OCM_협진과" + ? PatientContext.Value(r, "DepKorNam").Trim() + : $"{PatientContext.Value(r, "DepKorNam").Trim()}[{PatientContext.Value(r, "UidNam").Trim()}]")); + return new TagValue(true, joined); + } if (admission) { var acp = PatientContext.Value(context.ComInf, "ComAcpDtm"); @@ -996,12 +1010,20 @@ public sealed class PatientTagResolver : ITagValueResolver { return new TagValue(false, "이 내원의 접수일시를 읽지 못했습니다"); } - var depCod = store.DepartmentCodeAt(m, acp[..12]); - if (depCod.Length == 0) + var codRow = store.CodInfAt(m, acp[..12]); + if (codRow.Count == 0) { return new TagValue(false, "접수 시점의 진료 행(P_CodInf)이 없습니다"); } - var row = store.Department(depCod, acp[..8]); + if (tag == "OCM_입원의사") + { + // 접수 시점 담당의 코드 → 접수일자 기준 이름(:6499-6527) + var name = store.UserNameAt(PatientContext.Value(codRow, "CodDtrCod"), acp[..8]); + return name.Length > 0 + ? new TagValue(true, name) + : new TagValue(false, "접수 시점 담당의가 사용자 마스터에 없습니다"); + } + var row = store.Department(PatientContext.Value(codRow, "CodDepCod"), acp[..8]); if (row.Count == 0) { return new TagValue(true, string.Empty); // 레거시도 마스터 0행이면 "" 다 @@ -1012,15 +1034,27 @@ public sealed class PatientTagResolver : ITagValueResolver } var prg = PatientContext.Value(context.ComInf, "ComPrgStt"); var lev = PatientContext.Value(context.ComInf, "ComLevDtm"); - var depAtLeave = store.DepartmentCodeAt(m, + if (tag == "OCM_퇴원의사" && lev.Trim().Length == 0) + { + return new TagValue(true, string.Empty); // 퇴원일시가 없으면 레거시가 바로 "" 다(:6592) + } + var leaveRow = store.CodInfAt(m, prg == "ILV" && lev.Length >= 12 ? lev[..12] : null); - if (depAtLeave.Length == 0) + if (leaveRow.Count == 0) { return new TagValue(true, string.Empty); // 레거시가 명시적으로 "" 를 돌려준다(:6189) } + if (tag == "OCM_퇴원의사") + { + var name = store.UserNameAt(PatientContext.Value(leaveRow, "CodDtrCod"), lev[..8]); + return name.Length > 0 + ? new TagValue(true, name) + : new TagValue(false, "퇴원 시점 담당의가 사용자 마스터에 없습니다"); + } var anchor = tag == "OCM_퇴원과_한글명칭" && lev.Trim().Length == 0 ? "ComAcpDtm" : "ComLevDtm"; - var deptRow = store.DischargeDepartment(m, depAtLeave, anchor); + var deptRow = store.DischargeDepartment(m, + PatientContext.Value(leaveRow, "CodDepCod"), anchor); if (deptRow.Count == 0) { return new TagValue(true, string.Empty); @@ -1418,6 +1452,7 @@ public sealed class PatientTagResolver : ITagValueResolver "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_재원일수", "OCM_퇴원예고재원일수", "OCM_입원일자_낮병동",