STFDTR(주치의)을 옮기고 _Refer 계열의 진실을 밝힌다 — 75종 → 76종 + 사유 정정 ~40종

## OCM_STFDTR

담당의 조회를 코드 컬럼만 일반화했다(CodDtrCod → CodStfDtr 파라미터).
시각 갈래는 담당의와 같다(bzDataInterface.vb:4026-4067).
코드가 비어 있으면 레거시도 즉시 "" — 진료과 행이 돌아오면 주치의 행이 아니므로 비운다.

## _Refer 계열은 "안 옮긴 것"이 아니었다

참조 내원은 EMR 실행 화면(ActionTag — fmCodLstActionTag.vb:54 등)에서
<b>사용자가 지정하는 값</b>이고, 레거시 서식생성기 미리보기(TestPatientSetting)는
그것을 설정하지 않는다. 즉 <b>레거시 미리보기에서도 _Refer 태그는 전부 빈 값</b>이다.

지금까지 그 태그들이 "환자 태그이지만 아직 옮기지 않았습니다"로 나왔는데 그건 틀린 사유다 —
옮길 것이 없는 게 아니라, 이 화면에는 참조 내원이라는 개념 자체가 없다.
사유를 갈랐다: "참조 내원 태그입니다 — 참조 내원은 EMR 실행 화면에서 지정되며,
레거시 미리보기에서도 빈 값입니다". _Refer ~40종의 표시가 이것으로 바뀐다.

이로써 <b>서식생성기 미리보기 기준의 레거시 호환</b>에서 _Refer 계열은 완결이다 —
레거시가 빈 값인 자리는 빈 값(+정확한 사유)이 맞는 이식이다.

