From ef4f36419f55690513c608d1b42493cee1e35d8f Mon Sep 17 00:00:00 2001 From: Msystech Date: Wed, 19 Aug 2026 15:25:49 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=AC=EC=82=AC=20=EC=83=81=EB=B3=91=204?= =?UTF-8?q?=EC=A2=85=EC=9D=84=20=EC=98=AE=EA=B8=B4=EB=8B=A4=20=E2=80=94=20?= =?UTF-8?q?59=EC=A2=85=20=E2=86=92=2063=EC=A2=85.=20=EC=9B=8C=ED=81=AC?= =?UTF-8?q?=ED=94=8C=EB=A1=9C=EA=B0=80=20=EC=A1=B0=EC=82=AC=ED=95=9C=20new?= =?UTF-8?q?=20=EB=8A=94=20=EC=9D=B4=EC=A0=9C=20=EC=83=81=EB=B3=91=C2=B7?= =?UTF-8?q?=EC=88=98=EC=88=A0=C2=B7=EC=8B=AC=EC=82=AC=EA=B9=8C=EC=A7=80=20?= =?UTF-8?q?=EB=81=9D=EB=82=AC=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bkd_상병쿼리(bzDataInterface.vb:14625-14811)의 이식. DiagnosisStore 와 구조가 같고 표만 B_OkdInf(심사분)다. SQL 52줄 원문 기계 추출(블록1 UNION 블록2, 주/부 필터는 각 블록의 RN 접기 앞). 병원 분기는 KIMEYE 하나 — 그 병원만 미이식 안내. 태그 4종(본문 확인): 심사_주상병명/코드는 첫 행, 심사_부상병명/코드는 줄바꿈 결합 (가로형이 없다). ## 게이트 - dotnet test 338/338 · --edit-smoke 실패 0(이름 검사 63종) - --db-patient 전건 통과 (B_OkdInf 도 이 시험 DB 에 없을 수 있어 수술과 같은 취급 — 런타임에서 해석기가 예외를 사유로 바꾼다) - --db-render P062 md5 불변 Co-Authored-By: Claude Opus 5 --- .../Stores/ReviewDiagnosisStore.cs | 109 ++++++++++++++++++ .../Services/PatientIdentity.cs | 10 ++ .../Services/PatientSession.cs | 3 +- .../Services/PatientTagResolver.cs | 68 ++++++++++- .../Views/PreviewWindow.xaml.cs | 3 +- 5 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 src/SheetMe.Data/Stores/ReviewDiagnosisStore.cs diff --git a/src/SheetMe.Data/Stores/ReviewDiagnosisStore.cs b/src/SheetMe.Data/Stores/ReviewDiagnosisStore.cs new file mode 100644 index 0000000..6bd95df --- /dev/null +++ b/src/SheetMe.Data/Stores/ReviewDiagnosisStore.cs @@ -0,0 +1,109 @@ +using Oracle.ManagedDataAccess.Client; + +namespace SheetMe.Data.Stores; + +/// +/// 심사 상병 조회 — 레거시 Bkd_상병쿼리(bzDataInterface.vb:14625-14811)의 이식. +/// (O_OkdInf)와 구조가 같고 표만 B_OkdInf(심사분)다. +/// 병원 분기는 KIMEYE 하나뿐이라 그 병원만 미이식 안내가 필요하다. +/// SQL 은 원문 기계 추출(52줄) — 블록1 UNION 블록2, 주/부 필터는 각 블록의 RN 접기 앞. +/// +public sealed class ReviewDiagnosisStore +{ + #region Member Fields + private readonly string connectionString; + + private const string Template = @" +SELECT * +FROM ( + SELECT * + FROM ( + SELECT OkdKcdCod, OkdNam, OkdKcdSeq, OkdDspSeq, OkdCfmSus, OkdEntDtf + , KcdCod, KcdElcCod, KcdKorNam, KcdEngNam + , ROW_NUMBER() OVER(PARTITION BY KcdCod ORDER BY KcdStrDte DESC) AS OKD_RN + FROM B_OkdInf + INNER JOIN P_ComInf + ON OkdComNum = ComNum + INNER JOIN M_KcdMst + ON KcdCod = TRIM(OkdKcdCod) + AND OkdKcdSeq = KcdSeq + AND KcdEndDte >= SUBSTR(ComAcpDtm, 1, 8) + AND KcdStrDte <= SUBSTR(ComAcpDtm, 1, 8) + WHERE OkdComNum = :piComNum + AND OkdStt = 'E' +{TYPE} + ) OKD + WHERE OKD_RN = 1 + + UNION + + SELECT * + FROM ( + SELECT OkdKcdCod, OkdNam, OkdKcdSeq, OkdDspSeq, OkdCfmSus, OkdEntDtf + , KCD.KcdCod, KCD.KcdElcCod, KCD.KcdKorNam, KCD.KcdEngNam + , ROW_NUMBER() OVER(PARTITION BY KCD.KcdCod ORDER BY KCD.KcdStrDte DESC) AS OKD_RN + FROM ( + SELECT OkdKcdCod, OkdNam, OkdKcdSeq, OkdDspSeq, OkdCfmSus, OkdEntDtf + , KcdCod, KcdElcCod, KcdKorNam, KcdEngNam + FROM B_OkdInf + INNER JOIN P_ComInf + ON OkdComNum = ComNum + LEFT JOIN M_KcdMst + ON KcdCod = TRIM(OkdKcdCod) + AND OkdKcdSeq = KcdSeq + AND KcdEndDte >= SUBSTR(ComAcpDtm, 1, 8) + AND KcdStrDte <= SUBSTR(ComAcpDtm, 1, 8) + WHERE OkdComNum = :piComNum + AND OkdStt = 'E' + ) OKD + INNER JOIN M_KcdMst KCD + ON KCD.KcdCod = TRIM(OkdKcdCod) + AND OkdKcdSeq = KCD.KcdSeq + WHERE 1=1 + AND (OKD.KcdCod IS NULL OR OKD.KcdCod = ' ') +{TYPE} + ) OKD + WHERE OKD_RN = 1 + ) +ORDER BY OkdDspSeq +"; + #endregion + + #region Constructors + public ReviewDiagnosisStore(string connectionString) => this.connectionString = connectionString; + #endregion + + #region Methods + /// 이 내원의 심사 상병 행들 — 표시 순서대로. 없으면 빈 목록 + public List> Diagnoses(decimal comNum, DiagnosisStore.Scope scope) + { + var filter = scope switch + { + DiagnosisStore.Scope.Main => " AND OkdDspSeq = '1' ", + DiagnosisStore.Scope.Sub => " AND OkdDspSeq <> '1' ", + _ => string.Empty, + }; + using var connection = new OracleConnection(connectionString); + connection.Open(); + using var command = connection.CreateCommand(); + command.BindByName = true; + command.CommandText = Template.Replace("{TYPE}", filter); + command.Parameters.Add(new OracleParameter("piComNum", comNum)); + + var rows = new List>(); + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + var row = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < reader.FieldCount; i++) + { + row[reader.GetName(i)] = reader.IsDBNull(i) + ? string.Empty + : reader.GetValue(i).ToString()!.Trim(); + } + rows.Add(row); + } + return rows; + } + #endregion +} diff --git a/src/SheetMe.Designer/Services/PatientIdentity.cs b/src/SheetMe.Designer/Services/PatientIdentity.cs index f89fe5a..f0299ea 100644 --- a/src/SheetMe.Designer/Services/PatientIdentity.cs +++ b/src/SheetMe.Designer/Services/PatientIdentity.cs @@ -109,6 +109,16 @@ public static class PatientIdentity : new DiagnosisStore(connection).Diagnoses(context.ComNum, scope); } + /// 심사 상병(B_OkdInf) 공급자 — 상병과 같은 정책 + public static Func>> + ReviewDiagnosisSourceFor(PatientContext context) + { + var connection = ConfigService.Current.ConnectionString; + return scope => connection.Length == 0 + ? new List>() + : new ReviewDiagnosisStore(connection).Diagnoses(context.ComNum, scope); + } + /// 수술 행 공급자 — 상병과 같은 정책(필요할 때 1회, 실패는 던져 사유로) public static Func>> SurgerySourceFor(PatientContext context) { diff --git a/src/SheetMe.Designer/Services/PatientSession.cs b/src/SheetMe.Designer/Services/PatientSession.cs index 1de2040..b7dfdd4 100644 --- a/src/SheetMe.Designer/Services/PatientSession.cs +++ b/src/SheetMe.Designer/Services/PatientSession.cs @@ -69,7 +69,8 @@ public static class PatientSession PatientIdentity.DoctorOf(picked), PatientIdentity.DepartmentOf(picked), PatientIdentity.HospitalNameOf(picked), PatientIdentity.DiagnosisSourceFor(picked), - PatientIdentity.SurgerySourceFor(picked)) + PatientIdentity.SurgerySourceFor(picked), + PatientIdentity.ReviewDiagnosisSourceFor(picked)) : session; return (tags, new MDataTableRunner(document, Variables())); } diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs index 8638c8a..bcc7e12 100644 --- a/src/SheetMe.Designer/Services/PatientTagResolver.cs +++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs @@ -179,12 +179,18 @@ public sealed class PatientTagResolver : ITagValueResolver private IReadOnlyList>? surgeryCache; + /// 심사 상병(B_OkdInf) 공급자 — 상병과 같은 정책 + private readonly Func>>? reviewSource; + + private readonly Dictionary>> reviewCache = new(); + public PatientTagResolver(PatientContext context, ITagValueResolver next, string residentNumber = "", IReadOnlyDictionary? address = null, IReadOnlyDictionary? doctor = null, IReadOnlyDictionary? department = null, string hospitalName = "", Func>>? diagnosisSource = null, - Func>>? surgerySource = null) + Func>>? surgerySource = null, + Func>>? reviewSource = null) { this.context = context; this.next = next; @@ -195,6 +201,7 @@ public sealed class PatientTagResolver : ITagValueResolver this.hospitalName = hospitalName; this.diagnosisSource = diagnosisSource; this.surgerySource = surgerySource; + this.reviewSource = reviewSource; } #endregion @@ -226,6 +233,10 @@ public sealed class PatientTagResolver : ITagValueResolver { return surgeryValue; } + if (FromReviewDiagnosis(tag) is { } reviewValue) + { + return reviewValue; + } if (Computed.TryGetValue(tag, out var compute)) { var made = compute(context); @@ -562,6 +573,60 @@ public sealed class PatientTagResolver : ITagValueResolver : new TagValue(false, $"수술 행은 있는데 {column} 값이 비어 있습니다"); } + /// + /// 심사 상병 4종 — B_OkdInf(심사분). 결합 규칙(본문 확인): + /// 주상병명/코드는 첫 행, 부상병명/코드는 줄바꿈 결합(가로형이 없다). + /// 병원 분기는 KIMEYE 하나뿐이다(Bkd_상병쿼리:14631). + /// + private TagValue? FromReviewDiagnosis(string tag) + { + var scope = tag switch + { + "OCM_심사_주상병명" or "OCM_심사_주상병코드" => DiagnosisStore.Scope.Main, + "OCM_심사_부상병명" or "OCM_심사_부상병코드" => DiagnosisStore.Scope.Sub, + _ => (DiagnosisStore.Scope?)null, + } is { } s ? s : (DiagnosisStore.Scope?)null; + if (scope is null) + { + return null; + } + if (UserSession.Current.HspCod == "KIMEYE") + { + return new TagValue(false, "이 병원(KIMEYE)의 심사 상병 분기는 아직 옮기지 않았습니다"); + } + if (reviewSource is null) + { + return new TagValue(false, "심사 상병 조회가 연결되지 않았습니다"); + } + if (!reviewCache.TryGetValue(scope.Value, out var rows)) + { + try + { + rows = reviewSource(scope.Value); + } + catch (Exception ex) + { + return new TagValue(false, $"심사 상병 조회에 실패했습니다: {ex.GetType().Name}"); + } + reviewCache[scope.Value] = rows; + } + if (rows.Count == 0) + { + return new TagValue(false, "이 내원에 심사 상병이 없습니다"); + } + static string Cell(Dictionary r, string c) => r.TryGetValue(c, out var v) ? v.Trim() : string.Empty; + var value = tag switch + { + "OCM_심사_주상병명" => Cell(rows[0], "KcdKorNam"), + "OCM_심사_주상병코드" => Cell(rows[0], "KcdElcCod"), + "OCM_심사_부상병명" => string.Join(Environment.NewLine, rows.Select(r => Cell(r, "KcdKorNam"))), + _ => string.Join(Environment.NewLine, rows.Select(r => Cell(r, "KcdElcCod"))), + }; + return value.Trim().Length > 0 + ? new TagValue(true, value) + : new TagValue(false, "심사 상병 행은 있는데 그 값이 비어 있습니다"); + } + /// 주민번호에서 나온 값 — 비면 비었는지 말한다 private TagValue Wrap(string value) => value.Length > 0 ? new TagValue(true, value) @@ -584,6 +649,7 @@ public sealed class PatientTagResolver : ITagValueResolver "OCM_주상병명", "OCM_주상병코드", "OCM_주상병POA", "OCM_OPNAME", "OCM_OPRCODNAM", "OCM_DX", "OCM_수술상병", "OCM_수술부위", "OCM_수술_OprPatETC", "OCM_수술집도의", "OCM_수술마취의사", + "OCM_심사_주상병명", "OCM_심사_주상병코드", "OCM_심사_부상병명", "OCM_심사_부상병코드", }; /// diff --git a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs index 663bd1f..416c972 100644 --- a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs +++ b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs @@ -226,7 +226,8 @@ public partial class PreviewWindow : Window PatientIdentity.DoctorOf(picked), PatientIdentity.DepartmentOf(picked), PatientIdentity.HospitalNameOf(picked), PatientIdentity.DiagnosisSourceFor(picked), - PatientIdentity.SurgerySourceFor(picked)); + PatientIdentity.SurgerySourceFor(picked), + PatientIdentity.ReviewDiagnosisSourceFor(picked)); // 러너를 새로 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아 // 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다. fields = new MDataTableRunner(designer.Document,