From 975aff42118076ce382be07397137edcfdce7171 Mon Sep 17 00:00:00 2001 From: Msystech Date: Wed, 19 Aug 2026 16:56:54 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9E=85=C2=B7=ED=87=B4=EC=9B=90=20=EC=9D=BC?= =?UTF-8?q?=EC=8B=9C=EC=99=80=20=EC=9E=AC=EC=9B=90=EC=9D=BC=EC=88=98=2013?= =?UTF-8?q?=EC=A2=85=20=E2=80=94=20131=EC=A2=85=20=E2=86=92=20144=EC=A2=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FromStay 12종(bzDataInterface.vb:7152-7940, 전부 Case Else 갈래): 입원시간(HH:MM)·입원일시(이름과 달리 날짜만 — SJRCH·BSYD 만 시각 포함)·입원일시_영문·입실시간(ComTrsDtm 슬래시 12자)·의사퇴원예고일시 (ComSchLev)·퇴원예정일시(ComLevSch)·퇴원시간·퇴원일시·퇴원예고일시 (ILV→ComLevDtm / ILP→ComLevSch)·재원일수·퇴원예고재원일수 (DateDiff(Day)+1). FromExtra 에 입원일자_낮병동(:7260 — P_DcpInf 구간 매칭, 시작일 최신 행 고정). MsgBox 갈래("퇴원일자를 현재로 설정하시겠습니까?")는 조회 중 대화 상자라 옮기지 않고 사유로 말한다. 그 질문을 건너뛰는 개원일 분기 (HspStrDte>=20200901 → 빈, 외래 <20190501 → 오늘)는 그대로 옮겼다 — 개원일과 서버 오늘을 요양기관 행에 컬럼 2개로 추가(HspStrDte, ServerToday). 재원일수의 HspCod >= "KBJY" 는 등호가 아닌 크기비교 (레거시 오타로 보임) — MsgBox 갈래라 사유로 흡수된다. - dotnet test 338/338 · --edit-smoke 실패 0 (이름 검사 144종) - --db-patient ①~㉝ 전건 통과 - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 Co-Authored-By: Claude Fable 5 --- .../Stores/PatientContextStore.cs | 5 +- src/SheetMe.Data/Stores/PatientExtraStore.cs | 15 ++ .../Services/PatientTagResolver.cs | 136 +++++++++++++++++- 3 files changed, 154 insertions(+), 2 deletions(-) diff --git a/src/SheetMe.Data/Stores/PatientContextStore.cs b/src/SheetMe.Data/Stores/PatientContextStore.cs index 43f1066..eb500fd 100644 --- a/src/SheetMe.Data/Stores/PatientContextStore.cs +++ b/src/SheetMe.Data/Stores/PatientContextStore.cs @@ -218,9 +218,12 @@ public sealed class PatientContextStore } using var connection = new OracleConnection(connectionString); connection.Open(); + // HspStrDte(개원일)는 퇴원 계열 태그의 분기 재료(HspCfg.HspStrDte 대응), + // ServerToday 는 "오늘" 폴백·재원일수 계산용 서버 날짜 — 단말 시계를 쓰지 않는다 return Row(connection, "SELECT * FROM (SELECT HspInsNam, HspOwnNam, HspTelNum, HspFaxNum," - + " HspKorAdr, HspEngNam, HspEngAdr, HspRgnNum, HspInsNum FROM M_DepMst" + + " HspKorAdr, HspEngNam, HspEngAdr, HspRgnNum, HspInsNum, HspStrDte," + + " TO_CHAR(SYSDATE, 'YYYYMMDD') AS ServerToday 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])); diff --git a/src/SheetMe.Data/Stores/PatientExtraStore.cs b/src/SheetMe.Data/Stores/PatientExtraStore.cs index 166c6e7..813c3c4 100644 --- a/src/SheetMe.Data/Stores/PatientExtraStore.cs +++ b/src/SheetMe.Data/Stores/PatientExtraStore.cs @@ -181,6 +181,21 @@ public sealed class PatientExtraStore + " ORDER BY ComAcpDtm) WHERE ROWNUM = 1", ("c", chtNum), ("a", acpDtm), ("p", depCod2)); + /// + /// 낮병동 입원일자(OCM_입원일자_낮병동, :7260) — 이 내원의 접수일시를 포함하는 + /// 낮병동 신청(P_DcpInf, DcpAcpStt='A') 구간의 시작일. 원문 Rows(0) 은 구간이 + /// 겹치면 임의 행이었다 — 시작일 최신 행으로 고정한다. + /// + public string DayCareAdmissionDate(decimal comNum) + => Scalar( + "SELECT v FROM (SELECT DcpStrDte v FROM P_ComInf" + + " INNER JOIN P_DcpInf ON ComChtNum = DcpChtNum" + + " AND ComAcpDtm BETWEEN DcpStrDte AND DcpEndDte || '2359'" + + " AND DcpAcpStt = 'A'" + + " WHERE ComNum = :m AND ComAcpStt <> 'OC' AND ComDayCar = 'Y'" + + " ORDER BY DcpStrDte DESC) WHERE ROWNUM = 1", + ("m", comNum)); + /// 스칼라 1칸 — 0행이거나 NULL 이면 빈 문자열. 잔여 공백은 걷는다(문맥 행과 같은 규칙) private string Scalar(string sql, params (string Name, object Value)[] binds) { diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs index bcf751a..581c32f 100644 --- a/src/SheetMe.Designer/Services/PatientTagResolver.cs +++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs @@ -315,6 +315,10 @@ public sealed class PatientTagResolver : ITagValueResolver { return extraValue; } + if (FromStay(tag) is { } stayValue) + { + return stayValue; + } if (Computed.TryGetValue(tag, out var compute)) { var made = compute(context); @@ -891,6 +895,120 @@ public sealed class PatientTagResolver : ITagValueResolver : new TagValue(false, "수술일자 값이 형식에 맞지 않습니다"); } + /// + /// 입·퇴원 일시와 재원일수 12종(bzDataInterface.vb:7152-7940, 전부 Case Else 갈래). + /// + /// MsgBox 갈래는 옮기지 않는다. 레거시는 퇴원 전 환자에서 "퇴원일자를 현재로 + /// 설정하시겠습니까?"를 조회 도중에 묻는다 — 조회 함수의 대화 상자는 부작용이므로 + /// 사유로 말한다. 다만 개원일(HspStrDte) 분기로 그 질문 자체를 건너뛰는 병원이 + /// 있어(:7573 — 2020-09-01 이후 개원이면 무조건 "") 그 분기는 그대로 옮겼다. + /// 재원일수의 HspCod >= "KBJY" 는 등호가 아니라 크기비교(레거시 오타로 + /// 보인다) — 그 갈래도 MsgBox 라 사유로 흡수되므로 따로 흉내내지 않는다. + /// + private TagValue? FromStay(string tag) + { + if (tag is not ("OCM_입원시간" or "OCM_입원일시" or "OCM_입원일시_영문" or "OCM_입실시간" + or "OCM_의사퇴원예고일시" or "OCM_퇴원예정일시" or "OCM_퇴원시간" or "OCM_퇴원일시" + or "OCM_퇴원예고일시" or "OCM_재원일수" or "OCM_퇴원예고재원일수")) + { + return null; + } + var inpatient = Com(context, "ComPatTyp") == "I"; + var acp = Com(context, "ComAcpDtm"); + var prg = Com(context, "ComPrgStt"); + var lev = Com(context, "ComLevDtm"); + var sch = Com(context, "ComLevSch"); + var openDate = PatientContext.Value(hospital, "HspStrDte"); + var today = PatientContext.Value(hospital, "ServerToday"); + + // 레거시가 사용자에게 묻는 자리(:7580 등) — 자동 값을 만들지 않는다 + static TagValue Ask() => new(false, + "레거시는 퇴원 전 환자에서 이 값을 사용자에게 물어 정합니다 — 자동으로 만들지 않습니다"); + // 개원일·서버 오늘이 필요한 갈래에서 요양기관 행이 없으면 판정을 못 한다 + TagValue? NeedFacility() => openDate.Length == 0 || today.Length == 0 + ? new TagValue(false, "요양기관 행(개원일)을 읽지 못해 퇴원 전 분기를 판정할 수 없습니다") + : null; + + switch (tag) + { + case "OCM_입원시간": + return Ok(inpatient ? Time4(acp) : string.Empty); + case "OCM_입원일시": + // 이름과 달리 날짜만 나온다(:7203 — SJRCH·BSYD 만 시각 포함, 여기는 Else) + return Ok(inpatient ? Hyphenate(Head(acp, 8)) : string.Empty); + case "OCM_입원일시_영문": + return Ok(inpatient ? Reorder(Head(acp, 8), 4, 6, 0) : string.Empty); + case "OCM_입실시간": + return Ok(SlashStamp(Com(context, "ComTrsDtm"))); + case "OCM_의사퇴원예고일시": + return Ok(inpatient && Com(context, "ComSchLev").Length > 0 + ? Stamp(Com(context, "ComSchLev")) : string.Empty); + case "OCM_퇴원예정일시": + return Ok(inpatient && sch.Length > 0 ? Stamp(sch) : string.Empty); + case "OCM_퇴원시간": + return prg == "ILV" ? Ok(Time4(lev)) + : inpatient ? Ask() + : Ok(string.Empty); + case "OCM_퇴원일시": + return prg == "ILV" ? Ok(Hyphenate(Head(lev, 8))) + : inpatient + ? NeedFacility() ?? (string.CompareOrdinal(openDate, "20200901") >= 0 + ? Ok(string.Empty) : Ask()) + : NeedFacility() ?? Ok(string.CompareOrdinal(openDate, "20190501") < 0 + ? Hyphenate(today) : string.Empty); + case "OCM_퇴원예고일시": + return prg == "ILV" ? Ok(Hyphenate(Head(lev, 8))) + : prg == "ILP" ? Ok(Hyphenate(Head(sch, 8))) + : inpatient + ? NeedFacility() ?? (string.CompareOrdinal(openDate, "20200901") >= 0 + ? Ok(string.Empty) : Ask()) + : NeedFacility() ?? Ok(string.CompareOrdinal(openDate, "20190501") < 0 + ? Hyphenate(today) : string.Empty); + case "OCM_재원일수": + return prg == "ILV" ? StayDays(acp, lev) + : inpatient + ? NeedFacility() ?? (string.CompareOrdinal(openDate, "20200901") >= 0 + ? StayDays(acp, today) : Ask()) + : NeedFacility() ?? StayDays(acp, today); + default: // OCM_퇴원예고재원일수 + return prg == "ILV" ? StayDays(acp, lev) + : prg == "ILP" ? StayDays(acp, sch) + : inpatient + ? NeedFacility() ?? (string.CompareOrdinal(openDate, "20200901") >= 0 + ? Ok(string.Empty) : Ask()) + : NeedFacility() ?? StayDays(acp, today); + } + + static TagValue Ok(string value) => new(true, value); + } + + /// YYYYMMDDHHMM 의 시각만 — "HH:MM". 12자리가 안 되면 빈 값 + private static string Time4(string value) + => value.Length >= 12 ? $"{value[8..10]}:{value[10..12]}" : string.Empty; + + /// YYYYMMDDHHMM → "YYYY/MM/DD HH:MM". 12자리가 안 되면 빈 값 + private static string SlashStamp(string value) + => value.Length >= 12 + ? $"{value[..4]}/{value[4..6]}/{value[6..8]} {value[8..10]}:{value[10..12]}" + : string.Empty; + + /// + /// 재원일수 — (끝 날짜 - 시작 날짜) + 1 (레거시 DateDiff(Day)+1). + /// 날짜를 못 읽으면 레거시는 예외→"" 였다 — 여기서는 사유로 말한다. + /// + private static TagValue StayDays(string from, string to) + { + if (from.Length < 8 || to.Length < 8 + || !DateTime.TryParseExact(from[..8], "yyyyMMdd", null, + System.Globalization.DateTimeStyles.None, out var start) + || !DateTime.TryParseExact(to[..8], "yyyyMMdd", null, + System.Globalization.DateTimeStyles.None, out var end)) + { + return new TagValue(false, "재원일수를 만들 날짜를 읽지 못했습니다"); + } + return new TagValue(true, ((end - start).Days + 1).ToString()); + } + /// /// 환자 부가정보 7종 — 태그별 자기 SQL(). /// 전부 지연 + 태그별 캐시다: 이 태그가 없는 서식이 대부분이라 미리 당기지 않고, @@ -901,7 +1019,8 @@ public sealed class PatientTagResolver : ITagValueResolver if (tag is not ("PAT_실제생년월일" or "PAT_국적" or "PAT_장애등급" or "PAT_건보세대주명" or "PAT_건보_급여_세대주명" or "PAT_협력업체" or "PAT_보호자연락처" or "OCM_FollowUp" or "OCM_외출외박신청일" or "OCM_외출외박종료일" - or "OCM_최초내원일" or "OCM_초진일_발병일" or "OCM_신환_초진일")) + or "OCM_최초내원일" or "OCM_초진일_발병일" or "OCM_신환_초진일" + or "OCM_입원일자_낮병동")) { return null; } @@ -986,6 +1105,18 @@ public sealed class PatientTagResolver : ITagValueResolver } case "PAT_보호자연락처": return Wrap(store.GuardianPhone(chtNum), "부가정보에 보호자연락처가 없습니다"); + case "OCM_입원일자_낮병동": + { + var comNum = PatientContext.Value(context.ComInf, "ComNum"); + if (!decimal.TryParse(comNum, out var m)) + { + return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); + } + var day = store.DayCareAdmissionDate(m); + return day.Length >= 8 + ? new TagValue(true, Hyphenate(day[..8])) + : new TagValue(false, "낮병동 신청(P_DcpInf) 구간이 이 내원을 포함하지 않습니다"); + } case "OCM_FollowUp": { // 지금보다 뒤의 내원이 없으면 레거시도 "" 다(:1055) — 사유로 말한다 @@ -1144,6 +1275,9 @@ 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_입원일자_낮병동", "PAT_우편번호", "PAT_도로명주소", "PAT_지번주소", "PAT_영문도로명주소", "OCM_담당의사", "OCM_담당의사_담당의사코드", "OCM_담당의사_연락처", "OCM_진료과", "ETC_요양기관명칭_병원명",