diff --git a/src/SheetMe.Data/Stores/PatientContextStore.cs b/src/SheetMe.Data/Stores/PatientContextStore.cs
index eb500fd..cf080f1 100644
--- a/src/SheetMe.Data/Stores/PatientContextStore.cs
+++ b/src/SheetMe.Data/Stores/PatientContextStore.cs
@@ -94,6 +94,50 @@ public sealed class PatientContextStore
return new PatientContext(comNum, adpDtm, patInf, comInf, codInf, coiInf, cowInf);
}
+ ///
+ /// 특정 시점의 진료과 코드 — 레거시 GetCodInfDT(comNum, moment, 1) 의 첫 행 대응.
+ /// 입원과(접수 시점)·퇴원과(퇴원/현재 시점) 태그가 문맥의 적용일시와 다른 시점을
+ /// 물을 때 쓴다. 원문 GetCodInfDT 는 ORDER BY 가 없어 임의 행이었다 — 문맥 로드와
+ /// 같은 규칙(시작일시 최신 1행)으로 고정한다. moment 가 비면 서버 현재시각이다.
+ ///
+ public string DepartmentCodeAt(decimal comNum, string? moment)
+ {
+ using var connection = new OracleConnection(connectionString);
+ connection.Open();
+ var at = (moment ?? string.Empty).Trim();
+ var row = Row(connection,
+ "SELECT CodDepCod FROM (SELECT CodDepCod FROM P_CodInf WHERE CodComNum = :k AND CodMtiSeq = 0"
+ + (at.Length >= 12
+ ? " AND :t BETWEEN CodStrDtm AND CodEndDtm"
+ : " AND TO_CHAR(SYSDATE, 'YYYYMMDDHH24MI') BETWEEN CodStrDtm AND CodEndDtm")
+ + " ORDER BY CodStrDtm DESC) WHERE ROWNUM = 1",
+ at.Length >= 12
+ ? new (string, object)[] { ("k", comNum), ("t", at[..12]) }
+ : new (string, object)[] { ("k", comNum) });
+ return PatientContext.Value(row, "CodDepCod");
+ }
+
+ ///
+ /// 퇴원과 조회(bzDataInterface.vb:6162-6231) — 내원의 퇴원일시(또는 접수일시)가
+ /// 유효기간 안인 진료과 행. 원문이 P_ComInf 에 M_DepMst 를 날짜 조건만으로 조인하는
+ /// 특이한 모양이라 그대로 옮겼다. anchorColumn 은 ComLevDtm 또는 ComAcpDtm.
+ ///
+ public IReadOnlyDictionary DischargeDepartment(decimal comNum, string depCod, string anchorColumn)
+ {
+ if (anchorColumn is not ("ComLevDtm" or "ComAcpDtm"))
+ {
+ throw new ArgumentOutOfRangeException(nameof(anchorColumn));
+ }
+ using var connection = new OracleConnection(connectionString);
+ connection.Open();
+ return Row(connection,
+ "SELECT * FROM (SELECT DepOfcNam, DepKorNam FROM P_ComInf"
+ + " INNER JOIN M_DepMst ON DepCod = :d"
+ + $" AND {anchorColumn} BETWEEN DepStrDte AND DepEndDte || '2359'"
+ + " WHERE ComNum = :k) WHERE ROWNUM <= 1",
+ ("d", depCod), ("k", comNum));
+ }
+
///
/// 한 행을 컬럼명 → 값 사전으로. 없으면 빈 사전.
/// 컬럼명은 대문자로 담는다 — 오라클이 대문자로 돌려주고, 호출부가 대소문자로 헷갈리면
diff --git a/src/SheetMe.Designer/Services/PatientIdentity.cs b/src/SheetMe.Designer/Services/PatientIdentity.cs
index 569f364..bb7cfb2 100644
--- a/src/SheetMe.Designer/Services/PatientIdentity.cs
+++ b/src/SheetMe.Designer/Services/PatientIdentity.cs
@@ -185,6 +185,13 @@ public static class PatientIdentity
return () => connection.Length == 0 ? null : new PatientExtraStore(connection);
}
+ /// 문맥 저장소 공급자 — 입원과·퇴원과의 시점별 진료과 조회용
+ public static Func ContextStoreSource()
+ {
+ var connection = ConfigService.Current.ConnectionString;
+ return () => connection.Length == 0 ? null : new PatientContextStore(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 616d463..6e7feed 100644
--- a/src/SheetMe.Designer/Services/PatientSession.cs
+++ b/src/SheetMe.Designer/Services/PatientSession.cs
@@ -75,7 +75,7 @@ public static class PatientSession
PatientIdentity.VitalSourceFor(picked),
PatientIdentity.ScheduledSurgerySourceFor(picked),
PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource(),
- PatientIdentity.ExtraStoreSource())
+ PatientIdentity.ExtraStoreSource(), PatientIdentity.ContextStoreSource())
: session;
return (tags, new MDataTableRunner(document, Variables()));
}
diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs
index 581c32f..5472038 100644
--- a/src/SheetMe.Designer/Services/PatientTagResolver.cs
+++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs
@@ -231,6 +231,14 @@ public sealed class PatientTagResolver : ITagValueResolver
private readonly Dictionary extraCache = new(StringComparer.Ordinal);
+ ///
+ /// 문맥 저장소 공급자 — 입원과·퇴원과처럼 문맥의 적용일시와 다른 시점을
+ /// 물어야 하는 태그가 쓴다(접수 시점·퇴원 시점의 진료과).
+ ///
+ private readonly Func? contextStoreSource;
+
+ private readonly Dictionary deptAtCache = new(StringComparer.Ordinal);
+
private readonly Dictionary>> reviewCache = new();
public PatientTagResolver(PatientContext context, ITagValueResolver next,
@@ -246,7 +254,8 @@ public sealed class PatientTagResolver : ITagValueResolver
Func>? scheduledSurgerySource = null,
Func>? pathologyDoctorSource = null,
Func>? labDoctorSource = null,
- Func? extraStoreSource = null)
+ Func? extraStoreSource = null,
+ Func? contextStoreSource = null)
{
this.context = context;
this.next = next;
@@ -264,6 +273,7 @@ public sealed class PatientTagResolver : ITagValueResolver
this.pathologyDoctorSource = pathologyDoctorSource;
this.labDoctorSource = labDoctorSource;
this.extraStoreSource = extraStoreSource;
+ this.contextStoreSource = contextStoreSource;
}
#endregion
@@ -319,6 +329,10 @@ public sealed class PatientTagResolver : ITagValueResolver
{
return stayValue;
}
+ if (FromDeptAt(tag) is { } deptAtValue)
+ {
+ return deptAtValue;
+ }
if (Computed.TryGetValue(tag, out var compute))
{
var made = compute(context);
@@ -514,14 +528,28 @@ public sealed class PatientTagResolver : ITagValueResolver
///
private TagValue? FromFacility(string tag)
{
+ string Dep(string column) => department.TryGetValue(column, out var v) ? v.Trim() : string.Empty;
+ TagValue DeptColumn(string column, string reasonWhenEmpty)
+ {
+ if (department.Count == 0)
+ {
+ return new TagValue(false, "이 내원의 진료과를 찾지 못했습니다");
+ }
+ var value = Dep(column);
+ return value.Length > 0 ? new TagValue(true, value) : new TagValue(false, reasonWhenEmpty);
+ }
switch (tag)
{
case "OCM_진료과":
{
- string Dep(string column) => department.TryGetValue(column, out var v) ? v.Trim() : string.Empty;
+ // 이름 선택(bzDataInterface.vb:5474-5484): NH 는 무조건 DepOfcNam,
+ // 그 밖은 개원일 2022-07-01 이후 + DepOfcNam 있음 → DepOfcNam, 아니면 DepKorNam.
+ // 개원일은 요양기관 행의 HspStrDte 다 — 처음엔 없어서 DepKorNam 으로 고정했었다.
var office = Dep("DepOfcNam");
var value = string.Equals(UserSession.Current.HspCod, "NH", StringComparison.OrdinalIgnoreCase)
? office
+ : string.CompareOrdinal(PatientContext.Value(hospital, "HspStrDte"), "20220701") >= 0
+ && office.Length > 0 ? office
: Dep("DepKorNam");
return value.Length > 0
? new TagValue(true, value)
@@ -529,6 +557,20 @@ public sealed class PatientTagResolver : ITagValueResolver
? "이 내원의 진료과를 찾지 못했습니다"
: "진료과 마스터에 이름이 비어 있습니다");
}
+ // ── 진료과 파생 4종 — 같은 행(M_DepMst SELECT *)의 다른 컬럼(:5556-6007) ──
+ // 레거시의 시점(퇴원했으면 퇴원 시점, 재원 중이면 현재, 외래면 접수)은 문맥
+ // 적용일시와 같은 판정이다 — 자격이 퇴원보다 먼저 끝난 희귀 케이스에서만 갈린다.
+ case "OCM_진료과_그룹코드":
+ // 행이 있으면 무조건 "이름(그룹코드)" 결합이다(:5593) — 조각이 비어도 그대로
+ return department.Count == 0
+ ? new TagValue(false, "이 내원의 진료과를 찾지 못했습니다")
+ : new TagValue(true, $"{Dep("DepKorNam")}({Dep("DepGrpCod")})");
+ case "OCM_진료과_영어":
+ return DeptColumn("DepEngNam", "진료과 마스터에 영문 이름이 비어 있습니다");
+ case "OCM_진료과대외명칭":
+ return DeptColumn("DepOfcNam", "진료과 마스터에 대외 명칭이 비어 있습니다");
+ case "OCM_진료과약어명칭":
+ return DeptColumn("DepBrfNam", "진료과 마스터에 약어 명칭이 비어 있습니다");
// ── 요양기관 12종 — 같은 조인의 다른 컬럼(bzDataInterface.vb:15656-16709) ──
// 접미·결합도 본문 그대로: 병원장 = 병원명+"장", 귀하 = +"장 귀하",
// 전화_팩스 = "Tel : … FAX : …", 영문판은 "+82 - " 접두가 붙는다.
@@ -895,6 +937,105 @@ public sealed class PatientTagResolver : ITagValueResolver
: new TagValue(false, "수술일자 값이 형식에 맞지 않습니다");
}
+ ///
+ /// 입원과·퇴원과 4종(bzDataInterface.vb:6009-6318) — 문맥의 적용일시와 다른 시점의
+ /// 진료과를 묻는다. 입원과는 접수 시점(전과했으면 처음 과), 퇴원과는
+ /// 퇴원(ILV → ComLevDtm) 또는 현재 시점의 과다.
+ ///
+ /// 퇴원과 본체(:6194-6210)는 "낮병동은 퇴원일시가 빈다"는 주석과 달리 If/Else 가
+ /// 똑같은 쿼리다(복붙 결함으로 보인다 — ComLevDtm 이 비면 0행 → 빈 값).
+ /// 한글명칭(:6265-6281)만 빈 퇴원일시에서 접수일시로 대신한다. 둘 다 그대로 옮겼다.
+ ///
+ private TagValue? FromDeptAt(string tag)
+ {
+ var admission = tag is "OCM_입원과" or "OCM_입원과_한글명칭";
+ var discharge = tag is "OCM_퇴원과" or "OCM_퇴원과_한글명칭";
+ if (!admission && !discharge)
+ {
+ return null;
+ }
+ if (deptAtCache.TryGetValue(tag, out var cached))
+ {
+ return cached;
+ }
+ var value = Compute();
+ deptAtCache[tag] = value;
+ return value;
+
+ TagValue Compute()
+ {
+ if (contextStoreSource is null)
+ {
+ return new TagValue(false, "진료과 시점 조회가 연결되지 않았습니다");
+ }
+ var comNum = PatientContext.Value(context.ComInf, "ComNum");
+ if (!decimal.TryParse(comNum, out var m))
+ {
+ return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다");
+ }
+ try
+ {
+ var store = contextStoreSource();
+ if (store is null)
+ {
+ return new TagValue(false, "진료과 시점 조회가 연결되지 않았습니다");
+ }
+ var open = PatientContext.Value(hospital, "HspStrDte");
+ string PickName(IReadOnlyDictionary row)
+ {
+ // 개원일 2022-07-01 이후 + 대외명칭 있음 → 대외명칭, 아니면 한글명(:6044-6048)
+ var office = PatientContext.Value(row, "DepOfcNam").Trim();
+ return string.CompareOrdinal(open, "20220701") >= 0 && office.Length > 0
+ ? office
+ : PatientContext.Value(row, "DepKorNam").Trim();
+ }
+ if (admission)
+ {
+ var acp = PatientContext.Value(context.ComInf, "ComAcpDtm");
+ if (acp.Length < 12)
+ {
+ return new TagValue(false, "이 내원의 접수일시를 읽지 못했습니다");
+ }
+ var depCod = store.DepartmentCodeAt(m, acp[..12]);
+ if (depCod.Length == 0)
+ {
+ return new TagValue(false, "접수 시점의 진료 행(P_CodInf)이 없습니다");
+ }
+ var row = store.Department(depCod, acp[..8]);
+ if (row.Count == 0)
+ {
+ return new TagValue(true, string.Empty); // 레거시도 마스터 0행이면 "" 다
+ }
+ return new TagValue(true, tag == "OCM_입원과"
+ ? PickName(row)
+ : PatientContext.Value(row, "DepKorNam").Trim());
+ }
+ var prg = PatientContext.Value(context.ComInf, "ComPrgStt");
+ var lev = PatientContext.Value(context.ComInf, "ComLevDtm");
+ var depAtLeave = store.DepartmentCodeAt(m,
+ prg == "ILV" && lev.Length >= 12 ? lev[..12] : null);
+ if (depAtLeave.Length == 0)
+ {
+ return new TagValue(true, string.Empty); // 레거시가 명시적으로 "" 를 돌려준다(:6189)
+ }
+ var anchor = tag == "OCM_퇴원과_한글명칭" && lev.Trim().Length == 0
+ ? "ComAcpDtm" : "ComLevDtm";
+ var deptRow = store.DischargeDepartment(m, depAtLeave, anchor);
+ if (deptRow.Count == 0)
+ {
+ return new TagValue(true, string.Empty);
+ }
+ return new TagValue(true, tag == "OCM_퇴원과"
+ ? PickName(deptRow)
+ : PatientContext.Value(deptRow, "DepKorNam").Trim());
+ }
+ catch (Exception ex)
+ {
+ return new TagValue(false, $"진료과 시점 조회에 실패했습니다: {ex.GetType().Name}");
+ }
+ }
+ }
+
///
/// 입·퇴원 일시와 재원일수 12종(bzDataInterface.vb:7152-7940, 전부 Case Else 갈래).
///
@@ -1275,6 +1416,8 @@ public sealed class PatientTagResolver : ITagValueResolver
"PAT_건보_급여_세대주명", "PAT_협력업체", "PAT_보호자연락처",
"OCM_FollowUp", "OCM_외출외박신청일", "OCM_외출외박종료일",
"OCM_최초내원일", "OCM_초진일_발병일", "OCM_신환_초진일",
+ "OCM_진료과_그룹코드", "OCM_진료과_영어", "OCM_진료과대외명칭", "OCM_진료과약어명칭",
+ "OCM_입원과", "OCM_입원과_한글명칭", "OCM_퇴원과", "OCM_퇴원과_한글명칭",
"OCM_입원시간", "OCM_입원일시", "OCM_입원일시_영문", "OCM_입실시간",
"OCM_의사퇴원예고일시", "OCM_퇴원예정일시", "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 4067cc0..fdc0dcd 100644
--- a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs
+++ b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs
@@ -232,7 +232,7 @@ public partial class PreviewWindow : Window
PatientIdentity.VitalSourceFor(picked),
PatientIdentity.ScheduledSurgerySourceFor(picked),
PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource(),
- PatientIdentity.ExtraStoreSource());
+ PatientIdentity.ExtraStoreSource(), PatientIdentity.ContextStoreSource());
// 러너를 새로 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아
// 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다.
fields = new MDataTableRunner(designer.Document,