diff --git a/src/SheetMe.Data/Stores/PatientContextStore.cs b/src/SheetMe.Data/Stores/PatientContextStore.cs
index ae30734..fcdf861 100644
--- a/src/SheetMe.Data/Stores/PatientContextStore.cs
+++ b/src/SheetMe.Data/Stores/PatientContextStore.cs
@@ -173,6 +173,53 @@ public sealed class PatientContextStore
("c", code));
}
+ ///
+ /// 진료과 마스터 한 행 — OCM_진료과(bzDataInterface.vb:5437-5495)의 조회.
+ /// WHERE DepCod = :d AND :day BETWEEN DepStrDte AND DepEndDte.
+ ///
+ /// 유효기간 판정 일자는 레거시가 입원·외래 무관하게 접수일자를 쓴다(:5445 sAcpDte).
+ /// 원본에는 ORDER BY 가 없고 Rows(0) 을 집는다 — 담당의 때와 같은 이유로
+ /// 최신순 1행으로 고정한다(기간이 겹친 이력에서 비결정적이 되는 것을 막는다).
+ ///
+ public IReadOnlyDictionary Department(string? depCod, string? day)
+ {
+ var code = (depCod ?? string.Empty).Trim();
+ var date = (day ?? string.Empty).Trim();
+ if (code.Length == 0 || date.Length < 8)
+ {
+ return new Dictionary(StringComparer.OrdinalIgnoreCase);
+ }
+ using var connection = new OracleConnection(connectionString);
+ connection.Open();
+ return Row(connection,
+ "SELECT * FROM (SELECT * FROM M_DepMst WHERE TRIM(DepCod) = :d"
+ + " AND :y BETWEEN DepStrDte AND DepEndDte ORDER BY DepStrDte DESC) WHERE ROWNUM = 1",
+ ("d", code), ("y", date[..8]));
+ }
+
+ ///
+ /// 요양기관명 — ETC_요양기관명칭_병원명(bzDataInterface.vb:15792-15860)의 조회.
+ /// M_DepMst ⨝ M_HspMst(DepHspCod=HspCod) 에서 HspInsNam 하나를 뽑아 Trim.
+ /// 진료과 코드로 병원을 찾는 구조다 — 다부지(병원 코드가 다른 진료과) 구성 때문으로 보인다.
+ ///
+ public string HospitalName(string? depCod, string? day)
+ {
+ var code = (depCod ?? string.Empty).Trim();
+ var date = (day ?? string.Empty).Trim();
+ if (code.Length == 0 || date.Length < 8)
+ {
+ return string.Empty;
+ }
+ using var connection = new OracleConnection(connectionString);
+ connection.Open();
+ var row = Row(connection,
+ "SELECT HspInsNam FROM (SELECT HspInsNam FROM M_DepMst"
+ + " INNER JOIN M_HspMst ON DepHspCod = HspCod AND :y BETWEEN HspStrDte AND HspEndDte"
+ + " WHERE TRIM(DepCod) = :d AND :y BETWEEN DepStrDte AND DepEndDte) WHERE ROWNUM = 1",
+ ("d", code), ("y", date[..8]));
+ return PatientContext.Value(row, "HspInsNam");
+ }
+
///
/// 건물관리번호로 주소 — 레거시 GetZipInfoByBuildingNumber(clsCommonLib.vb:16600).
///
diff --git a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs
index fe6f2bb..4c6d5cb 100644
--- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs
+++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs
@@ -2186,6 +2186,25 @@ public static class DbSmoke
+ $" 이름 {uidNam.Length}자 (값은 기록하지 않는다)");
Check("㉒ 담당의 조회가 예외 없이 돌고 행을 돌려준다", doctor.Count > 0,
"0열 — 적용일시 BETWEEN 조건이나 CodDtrCod 를 의심할 것");
+
+ // ㉓ 진료과·요양기관명 — 같은 내원의 CodDepCod 로.
+ // 코드가 이름이 되는 것까지 확인해야 태그가 값을 낸다.
+ if (ownerCtx is { } octx)
+ {
+ var depCod = PatientContext.Value(octx.CodInf, "CodDepCod");
+ var acp = PatientContext.Value(octx.ComInf, "ComAcpDtm");
+ var dep = contexts.Department(depCod, acp);
+ var korNam = PatientContext.Value(dep, "DepKorNam");
+ lines.Add($" 진료과 조회: 코드 '{depCod}' → {dep.Count}열,"
+ + $" 한글명 {korNam.Length}자");
+ Check("㉓ 진료과 마스터가 이름을 돌려준다", korNam.Trim().Length > 0,
+ $"코드 '{depCod}' 로 M_DepMst 에서 이름을 못 찾았다");
+
+ var hosp = contexts.HospitalName(depCod, octx.AdpDtm);
+ lines.Add($" 요양기관명: {hosp.Length}자");
+ Check("㉔ 요양기관명이 나온다", hosp.Trim().Length > 0,
+ "M_DepMst⨝M_HspMst 조인이 0행 — 유효기간이나 DepHspCod 를 의심할 것");
+ }
}
}
}
diff --git a/src/SheetMe.Designer/Services/PatientIdentity.cs b/src/SheetMe.Designer/Services/PatientIdentity.cs
index fa8214b..b1994f4 100644
--- a/src/SheetMe.Designer/Services/PatientIdentity.cs
+++ b/src/SheetMe.Designer/Services/PatientIdentity.cs
@@ -94,6 +94,54 @@ public static class PatientIdentity
}
}
+ ///
+ /// 이 내원의 진료과 마스터 행 — 못 읽으면 빈 사전.
+ /// 진료과 코드는 문맥의 CodInf(적용일시로 고른 행)에서, 유효기간 일자는
+ /// 레거시대로 접수일자다(bzDataInterface.vb:5445 — 입원·외래 무관).
+ ///
+ public static IReadOnlyDictionary DepartmentOf(PatientContext context)
+ {
+ var connection = ConfigService.Current.ConnectionString;
+ var empty = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ if (connection.Length == 0)
+ {
+ return empty;
+ }
+ try
+ {
+ return new PatientContextStore(connection).Department(
+ PatientContext.Value(context.CodInf, "CodDepCod"),
+ PatientContext.Value(context.ComInf, "ComAcpDtm"));
+ }
+ catch
+ {
+ return empty;
+ }
+ }
+
+ ///
+ /// 요양기관명 — 못 읽으면 빈 문자열.
+ /// 판정 일자가 진료과와 다르다(bzDataInterface.vb:15815-15825):
+ /// 입원·퇴원(ILV)은 퇴원일시, 입원 중은 지금, 외래는 접수일시 — 문맥의 적용일시가 그 판정과 같다.
+ ///
+ public static string HospitalNameOf(PatientContext context)
+ {
+ var connection = ConfigService.Current.ConnectionString;
+ if (connection.Length == 0)
+ {
+ return string.Empty;
+ }
+ try
+ {
+ return new PatientContextStore(connection).HospitalName(
+ PatientContext.Value(context.CodInf, "CodDepCod"), context.AdpDtm);
+ }
+ catch
+ {
+ return string.Empty;
+ }
+ }
+
///
/// 이 환자의 주소 조각들(구역번호·도로명·지번·영문) — 못 읽으면 빈 사전.
///
diff --git a/src/SheetMe.Designer/Services/PatientSession.cs b/src/SheetMe.Designer/Services/PatientSession.cs
index 734c4a3..e8a3692 100644
--- a/src/SheetMe.Designer/Services/PatientSession.cs
+++ b/src/SheetMe.Designer/Services/PatientSession.cs
@@ -66,7 +66,8 @@ public static class PatientSession
SheetMe.Core.Catalog.ITagValueResolver tags = Current is { } picked
? new PatientTagResolver(picked, session,
PatientIdentity.ResidentNumberOf(picked), PatientIdentity.AddressOf(picked),
- PatientIdentity.DoctorOf(picked))
+ PatientIdentity.DoctorOf(picked), PatientIdentity.DepartmentOf(picked),
+ PatientIdentity.HospitalNameOf(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 136694f..e548087 100644
--- a/src/SheetMe.Designer/Services/PatientTagResolver.cs
+++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs
@@ -81,6 +81,12 @@ public sealed class PatientTagResolver : ITagValueResolver
["OCM_외래내원일자"] = c => Hyphenate(Head(Com(c, "ComAcpDtm"), 8)),
["OCM_입원전환일자"] = c => Hyphenate(Head(Com(c, "ComTrsDtm"), 8)),
["OCM_입원전환일시"] = c => Stamp(Com(c, "ComTrsDtm")),
+
+ // 병동·병실 — 문맥의 CowInf 행에서 코드값 그대로다(bzDataInterface.vb:6734, 6817).
+ // 마스터로 이름을 찾지 않는다 — 레거시가 CowWadCod/CowRomCod 를 그대로 찍는다.
+ // 외래는 CowInf 가 0행이라 자연히 빈 값이 된다(레거시도 외래·비낮병동은 "" 다).
+ ["OCM_병동"] = c => PatientContext.Value(c.CowInf, "CowWadCod"),
+ ["OCM_병실"] = c => PatientContext.Value(c.CowInf, "CowRomCod"),
};
private static string Pat(PatientContext c, string column) => PatientContext.Value(c.PatInf, column);
@@ -134,15 +140,24 @@ public sealed class PatientTagResolver : ITagValueResolver
/// 이 내원의 담당의 행(M_UidMst) — 없으면 빈 사전
private readonly IReadOnlyDictionary doctor;
+ /// 이 내원의 진료과 마스터 행(M_DepMst) — 없으면 빈 사전
+ private readonly IReadOnlyDictionary department;
+
+ /// 요양기관명(HspInsNam) — 못 읽었으면 빈 문자열
+ private readonly string hospitalName;
+
public PatientTagResolver(PatientContext context, ITagValueResolver next,
string residentNumber = "", IReadOnlyDictionary? address = null,
- IReadOnlyDictionary? doctor = null)
+ IReadOnlyDictionary? doctor = null,
+ IReadOnlyDictionary? department = null, string hospitalName = "")
{
this.context = context;
this.next = next;
this.residentNumber = residentNumber;
this.address = address ?? new Dictionary(StringComparer.OrdinalIgnoreCase);
this.doctor = doctor ?? new Dictionary(StringComparer.OrdinalIgnoreCase);
+ this.department = department ?? new Dictionary(StringComparer.OrdinalIgnoreCase);
+ this.hospitalName = hospitalName;
}
#endregion
@@ -162,6 +177,10 @@ public sealed class PatientTagResolver : ITagValueResolver
{
return doctorValue;
}
+ if (FromFacility(tag) is { } facilityValue)
+ {
+ return facilityValue;
+ }
if (Computed.TryGetValue(tag, out var compute))
{
var made = compute(context);
@@ -295,6 +314,40 @@ public sealed class PatientTagResolver : ITagValueResolver
: "담당의에게 그 값이 없습니다");
}
+ ///
+ /// 진료과·요양기관명 — 해당 태그가 아니면 null.
+ ///
+ /// OCM_진료과 의 이름 선택(bzDataInterface.vb:5474-5484):
+ /// NH 는 무조건 DepOfcNam, 그 밖은 개원일이 2022-07-01 이후이고 DepOfcNam 이 있으면
+ /// DepOfcNam, 아니면 DepKorNam. 개원일(HspStrDte)은 세션에 없어 빈 값으로 두므로
+ /// (FormDesignDataBusiness.HospitalStartDate 와 같은 결정) 그 갈래는 DepKorNam 으로 간다.
+ ///
+ private TagValue? FromFacility(string tag)
+ {
+ switch (tag)
+ {
+ case "OCM_진료과":
+ {
+ string Dep(string column) => department.TryGetValue(column, out var v) ? v.Trim() : string.Empty;
+ var office = Dep("DepOfcNam");
+ var value = string.Equals(UserSession.Current.HspCod, "NH", StringComparison.OrdinalIgnoreCase)
+ ? office
+ : Dep("DepKorNam");
+ return value.Length > 0
+ ? new TagValue(true, value)
+ : new TagValue(false, department.Count == 0
+ ? "이 내원의 진료과를 찾지 못했습니다"
+ : "진료과 마스터에 이름이 비어 있습니다");
+ }
+ case "ETC_요양기관명칭_병원명":
+ return hospitalName.Trim().Length > 0
+ ? new TagValue(true, hospitalName.Trim())
+ : new TagValue(false, "요양기관명을 찾지 못했습니다(M_DepMst·M_HspMst 조인)");
+ default:
+ return null;
+ }
+ }
+
/// 주민번호에서 나온 값 — 비면 왜 비었는지 말한다
private TagValue Wrap(string value) => value.Length > 0
? new TagValue(true, value)
@@ -309,6 +362,7 @@ public sealed class PatientTagResolver : ITagValueResolver
"PAT_주민번호", "PAT_주민번호_Dash", "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 595a9d1..65913e2 100644
--- a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs
+++ b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs
@@ -223,7 +223,8 @@ public partial class PreviewWindow : Window
? session
: new PatientTagResolver(picked, session,
PatientIdentity.ResidentNumberOf(picked), PatientIdentity.AddressOf(picked),
- PatientIdentity.DoctorOf(picked));
+ PatientIdentity.DoctorOf(picked), PatientIdentity.DepartmentOf(picked),
+ PatientIdentity.HospitalNameOf(picked));
// 러너를 새로 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아
// 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다.
fields = new MDataTableRunner(designer.Document,