From 4642439ffd88db243bf72bc8d5c2de75cf9fe800 Mon Sep 17 00:00:00 2001 From: Msystech Date: Wed, 19 Aug 2026 16:45:03 +0900 Subject: [PATCH] =?UTF-8?q?=ED=99=98=EC=9E=90=20=EB=B6=80=EA=B0=80?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=207=EC=A2=85=20=E2=80=94=20118=EC=A2=85=20?= =?UTF-8?q?=E2=86=92=20125=EC=A2=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PAT_실제생년월일(bzDataInterface.vb:226 — P_PatEtcInf.PatEtcBirDte, 8자리·연월일 조각이 0 이 아닐 때만, YYYYMMDD 원문 그대로), PAT_국적(:639 — M_DtlMst NATCOD), PAT_장애등급(:687 — P_pdsoInf PdsoFlg='E', PdsoGrd 두 번째 글자), PAT_건보세대주명(:732 — P_PISINF ⨝P_CoiInf, PisInsCod='1'), PAT_건보_급여_세대주명(:780 — 자격 (코드·순번) 직접 지정; _Refer 갈래는 미리보기 미설정이라 자기 내원만), PAT_협력업체(:837 — ComCoopHsp→M_DtlMst, 원문대로 DtlTblCod 없음), PAT_보호자연락처(:881 — PatEtcGrdnPhn IS NOT NULL). 새 PatientExtraStore 에 자기 SQL 을 모았다. 차트번호는 문맥이 Trim 하므로 CHAR 컬럼은 RPAD(:c,10) 복원 매칭 — ㉜ 가 실값으로 증명한다. P_pdsoInf 만 VARCHAR2 라 IN (:c, RPAD(:c,10)) 양쪽을 본다 (㉛ [조사]: 이 DB 는 패딩 행 0 — 운영 DB 가 다를 수 있어 유지). ORDER BY 없는 Rows(0)/RowNum 1 은 전부 정렬 명시로 고정(갱신일시· 시작일 최신) — 장애등급의 P_ComInf 조인은 행만 곱해서 EXISTS 로 대체. 해석기는 지연 + 태그별 캐시(이 태그 없는 서식이 대부분). - dotnet test 338/338 · --edit-smoke 실패 0 (이름 검사 125종) - --db-patient ①~㉜ 전건 통과 — ㉛ 7종 실행, ㉜ 국적 실값 검증 신설 - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 Co-Authored-By: Claude Fable 5 --- src/SheetMe.Data/Stores/PatientExtraStore.cs | 130 ++++++++++++++++++ src/SheetMe.Designer/Diagnostics/DbSmoke.cs | 78 +++++++++++ .../Services/PatientIdentity.cs | 7 + .../Services/PatientSession.cs | 3 +- .../Services/PatientTagResolver.cs | 120 +++++++++++++++- .../Views/PreviewWindow.xaml.cs | 3 +- 6 files changed, 338 insertions(+), 3 deletions(-) create mode 100644 src/SheetMe.Data/Stores/PatientExtraStore.cs diff --git a/src/SheetMe.Data/Stores/PatientExtraStore.cs b/src/SheetMe.Data/Stores/PatientExtraStore.cs new file mode 100644 index 0000000..821442e --- /dev/null +++ b/src/SheetMe.Data/Stores/PatientExtraStore.cs @@ -0,0 +1,130 @@ +using Oracle.ManagedDataAccess.Client; + +namespace SheetMe.Data.Stores; + +/// +/// 환자 부가정보 태그들의 자기 SQL — 레거시 bzDataInterface.vb 의 함수 본문에서 +/// 기계적으로 옮겼다(실제생년월일 :226 · 국적 :639 · 장애등급 :687 · 건보세대주명 :732 · +/// 건보_급여_세대주명 :780 · 협력업체 :837 · 보호자연락처 :881). +/// +/// 차트번호 매칭. 레거시는 PatChtNum(CHAR 10) 원문 — 패딩 포함 — 을 바인드한다. +/// 우리 문맥은 행을 저장할 때 Trim 하므로(PatientContextStore.Row), CHAR 컬럼은 +/// RPAD(:c,10) 으로 패딩을 되살려 비교한다(인덱스도 산다). P_pdsoInf 만 VARCHAR2 라 +/// 저장값의 패딩 여부를 확정할 수 없어 두 형태 모두 본다(IN). +/// +/// Rows(0) 결정 규칙. 원문은 전부 ORDER BY 없는 첫 행이다. 여기서는 +/// 담당의 때 정한 규칙대로 정렬을 명시해 한 행으로 고정한다 — 각 메서드 주석에 근거를 남겼다. +/// +public sealed class PatientExtraStore +{ + #region Member Fields + private readonly string connectionString; + #endregion + + #region Constructors + public PatientExtraStore(string connectionString) + { + this.connectionString = connectionString; + } + #endregion + + #region Methods + /// + /// 실제생년월일(P_PatEtcInf.PatEtcBirDte, YYYYMMDD 원문) — 없으면 빈 문자열. + /// 원문은 SELECT * 의 Rows(0) 인데 이 표는 차트번호가 사실상 유일키(환자 부가정보 마스터)다. + /// 만에 하나 다중이면 갱신일시 최신 행 — 마지막으로 저장한 값이 현재 값이라는 뜻이다. + /// + public string ActualBirthDate(string chtNum) + => Scalar( + "SELECT PatEtcBirDte FROM (SELECT PatEtcBirDte FROM P_PatEtcInf" + + " WHERE PatEtcChtNum = RPAD(:c, 10) ORDER BY PatEtcUpdDtf DESC) WHERE ROWNUM = 1", + ("c", chtNum)); + + /// 보호자연락처(P_PatEtcInf.PatEtcGrdnPhn) — 원문의 IS NOT NULL 조건 그대로 + public string GuardianPhone(string chtNum) + => Scalar( + "SELECT PatEtcGrdnPhn FROM (SELECT PatEtcGrdnPhn FROM P_PatEtcInf" + + " WHERE PatEtcChtNum = RPAD(:c, 10) AND PatEtcGrdnPhn IS NOT NULL" + + " ORDER BY PatEtcUpdDtf DESC) WHERE ROWNUM = 1", + ("c", chtNum)); + + /// + /// 국적 한글명 — P_PatInf.PatNatCod 를 M_DtlMst(NATCOD) 로 푼다. + /// P_PatInf 는 차트번호당 1행, 조인 키(DtlTblCod+DtlCod)도 유일이라 정렬이 필요 없다. + /// + public string Nationality(string chtNum) + => Scalar( + "SELECT DtlCodNam FROM P_PatInf" + + " INNER JOIN M_DtlMst ON DtlCod = PatNatCod AND DtlTblCod = 'NATCOD'" + + " WHERE PatChtNum = RPAD(:c, 10)", + ("c", chtNum)); + + /// + /// 장애등급 — P_pdsoInf(PdsoFlg='E') 의 PdsoGrd. 호출부가 두 번째 글자를 뽑는다(레거시 그대로). + /// + /// 원문(:701-710)은 P_ComInf 를 INNER JOIN 해 행을 내원 수만큼 곱해 놓고 RowNum 1 로 + /// 접는다 — 조인의 실효는 "내원이 있는 환자인가" 확인뿐이라 EXISTS 로 바꿨다(의미 동일). + /// ORDER BY 없는 RowNum 1 은 등급 변경 이력이 있으면 임의 행이다 — 주석의 의도 + /// ("현재 작성일 기준 등급", 2017-10-25 푸르메 요청)에 맞게 시작일 최신 행으로 고정한다. + /// + public string DisabilityGrade(string chtNum) + => Scalar( + "SELECT PdsoGrd FROM (SELECT PdsoGrd FROM P_pdsoInf" + + " WHERE PdsoChtNum IN (:c, RPAD(:c, 10)) AND PdsoFlg = 'E'" + + " AND EXISTS (SELECT 1 FROM P_ComInf WHERE ComChtNum = RPAD(:c, 10))" + + " ORDER BY PdsoStrDte DESC, PdsoSeq DESC) WHERE ROWNUM = 1", + ("c", chtNum)); + + /// + /// 건보 세대주명 — P_PISINF(PisInsCod='1') 를 이 내원의 자격(P_CoiInf.CoiInsSeq)과 맞춘다. + /// 자격 이력이 여러 건이면 원문은 임의 행 — 시작일 최신 행으로 고정한다. + /// + public string HouseholderName(string chtNum, decimal comNum) + => Scalar( + "SELECT Pis1SedaejuNm FROM (SELECT Pis1SedaejuNm FROM P_PISINF" + + " INNER JOIN P_CoiInf ON CoiInsSeq = PisInsSeq AND CoiComNum = :m" + + " WHERE PisChtNum = RPAD(:c, 10) AND PisInsCod = '1'" + + " ORDER BY PisStrDte DESC) WHERE ROWNUM = 1", + ("m", comNum), ("c", chtNum)); + + /// + /// 세대주명(건보·급여) — 문맥 자격의 (보험코드, 순번)으로 P_PISINF 를 직접 짚는다. + /// 레거시(:780-)는 참조 내원이 있으면 그쪽 자격을 쓰지만 미리보기는 참조를 설정하지 + /// 않으므로(TestPatientSetting) 자기 내원 갈래만 옮겼다. + /// + public string HouseholderNameByInsurance(string chtNum, string insSeq, string insCod) + => Scalar( + "SELECT Pis1SedaejuNm FROM (SELECT Pis1SedaejuNm FROM P_PISINF" + + " WHERE PisChtNum = RPAD(:c, 10) AND PisInsSeq = :s AND PisInsCod = :d" + + " ORDER BY PisStrDte DESC) WHERE ROWNUM = 1", + ("c", chtNum), ("s", insSeq), ("d", insCod)); + + /// + /// 협력업체 한글명 — 내원행의 ComCoopHsp 를 M_DtlMst 로 푼다. + /// 원문(:850-854)은 DtlTblCod 조건 없이 DtlCod 만으로 조인한다 — 같은 코드가 다른 + /// 코드표에도 있으면 다중행이다. 레거시 조인을 그대로 두되 테이블코드 순 첫 행으로 고정한다. + /// + public string Cooperator(decimal comNum) + => Scalar( + "SELECT DtlCodNam FROM (SELECT DtlCodNam FROM P_ComInf" + + " INNER JOIN M_DtlMst ON ComCoopHsp = DtlCod" + + " WHERE ComNum = :m ORDER BY DtlTblCod) WHERE ROWNUM = 1", + ("m", comNum)); + + /// 스칼라 1칸 — 0행이거나 NULL 이면 빈 문자열. 잔여 공백은 걷는다(문맥 행과 같은 규칙) + private string Scalar(string sql, params (string Name, object Value)[] binds) + { + using var connection = new OracleConnection(connectionString); + connection.Open(); + using var command = connection.CreateCommand(); + command.BindByName = true; + command.CommandText = sql; + foreach (var (name, value) in binds) + { + command.Parameters.Add(new OracleParameter(name, value)); + } + var result = command.ExecuteScalar(); + return result is null or DBNull ? string.Empty : result.ToString()!.Trim(); + } + #endregion +} diff --git a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs index bad69f7..f5e89ce 100644 --- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs @@ -2314,6 +2314,66 @@ public static class DbSmoke Check("㉚ 병리·진단검사 의사 조회가 예외 없이 돈다", false, $"{ex.GetType().Name}: {ex.Message.Split('\n')[0]}"); } + + // ㉛ 부가정보 7종(자기 SQL) — P_PatEtcInf 에 행이 있는 차트로 돌려 + // 컬럼·조인 이름이 실제 DB 와 맞는지 본다. 0값은 실패가 아니라 + // 그 환자에 그 정보가 없는 것이다(태그는 사유로 완결된다). + try + { + var extraOwner = FirstText(conn, + "SELECT TRIM(PatEtcChtNum) FROM P_PatEtcInf WHERE ROWNUM = 1"); + // 패딩 조사 — P_pdsoInf 는 VARCHAR2 라 저장 형태를 확정할 수 없다. + // IN (:c, RPAD(:c,10)) 이 실제로 필요한지 근거를 남긴다. + var padded = FirstText(conn, + "SELECT COUNT(*) FROM P_pdsoInf WHERE PdsoChtNum <> TRIM(PdsoChtNum)"); + var multiEtc = FirstText(conn, + "SELECT NVL(MAX(cnt),0) FROM (SELECT COUNT(*) cnt FROM P_PatEtcInf" + + " GROUP BY PatEtcChtNum)"); + lines.Add($" [조사] P_pdsoInf 패딩 행={padded}," + + $" P_PatEtcInf 차트당 최대 행수={multiEtc}"); + if (extraOwner.Length == 0) + { + lines.Add("SKIP ㉛ 부가정보 — P_PatEtcInf 가 비어 있어 확인 불가"); + } + else + { + var extra = new PatientExtraStore(conn); + var owner2 = FirstComNum(conn, "P_ComInf", "ComNum", "ComNum"); + var shapes = + $"실제생년월일 {extra.ActualBirthDate(extraOwner).Length}자" + + $" · 국적 {extra.Nationality(extraOwner).Length}자" + + $" · 장애등급 {extra.DisabilityGrade(extraOwner).Length}자" + + $" · 보호자연락처 {extra.GuardianPhone(extraOwner).Length}자" + + $" · 급여세대주 {extra.HouseholderNameByInsurance(extraOwner, "0", "1").Length}자" + + (owner2 is null ? "" + : $" · 건보세대주 {extra.HouseholderName(extraOwner, owner2.Value).Length}자" + + $" · 협력업체 {extra.Cooperator(owner2.Value).Length}자"); + lines.Add($" 부가정보: {shapes} (값은 기록하지 않는다)"); + Check("㉛ 부가정보 7종 조회가 예외 없이 돈다", true, ""); + + // "돌지만 항상 0행"인 매칭 실수(RPAD·조인)를 가른다 — + // 값이 있는 차트를 골라서 물으면 반드시 값이 나와야 한다. + var natOwner = FirstText(conn, + "SELECT TRIM(PatChtNum) FROM P_PatInf" + + " INNER JOIN M_DtlMst ON DtlCod = PatNatCod AND DtlTblCod = 'NATCOD'" + + " WHERE ROWNUM = 1"); + if (natOwner.Length == 0) + { + lines.Add("SKIP ㉜ 국적 값 — 이 DB 에 국적 코드가 잡힌 환자가 없어 판정 불가"); + } + else + { + Check("㉜ 국적 코드가 있는 차트에서는 값이 나온다", + extra.Nationality(natOwner).Length > 0, + "RPAD 매칭이나 NATCOD 조인을 의심할 것"); + } + } + } + catch (Exception ex) + { + Check("㉛ 부가정보 7종 조회가 예외 없이 돈다", false, + $"{ex.GetType().Name}: {ex.Message.Split('\n')[0]}"); + } } } } @@ -2336,6 +2396,24 @@ public static class DbSmoke /// 최근 것을 고른다(오래된 내원은 구간이 이미 닫혀 있어 판정이 덜 대표적이다). /// 표 이름·컬럼 이름은 호출부의 리터럴만 들어온다 — 사용자 입력이 닿는 경로가 아니다. /// + /// 스칼라 한 칸을 문자열로 — 0행이거나 죽으면 빈 문자열(판정은 호출부가 한다) + private static string FirstText(string connectionString, string sql) + { + try + { + using var connection = new Oracle.ManagedDataAccess.Client.OracleConnection(connectionString); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = sql; + var value = command.ExecuteScalar(); + return value is null or DBNull ? string.Empty : value.ToString()!.Trim(); + } + catch + { + return string.Empty; + } + } + private static decimal? FirstComNum(string connectionString, string table, string key, string stamp) { try diff --git a/src/SheetMe.Designer/Services/PatientIdentity.cs b/src/SheetMe.Designer/Services/PatientIdentity.cs index a717bf3..569f364 100644 --- a/src/SheetMe.Designer/Services/PatientIdentity.cs +++ b/src/SheetMe.Designer/Services/PatientIdentity.cs @@ -178,6 +178,13 @@ public static class PatientIdentity : new PatientContextStore(connection).LabDoctor(); } + /// 환자 부가정보(자기 SQL 7종) 공급자 — 연결이 없으면 null(해석기가 사유로 말한다) + public static Func ExtraStoreSource() + { + var connection = ConfigService.Current.ConnectionString; + return () => connection.Length == 0 ? null : new PatientExtraStore(connection); + } + /// 수술 일정(S_OprInf) 공급자 — 최신순 여부별 1회 public static Func> ScheduledSurgerySourceFor(PatientContext context) { diff --git a/src/SheetMe.Designer/Services/PatientSession.cs b/src/SheetMe.Designer/Services/PatientSession.cs index 03e7dac..616d463 100644 --- a/src/SheetMe.Designer/Services/PatientSession.cs +++ b/src/SheetMe.Designer/Services/PatientSession.cs @@ -74,7 +74,8 @@ public static class PatientSession PatientIdentity.ReviewDiagnosisSourceFor(picked), PatientIdentity.VitalSourceFor(picked), PatientIdentity.ScheduledSurgerySourceFor(picked), - PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource()) + PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource(), + PatientIdentity.ExtraStoreSource()) : session; return (tags, new MDataTableRunner(document, Variables())); } diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs index f442d1b..ba70e6b 100644 --- a/src/SheetMe.Designer/Services/PatientTagResolver.cs +++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs @@ -220,6 +220,11 @@ public sealed class PatientTagResolver : ITagValueResolver private readonly Dictionary> specialDoctorCache = new(StringComparer.Ordinal); + /// 환자 부가정보(자기 SQL 7종) 공급자 — 태그가 물을 때 만든다 + private readonly Func? extraStoreSource; + + private readonly Dictionary extraCache = new(StringComparer.Ordinal); + private readonly Dictionary>> reviewCache = new(); public PatientTagResolver(PatientContext context, ITagValueResolver next, @@ -234,7 +239,8 @@ public sealed class PatientTagResolver : ITagValueResolver Func? vitalSource = null, Func>? scheduledSurgerySource = null, Func>? pathologyDoctorSource = null, - Func>? labDoctorSource = null) + Func>? labDoctorSource = null, + Func? extraStoreSource = null) { this.context = context; this.next = next; @@ -251,6 +257,7 @@ public sealed class PatientTagResolver : ITagValueResolver this.scheduledSurgerySource = scheduledSurgerySource; this.pathologyDoctorSource = pathologyDoctorSource; this.labDoctorSource = labDoctorSource; + this.extraStoreSource = extraStoreSource; } #endregion @@ -298,6 +305,10 @@ public sealed class PatientTagResolver : ITagValueResolver { return specialValue; } + if (FromExtra(tag) is { } extraValue) + { + return extraValue; + } if (Computed.TryGetValue(tag, out var compute)) { var made = compute(context); @@ -874,6 +885,111 @@ public sealed class PatientTagResolver : ITagValueResolver : new TagValue(false, "수술일자 값이 형식에 맞지 않습니다"); } + /// + /// 환자 부가정보 7종 — 태그별 자기 SQL(). + /// 전부 지연 + 태그별 캐시다: 이 태그가 없는 서식이 대부분이라 미리 당기지 않고, + /// 같은 태그가 한 장에 여러 번 있어도 왕복은 한 번이다. + /// + private TagValue? FromExtra(string tag) + { + if (tag is not ("PAT_실제생년월일" or "PAT_국적" or "PAT_장애등급" or "PAT_건보세대주명" + or "PAT_건보_급여_세대주명" or "PAT_협력업체" or "PAT_보호자연락처")) + { + return null; + } + if (extraStoreSource is null) + { + return new TagValue(false, "부가정보 조회가 연결되지 않았습니다"); + } + if (extraCache.TryGetValue(tag, out var cached)) + { + return cached; + } + var chtNum = PatientContext.Value(context.ComInf, "ComChtNum"); + var value = Compute(); + extraCache[tag] = value; + return value; + + TagValue Compute() + { + if (chtNum.Length == 0) + { + return new TagValue(false, "이 내원에 차트번호가 없습니다"); + } + try + { + var store = extraStoreSource(); + if (store is null) + { + return new TagValue(false, "부가정보 조회가 연결되지 않았습니다"); + } + switch (tag) + { + case "PAT_실제생년월일": + { + // 레거시(:251-269)는 8자리이고 연·월·일 어느 조각도 "0000"/"00" 이 + // 아닐 때만 돌려준다 — YYYYMMDD 원문 그대로, 하이픈을 붙이지 않는다. + var day = store.ActualBirthDate(chtNum); + if (day.Length == 0) + { + return new TagValue(false, "부가정보(P_PatEtcInf)에 실제생년월일이 없습니다"); + } + return day.Length == 8 && day[..4] != "0000" && day[4..6] != "00" && day[6..8] != "00" + ? new TagValue(true, day) + : new TagValue(false, $"실제생년월일이 온전한 날짜가 아닙니다({day.Length}자)"); + } + case "PAT_국적": + return Wrap(store.Nationality(chtNum), "국적 코드가 없거나 코드표(NATCOD)에 이름이 없습니다"); + case "PAT_장애등급": + { + // 레거시(:721)는 PdsoGrd 의 두 번째 글자만 뽑는다(Substring(1,1)). + // 두 글자가 안 되면 레거시는 예외→"" 였다 — 여기서는 사유로 말한다. + var grade = store.DisabilityGrade(chtNum); + if (grade.Length == 0) + { + return new TagValue(false, "장애 등록(P_pdsoInf, PdsoFlg='E') 행이 없습니다"); + } + return grade.Length >= 2 + ? new TagValue(true, grade[1..2]) + : new TagValue(false, $"장애등급 값이 두 글자가 안 됩니다({grade.Length}자)"); + } + case "PAT_건보세대주명": + { + var comNum = PatientContext.Value(context.ComInf, "ComNum"); + return decimal.TryParse(comNum, out var m) + ? Wrap(store.HouseholderName(chtNum, m), "건보(PisInsCod='1') 세대주 행이 없습니다") + : new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); + } + case "PAT_건보_급여_세대주명": + { + var insSeq = PatientContext.Value(context.CoiInf, "CoiInsSeq"); + var insCod = PatientContext.Value(context.CoiInf, "CoiInsCod"); + return insSeq.Length == 0 && insCod.Length == 0 + ? new TagValue(false, "이 내원에 보험 자격(P_CoiInf) 행이 없습니다") + : Wrap(store.HouseholderNameByInsurance(chtNum, insSeq, insCod), + "이 자격의 세대주 행이 없습니다"); + } + case "PAT_협력업체": + { + var comNum = PatientContext.Value(context.ComInf, "ComNum"); + return decimal.TryParse(comNum, out var m) + ? Wrap(store.Cooperator(m), "협력업체 코드가 없거나 코드표에 이름이 없습니다") + : new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다"); + } + default: + return Wrap(store.GuardianPhone(chtNum), "부가정보에 보호자연락처가 없습니다"); + } + } + catch (Exception ex) + { + return new TagValue(false, $"부가정보 조회에 실패했습니다: {ex.GetType().Name}"); + } + } + + static TagValue Wrap(string value, string reasonWhenEmpty) + => value.Length > 0 ? new TagValue(true, value) : new TagValue(false, reasonWhenEmpty); + } + /// /// 병리판독의·진단검사의 4종 — 환자와 무관한 원내 의사 조회지만 레거시가 환자 태그로 노출한다. /// 이름과 번호가 같은 행에서 나온다(레거시는 태그마다 따로 조회해 다른 사람이 될 수 있었다). @@ -965,6 +1081,8 @@ public sealed class PatientTagResolver : ITagValueResolver { "PAT_주민번호", "PAT_주민번호_Dash", "PAT_성별", "PAT_한글성별", "PAT_성별_남", "PAT_성별_여", + "PAT_실제생년월일", "PAT_국적", "PAT_장애등급", "PAT_건보세대주명", + "PAT_건보_급여_세대주명", "PAT_협력업체", "PAT_보호자연락처", "PAT_우편번호", "PAT_도로명주소", "PAT_지번주소", "PAT_영문도로명주소", "OCM_담당의사", "OCM_담당의사_담당의사코드", "OCM_담당의사_연락처", "OCM_진료과", "ETC_요양기관명칭_병원명", diff --git a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs index 2a13915..4067cc0 100644 --- a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs +++ b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs @@ -231,7 +231,8 @@ public partial class PreviewWindow : Window PatientIdentity.ReviewDiagnosisSourceFor(picked), PatientIdentity.VitalSourceFor(picked), PatientIdentity.ScheduledSurgerySourceFor(picked), - PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource()); + PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource(), + PatientIdentity.ExtraStoreSource()); // 러너를 새로 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아 // 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다. fields = new MDataTableRunner(designer.Document,