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
@@ -145,7 +145,9 @@ public sealed class PatientContextStore
/// </summary> /// </summary>
/// <param name="moment">진료과 행을 고를 적용일시 12자리</param> /// <param name="moment">진료과 행을 고를 적용일시 12자리</param>
/// <param name="doctorDay">담당의 유효기간을 볼 날짜 8자리. 비면 <paramref name="moment"/> 의 앞 8자리</param> /// <param name="doctorDay">담당의 유효기간을 볼 날짜 8자리. 비면 <paramref name="moment"/> 의 앞 8자리</param>
public IReadOnlyDictionary<string, string> Doctor(decimal comNum, string moment, string? doctorDay = null) /// <param name="codeColumn">진료과 행에서 의사 코드를 집을 컬럼 — 담당의 CodDtrCod, 주치의(전공의) CodStfDtr</param>
public IReadOnlyDictionary<string, string> Doctor(decimal comNum, string moment,
string? doctorDay = null, string codeColumn = "CodDtrCod")
{ {
using var connection = new OracleConnection(connectionString); using var connection = new OracleConnection(connectionString);
connection.Open(); connection.Open();
@@ -154,7 +156,7 @@ public sealed class PatientContextStore
"SELECT * FROM (SELECT * FROM P_CodInf WHERE CodComNum = :k AND CodMtiSeq = 0" "SELECT * FROM (SELECT * FROM P_CodInf WHERE CodComNum = :k AND CodMtiSeq = 0"
+ " AND :t BETWEEN CodStrDtm AND CodEndDtm ORDER BY CodStrDtm DESC) WHERE ROWNUM = 1", + " AND :t BETWEEN CodStrDtm AND CodEndDtm ORDER BY CodStrDtm DESC) WHERE ROWNUM = 1",
("k", comNum), ("t", moment)); ("k", comNum), ("t", moment));
var code = PatientContext.Value(cod, "CodDtrCod"); var code = PatientContext.Value(cod, codeColumn);
if (code.Length == 0) if (code.Length == 0)
{ {
return cod; return cod;
@@ -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> /// <summary>
/// 상병 행 공급자 — 해석기가 <b>필요할 때</b> 갈래(전체/주/부)별로 부른다. /// 상병 행 공급자 — 해석기가 <b>필요할 때</b> 갈래(전체/주/부)별로 부른다.
/// 여기서 미리 읽지 않는 이유는 상병 태그가 없는 서식이 다수이기 때문이다. /// 여기서 미리 읽지 않는 이유는 상병 태그가 없는 서식이 다수이기 때문이다.
@@ -66,7 +66,8 @@ public static class PatientSession
SheetMe.Core.Catalog.ITagValueResolver tags = Current is { } picked SheetMe.Core.Catalog.ITagValueResolver tags = Current is { } picked
? new PatientTagResolver(picked, session, ? new PatientTagResolver(picked, session,
PatientIdentity.ResidentNumberOf(picked), PatientIdentity.AddressOf(picked), 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.HospitalRowOf(picked),
PatientIdentity.DiagnosisSourceFor(picked), PatientIdentity.DiagnosisSourceFor(picked),
PatientIdentity.SurgerySourceFor(picked), PatientIdentity.SurgerySourceFor(picked),
@@ -157,6 +157,9 @@ public sealed class PatientTagResolver : ITagValueResolver
/// <summary>이 내원의 담당의 행(M_UidMst) — 없으면 빈 사전</summary> /// <summary>이 내원의 담당의 행(M_UidMst) — 없으면 빈 사전</summary>
private readonly IReadOnlyDictionary<string, string> doctor; private readonly IReadOnlyDictionary<string, string> doctor;
/// <summary>이 내원의 주치의(전공의, CodStfDtr) 행 — 없으면 빈 사전</summary>
private readonly IReadOnlyDictionary<string, string> staffDoctor;
/// <summary>이 내원의 진료과 마스터 행(M_DepMst) — 없으면 빈 사전</summary> /// <summary>이 내원의 진료과 마스터 행(M_DepMst) — 없으면 빈 사전</summary>
private readonly IReadOnlyDictionary<string, string> department; private readonly IReadOnlyDictionary<string, string> department;
@@ -187,6 +190,7 @@ public sealed class PatientTagResolver : ITagValueResolver
public PatientTagResolver(PatientContext context, ITagValueResolver next, public PatientTagResolver(PatientContext context, ITagValueResolver next,
string residentNumber = "", IReadOnlyDictionary<string, string>? address = null, string residentNumber = "", IReadOnlyDictionary<string, string>? address = null,
IReadOnlyDictionary<string, string>? doctor = null, IReadOnlyDictionary<string, string>? doctor = null,
IReadOnlyDictionary<string, string>? staffDoctor = null,
IReadOnlyDictionary<string, string>? department = null, IReadOnlyDictionary<string, string>? department = null,
IReadOnlyDictionary<string, string>? hospital = null, IReadOnlyDictionary<string, string>? hospital = null,
Func<DiagnosisStore.Scope, IReadOnlyList<Dictionary<string, string>>>? diagnosisSource = null, Func<DiagnosisStore.Scope, IReadOnlyList<Dictionary<string, string>>>? diagnosisSource = null,
@@ -198,6 +202,7 @@ public sealed class PatientTagResolver : ITagValueResolver
this.residentNumber = residentNumber; this.residentNumber = residentNumber;
this.address = address ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); this.address = address ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
this.doctor = doctor ?? 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.department = department ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
this.hospital = hospital ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); this.hospital = hospital ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
this.diagnosisSource = diagnosisSource; this.diagnosisSource = diagnosisSource;
@@ -254,6 +259,15 @@ public sealed class PatientTagResolver : ITagValueResolver
{ {
return fallback; 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, "환자 태그이지만 아직 옮기지 않았습니다"); return new TagValue(false, "환자 태그이지만 아직 옮기지 않았습니다");
} }
@@ -352,10 +366,22 @@ public sealed class PatientTagResolver : ITagValueResolver
private TagValue? FromDoctor(string tag) private TagValue? FromDoctor(string tag)
{ {
if (tag is not ("OCM_담당의사" or "OCM_담당의사_담당의사코드" or "OCM_담당의사_연락처" 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; 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; string Field(string column) => doctor.TryGetValue(column, out var v) ? v.Trim() : string.Empty;
var name = Field("UidNam"); var name = Field("UidNam");
@@ -698,7 +724,7 @@ public sealed class PatientTagResolver : ITagValueResolver
"ETC_요양기관명칭_병원전화_팩스번호", "ETC_요양기관명칭_영문_병원전화_팩스번호", "ETC_요양기관명칭_병원전화_팩스번호", "ETC_요양기관명칭_영문_병원전화_팩스번호",
"ETC_요양기관명칭_병원주소", "ETC_요양기관명칭_병원주소_영문", "ETC_요양기관명칭_병원주소", "ETC_요양기관명칭_병원주소_영문",
"ETC_요양기관명칭_사업자등록번호", "ETC_요양기관기호", "ETC_요양기관명칭_사업자등록번호", "ETC_요양기관기호",
"OCM_담당의사_영문", "OCM_담당의사면허번호", "OCM_담당의사전문의번호", "OCM_담당의사_영문", "OCM_담당의사면허번호", "OCM_담당의사전문의번호", "OCM_STFDTR",
"OCM_User진료과", "OCM_User진료과_한글명칭", "OCM_User진료의사명", "OCM_User진료과", "OCM_User진료과_한글명칭", "OCM_User진료의사명",
"OCM_상병명_가로", "OCM_상병명_세로", "OCM_상병명_영어_가로", "OCM_상병명_영어_세로", "OCM_상병명_가로", "OCM_상병명_세로", "OCM_상병명_영어_가로", "OCM_상병명_영어_세로",
"OCM_상병코드_가로", "OCM_상병코드_세로", "OCM_부상병코드", "OCM_부상병POA", "OCM_상병코드_가로", "OCM_상병코드_세로", "OCM_부상병코드", "OCM_부상병POA",
@@ -223,7 +223,8 @@ public partial class PreviewWindow : Window
? session ? session
: new PatientTagResolver(picked, session, : new PatientTagResolver(picked, session,
PatientIdentity.ResidentNumberOf(picked), PatientIdentity.AddressOf(picked), 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.HospitalRowOf(picked),
PatientIdentity.DiagnosisSourceFor(picked), PatientIdentity.DiagnosisSourceFor(picked),
PatientIdentity.SurgerySourceFor(picked), PatientIdentity.SurgerySourceFor(picked),