From edb9cc65cf374e41eb5b3990e4dc79cb8ffa9ec0 Mon Sep 17 00:00:00 2001 From: Msystech Date: Thu, 20 Aug 2026 08:59:49 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B9=84=EA=B8=89=EC=97=AC=EC=B2=98=EB=B0=A9?= =?UTF-8?q?=C2=B7=EC=A0=84=EB=AC=B8=EA=B7=B8=EB=A3=B9=EA=B3=BC=EB=AA=A9?= =?UTF-8?q?=C2=B7=EC=B2=AD=EA=B5=AC=EC=A7=84=EB=A3=8C=EA=B3=BC=EB=AA=A9?= =?UTF-8?q?=C2=B7=EC=9E=84=EC=8B=A0=EC=A3=BC=EA=B8=B0=204=EC=A2=85=20?= =?UTF-8?q?=E2=80=94=20250=EC=A2=85=20=E2=86=92=20254=EC=A2=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OCM_비급여처방(bzDataInterface.vb:10964, Case Else + SRCH 조건 갈래 — 고지 대상(SgaNtiNon='Y')·수가 그룹 존재만. TYRCH UNION·BSGH 치료실· GNRH·BSQ·SUSS 분기 미이식). 보험 코드로 금액 컬럼을 골라 0 초과만 "이름(##,###)원" CRLF 결합. 특수 계산코드(SGACFG Use_SgaCA1/2) 환자군은 레거시가 SELECT 에 없는 컬럼을 읽어 항상 빈 값(무증상 실패) — 설정을 읽어 그 환자군이면 사유로 말한다(이 DB 에 설정이 실제로 1건 있다 — ㉟ 확인). OCM_진단서전문그룹과목(:11245 — DepBilCod='01'+세부과목이면 MEDDTL 이름, 아니면 GDPTBL 그룹과목. 원문의 0행 Rows(0) 크래시는 사유로). OCM_청구진료과목(:11398 — 문맥 진료과 행의 DepBilCod 를 BDPTBL 로. 부서 시점과 유효기간 시점이 어긋나는 원문 특이점 그대로). OCM_임신주기(:11845 — 사용자 설정(EDD/LMP) 조회가 코드 컬럼에 사용자 이름을 비교하는 결함으로 항상 0행 → 기본값 LMP 고정. LMP 산식만 옮기고 도달 불가한 EDD 산식은 미이식. 주수는 총일수/7· 나머지, 월경일이 미래면 음수 일수만 — 원문 그대로). - dotnet test 342/342 · --edit-smoke 실패 0 (이름 검사 254종) - --db-patient ①~㊱ 전건 통과 (그룹과목 실값 3컬럼) - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 Co-Authored-By: Claude Fable 5 --- src/SheetMe.Data/Stores/PatientExtraStore.cs | 76 +++++++++++ src/SheetMe.Designer/Diagnostics/DbSmoke.cs | 4 + .../Services/PatientTagResolver.cs | 121 ++++++++++++++++++ 3 files changed, 201 insertions(+) diff --git a/src/SheetMe.Data/Stores/PatientExtraStore.cs b/src/SheetMe.Data/Stores/PatientExtraStore.cs index 2d2a516..d34e2f6 100644 --- a/src/SheetMe.Data/Stores/PatientExtraStore.cs +++ b/src/SheetMe.Data/Stores/PatientExtraStore.cs @@ -763,6 +763,82 @@ public sealed class PatientExtraStore + " ORDER BY OdrDtm, OdrCod, OdrNam", ("m", comNum)); + /// + /// 비급여 오더 행들(OCM_비급여처방, :10964 — Case Else + SRCH 조건 갈래). + /// SRCH 는 고지 대상(SgaNtiNon='Y')이고 수가 그룹(SgaHgnCod)이 있는 것만 본다. + /// TYRCH(UNION)·BSGH(치료실)·GNRH·BSQ·SUSS 분기는 미이식. + /// + public IReadOnlyList> NonInsuredOrders(decimal comNum) + => Rows( + "SELECT SgaKorNam, SgaHthNon, SgaGudNon, SgaCarNon, SgaWrkNon, SgaGenFor, SgaGenNon" + + " FROM O_OdrInf" + + " INNER JOIN O_OdsInf ON OdrKey = OdsOdrKey AND OdsStt = 'OE'" + + " LEFT JOIN M_SgaMst ON OdrCod = SgaCod" + + " AND OdrDtm BETWEEN SgaStrDte AND SgaEndDte || '2359'" + + " WHERE OdrComNum = :m AND OdrStt = 'OE' AND OdrStkStt = 'OE'" + + " AND OdrInsYon = '3'" + + " AND (OdrHidden IS NULL OR OdrHidden <> 1)" + + " AND OdrGrpSub = 0" + + " AND (OdrAutCod IS NULL OR OdrAutCod = '-')" + + " AND (OdrCod IS NOT NULL AND OdrCod <> '-')" + + " AND (OdrSimEdt IS NULL OR OdrSimEdt NOT IN ('I','E'))" + + " AND SgaNtiNon = 'Y'" + + " AND (SgaHgnCod IS NOT NULL AND SgaHgnCod <> ' ')" + + " GROUP BY SgaKorNam, SgaHthNon, SgaGudNon, SgaCarNon, SgaWrkNon, SgaGenFor, SgaGenNon" + + " ORDER BY SgaKorNam", + ("m", comNum)); + + /// + /// 특수 계산코드 2종(M_DtlMst SGACFG 의 Use_SgaCA1/Use_SgaCA2, DtlCodVal 의 첫 '/' 조각 — + /// clsCommonLib.vb:3946-3999). 환자 계산코드가 여기 맞으면 레거시 비급여처방은 + /// SELECT 에 없는 컬럼(SgaCA1Non)을 읽어 항상 빈 값이었다 — 해석기가 사유로 만든다. + /// + public IReadOnlyList SpecialNonInsuredCalCodes() + => Rows("SELECT DtlCod, DtlCodVal FROM M_DtlMst WHERE DtlTblCod = 'SGACFG'") + .Where(r => r.TryGetValue("DtlCod", out var c) + && c.Trim().ToUpperInvariant() is "USE_SGACA1" or "USE_SGACA2") + .Select(r => (r.TryGetValue("DtlCodVal", out var v) ? v : string.Empty) + .Split('/')[0].Trim()) + .Where(v => v.Length > 0) + .ToArray(); + + /// + /// 진료과의 그룹과목 행(OCM_진단서전문그룹과목, :11245 쿼리1) — 오늘 기준. + /// 원문은 0행이면 Rows(0) 크래시였다 — 여기는 빈 사전(해석기가 사유로). + /// + public IReadOnlyDictionary GroupSubjectRow(string depCod) + => depCod.Trim().Length == 0 + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : Row( + "SELECT * FROM (SELECT DepBilCod, DepMedDtl, DtlCodNam FROM M_DepMst" + + " INNER JOIN M_DtlMst ON DepGrpCod = DtlCod AND DtlTblCod = 'GDPTBL'" + + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN DtlStrDte AND DtlExpDte" + + " WHERE TRIM(DepCod) = :d" + + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN DepStrDte AND DepEndDte)" + + " WHERE ROWNUM = 1", + ("d", depCod.Trim())); + + /// + /// 코드표 이름 — 레거시 GetDtlCodNam(tbl, cod)(기간 조건 없음, clsCommonLib.vb:7020) 대응. + /// 다중이면 원문 첫 행 — 시작일 최신으로 고정한다. + /// + public string CodeName(string tableCod, string cod) + => cod.Trim().Length == 0 ? string.Empty + : Scalar( + "SELECT v FROM (SELECT DtlCodNam v FROM M_DtlMst" + + " WHERE DtlTblCod = :t AND DtlCod = :c ORDER BY DtlStrDte DESC) WHERE ROWNUM = 1", + ("t", tableCod), ("c", cod.Trim())); + + /// + /// 임신 정보 최신 행(OCM_임신주기, :11845) — 분만예정일(PrgEddDte)·최종월경일(PrgLmpDte) + /// 과 서버 오늘. 갱신일시 최신 행(원문 정렬 그대로). + /// + public IReadOnlyDictionary PregnancyRow(decimal comNum) + => Row( + "SELECT * FROM (SELECT PrgEddDte, PrgLmpDte, TO_CHAR(SYSDATE, 'YYYYMMDD') AS Today" + + " FROM O_PrgInf WHERE PrgComNum = :m ORDER BY PrgUpdDtf 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/Diagnostics/DbSmoke.cs b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs index 22b38e6..760b513 100644 --- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs @@ -2476,6 +2476,10 @@ public static class DbSmoke ("퇴원약목록", () => owner2 is null ? "-" : extra.DischargeMedications(owner2.Value).Count.ToString()), ("수술집도의과", () => owner2 is null ? "-" : extra.SurgeonDepartment(owner2.Value, false).Length.ToString()), ("수술명칭", () => owner2 is null ? "-" : extra.SurgeryNames(owner2.Value).Count.ToString()), + ("비급여", () => owner2 is null ? "-" : extra.NonInsuredOrders(owner2.Value).Count.ToString()), + ("특수계산코드", () => extra.SpecialNonInsuredCalCodes().Count.ToString()), + ("그룹과목", () => extra.GroupSubjectRow("0506").Count.ToString()), + ("임신정보", () => owner2 is null ? "-" : extra.PregnancyRow(owner2.Value).Count.ToString()), ("적용진료과", () => extra.AppliedDepartmentName("0506", "202601011200").Length.ToString()), ("전과목록", () => owner2 is null ? "-" : extra.TransferList(owner2.Value).Count.ToString()), ("간호과거력", () => owner2 is null ? "-" : extra.NurseHistoryRow(owner2.Value).Count.ToString()), diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs index 2952274..8ae72f1 100644 --- a/src/SheetMe.Designer/Services/PatientTagResolver.cs +++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs @@ -1720,6 +1720,7 @@ public sealed class PatientTagResolver : ITagValueResolver "OCM_수술명칭", "OCM_수술진단명", "OCM_수술처치", "OCM_퇴원약_코드", "OCM_퇴원약_명칭", "OCM_퇴원약_용량", "OCM_퇴원약_횟수", "OCM_퇴원약_일수", "OCM_퇴원약_용법", "OCM_퇴원약", "OCM_퇴원약_New", + "OCM_비급여처방", "OCM_진단서전문그룹과목", "OCM_청구진료과목", "OCM_임신주기", }; /// @@ -2358,6 +2359,125 @@ public sealed class PatientTagResolver : ITagValueResolver } return new TagValue(true, text.ToString()); } + case "OCM_비급여처방": + { + var comNum = PatientContext.Value(context.ComInf, "ComNum"); + if (!decimal.TryParse(comNum, out var m)) + { + return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); + } + // 특수 계산코드(SGACFG Use_SgaCA1/2) 환자군은 레거시가 SELECT 에 없는 + // 컬럼을 읽어 항상 빈 값이었다(:11144-11152) — 고치면 없던 값이 생긴다 + var calCod = PatientContext.Value(context.CoiInf, "CoiCalCod"); + if (calCod.Length > 0 && store.SpecialNonInsuredCalCodes().Contains(calCod)) + { + return new TagValue(false, + "레거시 결함(특수 계산코드 갈래의 컬럼 미선택)으로 항상 빈 값인 환자군입니다"); + } + var orders = store.NonInsuredOrders(m); + if (orders.Count == 0) + { + return new TagValue(false, "고지 대상 비급여 오더가 없습니다"); + } + // 보험 코드로 금액 컬럼을 고른다(:11157-11200) — 0 이하 행은 건너뛴다 + var insCod = PatientContext.Value(context.CoiInf, "CoiInsCod"); + var column = insCod switch + { + "1" => "SgaHthNon", + "2" => "SgaGudNon", + "3" => "SgaCarNon", + "4" => calCod == "4F00" ? "SgaGenFor" : "SgaWrkNon", + _ => calCod is "5F00" or "5F10" ? "SgaGenFor" : "SgaGenNon", + }; + var lines = orders + .Select(r => (Name: PatientContext.Value(r, "SgaKorNam"), + Amount: double.TryParse(PatientContext.Value(r, column), + System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, out var a) ? a : 0)) + .Where(x => x.Amount > 0) + .Select(x => $"{x.Name}({x.Amount.ToString("##,###", System.Globalization.CultureInfo.InvariantCulture)})원") + .ToArray(); + return lines.Length > 0 + ? new TagValue(true, string.Join("\r\n", lines)) + : new TagValue(false, "이 보험 구분에 금액이 있는 비급여 항목이 없습니다"); + } + case "OCM_진단서전문그룹과목": + { + // 로그인 의사면 로그인 부서, 아니면 문맥 진료과(:11245 — 시점 판정은 + // 진단서 계열과 동일). DepBilCod='01'+세부과목이 있으면 MEDDTL 이름, + // 아니면 GDPTBL 그룹과목 이름. 원문의 0행 크래시는 사유로 바꾼다. + var depCod = UserSession.Current.DoctorYn == "Y" + ? UserSession.Current.DepCod + : PatientContext.Value(context.CodInf, "CodDepCod"); + if (depCod.Length == 0) + { + return new TagValue(false, "진료과 코드를 읽지 못했습니다"); + } + var row = store.GroupSubjectRow(depCod); + if (row.Count == 0) + { + return new TagValue(false, "오늘 기준 그룹과목(GDPTBL) 행이 없습니다"); + } + var medDtl = PatientContext.Value(row, "DepMedDtl"); + if (PatientContext.Value(row, "DepBilCod") == "01" && medDtl.Length > 0) + { + return Wrap(store.CodeName("MEDDTL", medDtl), + "세부 전문과목(MEDDTL) 코드표에 이름이 없습니다"); + } + return Wrap(PatientContext.Value(row, "DtlCodNam"), + "그룹과목 이름이 비어 있습니다"); + } + case "OCM_청구진료과목": + { + // 문맥 진료과 행(접수일 기준 프리페치와 같은 조회)의 DepBilCod 를 + // 코드표(BDPTBL)로 푼다(:11398). 시점 어긋남 특이점은 원문 그대로다. + var billCod = PatientContext.Value(department, "DepBilCod").Trim(); + if (department.Count == 0) + { + return new TagValue(false, "이 내원의 진료과를 찾지 못했습니다"); + } + if (billCod.Length == 0) + { + return new TagValue(false, "진료과에 청구 코드(DepBilCod)가 없습니다"); + } + return Wrap(store.CodeName("BDPTBL", billCod), + "청구 코드표(BDPTBL)에 이름이 없습니다"); + } + case "OCM_임신주기": + { + var comNum = PatientContext.Value(context.ComInf, "ComNum"); + if (!decimal.TryParse(comNum, out var m)) + { + return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); + } + var row = store.PregnancyRow(m); + if (row.Count == 0) + { + return new TagValue(false, "임신 정보(O_PrgInf)가 없습니다"); + } + var lmp = PatientContext.Value(row, "PrgLmpDte"); + var edd = PatientContext.Value(row, "PrgEddDte"); + var today = PatientContext.Value(row, "Today"); + if (lmp.Length == 0 && edd.Length == 0) + { + return new TagValue(false, "임신 정보 행에 분만예정일·최종월경일이 모두 비어 있습니다"); + } + // 레거시(:11845)는 사용자 설정(EDD/LMP)을 읽지만 그 조회가 + // 코드 컬럼에 사용자 '이름'을 비교하는 결함으로 항상 0행 — + // 기본값 LMP 로 고정된다. EDD 산식은 도달 불가라 옮기지 않는다. + var days = ResidentNumber.DaysSince(lmp, today); + if (days is null) + { + return new TagValue(false, "최종월경일이 비어 임신 주수를 만들 수 없습니다"); + } + var week = days.Value / 7; + var day = days.Value % 7; + // 원문(:11908-11918): 일이 양수거나 오늘이 월경일 뒤면 "nWK dD", + // 월경일이 미래면 일수만 + return new TagValue(true, + day > 0 || string.CompareOrdinal(today, lmp) > 0 + ? $"{week}WK {day}D" : $"{day}D"); + } case "OCM_수술진단명": // PURME 전용(:3066) — 그 밖의 병원은 Case 미매칭으로 항상 빈 값이다 return new TagValue(false, @@ -2681,6 +2801,7 @@ public sealed class PatientTagResolver : ITagValueResolver "OCM_수술명칭", "OCM_수술진단명", "OCM_수술처치", "OCM_퇴원약_코드", "OCM_퇴원약_명칭", "OCM_퇴원약_용량", "OCM_퇴원약_횟수", "OCM_퇴원약_일수", "OCM_퇴원약_용법", "OCM_퇴원약", "OCM_퇴원약_New", + "OCM_비급여처방", "OCM_진단서전문그룹과목", "OCM_청구진료과목", "OCM_임신주기", "OCM_입원시간", "OCM_입원일시", "OCM_입원일시_영문", "OCM_입실시간", "OCM_의사퇴원예고일시", "OCM_퇴원예정일시", "OCM_퇴원시간", "OCM_퇴원일시", "OCM_퇴원예고일시", "OCM_재원일수", "OCM_퇴원예고재원일수", "OCM_입원일자_낮병동",