From 03fff0a3a41497269a88162a7023875f5b5fb3e2 Mon Sep 17 00:00:00 2001 From: Msystech Date: Thu, 20 Aug 2026 08:51:30 +0900 Subject: [PATCH] =?UTF-8?q?=EC=88=98=EC=88=A0=20=ED=8C=8C=EC=83=9D=206?= =?UTF-8?q?=EC=A2=85=20=E2=80=94=20236=EC=A2=85=20=E2=86=92=20242=EC=A2=85?= =?UTF-8?q?,=20=E3=89=98=20=EC=88=98=EC=88=A0=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=EC=9D=B4=20SKIP=20=EC=97=90=EC=84=9C=20=EC=8B=A4=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=EC=9C=BC=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OCM_수술주상병(bzDataInterface.vb:13148 + GetOprInfDT_OKDMain :13364 — 기존 수술 조회와의 실질 차이가 상병 집계의 주상병 한정(OPRVAR1='0') 한 줄이라 SurgeryStore 에 mainDiagnosisOnly 를 얹었다. 치환이 빗나가면 필터 없는 값이 조용히 나가므로 가드로 던진다), OCM_수술집도의_진료과/_그룹진료과(:3157/:3194 — 집도의(DTRUIDCOD)를 오늘 기준 마스터로 푸는 원문 특이점(OprStt 무필터·SYSDATE 유효기간) 그대로, 최신 수술 첫 행 고정), OCM_수술명칭(:3520 Case Else — OPRCOD 를 수술일 순으로, 각 행 뒤 공백 1칸 + CRLF 결합. GNBEDRO· HIMCHAN_BP 의 OPNAME 포함 분기 미이식). 사유 완결 2종: OCM_수술진단명(:3066 — PURME 전용, 그 외 병원은 Case 미매칭으로 항상 빈 값), OCM_수술처치(:3474 — SUSS·YJRCH 전용). ㉘ 정정: 수술 검증이 늘 SKIP 이던 원인은 소유자 탐색을 O_OprInf 로 한 것 — 실제 조회(GetOprInfDT)의 축은 S_OprInf(수술 신청)다. 표를 바로잡자 이 시험 DB 에서 수술 행 1건·OPNAME 실값이 검증됐다 (SurgeryStore 의 낡은 주석도 함께 정정). - dotnet test 342/342 · --edit-smoke 실패 0 (이름 검사 242종) - --db-patient ①~㊱ 전건 통과 — ㉘ 이 처음으로 실값 PASS - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 Co-Authored-By: Claude Fable 5 --- src/SheetMe.Data/Stores/PatientExtraStore.cs | 37 +++++++++++ src/SheetMe.Data/Stores/SurgeryStore.cs | 23 ++++++- src/SheetMe.Designer/Diagnostics/DbSmoke.cs | 11 ++-- .../Services/PatientTagResolver.cs | 64 +++++++++++++++++++ 4 files changed, 129 insertions(+), 6 deletions(-) diff --git a/src/SheetMe.Data/Stores/PatientExtraStore.cs b/src/SheetMe.Data/Stores/PatientExtraStore.cs index 3d0a220..dcaec54 100644 --- a/src/SheetMe.Data/Stores/PatientExtraStore.cs +++ b/src/SheetMe.Data/Stores/PatientExtraStore.cs @@ -704,6 +704,43 @@ public sealed class PatientExtraStore "SELECT UDF_GETAGE(PatBthDay, :a) FROM P_PatInf WHERE PatChtNum = RPAD(:c, 10)", ("a", day8), ("c", chtNum)); + /// + /// 수술 집도의의 진료과/그룹진료과 이름(OCM_수술집도의_진료과 :3157 · _그룹진료과 :3194) — + /// 수술 신청의 집도의(S_OprTyp OprTyp='DTRUIDCOD')를 오늘 기준 사용자·부서 마스터로 푼다. + /// 원문 특이점 그대로: OprStt 필터가 없고(취소 수술 포함), 유효기간이 수술일이 아니라 + /// 오늘 기준이다. 최신 수술(OprKey DESC) 첫 행으로 접는다(원문 Rows(0)). + /// + public string SurgeonDepartment(decimal comNum, bool groupName) + => Scalar( + "SELECT v FROM (SELECT " + + (groupName ? "DtlCodNam" : "DepKorNam") + " v FROM S_OprInf" + + " INNER JOIN S_OprTyp ON OprKey = OprKeyNum AND OprComNum = :m AND OprTyp = 'DTRUIDCOD'" + + " INNER JOIN M_UidMst ON UidCod = OprCod" + + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN UidStrDte AND UidEndDte" + + " INNER JOIN M_DepMst ON DepCod = UidDepCod" + + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN DepStrDte AND DepEndDte" + + (groupName + ? " INNER JOIN M_DtlMst ON DepGrpCod = DtlCod AND DtlTblCod = 'GDPTBL'" + + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN DtlStrDte AND DtlExpDte" + : string.Empty) + + " ORDER BY OprKey DESC) WHERE ROWNUM = 1", + ("m", comNum)); + + /// + /// 수술명 목록(OCM_수술명칭, :3520 Case Else) — 확정 수술의 수술코드명(OprTyp='OPRCOD') + /// 을 수술일 순으로. GNBEDRO·HIMCHAN_BP 의 OPNAME 포함 분기는 미이식. + /// 결합(첫 행 뒤 공백, 이후 CRLF)은 해석기가 원문대로 한다. + /// + public IReadOnlyList SurgeryNames(decimal comNum) + => Rows( + "SELECT OprCodNam FROM S_OprInf" + + " INNER JOIN S_OprTyp ON OprKeyNum = OprKey" + + " WHERE OprComNum = :m AND OprStt = 'E' AND OprTyp = 'OPRCOD'" + + " ORDER BY OprDte", + ("m", comNum)) + .Select(r => r.TryGetValue("OprCodNam", out var v) ? v : string.Empty) + .ToArray(); + /// 스칼라 1칸 — 0행이거나 NULL 이면 빈 문자열. 잔여 공백은 걷는다(문맥 행과 같은 규칙) private string Scalar(string sql, params (string Name, object Value)[] binds) { diff --git a/src/SheetMe.Data/Stores/SurgeryStore.cs b/src/SheetMe.Data/Stores/SurgeryStore.cs index cff38b6..e629873 100644 --- a/src/SheetMe.Data/Stores/SurgeryStore.cs +++ b/src/SheetMe.Data/Stores/SurgeryStore.cs @@ -8,7 +8,7 @@ namespace SheetMe.Data.Stores; /// 상병과 달리 병원 분기가 없다(Select Case 0회). 바인드는 내원번호 하나뿐이고 /// 접수일자 조건은 원문에서 이미 주석 처리되어 있다(:sAcpDte — 죽은 코드). /// -/// SQL 은 원문에서 기계 추출했다(133줄 그대로). O_OprInf 를 축으로 수술명·부위·상병· +/// SQL 은 원문에서 기계 추출했다(133줄 그대로). S_OprInf(수술 신청)를 축으로 수술명·부위·상병· /// 집도의·마취의를 LISTAGG 로 접는 큰 조회이고, 소비 태그 8종이 전부 /// 첫 행에서 컬럼 하나를 집는다 — 그래서 행 목록만 돌려주고 선택은 해석기가 한다. /// @@ -165,12 +165,31 @@ ORDER BY OprComNum, OprDte DESC, OprDayNum ASC #region Methods /// 이 내원의 수술 행들 — 없으면 빈 목록 public List> Surgeries(decimal comNum) + => Surgeries(comNum, mainDiagnosisOnly: false); + + /// + /// 가 true 면 상병 집계를 주상병(OPRVAR1='0')로 + /// 한정한다 — 레거시 GetOprInfDT_OKDMain(bzDataInterface.vb:13364)과 + /// GetOprInfDT 의 유일한 실질 차이가 이 조인 조건 한 줄이다 + /// (OKDMain 판의 수술부위·비고 컬럼 부재와 정렬 약화는 옮기지 않는다 — + /// 그 판으로 부위 태그를 부르면 레거시도 컬럼 예외였다). + /// + public List> Surgeries(decimal comNum, bool mainDiagnosisOnly) { using var connection = new OracleConnection(connectionString); connection.Open(); using var command = connection.CreateCommand(); command.BindByName = true; - command.CommandText = Query; + var sql = mainDiagnosisOnly + ? Query.Replace("AND OKDNAM.OprTyp = 'OKD'", + "AND OKDNAM.OprTyp = 'OKD' AND OKDNAM.OPRVAR1 = '0'") + : Query; + if (mainDiagnosisOnly && !sql.Contains("OPRVAR1", StringComparison.Ordinal)) + { + // 치환이 빗나가면 주상병 필터 없는 값이 조용히 나간다 — 빈 값보다 나쁘다 + throw new InvalidOperationException("주상병 필터 치환이 원문과 맞지 않습니다"); + } + command.CommandText = sql; command.Parameters.Add(new OracleParameter("iComNum", comNum)); var rows = new List>(); diff --git a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs index 4470512..25185a1 100644 --- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs @@ -2242,18 +2242,21 @@ public static class DbSmoke } // ㉘ 수술 — 원문 기계 추출 SQL 이 실제로 돈다. - var oprOwner = FirstComNum(conn, "O_OprInf", "OprComNum", "OprComNum"); + // 처음엔 O_OprInf 로 소유자를 찾아 늘 SKIP 이었다 — 실제 조회 + // (GetOprInfDT)의 축은 S_OprInf(수술 신청)다. 표를 바로잡으니 + // 이 시험 DB 에서도 수술 태그가 실검증된다. + var oprOwner = FirstComNum(conn, "S_OprInf", "OprComNum", "OprComNum"); if (oprOwner is null) { // "행이 없다"와 "표가 없다/조회가 죽었다"를 가른다 — // FirstComNum 은 예외를 삼키므로 이 줄 없이는 못 가른다. foreach (var line in ExplainPeriod(conn, 0, - "SELECT COUNT(*) OPRCNT FROM O_OprInf WHERE :k = 0")) + "SELECT COUNT(*) OPRCNT FROM S_OprInf WHERE :k = 0")) { - lines.Add($" [진단] O_OprInf: {line}"); + lines.Add($" [진단] S_OprInf: {line}"); } lines.Add("SKIP ㉘ 수술 — 확인 불가. 위 [진단] 줄이 원인이다:" - + " ORA-00942 면 이 시험 DB 에 O_OprInf 가 아예 없는 것이고," + + " ORA-00942 면 이 시험 DB 에 S_OprInf 가 아예 없는 것이고," + " 수술 태그는 운영 DB 에서만 검증할 수 있다"); } else diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs index 9df558e..cad9c66 100644 --- a/src/SheetMe.Designer/Services/PatientTagResolver.cs +++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs @@ -1716,6 +1716,8 @@ public sealed class PatientTagResolver : ITagValueResolver "ETC_승인의사", "OCM_수술소독간호사", "OCM_적용진료과", "OCM_적용진료의사", "OCM_전과", "OCM_간호정보조사지_과거력", "OCM_영상촬영이력", + "OCM_수술주상병", "OCM_수술집도의_진료과", "OCM_수술집도의_그룹진료과", + "OCM_수술명칭", "OCM_수술진단명", "OCM_수술처치", }; /// @@ -2246,6 +2248,66 @@ public sealed class PatientTagResolver : ITagValueResolver } return new TagValue(true, text.ToString()); } + case "OCM_수술주상병": + { + // GetOprInfDT_OKDMain(:13364) — 상병 집계를 주상병(OPRVAR1='0')로 + // 한정한 수술 조회의 첫 행 OkdNam. 이 시험 DB 엔 O_OprInf 계열이 + // 없어 운영 DB 에서만 값 검증이 된다(수술 8종과 동일). + var comNum = PatientContext.Value(context.ComInf, "ComNum"); + if (!decimal.TryParse(comNum, out var m)) + { + return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); + } + if (contextStoreSource is null) + { + return new TagValue(false, "수술 조회가 연결되지 않았습니다"); + } + var rows = new SurgeryStore( + Services.ConfigService.Current.ConnectionString) + .Surgeries(m, mainDiagnosisOnly: true); + if (rows.Count == 0) + { + return new TagValue(false, "확정(OprStt='E')된 수술 신청이 없습니다"); + } + var okd = rows[0].TryGetValue("OKDNAM", out var v) ? v : string.Empty; + return okd.Length > 0 + ? new TagValue(true, okd) + : new TagValue(false, "수술 행에 주상병(OPRVAR1='0') 표기가 없습니다"); + } + case "OCM_수술집도의_진료과": + case "OCM_수술집도의_그룹진료과": + { + var comNum = PatientContext.Value(context.ComInf, "ComNum"); + if (!decimal.TryParse(comNum, out var m)) + { + return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); + } + return Wrap(store.SurgeonDepartment(m, tag == "OCM_수술집도의_그룹진료과"), + "집도의(DTRUIDCOD)와 오늘 기준 부서가 짝이 되는 수술이 없습니다"); + } + case "OCM_수술명칭": + { + var comNum = PatientContext.Value(context.ComInf, "ComNum"); + if (!decimal.TryParse(comNum, out var m)) + { + return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); + } + var names = store.SurgeryNames(m); + if (names.Count == 0) + { + return new TagValue(false, "확정 수술의 수술코드명(OPRCOD)이 없습니다"); + } + // 원문 결합(:3546-3556): 각 행 뒤 공백 1칸, 행 사이 CRLF(마지막에도 공백) + return new TagValue(true, string.Join("\r\n", names.Select(n => $"{n} "))); + } + case "OCM_수술진단명": + // PURME 전용(:3066) — 그 밖의 병원은 Case 미매칭으로 항상 빈 값이다 + return new TagValue(false, + "PURME 전용 태그입니다 — 그 밖의 병원에서는 레거시도 항상 빈 값입니다"); + case "OCM_수술처치": + // SUSS·YJRCH 전용(:3474) — 그 밖의 병원은 Case 미매칭으로 항상 빈 값이다 + return new TagValue(false, + "SUSS·YJRCH 전용 태그입니다 — 그 밖의 병원에서는 레거시도 항상 빈 값입니다"); case "OCM_간호정보조사지_과거력": { var comNum = PatientContext.Value(context.ComInf, "ComNum"); @@ -2491,6 +2553,8 @@ public sealed class PatientTagResolver : ITagValueResolver "ETC_승인의사", "OCM_수술소독간호사", "OCM_적용진료과", "OCM_적용진료의사", "OCM_전과", "OCM_간호정보조사지_과거력", "OCM_영상촬영이력", + "OCM_수술주상병", "OCM_수술집도의_진료과", "OCM_수술집도의_그룹진료과", + "OCM_수술명칭", "OCM_수술진단명", "OCM_수술처치", "OCM_입원시간", "OCM_입원일시", "OCM_입원일시_영문", "OCM_입실시간", "OCM_의사퇴원예고일시", "OCM_퇴원예정일시", "OCM_퇴원시간", "OCM_퇴원일시", "OCM_퇴원예고일시", "OCM_재원일수", "OCM_퇴원예고재원일수", "OCM_입원일자_낮병동",