From 4e6e086ece2248ac1134efafe55ba3e1c40dfd7b Mon Sep 17 00:00:00 2001 From: Msystech Date: Wed, 19 Aug 2026 19:16:37 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9C=A0=ED=98=95=C2=B7=EC=88=98=EB=82=A9?= =?UTF-8?q?=C2=B7=EC=A7=84=EB=A3=8C=EA=B8=B0=EA=B0=84=C2=B7=EC=9D=91?= =?UTF-8?q?=EA=B8=89=EC=A6=9D=EC=83=81=C2=B7=EC=A0=84=EA=B3=BC=EB=82=B4?= =?UTF-8?q?=EC=97=AD=208=EC=A2=85=20=E2=80=94=20185=EC=A2=85=20=E2=86=92?= =?UTF-8?q?=20193=EC=A2=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OCM_계산유형(bzDataInterface.vb:5133 — 문맥 자격의 보험·계산 코드로 M_CalMst.CalNam), OCM_보험유형(:5186 — M_InsMst.InsNam), OCM_수납일시(:5287 — 자격의 수납청구→수납 갱신일시. 원문 두 쿼리 다 비결정 Rows(0) — 최신 청구의 최신 수납으로 고정, 한 왕복으로 접음), OCM_진료기간(:2236 — SRCH 갈래: 퇴원이면 접수~퇴원, 재원이면 최신 퇴원약 오더→퇴원예정→오늘 순. 외래: 최초 접수~최신 접수/오늘. HANKOOK·SRH·GUPOSS 분기 미이식), OCM_진료기간_진료과(:2575 — 입원만 값. 외래는 두 번째 쿼리의 진료과 바인드 미등록(레거시 결함, :2644)으로 상시 ORA-01008→빈 값 — 사유로 완결), OCM_응급증상여부_YES/NO (:12748/:12830 — 응급의료관리료 오더의 산정 여부. 코드 목록은 GetDtlMst("EMGEMGCOD") 그대로 + 0건이면 하드코딩 6종 폴백. 행이 없으면 레거시 $NoCheck$ — 체크 상태를 정하지 않는다. SJRCH 분기 미이식), OCM_전과내역(:1103 — 과명+공백7+구간, CRLF 결합. 과 조인이 의사 조인에 종속되는 원문 모양 유지). 체크류 렌더 보완: 값을 못 만든 태그(사유)는 체크박스·라디오의 컨트롤을 그대로 둔다 — 레거시도 SetValue 를 건너뛴다. 라벨에 사유를 쓰면 "남 ☑" 이 "[태그 — 사유] ☑" 가 되어 서식이 깨진다. - dotnet test 338/338 · --edit-smoke 실패 0 (이름 검사 193종) - --db-patient ①~㉟ 전건 통과 (㉟ 에 7종 추가 — 보험유형 실값 4자, 응급 쿼리의 ORDER BY ROWID 모호성은 OdrKey 타이브레이크로 수정) - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 Co-Authored-By: Claude Fable 5 --- src/SheetMe.Data/Stores/PatientExtraStore.cs | 141 ++++++++++++++++ src/SheetMe.Designer/Diagnostics/DbSmoke.cs | 7 + .../Services/PatientTagResolver.cs | 159 ++++++++++++++++++ src/SheetMe.Designer/Services/PrintService.cs | 7 + 4 files changed, 314 insertions(+) diff --git a/src/SheetMe.Data/Stores/PatientExtraStore.cs b/src/SheetMe.Data/Stores/PatientExtraStore.cs index c03343e..3bfea53 100644 --- a/src/SheetMe.Data/Stores/PatientExtraStore.cs +++ b/src/SheetMe.Data/Stores/PatientExtraStore.cs @@ -385,6 +385,147 @@ public sealed class PatientExtraStore + " WHERE ComNum = :m ORDER BY CowStrDtm ASC) WHERE ROWNUM = 1", ("m", comNum)); + /// + /// 계산유형 이름(OCM_계산유형, :5133) — 자격의 보험·계산 코드로 M_CalMst.CalNam. + /// 원문 Rows(0) 에 ORDER BY 가 없다 — 시작일 최신 행으로 고정한다. + /// + public string CalculationTypeName(string insCod, string calCod, string strDte) + => Scalar( + "SELECT v FROM (SELECT CalNam v FROM M_CalMst" + + " WHERE CalInsCod = :i AND CalCod = :l AND :d BETWEEN CalStrDte AND CalEndDte" + + " ORDER BY CalStrDte DESC) WHERE ROWNUM = 1", + ("i", insCod), ("l", calCod), ("d", strDte)); + + /// 보험유형 이름(OCM_보험유형, :5186) — M_InsMst.InsNam. 같은 규칙으로 고정 + public string InsuranceTypeName(string insCod, string strDte) + => Scalar( + "SELECT v FROM (SELECT InsNam v FROM M_InsMst" + + " WHERE InsCod = :i AND :d BETWEEN InsStrDte AND InsEndDte" + + " ORDER BY InsStrDte DESC) WHERE ROWNUM = 1", + ("i", insCod), ("d", strDte)); + + /// + /// 수납일시(OCM_수납일시, :5287) — 자격에 걸린 수납청구(P_RblInf, RblCalNxt=0)의 + /// 수납(P_RcpInf) 갱신일시 12자리. 원문은 두 쿼리 모두 ORDER BY 없는 Rows(0) — + /// 청구·수납 다건이면 임의였다. 최신 청구의 최신 수납으로 고정하고 한 왕복으로 접었다. + /// + public string ReceiptStamp(decimal comNum, string mtiSeq, string strDte) + => Scalar( + "SELECT v FROM (SELECT RcpUpdDtf v FROM P_RcpInf WHERE RcpRblNum IN (" + + " SELECT n FROM (SELECT RblNum n FROM P_RblInf" + + " WHERE RblComNum = :m AND RblMtiSeq = :s AND RblStrDte = :d AND RblCalNxt = 0" + + " ORDER BY RblNum DESC) WHERE ROWNUM = 1)" + + " ORDER BY RcpUpdDtf DESC) WHERE ROWNUM = 1", + ("m", comNum), ("s", mtiSeq), ("d", strDte)); + + /// + /// 최신 퇴원약 오더 일시(OCM_진료기간 SRCH 갈래, :2299-2318) — 인라인뷰 정렬이 + /// 원문부터 있어 결정적이다. 필터(처방의약·숨김 제외 등)는 원문 그대로. + /// + public string DischargeMedicationOrderDate(decimal comNum) + => Scalar( + "SELECT v FROM (SELECT OdrDtm v FROM O_OdrInf" + + " 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 DESC) WHERE ROWNUM = 1", + ("m", comNum)); + + /// + /// 외래 진료기간 시작(OCM_진료기간 외래 갈래, :2325-2345) — 이 내원 이하의 취소 아닌 + /// 최초 접수일시. OCM_최초내원일과 달리 진료행 조인·재진 필터가 없다(원문 그대로 — + /// ComNfsTyp='N' 조건은 주석 처리돼 있어 옮기지 않는다). + /// + public string TreatmentPeriodStart(string chtNum, string acpDtm) + => Scalar( + "SELECT v FROM (SELECT ComAcpDtm v FROM P_ComInf" + + " WHERE ComChtNum = RPAD(:c, 10) AND ComAcpDtm <= :a AND ComAcpStt <> 'OC'" + + " ORDER BY ComAcpDtm) WHERE ROWNUM = 1", + ("c", chtNum), ("a", acpDtm)); + + /// 외래 진료기간 끝(:2352-2372) — 시작 이후 오늘(2359)까지의 최신 접수일시 + public string TreatmentPeriodEnd(string chtNum, string strDte) + => Scalar( + "SELECT v FROM (SELECT ComAcpDtm v FROM P_ComInf" + + " WHERE ComChtNum = RPAD(:c, 10) AND ComAcpDtm >= :s" + + " AND ComAcpDtm <= TO_CHAR(SYSDATE, 'YYYYMMDD') || '2359' AND ComAcpStt <> 'OC'" + + " ORDER BY ComAcpDtm DESC) WHERE ROWNUM = 1", + ("c", chtNum), ("s", strDte)); + + /// + /// 응급증상 오더의 산정 여부(OCM_응급증상여부_YES/NO, :12748/:12830) — 응급 내원에 + /// 걸린 응급의료관리료 오더 중 한 행의 OdrInsYon. 행이 없으면 null(레거시 $NoCheck$ — + /// 체크 상태를 정하지 않는다). 원문 Rows(0) 에 ORDER BY 가 없다 — 오더일시 최신으로 + /// 고정한다. 코드 목록은 레거시 GetDtlMst("EMGEMGCOD") 그대로: 코드표에 있으면 그것, + /// 없으면 하드코딩 6종(레거시 폴백). + /// + public string? EmergencySymptomInsYon(decimal comNum) + { + var codes = Rows("SELECT DtlCod FROM M_DtlMst WHERE DtlTblCod = 'EMGEMGCOD' ORDER BY DtlCod") + .Select(r => r.TryGetValue("DtlCod", out var v) ? v : string.Empty) + .Where(v => v.Length > 0) + .ToArray(); + if (codes.Length == 0) + { + codes = new[] { "V1200", "V1300", "V1400", "V1500", "AC103", "AC105" }; + } + var placeholders = string.Join(", ", codes.Select((_, i) => $":g{i}")); + var binds = new List<(string, object)> { ("m", comNum) }; + for (var i = 0; i < codes.Length; i++) + { + binds.Add(($"g{i}", codes[i])); + } + var value = Scalar( + "SELECT v FROM (SELECT OdrInsYon v FROM S_EmgInf OCMEMG" + + " LEFT JOIN S_EmgIcmInf ICMEMG ON OCMEMG.EmgComNum = ICMEMG.EmgOcmNum" + + " INNER JOIN O_OdrInf ON CASE WHEN NVL(ICMEMG.EmgComNum, 0) > 0" + + " THEN ICMEMG.EmgComNum ELSE OCMEMG.EmgComNum END = OdrComNum" + + " INNER JOIN O_OdsInf ON OdrKey = OdsOdrKey AND OdsStt = 'OE'" + + " INNER JOIN M_SgaMst ON OdrCod = SgaCod" + + " AND OdrDtm BETWEEN SgaStrDte AND SgaEndDte || '2359'" + + " WHERE OCMEMG.EmgComNum = :m" + + " AND OdrStt = 'OE' AND (OdrHidden IS NULL OR OdrHidden <> 1)" + + $" AND OdrElcCod IN ({placeholders})" + + " ORDER BY OdrDtm DESC, OdrKey DESC) WHERE ROWNUM = 1", + binds.ToArray()); + // 빈 문자열은 "행은 있는데 값이 NULL"과 "행이 없다"를 못 가른다 — COUNT 로 가른다 + if (value.Length > 0) + { + return value; + } + var count = Scalar( + "SELECT COUNT(*) FROM S_EmgInf OCMEMG" + + " LEFT JOIN S_EmgIcmInf ICMEMG ON OCMEMG.EmgComNum = ICMEMG.EmgOcmNum" + + " INNER JOIN O_OdrInf ON CASE WHEN NVL(ICMEMG.EmgComNum, 0) > 0" + + " THEN ICMEMG.EmgComNum ELSE OCMEMG.EmgComNum END = OdrComNum" + + " INNER JOIN O_OdsInf ON OdrKey = OdsOdrKey AND OdsStt = 'OE'" + + " INNER JOIN M_SgaMst ON OdrCod = SgaCod" + + " AND OdrDtm BETWEEN SgaStrDte AND SgaEndDte || '2359'" + + " WHERE OCMEMG.EmgComNum = :m" + + " AND OdrStt = 'OE' AND (OdrHidden IS NULL OR OdrHidden <> 1)" + + $" AND OdrElcCod IN ({placeholders})", + binds.ToArray()); + return count == "0" ? null : string.Empty; + } + + /// + /// 전과 내역(OCM_전과내역, :1103) — 진료 행 전부를 과명·구간과 함께. + /// 원문 조인 그대로: 과 조인이 담당의 조인의 병원 코드에 종속돼(DepHspCod=Usr.UidHspCod) + /// 의사 매칭이 빠지면 과명도 빈다. 정렬(CodStrDtm)이 원문부터 있어 결정적이다. + /// + public IReadOnlyList> TransferHistory(decimal comNum) + => Rows( + "SELECT DepKorNam AS DepNam, Cod.CodStrDtm, Cod.CodEndDtm FROM P_CodInf Cod" + + " LEFT OUTER JOIN M_UidMst Usr ON Usr.UidCod = CodDtrCod AND Usr.UidDtrYon = 'Y'" + + " AND Cod.CodEndDtm BETWEEN Usr.UidStrDte AND Usr.UidEndDte || '2359'" + + " LEFT OUTER JOIN M_DepMst Dep ON DepCod = CodDepCod AND DepHspCod = Usr.UidHspCod" + + " AND DepWrkTyp = 'D' AND Cod.CodEndDtm BETWEEN DepStrDte AND DepEndDte || '2359'" + + " WHERE CodComNum = :m AND CodMtiSeq = '0' ORDER BY CodStrDtm", + ("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 0985b79..8fbe010 100644 --- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs @@ -2437,6 +2437,13 @@ public static class DbSmoke ("병실인실", () => owner2 is null ? "-" : extra.RoomBedCount(owner2.Value).Length.ToString()), ("병실차액", () => owner2 is null ? "-" : extra.RoomExtraCharge(owner2.Value).Length.ToString()), ("산재지정번호", () => extra.IndustrialAccidentNumber("0506", "20260819").Length.ToString()), + ("계산유형", () => extra.CalculationTypeName("1", "01", "20260101").Length.ToString()), + ("보험유형", () => extra.InsuranceTypeName("1", "20260101").Length.ToString()), + ("수납일시", () => owner2 is null ? "-" : extra.ReceiptStamp(owner2.Value, "0", "20260101").Length.ToString()), + ("진료기간시작", () => extra.TreatmentPeriodStart(extraOwner, "999912312359").Length.ToString()), + ("퇴원약오더", () => owner2 is null ? "-" : extra.DischargeMedicationOrderDate(owner2.Value).Length.ToString()), + ("응급증상", () => owner2 is null ? "-" : (extra.EmergencySymptomInsYon(owner2.Value) ?? "[없음]")), + ("전과내역", () => owner2 is null ? "-" : extra.TransferHistory(owner2.Value).Count.ToString()), }; var shapes2 = new List(); var failed = new List(); diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs index 906b5b0..746c7a2 100644 --- a/src/SheetMe.Designer/Services/PatientTagResolver.cs +++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs @@ -1380,6 +1380,9 @@ public sealed class PatientTagResolver : ITagValueResolver "OCM_감염분류_감염정보", "OCM_감염정보", "OCM_퇴원분석_수술처치명", "OCM_Cosign_의사명", "OCM_병실인실", "OCM_병실차액", + "OCM_계산유형", "OCM_보험유형", "OCM_수납일시", + "OCM_진료기간", "OCM_진료기간_진료과", + "OCM_응급증상여부_YES", "OCM_응급증상여부_NO", "OCM_전과내역", }; /// @@ -1638,6 +1641,160 @@ public sealed class PatientTagResolver : ITagValueResolver ? Wrap(store.RoomExtraCharge(m), "병실 등급·산정 마스터가 짝이 되는 행이 없습니다") : new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); } + case "OCM_계산유형": + case "OCM_보험유형": + case "OCM_수납일시": + { + // 자격의 코드·시작일은 문맥 CoiInf 행 — 레거시의 시점 분기 + // (퇴원/현재/접수, :5148-5156)와 같은 판정이다. + // 레거시는 자격 0행이면 Rows(0) 예외 → 오류창 → "" 였다. + if (context.CoiInf.Count == 0) + { + return new TagValue(false, "이 내원에 보험 자격(P_CoiInf) 행이 없습니다"); + } + var insCod = PatientContext.Value(context.CoiInf, "CoiInsCod"); + var strDte = PatientContext.Value(context.CoiInf, "CoiStrDte"); + if (tag == "OCM_계산유형") + { + return Wrap(store.CalculationTypeName(insCod, + PatientContext.Value(context.CoiInf, "CoiCalCod"), strDte), + "계산 마스터(M_CalMst)에 짝이 되는 행이 없습니다"); + } + if (tag == "OCM_보험유형") + { + return Wrap(store.InsuranceTypeName(insCod, strDte), + "보험 마스터(M_InsMst)에 짝이 되는 행이 없습니다"); + } + var comNum = PatientContext.Value(context.ComInf, "ComNum"); + if (!decimal.TryParse(comNum, out var m)) + { + return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); + } + var stamp = store.ReceiptStamp(m, + PatientContext.Value(context.CoiInf, "CoiMtiSeq"), strDte); + return stamp.Length >= 12 + ? new TagValue(true, Stamp(stamp)) + : new TagValue(false, "이 자격의 수납(P_RcpInf) 기록이 없습니다"); + } + case "OCM_진료기간": + { + // SRCH 갈래(:2295-2323)와 외래 공통 갈래(:2325-2380)를 옮겼다. + // HANKOOK·SRH·GUPOSS·Case Else 분기는 우리 병원이 아니라 미이식. + var acp = PatientContext.Value(context.ComInf, "ComAcpDtm"); + if (acp.Length < 8) + { + return new TagValue(false, "이 내원의 접수일시를 읽지 못했습니다"); + } + var start = Hyphenate(acp[..8]); + var today = PatientContext.Value(hospital, "ServerToday"); + if (Com(context, "ComPatTyp") == "I") + { + if (Com(context, "ComPrgStt") == "ILV") + { + var lev = Com(context, "ComLevDtm"); + return new TagValue(true, $"{start} ~ {Hyphenate(Head(lev, 8))}"); + } + var comNum = PatientContext.Value(context.ComInf, "ComNum"); + if (!decimal.TryParse(comNum, out var m)) + { + return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); + } + // 재원 중 — 끝은 최신 퇴원약 오더일 → 퇴원예정일 → 오늘 순(:2299-2321) + var order = store.DischargeMedicationOrderDate(m); + var end = order.Length >= 8 ? order[..8] + : Com(context, "ComLevSch").Length >= 8 ? Com(context, "ComLevSch")[..8] + : today; + return end.Length >= 8 + ? new TagValue(true, $"{start} ~ {Hyphenate(end[..8])}") + : new TagValue(false, "요양기관 행(서버 오늘)을 읽지 못했습니다"); + } + // 외래 — 최초 접수 ~ (그 이후 최신 접수 또는 오늘) + var first = store.TreatmentPeriodStart(chtNum, acp); + if (first.Length < 8) + { + return new TagValue(false, "취소 아닌 이전 내원이 없습니다"); + } + var last = store.TreatmentPeriodEnd(chtNum, first); + var tail = last.Length >= 8 ? last[..8] : today; + return tail.Length >= 8 + ? new TagValue(true, $"{Hyphenate(first[..8])} ~ {Hyphenate(tail[..8])}") + : new TagValue(false, "요양기관 행(서버 오늘)을 읽지 못했습니다"); + } + case "OCM_진료기간_진료과": + { + // 입원 갈래(:2600-2610)만 값이 된다. 외래 갈래는 두 번째 쿼리의 + // 진료과 바인드(sDepCod)가 파라미터에 등록되지 않아(레거시 결함, + // :2644-2647) 항상 ORA-01008 → 오류창 → 빈 값이었다 — 사유로 말한다. + if (Com(context, "ComPatTyp") != "I") + { + return new TagValue(false, + "레거시 결함(진료과 바인드 미등록)으로 외래에서는 항상 빈 값인 태그입니다"); + } + var acp = PatientContext.Value(context.ComInf, "ComAcpDtm"); + if (acp.Length < 8) + { + return new TagValue(false, "이 내원의 접수일시를 읽지 못했습니다"); + } + var start = Hyphenate(acp[..8]); + if (Com(context, "ComPrgStt") == "ILV") + { + return new TagValue(true, + $"{start} ~ {Hyphenate(Head(Com(context, "ComLevDtm"), 8))}"); + } + var today = PatientContext.Value(hospital, "ServerToday"); + return today.Length >= 8 + ? new TagValue(true, $"{start} ~ {Hyphenate(today)}") + : new TagValue(false, "요양기관 행(서버 오늘)을 읽지 못했습니다"); + } + case "OCM_응급증상여부_YES": + case "OCM_응급증상여부_NO": + { + // 체크박스 태그다: "Y"=체크, ""=언체크, 행 없음=상태를 정하지 않음 + // (레거시 $NoCheck$ — 사유로 돌려주면 체크류 렌더가 컨트롤을 그대로 둔다). + // SJRCH 분기(:12878-12886)는 미이식 — Case Else 갈래만 옮겼다. + var comNum = PatientContext.Value(context.ComInf, "ComNum"); + if (!decimal.TryParse(comNum, out var m)) + { + return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); + } + var insYon = store.EmergencySymptomInsYon(m); + if (insYon is null) + { + return new TagValue(false, + "응급의료관리료 오더가 없어 체크 여부를 정하지 않습니다"); + } + var yes = insYon == "0"; + return new TagValue(true, + (tag == "OCM_응급증상여부_YES") == yes ? "Y" : string.Empty); + } + case "OCM_전과내역": + { + var comNum = PatientContext.Value(context.ComInf, "ComNum"); + if (!decimal.TryParse(comNum, out var m)) + { + return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); + } + var history = store.TransferHistory(m); + if (history.Count == 0) + { + return new TagValue(false, "진료 행(P_CodInf) 이력이 없습니다"); + } + // 원문 결합(:1140-1150): 과명 + 공백 7 + 시작 ~ 끝, 행 간 CRLF(끝에는 없음) + var text = new System.Text.StringBuilder(); + foreach (var row in history) + { + if (text.Length > 0) + { + text.Append("\r\n"); + } + text.Append(PatientContext.Value(row, "DepNam")) + .Append(" ") + .Append(Stamp(PatientContext.Value(row, "CodStrDtm"))) + .Append(" ~ ") + .Append(Stamp(PatientContext.Value(row, "CodEndDtm"))); + } + return new TagValue(true, text.ToString()); + } case "OCM_LMP": { var comNum = PatientContext.Value(context.ComInf, "ComNum"); @@ -1814,6 +1971,8 @@ public sealed class PatientTagResolver : ITagValueResolver "ETC_건강보험증번호", "ETC_재해발생일", "ETC_산재지정번호", "OCM_감염분류_감염정보", "OCM_감염정보", "OCM_퇴원분석_수술처치명", "OCM_Cosign_의사명", "OCM_병실인실", "OCM_병실차액", "OCM_진료과_전화번호", "OCM_전과일시", "OCM_전과일자", + "OCM_계산유형", "OCM_보험유형", "OCM_수납일시", "OCM_진료기간", "OCM_진료기간_진료과", + "OCM_응급증상여부_YES", "OCM_응급증상여부_NO", "OCM_전과내역", "OCM_입원시간", "OCM_입원일시", "OCM_입원일시_영문", "OCM_입실시간", "OCM_의사퇴원예고일시", "OCM_퇴원예정일시", "OCM_퇴원시간", "OCM_퇴원일시", "OCM_퇴원예고일시", "OCM_재원일수", "OCM_퇴원예고재원일수", "OCM_입원일자_낮병동", diff --git a/src/SheetMe.Designer/Services/PrintService.cs b/src/SheetMe.Designer/Services/PrintService.cs index 5fd598d..4dfc49b 100644 --- a/src/SheetMe.Designer/Services/PrintService.cs +++ b/src/SheetMe.Designer/Services/PrintService.cs @@ -196,6 +196,13 @@ public static class PrintService { return control; } + // 체크류에서 값을 못 만들면 컨트롤을 그대로 둔다 — 레거시도 SetValue 를 건너뛰고 + // ($NoCheck$ · 오류) 라벨과 기본 체크 상태를 유지한다. 라벨에 사유를 쓰면 + // "남 ☑" 이 "[태그 — 사유] ☑" 가 되어 서식이 깨진다. + if (!value.Resolved && control is CheckBoxViewModel or RadioButtonViewModel) + { + return control; + } var model = control.Model.Clone(); if (value.Resolved && control is CheckBoxViewModel or RadioButtonViewModel) {