From 199a4142eec981d93d11a9cb5108c33f46808628 Mon Sep 17 00:00:00 2001 From: Msystech Date: Thu, 20 Aug 2026 08:55:08 +0900 Subject: [PATCH] =?UTF-8?q?=ED=87=B4=EC=9B=90=EC=95=BD=208=EC=A2=85=20?= =?UTF-8?q?=E2=80=94=20242=EC=A2=85=20=E2=86=92=20250=EC=A2=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 공통 조회(퇴원약DT, bzDataInterface.vb:10482 — 퇴원의약 오더를 일시·코드·이름 순, 1회 투여량은 SQL 계산) 하나에 파생 6종 (코드/명칭/용량/횟수/일수/용법 — 열 하나를 줄단위로, 마지막 뒤에도 개행, 용법은 LEFT JOIN 미매칭이면 빈 줄) + 고정폭 조립 2종: OCM_퇴원약(:10617) — PadRight 를 "채울 개수"로 오용한 원문 결함 그대로(실제 의미는 총폭이라 정렬이 어긋난다), 코드가 20바이트를 넘으면 음수 인자 예외 → 사유(레거시는 오류창+빈 값). SATCH 비고 괄호 분기 미이식. OCM_퇴원약_New(:10733) — 바이트 고정폭(코드 10· 명칭 37), 용량·횟수·일수 패딩이 잘라낸 값이 아닌 원본 전체 바이트로 계산되는 결함 보존(5자 이상이면 음수 예외 → 사유). 행이 없으면 SRCH 만 "해당없음." 을 찍는다(:10704/:10864) — 우리 병원 분기라 그대로 값이 된다. CP949 바이트 길이는 ASCII 1·그 외 2 근사(레거시 LenK 대응), 바이트 절단은 문자 경계라 레거시의 반각 깨짐("?") 재절단 분기가 필요 없다(폭 최대 1바이트 차이 — 주석 기록). - dotnet test 342/342 · --edit-smoke 실패 0 (이름 검사 250종) - --db-patient ①~㊱ 전건 통과 (㉟ 에 퇴원약·수술집도의과·수술명칭 실행 확인 추가) - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 Co-Authored-By: Claude Fable 5 --- src/SheetMe.Data/Stores/PatientExtraStore.cs | 22 +++ src/SheetMe.Designer/Diagnostics/DbSmoke.cs | 3 + .../Services/PatientTagResolver.cs | 126 ++++++++++++++++++ 3 files changed, 151 insertions(+) diff --git a/src/SheetMe.Data/Stores/PatientExtraStore.cs b/src/SheetMe.Data/Stores/PatientExtraStore.cs index dcaec54..2d2a516 100644 --- a/src/SheetMe.Data/Stores/PatientExtraStore.cs +++ b/src/SheetMe.Data/Stores/PatientExtraStore.cs @@ -741,6 +741,28 @@ public sealed class PatientExtraStore .Select(r => r.TryGetValue("OprCodNam", out var v) ? v : string.Empty) .ToArray(); + /// + /// 퇴원약 목록(퇴원약DT :10482 — 파생 6종·OCM_퇴원약·_New 의 공통 조회). + /// 퇴원의약(OdrDscMed) 오더를 일시·코드·이름 순으로. 1회 투여량(QtyTmsQty)은 + /// SQL 계산 그대로(수량형 '*' 이면 원량, 아니면 총량/횟수 반올림 3자리). + /// OdrRefCmt 는 OCM_퇴원약(SATCH 분기)용 컬럼 — 파생 6종은 쓰지 않는다. + /// + public IReadOnlyList> DischargeMedications(decimal comNum) + => Rows( + "SELECT OdrCod, OdrNam, OdrTms, OdrDay, UsgKorNam, OdrRefCmt," + + " CASE WHEN OdrQtyTyp = '*' THEN OdrDtrQty ELSE ROUND(OdrDtrQty / OdrTms, 3) END AS QtyTmsQty" + + " FROM O_OdrInf" + + " LEFT JOIN M_UsgMst ON OdrUsgCod = UsgCod" + + " AND OdrDtm BETWEEN UsgStrDte || '0000' AND UsgEndDte || '2359'" + + " WHERE OdrComNum = :m AND OdrStt = 'OE'" + + " AND (OdrDscMed IS NOT NULL AND OdrDscMed <> ' ')" + + " AND (OdrHidden IS NULL OR OdrHidden <> 1)" + + " AND OdrGrpSub = 0" + + " AND (OdrAutCod IS NULL OR OdrAutCod = '-')" + + " AND (OdrSimEdt IS NULL OR OdrSimEdt NOT IN ('I','E'))" + + " ORDER BY OdrDtm, OdrCod, OdrNam", + ("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 25185a1..22b38e6 100644 --- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs @@ -2473,6 +2473,9 @@ public static class DbSmoke ("수술일자", () => owner2 is null ? "-" : extra.SurgeryDate(owner2.Value, false).Length.ToString()), ("외래실진료일", () => extra.OutpatientVisitDays(extraOwner, "999912312359", "05", false).Count.ToString()), ("청구과목", () => extra.BillingDepartmentName("0506").Length.ToString()), + ("퇴원약목록", () => 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()), ("적용진료과", () => 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 cad9c66..2952274 100644 --- a/src/SheetMe.Designer/Services/PatientTagResolver.cs +++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs @@ -1718,6 +1718,8 @@ public sealed class PatientTagResolver : ITagValueResolver "OCM_영상촬영이력", "OCM_수술주상병", "OCM_수술집도의_진료과", "OCM_수술집도의_그룹진료과", "OCM_수술명칭", "OCM_수술진단명", "OCM_수술처치", + "OCM_퇴원약_코드", "OCM_퇴원약_명칭", "OCM_퇴원약_용량", "OCM_퇴원약_횟수", + "OCM_퇴원약_일수", "OCM_퇴원약_용법", "OCM_퇴원약", "OCM_퇴원약_New", }; /// @@ -2300,6 +2302,62 @@ public sealed class PatientTagResolver : ITagValueResolver // 원문 결합(:3546-3556): 각 행 뒤 공백 1칸, 행 사이 CRLF(마지막에도 공백) return new TagValue(true, string.Join("\r\n", names.Select(n => $"{n} "))); } + case "OCM_퇴원약_코드": + case "OCM_퇴원약_명칭": + case "OCM_퇴원약_용량": + case "OCM_퇴원약_횟수": + case "OCM_퇴원약_일수": + case "OCM_퇴원약_용법": + { + var comNum = PatientContext.Value(context.ComInf, "ComNum"); + if (!decimal.TryParse(comNum, out var m)) + { + return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); + } + var meds = store.DischargeMedications(m); + if (meds.Count == 0) + { + return new TagValue(false, "퇴원의약 오더가 없습니다"); + } + // 열 하나를 줄단위로(:10516-10615) — 마지막 뒤에도 개행(AppendLine 원문). + // 용법은 LEFT JOIN 미매칭이면 빈 줄이 들어간다(레거시 그대로). + var column = tag switch + { + "OCM_퇴원약_코드" => "OdrCod", + "OCM_퇴원약_명칭" => "OdrNam", + "OCM_퇴원약_용량" => "QtyTmsQty", + "OCM_퇴원약_횟수" => "OdrTms", + "OCM_퇴원약_일수" => "OdrDay", + _ => "UsgKorNam", + }; + return new TagValue(true, string.Concat( + meds.Select(r => $"{PatientContext.Value(r, column)}\r\n"))); + } + case "OCM_퇴원약": + case "OCM_퇴원약_New": + { + var comNum = PatientContext.Value(context.ComInf, "ComNum"); + if (!decimal.TryParse(comNum, out var m)) + { + return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); + } + var meds = store.DischargeMedications(m); + if (meds.Count == 0) + { + // 행이 없으면 SRCH 만 "해당없음." 을 찍는다(:10704/:10864) — + // 우리 병원 분기라 그대로 값이 된다 + return new TagValue(true, "해당없음."); + } + var text = new System.Text.StringBuilder(); + foreach (var row in meds) + { + text.Append(tag == "OCM_퇴원약" + ? MedicationLineLegacy(row) + : MedicationLineNew(row)); + text.Append("\r\n"); + } + return new TagValue(true, text.ToString()); + } case "OCM_수술진단명": // PURME 전용(:3066) — 그 밖의 병원은 Case 미매칭으로 항상 빈 값이다 return new TagValue(false, @@ -2434,6 +2492,72 @@ public sealed class PatientTagResolver : ITagValueResolver => value.Length > 0 ? new TagValue(true, value) : new TagValue(false, reasonWhenEmpty); } + /// CP949 바이트 길이 근사 — ASCII 1, 그 밖(한글 등) 2. 레거시 LenK 대응 + private static int ByteLen(string value) => value.Sum(c => c > 127 ? 2 : 1); + + /// 바이트 기준 절단 — 문자 경계에서 자르므로 레거시의 반각 깨짐("?") 재절단 분기는 필요 없다 + private static string CutBytes(string value, int maxBytes) + { + var used = 0; + for (var i = 0; i < value.Length; i++) + { + used += value[i] > 127 ? 2 : 1; + if (used > maxBytes) + { + return value[..i]; + } + } + return value; + } + + /// + /// 퇴원약 한 줄(OCM_퇴원약, :10640-10700) — 원문 결함까지 그대로다: + /// PadRight 를 "채울 개수"로 오용해(실제 의미는 총폭) 고정폭 정렬이 어긋나고, + /// 코드가 20바이트를 넘으면 음수 인자 예외 → 레거시는 오류창 후 빈 값이었다 + /// (여기서는 위 catch 가 사유로 바꾼다). SATCH 의 비고 괄호 분기는 미이식. + /// + private static string MedicationLineLegacy(IReadOnlyDictionary row) + { + static string Get(IReadOnlyDictionary r, string c) + => r.TryGetValue(c, out var v) ? v.Trim() : string.Empty; + var cod = Get(row, "OdrCod"); + var nam = Get(row, "OdrNam"); + var qty = Get(row, "QtyTmsQty"); + var tms = Get(row, "OdrTms"); + var day = Get(row, "OdrDay"); + var codText = cod.PadRight(20 - ByteLen(cod)); + var namText = ByteLen(nam) < 50 ? nam.PadRight(50 - ByteLen(nam)) : CutBytes(nam, 50); + var qtyText = qty.Length < 6 ? qty.PadRight(5 - qty.Length) : qty; + var tmsText = tms.Length < 6 ? tms.PadRight(5 - tms.Length) : tms; + var dayText = day.Length < 6 ? day.PadRight(5 - day.Length) : day; + return $"{codText}{namText}{qtyText} {tmsText} {dayText} {Get(row, "UsgKorNam")}"; + } + + /// + /// 퇴원약 한 줄(OCM_퇴원약_New, :10760-10860) — 바이트 고정폭(코드 10·명칭 37). + /// 용량·횟수·일수의 패딩이 잘라낸 값이 아니라 원본 전체 바이트로 계산되는 + /// 원문 결함 그대로 — 5자 이상이면 음수 예외 → 사유가 된다(레거시는 오류창+빈 값). + /// + private static string MedicationLineNew(IReadOnlyDictionary row) + { + static string Get(IReadOnlyDictionary r, string c) + => r.TryGetValue(c, out var v) ? v.Trim() : string.Empty; + static string PadTail(string value, int count) => value + string.Empty.PadLeft(count); + var cod = Get(row, "OdrCod"); + var codCut = cod.Length < 10 ? cod : cod[..10]; + var codText = PadTail(codCut, 10 - ByteLen(codCut)); + var nam = Get(row, "OdrNam"); + var namCut = ByteLen(nam) < 36 ? nam : CutBytes(nam, 36); + var namText = PadTail(namCut, 37 - ByteLen(namCut)); + var qty = Get(row, "QtyTmsQty"); + var qtyText = PadTail(qty.Length < 4 ? qty : qty[..4], 4 - ByteLen(qty)); + var tms = Get(row, "OdrTms"); + var tmsText = PadTail(tms.Length < 4 ? tms : tms[..4], 4 - ByteLen(tms)); + var day = Get(row, "OdrDay"); + var dayText = PadTail(day.Length < 4 ? day : day[..4], 4 - ByteLen(day)); + return $"{codText} {namText} {qtyText}{tmsText}{dayText}{Get(row, "UsgKorNam")}"; + } + /// /// 병리판독의·진단검사의 4종 — 환자와 무관한 원내 의사 조회지만 레거시가 환자 태그로 노출한다. /// 이름과 번호가 같은 행에서 나온다(레거시는 태그마다 따로 조회해 다른 사람이 될 수 있었다). @@ -2555,6 +2679,8 @@ public sealed class PatientTagResolver : ITagValueResolver "OCM_영상촬영이력", "OCM_수술주상병", "OCM_수술집도의_진료과", "OCM_수술집도의_그룹진료과", "OCM_수술명칭", "OCM_수술진단명", "OCM_수술처치", + "OCM_퇴원약_코드", "OCM_퇴원약_명칭", "OCM_퇴원약_용량", "OCM_퇴원약_횟수", + "OCM_퇴원약_일수", "OCM_퇴원약_용법", "OCM_퇴원약", "OCM_퇴원약_New", "OCM_입원시간", "OCM_입원일시", "OCM_입원일시_영문", "OCM_입실시간", "OCM_의사퇴원예고일시", "OCM_퇴원예정일시", "OCM_퇴원시간", "OCM_퇴원일시", "OCM_퇴원예고일시", "OCM_재원일수", "OCM_퇴원예고재원일수", "OCM_입원일자_낮병동",