병리판독의·진단검사의 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]));
|
("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>
|
/// <summary>
|
||||||
/// 수술 일정(S_OprInf) 한 행 — <c>OCM_수술일자</c>·<c>ETC_수술일자_*</c> 계열이 쓴다.
|
/// 수술 일정(S_OprInf) 한 행 — <c>OCM_수술일자</c>·<c>ETC_수술일자_*</c> 계열이 쓴다.
|
||||||
/// <c>WHERE OprComNum = :c AND OprStt = 'E' ORDER BY OprKey</c> —
|
/// <c>WHERE OprComNum = :c AND OprStt = 'E' ORDER BY OprKey</c> —
|
||||||
|
|||||||
@@ -2291,6 +2291,29 @@ public static class DbSmoke
|
|||||||
Check("㉙ 바이탈 조회가 값을 돌려준다", weight.Length > 0,
|
Check("㉙ 바이탈 조회가 값을 돌려준다", weight.Length > 0,
|
||||||
"EmdWeight 널 필터나 조인을 의심할 것");
|
"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);
|
: 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>
|
/// <summary>수술 일정(S_OprInf) 공급자 — 최신순 여부별 1회</summary>
|
||||||
public static Func<bool, IReadOnlyDictionary<string, string>> ScheduledSurgerySourceFor(PatientContext context)
|
public static Func<bool, IReadOnlyDictionary<string, string>> ScheduledSurgerySourceFor(PatientContext context)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ public static class PatientSession
|
|||||||
PatientIdentity.SurgerySourceFor(picked),
|
PatientIdentity.SurgerySourceFor(picked),
|
||||||
PatientIdentity.ReviewDiagnosisSourceFor(picked),
|
PatientIdentity.ReviewDiagnosisSourceFor(picked),
|
||||||
PatientIdentity.VitalSourceFor(picked),
|
PatientIdentity.VitalSourceFor(picked),
|
||||||
PatientIdentity.ScheduledSurgerySourceFor(picked))
|
PatientIdentity.ScheduledSurgerySourceFor(picked),
|
||||||
|
PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource())
|
||||||
: session;
|
: session;
|
||||||
return (tags, new MDataTableRunner(document, Variables()));
|
return (tags, new MDataTableRunner(document, Variables()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,6 +195,13 @@ public sealed class PatientTagResolver : ITagValueResolver
|
|||||||
|
|
||||||
private readonly Dictionary<bool, IReadOnlyDictionary<string, string>> scheduledSurgeryCache = new();
|
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();
|
private readonly Dictionary<DiagnosisStore.Scope, IReadOnlyList<Dictionary<string, string>>> reviewCache = new();
|
||||||
|
|
||||||
public PatientTagResolver(PatientContext context, ITagValueResolver next,
|
public PatientTagResolver(PatientContext context, ITagValueResolver next,
|
||||||
@@ -207,7 +214,9 @@ public sealed class PatientTagResolver : ITagValueResolver
|
|||||||
Func<IReadOnlyList<Dictionary<string, string>>>? surgerySource = null,
|
Func<IReadOnlyList<Dictionary<string, string>>>? surgerySource = null,
|
||||||
Func<DiagnosisStore.Scope, IReadOnlyList<Dictionary<string, string>>>? reviewSource = null,
|
Func<DiagnosisStore.Scope, IReadOnlyList<Dictionary<string, string>>>? reviewSource = null,
|
||||||
Func<string, string, string[], string, string>? vitalSource = 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.context = context;
|
||||||
this.next = next;
|
this.next = next;
|
||||||
@@ -222,6 +231,8 @@ public sealed class PatientTagResolver : ITagValueResolver
|
|||||||
this.reviewSource = reviewSource;
|
this.reviewSource = reviewSource;
|
||||||
this.vitalSource = vitalSource;
|
this.vitalSource = vitalSource;
|
||||||
this.scheduledSurgerySource = scheduledSurgerySource;
|
this.scheduledSurgerySource = scheduledSurgerySource;
|
||||||
|
this.pathologyDoctorSource = pathologyDoctorSource;
|
||||||
|
this.labDoctorSource = labDoctorSource;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -265,6 +276,10 @@ public sealed class PatientTagResolver : ITagValueResolver
|
|||||||
{
|
{
|
||||||
return scheduleValue;
|
return scheduleValue;
|
||||||
}
|
}
|
||||||
|
if (FromSpecialDoctor(tag) is { } specialValue)
|
||||||
|
{
|
||||||
|
return specialValue;
|
||||||
|
}
|
||||||
if (Computed.TryGetValue(tag, out var compute))
|
if (Computed.TryGetValue(tag, out var compute))
|
||||||
{
|
{
|
||||||
var made = compute(context);
|
var made = compute(context);
|
||||||
@@ -816,6 +831,49 @@ public sealed class PatientTagResolver : ITagValueResolver
|
|||||||
: new TagValue(false, "수술일자 값이 형식에 맞지 않습니다");
|
: 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)
|
private TagValue? FromVital(string tag)
|
||||||
{
|
{
|
||||||
// 머리둘레 두 종은 레거시 결함이다 — SELECT 는 EmdHc AS HC 인데 Item("BP") 를 읽어
|
// 머리둘레 두 종은 레거시 결함이다 — SELECT 는 EmdHc AS HC 인데 Item("BP") 를 읽어
|
||||||
@@ -886,6 +944,8 @@ public sealed class PatientTagResolver : ITagValueResolver
|
|||||||
"OCM_머리둘레", "OCM_머리둘레_LAST",
|
"OCM_머리둘레", "OCM_머리둘레_LAST",
|
||||||
"OCM_수술일자", "OCM_수술일자_마지막수술", "ETC_수술일자_1_몇년",
|
"OCM_수술일자", "OCM_수술일자_마지막수술", "ETC_수술일자_1_몇년",
|
||||||
"ETC_수술일자_1_몇년2자리", "ETC_수술일자_2_몇월", "ETC_수술일자_3_몇일",
|
"ETC_수술일자_1_몇년2자리", "ETC_수술일자_2_몇월", "ETC_수술일자_3_몇일",
|
||||||
|
"ETC_병리판독의사명", "ETC_병리판독의사전문의번호",
|
||||||
|
"ETC_진단검사의사명", "ETC_진단검사의사전문의번호",
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -230,7 +230,8 @@ public partial class PreviewWindow : Window
|
|||||||
PatientIdentity.SurgerySourceFor(picked),
|
PatientIdentity.SurgerySourceFor(picked),
|
||||||
PatientIdentity.ReviewDiagnosisSourceFor(picked),
|
PatientIdentity.ReviewDiagnosisSourceFor(picked),
|
||||||
PatientIdentity.VitalSourceFor(picked),
|
PatientIdentity.VitalSourceFor(picked),
|
||||||
PatientIdentity.ScheduledSurgerySourceFor(picked));
|
PatientIdentity.ScheduledSurgerySourceFor(picked),
|
||||||
|
PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource());
|
||||||
// 러너를 <b>새로</b> 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아
|
// 러너를 <b>새로</b> 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아
|
||||||
// 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다.
|
// 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다.
|
||||||
fields = new MDataTableRunner(designer.Document,
|
fields = new MDataTableRunner(designer.Document,
|
||||||
|
|||||||
Reference in New Issue
Block a user