diff --git a/src/SheetMe.Data/Stores/PatientContextStore.cs b/src/SheetMe.Data/Stores/PatientContextStore.cs
index 500c781..43f1066 100644
--- a/src/SheetMe.Data/Stores/PatientContextStore.cs
+++ b/src/SheetMe.Data/Stores/PatientContextStore.cs
@@ -226,6 +226,38 @@ public sealed class PatientContextStore
("d", code), ("y", date[..8]));
}
+ ///
+ /// 병리판독의(TLAB) 한 명 — ETC_병리판독의사명/전문의번호(bzDataInterface.vb:17318, 17515).
+ /// 원문에 ORDER BY 가 없어 TLAB 의사가 둘이면 이름과 번호가 서로 다른 사람이 될 수 있었다
+ /// (태그마다 따로 조회했으므로). 한 행을 UidCod 순으로 고정해 두 태그가 같은 사람을 가리키게 한다.
+ ///
+ public IReadOnlyDictionary PathologyDoctor()
+ {
+ using var connection = new OracleConnection(connectionString);
+ connection.Open();
+ return Row(connection,
+ "SELECT * FROM (SELECT * FROM M_UidMst WHERE UidDtrYon = 'Y' AND TRIM(UidDepCod) = 'TLAB'"
+ + " AND TO_CHAR(SYSDATE,'YYYYMMDD') BETWEEN UidStrDte AND UidEndDte"
+ + " ORDER BY UidCod) WHERE ROWNUM = 1");
+ }
+
+ ///
+ /// 진단검사의(부서그룹 LAB) 한 명 — ETC_진단검사의사명/전문의번호(:17550, 17655).
+ /// 조건은 원문 그대로(의사 + 면허 보유 + 유효기간 + LAB 그룹 부서), 선택 규칙만 고정한다.
+ ///
+ public IReadOnlyDictionary LabDoctor()
+ {
+ using var connection = new OracleConnection(connectionString);
+ connection.Open();
+ return Row(connection,
+ "SELECT * FROM (SELECT u.* FROM M_UidMst u"
+ + " INNER JOIN M_DepMst d ON u.UidDepCod = d.DepCod AND d.DepGrpCod = 'LAB'"
+ + " AND TO_CHAR(SYSDATE,'YYYYMMDD') BETWEEN d.DepStrDte AND d.DepEndDte"
+ + " WHERE u.UidDtrYon = 'Y' AND u.UidLicNum IS NOT NULL"
+ + " AND TO_CHAR(SYSDATE,'YYYYMMDD') BETWEEN u.UidStrDte AND u.UidEndDte"
+ + " ORDER BY u.UidCod) WHERE ROWNUM = 1");
+ }
+
///
/// 수술 일정(S_OprInf) 한 행 — OCM_수술일자·ETC_수술일자_* 계열이 쓴다.
/// WHERE OprComNum = :c AND OprStt = 'E' ORDER BY OprKey —
diff --git a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs
index 36f757f..bad69f7 100644
--- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs
+++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs
@@ -2291,6 +2291,29 @@ public static class DbSmoke
Check("㉙ 바이탈 조회가 값을 돌려준다", weight.Length > 0,
"EmdWeight 널 필터나 조인을 의심할 것");
}
+
+ // ㉚ 병리판독의·진단검사의 — 환자와 무관한 원내 의사 조회.
+ // 이 시험 DB 에 TLAB 소속·LAB 그룹 의사가 없을 수 있다 —
+ // 0행은 실패가 아니라 태그가 사유로 완결되는 정상 경로다.
+ // 여기서 지키는 것은 "예외 없이 돈다"(컬럼·조인 이름이 맞다)이다.
+ try
+ {
+ var contextStore = new PatientContextStore(conn);
+ var pathologyRow = contextStore.PathologyDoctor();
+ var labRow = contextStore.LabDoctor();
+ static string Shape(IReadOnlyDictionary row)
+ => row.Count == 0 ? "0행"
+ : $"이름 {(row.TryGetValue("UidNam", out var n) ? n.Trim().Length : 0)}자"
+ + $"·전문의번호 {(row.TryGetValue("UidSpcLic", out var s) ? s.Trim().Length : 0)}자";
+ lines.Add($" 병리판독의(TLAB): {Shape(pathologyRow)}"
+ + $" / 진단검사의(LAB그룹): {Shape(labRow)} (값은 기록하지 않는다)");
+ Check("㉚ 병리·진단검사 의사 조회가 예외 없이 돈다", true, "");
+ }
+ catch (Exception ex)
+ {
+ Check("㉚ 병리·진단검사 의사 조회가 예외 없이 돈다", false,
+ $"{ex.GetType().Name}: {ex.Message.Split('\n')[0]}");
+ }
}
}
}
diff --git a/src/SheetMe.Designer/Services/PatientIdentity.cs b/src/SheetMe.Designer/Services/PatientIdentity.cs
index f7ae446..a717bf3 100644
--- a/src/SheetMe.Designer/Services/PatientIdentity.cs
+++ b/src/SheetMe.Designer/Services/PatientIdentity.cs
@@ -161,6 +161,23 @@ public static class PatientIdentity
: new VitalStore(connection).One(context.ComNum, expr, pick, notNull, order);
}
+ /// 병리판독의·진단검사의 공급자 — 환자와 무관하지만 환자 태그로 노출된다
+ public static Func> PathologyDoctorSource()
+ {
+ var connection = ConfigService.Current.ConnectionString;
+ return () => connection.Length == 0
+ ? new Dictionary(StringComparer.OrdinalIgnoreCase)
+ : new PatientContextStore(connection).PathologyDoctor();
+ }
+
+ public static Func> LabDoctorSource()
+ {
+ var connection = ConfigService.Current.ConnectionString;
+ return () => connection.Length == 0
+ ? new Dictionary(StringComparer.OrdinalIgnoreCase)
+ : new PatientContextStore(connection).LabDoctor();
+ }
+
/// 수술 일정(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 e4705bc..03e7dac 100644
--- a/src/SheetMe.Designer/Services/PatientSession.cs
+++ b/src/SheetMe.Designer/Services/PatientSession.cs
@@ -73,7 +73,8 @@ public static class PatientSession
PatientIdentity.SurgerySourceFor(picked),
PatientIdentity.ReviewDiagnosisSourceFor(picked),
PatientIdentity.VitalSourceFor(picked),
- PatientIdentity.ScheduledSurgerySourceFor(picked))
+ PatientIdentity.ScheduledSurgerySourceFor(picked),
+ PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource())
: session;
return (tags, new MDataTableRunner(document, Variables()));
}
diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs
index e8acd66..f9f460f 100644
--- a/src/SheetMe.Designer/Services/PatientTagResolver.cs
+++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs
@@ -195,6 +195,13 @@ public sealed class PatientTagResolver : ITagValueResolver
private readonly Dictionary> scheduledSurgeryCache = new();
+ /// 병리판독의(TLAB)·진단검사의(LAB) 공급자 — 각 1회 캐시
+ private readonly Func>? pathologyDoctorSource;
+
+ private readonly Func>? labDoctorSource;
+
+ private readonly Dictionary> specialDoctorCache = new(StringComparer.Ordinal);
+
private readonly Dictionary>> reviewCache = new();
public PatientTagResolver(PatientContext context, ITagValueResolver next,
@@ -207,7 +214,9 @@ public sealed class PatientTagResolver : ITagValueResolver
Func>>? surgerySource = null,
Func>>? reviewSource = null,
Func? vitalSource = null,
- Func>? scheduledSurgerySource = null)
+ Func>? scheduledSurgerySource = null,
+ Func>? pathologyDoctorSource = null,
+ Func>? labDoctorSource = null)
{
this.context = context;
this.next = next;
@@ -222,6 +231,8 @@ public sealed class PatientTagResolver : ITagValueResolver
this.reviewSource = reviewSource;
this.vitalSource = vitalSource;
this.scheduledSurgerySource = scheduledSurgerySource;
+ this.pathologyDoctorSource = pathologyDoctorSource;
+ this.labDoctorSource = labDoctorSource;
}
#endregion
@@ -265,6 +276,10 @@ public sealed class PatientTagResolver : ITagValueResolver
{
return scheduleValue;
}
+ if (FromSpecialDoctor(tag) is { } specialValue)
+ {
+ return specialValue;
+ }
if (Computed.TryGetValue(tag, out var compute))
{
var made = compute(context);
@@ -816,6 +831,49 @@ public sealed class PatientTagResolver : ITagValueResolver
: new TagValue(false, "수술일자 값이 형식에 맞지 않습니다");
}
+ ///
+ /// 병리판독의·진단검사의 4종 — 환자와 무관한 원내 의사 조회지만 레거시가 환자 태그로 노출한다.
+ /// 이름과 번호가 같은 행에서 나온다(레거시는 태그마다 따로 조회해 다른 사람이 될 수 있었다).
+ ///
+ private TagValue? FromSpecialDoctor(string tag)
+ {
+ var pathology = tag is "ETC_병리판독의사명" or "ETC_병리판독의사전문의번호";
+ var lab = tag is "ETC_진단검사의사명" or "ETC_진단검사의사전문의번호";
+ if (!pathology && !lab)
+ {
+ return null;
+ }
+ var source = pathology ? pathologyDoctorSource : labDoctorSource;
+ var cacheKey = pathology ? "P" : "L";
+ if (source is null)
+ {
+ return new TagValue(false, "원내 의사 조회가 연결되지 않았습니다");
+ }
+ if (!specialDoctorCache.TryGetValue(cacheKey, out var row))
+ {
+ try
+ {
+ row = source();
+ }
+ catch (Exception ex)
+ {
+ return new TagValue(false, $"원내 의사 조회에 실패했습니다: {ex.GetType().Name}");
+ }
+ specialDoctorCache[cacheKey] = row;
+ }
+ if (row.Count == 0)
+ {
+ return new TagValue(false, pathology
+ ? "TLAB(병리) 소속의 유효한 의사가 없습니다"
+ : "LAB 그룹 부서의 유효한 의사가 없습니다");
+ }
+ var column = tag.EndsWith("전문의번호", StringComparison.Ordinal) ? "UidSpcLic" : "UidNam";
+ var value = row.TryGetValue(column, out var v) ? v.Trim() : string.Empty;
+ return value.Length > 0
+ ? new TagValue(true, value)
+ : new TagValue(false, $"의사 행은 있는데 {column} 값이 비어 있습니다");
+ }
+
private TagValue? FromVital(string tag)
{
// 머리둘레 두 종은 레거시 결함이다 — SELECT 는 EmdHc AS HC 인데 Item("BP") 를 읽어
@@ -886,6 +944,8 @@ public sealed class PatientTagResolver : ITagValueResolver
"OCM_머리둘레", "OCM_머리둘레_LAST",
"OCM_수술일자", "OCM_수술일자_마지막수술", "ETC_수술일자_1_몇년",
"ETC_수술일자_1_몇년2자리", "ETC_수술일자_2_몇월", "ETC_수술일자_3_몇일",
+ "ETC_병리판독의사명", "ETC_병리판독의사전문의번호",
+ "ETC_진단검사의사명", "ETC_진단검사의사전문의번호",
};
///
diff --git a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs
index 7b7c62c..2a13915 100644
--- a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs
+++ b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs
@@ -230,7 +230,8 @@ public partial class PreviewWindow : Window
PatientIdentity.SurgerySourceFor(picked),
PatientIdentity.ReviewDiagnosisSourceFor(picked),
PatientIdentity.VitalSourceFor(picked),
- PatientIdentity.ScheduledSurgerySourceFor(picked));
+ PatientIdentity.ScheduledSurgerySourceFor(picked),
+ PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource());
// 러너를 새로 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아
// 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다.
fields = new MDataTableRunner(designer.Document,