## 게이트

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-19 15:51:29 +09:00
co-authored by Claude Opus 5
parent 26d9606d1e
commit 3e9d5b5cf3
5 changed files with 69 additions and 6 deletions
@@ -94,6 +94,39 @@ public static class PatientIdentity
}
}
/// <summary>
/// 주치의(전공의) 행 — 진료과 행의 <b>CodStfDtr</b> 로 찾는다. 시각 갈래는 담당의와 같다
/// (bzDataInterface.vb:4026-4067). 코드가 비어 있으면 빈 사전 — 레거시도 즉시 "" 다.
/// </summary>
public static IReadOnlyDictionary<string, string> StaffDoctorOf(PatientContext context)
{
var connection = ConfigService.Current.ConnectionString;
var empty = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (connection.Length == 0)
{
return empty;
}
var inpatient = PatientContext.Value(context.ComInf, "ComPatTyp") == "I";
var accepted = PatientContext.Value(context.ComInf, "ComAcpDtm");
var left = PatientContext.Value(context.ComInf, "ComLevDtm");
var discharged = PatientContext.Value(context.ComInf, "ComPrgStt") == "ILV";
var moment = inpatient
? (discharged && left.Length >= 12 ? left[..12] : context.AdpDtm)
: (accepted.Length >= 12 ? accepted[..12] : context.AdpDtm);
var doctorDay = inpatient ? null : (accepted.Length >= 8 ? accepted[..8] : null);
try
{
var row = new PatientContextStore(connection)
.Doctor(context.ComNum, moment, doctorDay, "CodStfDtr");
// 코드가 비어 있으면 Doctor 가 진료과 행을 돌려준다 — 주치의 행이 아니므로 비운다
return row.ContainsKey("UidNam") ? row : empty;
}
catch
{
return empty;
}
}
/// <summary>
/// 상병 행 공급자 — 해석기가 <b>필요할 때</b> 갈래(전체/주/부)별로 부른다.
/// 여기서 미리 읽지 않는 이유는 상병 태그가 없는 서식이 다수이기 때문이다.
@@ -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.DepartmentOf(picked),
PatientIdentity.DoctorOf(picked), PatientIdentity.StaffDoctorOf(picked),
PatientIdentity.DepartmentOf(picked),
PatientIdentity.HospitalRowOf(picked),
PatientIdentity.DiagnosisSourceFor(picked),
PatientIdentity.SurgerySourceFor(picked),
@@ -157,6 +157,9 @@ public sealed class PatientTagResolver : ITagValueResolver
/// <summary>이 내원의 담당의 행(M_UidMst) — 없으면 빈 사전</summary>
private readonly IReadOnlyDictionary<string, string> doctor;
/// <summary>이 내원의 주치의(전공의, CodStfDtr) 행 — 없으면 빈 사전</summary>
private readonly IReadOnlyDictionary<string, string> staffDoctor;
/// <summary>이 내원의 진료과 마스터 행(M_DepMst) — 없으면 빈 사전</summary>
private readonly IReadOnlyDictionary<string, string> department;
@@ -187,6 +190,7 @@ public sealed class PatientTagResolver : ITagValueResolver
public PatientTagResolver(PatientContext context, ITagValueResolver next,
string residentNumber = "", IReadOnlyDictionary<string, string>? address = null,
IReadOnlyDictionary<string, string>? doctor = null,
IReadOnlyDictionary<string, string>? staffDoctor = null,
IReadOnlyDictionary<string, string>? department = null,
IReadOnlyDictionary<string, string>? hospital = null,
Func<DiagnosisStore.Scope, IReadOnlyList<Dictionary<string, string>>>? diagnosisSource = null,
@@ -198,6 +202,7 @@ public sealed class PatientTagResolver : ITagValueResolver
this.residentNumber = residentNumber;
this.address = address ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
this.doctor = doctor ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
this.staffDoctor = staffDoctor ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
this.department = department ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
this.hospital = hospital ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
this.diagnosisSource = diagnosisSource;
@@ -254,6 +259,15 @@ public sealed class PatientTagResolver : ITagValueResolver
{
return fallback;
}
// _Refer(참조 내원) 계열은 "안 옮긴 것"이 아니다 — 참조 내원은 EMR 실행 화면
// (ActionTag, fmCodLstActionTag.vb:54 등)에서 사용자가 지정하는 값이고,
// 레거시 서식생성기 미리보기(TestPatientSetting)도 그것을 설정하지 않아
// <b>레거시에서도 이 태그들은 미리보기에서 빈 값</b>이다. 사유를 갈라 말한다.
if (tag.EndsWith("_Refer", StringComparison.Ordinal))
{
return new TagValue(false,
"참조 내원 태그입니다 — 참조 내원은 EMR 실행 화면에서 지정되며, 레거시 미리보기에서도 빈 값입니다");
}
return new TagValue(false, "환자 태그이지만 아직 옮기지 않았습니다");
}
@@ -352,10 +366,22 @@ public sealed class PatientTagResolver : ITagValueResolver
private TagValue? FromDoctor(string tag)
{
if (tag is not ("OCM_담당의사" or "OCM_담당의사_담당의사코드" or "OCM_담당의사_연락처"
or "OCM_담당의사_영문" or "OCM_담당의사면허번호" or "OCM_담당의사전문의번호"))
or "OCM_담당의사_영문" or "OCM_담당의사면허번호" or "OCM_담당의사전문의번호"
or "OCM_STFDTR"))
{
return null;
}
// STFDTR(주치의/전공의)는 같은 진료과 행의 <b>CodStfDtr</b> 로 찾은 별도 행이다
// (bzDataInterface.vb:4026-4067 — 조회 갈래는 담당의와 같고 코드 컬럼만 다르다)
if (tag == "OCM_STFDTR")
{
var stf = staffDoctor.TryGetValue("UidNam", out var sn) ? sn.Trim() : string.Empty;
return stf.Length > 0
? new TagValue(true, stf)
: new TagValue(false, staffDoctor.Count == 0
? "이 내원에 주치의(전공의)가 지정되지 않았습니다"
: "주치의 이름이 비어 있습니다");
}
string Field(string column) => doctor.TryGetValue(column, out var v) ? v.Trim() : string.Empty;
var name = Field("UidNam");
@@ -698,7 +724,7 @@ public sealed class PatientTagResolver : ITagValueResolver
"ETC_요양기관명칭_병원전화_팩스번호", "ETC_요양기관명칭_영문_병원전화_팩스번호",
"ETC_요양기관명칭_병원주소", "ETC_요양기관명칭_병원주소_영문",
"ETC_요양기관명칭_사업자등록번호", "ETC_요양기관기호",
"OCM_담당의사_영문", "OCM_담당의사면허번호", "OCM_담당의사전문의번호",
"OCM_담당의사_영문", "OCM_담당의사면허번호", "OCM_담당의사전문의번호", "OCM_STFDTR",
"OCM_User진료과", "OCM_User진료과_한글명칭", "OCM_User진료의사명",
"OCM_상병명_가로", "OCM_상병명_세로", "OCM_상병명_영어_가로", "OCM_상병명_영어_세로",
"OCM_상병코드_가로", "OCM_상병코드_세로", "OCM_부상병코드", "OCM_부상병POA",