진료과·병동·병실·요양기관명을 옮긴다 — 26종 → 30종

사용자가 서식에서 빈칸으로 확인한 자리들이다(B·C 배치).

## 본문에서 확인한 것

- OCM_병동 / OCM_병실 (bzDataInterface.vb:6718, 6801)
  문맥의 CowInf 행에서 <b>코드값 그대로</b>다(CowWadCod·CowRomCod).
  마스터로 이름을 찾지 않는다 — 레거시가 코드를 그대로 찍는다.
  외래는 CowInf 가 0행이라 자연히 빈 값(레거시도 외래·비낮병동은 "").

- OCM_진료과 (:5437-5495)
  M_DepMst 를 CodDepCod + 접수일자 BETWEEN 으로 찾는다.
  유효기간 일자는 입원·외래 무관하게 <b>접수일자</b>다(:5445).
  이름 선택: NH 는 DepOfcNam, 그 밖은 개원일 2022-07-01 이후 + DepOfcNam 존재 시
  DepOfcNam, 아니면 DepKorNam. 개원일은 세션에 없어 빈 값 결정(기존과 동일)이라
  그 갈래는 DepKorNam 으로 간다.

- ETC_요양기관명칭_병원명 (:15792-15860)
  M_DepMst ⨝ M_HspMst(DepHspCod=HspCod) 에서 HspInsNam.Trim.
  진료과 코드로 병원을 찾는 구조다(다부지 구성).
  판정 일자가 진료과와 다르다 — ILV 는 퇴원일시·재원 중은 지금·외래는 접수일시.
  문맥의 적용일시가 정확히 그 판정이라 AdpDtm 을 그대로 쓴다.

원본 마스터 조회에 ORDER BY 가 없어 담당의 때와 같은 규칙(최신순 1행)으로 고정했다.

## 왕복은 환자당 두 번 추가

진료과·요양기관명을 환자를 붙일 때 한 번씩 읽는다(주민번호·주소·담당의와 같은 정책).
미리보기 창과 Ctrl+P 인쇄(PatientSession.ResolversFor) 둘 다 같은 재료를 받는다.

## 실DB 확인

--db-patient ㉓·㉔ 신규: 진료과 코드 '0506' → 44열·한글명 5자, 요양기관명 6자.
코드가 이름이 되는 것까지 확인해야 태그가 값을 낸다.

## 게이트

- dotnet test 338/338 · --edit-smoke 실패 0(이름 검사 30종)
- --db-patient ①~㉔ 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-19 14:54:18 +09:00
co-authored by Claude Opus 5
parent db969fc6b5
commit e62e43d176
6 changed files with 173 additions and 3 deletions
@@ -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 를 의심할 것");
}
}
}
}
@@ -94,6 +94,54 @@ public static class PatientIdentity
}
}
/// <summary>
/// 이 내원의 진료과 마스터 행 — 못 읽으면 빈 사전.
/// 진료과 코드는 문맥의 CodInf(적용일시로 고른 행)에서, 유효기간 일자는
/// 레거시대로 <b>접수일자</b>다(bzDataInterface.vb:5445 — 입원·외래 무관).
/// </summary>
public static IReadOnlyDictionary<string, string> DepartmentOf(PatientContext context)
{
var connection = ConfigService.Current.ConnectionString;
var empty = new Dictionary<string, string>(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;
}
}
/// <summary>
/// 요양기관명 — 못 읽으면 빈 문자열.
/// 판정 일자가 진료과와 <b>다르다</b>(bzDataInterface.vb:15815-15825):
/// 입원·퇴원(ILV)은 퇴원일시, 입원 중은 지금, 외래는 접수일시 — 문맥의 적용일시가 그 판정과 같다.
/// </summary>
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;
}
}
/// <summary>
/// 이 환자의 주소 조각들(구역번호·도로명·지번·영문) — 못 읽으면 빈 사전.
///
@@ -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()));
}
@@ -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 행에서 <b>코드값 그대로</b>다(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
/// <summary>이 내원의 담당의 행(M_UidMst) — 없으면 빈 사전</summary>
private readonly IReadOnlyDictionary<string, string> doctor;
/// <summary>이 내원의 진료과 마스터 행(M_DepMst) — 없으면 빈 사전</summary>
private readonly IReadOnlyDictionary<string, string> department;
/// <summary>요양기관명(HspInsNam) — 못 읽었으면 빈 문자열</summary>
private readonly string hospitalName;
public PatientTagResolver(PatientContext context, ITagValueResolver next,
string residentNumber = "", IReadOnlyDictionary<string, string>? address = null,
IReadOnlyDictionary<string, string>? doctor = null)
IReadOnlyDictionary<string, string>? doctor = null,
IReadOnlyDictionary<string, string>? department = null, string hospitalName = "")
{
this.context = context;
this.next = next;
this.residentNumber = residentNumber;
this.address = address ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
this.doctor = doctor ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
this.department = department ?? new Dictionary<string, string>(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
: "담당의에게 그 값이 없습니다");
}
/// <summary>
/// 진료과·요양기관명 — 해당 태그가 아니면 null.
///
/// <c>OCM_진료과</c> 의 이름 선택(bzDataInterface.vb:5474-5484):
/// NH 는 무조건 DepOfcNam, 그 밖은 개원일이 2022-07-01 이후이고 DepOfcNam 이 있으면
/// DepOfcNam, 아니면 DepKorNam. 개원일(HspStrDte)은 세션에 없어 빈 값으로 두므로
/// (FormDesignDataBusiness.HospitalStartDate 와 같은 결정) 그 갈래는 DepKorNam 으로 간다.
/// </summary>
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;
}
}
/// <summary>주민번호에서 나온 값 — 비면 <b>왜</b> 비었는지 말한다</summary>
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_요양기관명칭_병원명",
};
/// <summary>
@@ -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));
// 러너를 <b>새로</b> 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아
// 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다.
fields = new MDataTableRunner(designer.Document,