병리판독의·진단검사의 4종 — 106종 → 110종
ETC_병리판독의사명/전문의번호(bzDataInterface.vb:17318/:17515 — M_UidMst UidDtrYon='Y' AND UidDepCod='TLAB' + 유효기간), ETC_진단검사의사명/ 전문의번호(:17550/:17655 — M_DepMst DepGrpCod='LAB' 조인 + UidLicNum IS NOT NULL). 환자와 무관한 원내 의사 조회지만 레거시가 환자 태그로 노출하므로 PatientTagResolver 에 둔다(lazy 소스 + 캐시 — 이 태그가 없는 서식이 대부분이라 프리페치하지 않는다). 의도적 이탈 하나: 원문엔 ORDER BY 가 없어 TLAB 의사가 둘이면 이름과 전문의번호가 서로 다른 사람이 될 수 있었다(태그마다 따로 조회하므로). ORDER BY UidCod + 1행 고정으로 두 태그가 반드시 같은 사람을 가리키게 했다 — 담당의 때 정한 결정 규칙의 세 번째 적용. - dotnet test 338/338 · --edit-smoke 실패 0 (이름 검사 110종) - --db-patient ①~㉚ 전건 통과 — ㉚ 신설: 두 조회가 예외 없이 돈다 (이 시험 DB 는 TLAB·LAB 의사 0행 — 태그는 사유로 완결되는 정상 경로) - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4dad271e9c
commit
ba6da2252f
@@ -226,6 +226,38 @@ public sealed class PatientContextStore
|
||||
("d", code), ("y", date[..8]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 병리판독의(TLAB) 한 명 — <c>ETC_병리판독의사명/전문의번호</c>(bzDataInterface.vb:17318, 17515).
|
||||
/// 원문에 ORDER BY 가 없어 TLAB 의사가 둘이면 이름과 번호가 <b>서로 다른 사람</b>이 될 수 있었다
|
||||
/// (태그마다 따로 조회했으므로). 한 행을 UidCod 순으로 고정해 두 태그가 같은 사람을 가리키게 한다.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string> 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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 진단검사의(부서그룹 LAB) 한 명 — <c>ETC_진단검사의사명/전문의번호</c>(:17550, 17655).
|
||||
/// 조건은 원문 그대로(의사 + 면허 보유 + 유효기간 + LAB 그룹 부서), 선택 규칙만 고정한다.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string> 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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 수술 일정(S_OprInf) 한 행 — <c>OCM_수술일자</c>·<c>ETC_수술일자_*</c> 계열이 쓴다.
|
||||
/// <c>WHERE OprComNum = :c AND OprStt = 'E' ORDER BY OprKey</c> —
|
||||
|
||||
@@ -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<string, string> 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]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +161,23 @@ public static class PatientIdentity
|
||||
: new VitalStore(connection).One(context.ComNum, expr, pick, notNull, order);
|
||||
}
|
||||
|
||||
/// <summary>병리판독의·진단검사의 공급자 — 환자와 무관하지만 환자 태그로 노출된다</summary>
|
||||
public static Func<IReadOnlyDictionary<string, string>> PathologyDoctorSource()
|
||||
{
|
||||
var connection = ConfigService.Current.ConnectionString;
|
||||
return () => connection.Length == 0
|
||||
? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
: new PatientContextStore(connection).PathologyDoctor();
|
||||
}
|
||||
|
||||
public static Func<IReadOnlyDictionary<string, string>> LabDoctorSource()
|
||||
{
|
||||
var connection = ConfigService.Current.ConnectionString;
|
||||
return () => connection.Length == 0
|
||||
? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
: new PatientContextStore(connection).LabDoctor();
|
||||
}
|
||||
|
||||
/// <summary>수술 일정(S_OprInf) 공급자 — 최신순 여부별 1회</summary>
|
||||
public static Func<bool, IReadOnlyDictionary<string, string>> ScheduledSurgerySourceFor(PatientContext context)
|
||||
{
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -195,6 +195,13 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
|
||||
private readonly Dictionary<bool, IReadOnlyDictionary<string, string>> scheduledSurgeryCache = new();
|
||||
|
||||
/// <summary>병리판독의(TLAB)·진단검사의(LAB) 공급자 — 각 1회 캐시</summary>
|
||||
private readonly Func<IReadOnlyDictionary<string, string>>? pathologyDoctorSource;
|
||||
|
||||
private readonly Func<IReadOnlyDictionary<string, string>>? labDoctorSource;
|
||||
|
||||
private readonly Dictionary<string, IReadOnlyDictionary<string, string>> specialDoctorCache = new(StringComparer.Ordinal);
|
||||
|
||||
private readonly Dictionary<DiagnosisStore.Scope, IReadOnlyList<Dictionary<string, string>>> reviewCache = new();
|
||||
|
||||
public PatientTagResolver(PatientContext context, ITagValueResolver next,
|
||||
@@ -207,7 +214,9 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
Func<IReadOnlyList<Dictionary<string, string>>>? surgerySource = null,
|
||||
Func<DiagnosisStore.Scope, IReadOnlyList<Dictionary<string, string>>>? reviewSource = null,
|
||||
Func<string, string, string[], string, string>? vitalSource = null,
|
||||
Func<bool, IReadOnlyDictionary<string, string>>? scheduledSurgerySource = null)
|
||||
Func<bool, IReadOnlyDictionary<string, string>>? scheduledSurgerySource = null,
|
||||
Func<IReadOnlyDictionary<string, string>>? pathologyDoctorSource = null,
|
||||
Func<IReadOnlyDictionary<string, string>>? 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, "수술일자 값이 형식에 맞지 않습니다");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 병리판독의·진단검사의 4종 — 환자와 무관한 원내 의사 조회지만 레거시가 환자 태그로 노출한다.
|
||||
/// 이름과 번호가 <b>같은 행</b>에서 나온다(레거시는 태그마다 따로 조회해 다른 사람이 될 수 있었다).
|
||||
/// </summary>
|
||||
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_진단검사의사전문의번호",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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());
|
||||
// 러너를 <b>새로</b> 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아
|
||||
// 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다.
|
||||
fields = new MDataTableRunner(designer.Document,
|
||||
|
||||
Reference in New Issue
Block a user