혈액형·LMP·BST·체중 파생 8종 — 156종 → 164종
OCM_혈액형(bzDataInterface.vb:11783 — S_BlpInf.BlpAboTyp, 갱신일시 최신 고정), OCM_LMP(:11813 — O_PrgInf.PrgLmpDte, 같은 표를 읽는 임신주기의 갱신일시 최신순으로 고정), OCM_BST/BST_LAST(:12042/:12710 — 바이탈과 다른 표 E_EmdInf_BST, VitalStore 를 표 인자화), OCM_BMI(:12159 — 차트 전체 최신 체중/키², 반올림 2자리), OCM_표준체중_LAST(:12234 — (키/100)²×여21·남22), OCM_조정체중_LAST(:12284 — 표준+(실제-표준)×0.25, 키·체중이 같은 행), OCM_비만도(:12333 — 이것만 내원 기준, 체중/Round(표준,0)×100). BMI·표준·조정은 내원(EmrComNum)이 아니라 차트(EmrChtNum) 기준이다 — 주석 처리된 EmrComNum 이 그 흔적. 성별 불명이면 표준·조정은 레거시 그대로 "0" 이 찍히고, 비만도는 0 나눗셈이라 사유로 말한다. OCM_임신주기는 사용자별 레지스트리 설정(DB_REGISTRY, UidCod 컬럼에 UidNam 을 비교하는 수상한 조건 포함) 의존이라 보류 — 별도 조사 대상. - dotnet test 338/338 · --edit-smoke 실패 0 (이름 검사 164종) - --db-patient ①~㉞ 전건 통과 (S_BlpInf·O_PrgInf·E_EmdInf_BST 실행 확인) - --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
f2f4cf81d3
commit
7f147a3012
@@ -161,6 +161,25 @@ public static class PatientIdentity
|
||||
: new VitalStore(connection).One(context.ComNum, expr, pick, notNull, order);
|
||||
}
|
||||
|
||||
/// <summary>BST 공급자 — 표가 달라(E_EmdInf_BST) 바이탈과 분리</summary>
|
||||
public static Func<string, string> BstSourceFor(PatientContext context)
|
||||
{
|
||||
var connection = ConfigService.Current.ConnectionString;
|
||||
return order => connection.Length == 0
|
||||
? string.Empty
|
||||
: new VitalStore(connection).OneBst(context.ComNum, order);
|
||||
}
|
||||
|
||||
/// <summary>차트 전체 기준 바이탈 공급자 — BMI·표준·조정체중이 쓴다</summary>
|
||||
public static Func<string, string, string[], string, string> ChartVitalSourceFor(PatientContext context)
|
||||
{
|
||||
var connection = ConfigService.Current.ConnectionString;
|
||||
var chtNum = PatientContext.Value(context.ComInf, "ComChtNum");
|
||||
return (expr, pick, notNull, order) => connection.Length == 0 || chtNum.Length == 0
|
||||
? string.Empty
|
||||
: new VitalStore(connection).OneByChart(chtNum, expr, pick, notNull, order);
|
||||
}
|
||||
|
||||
/// <summary>병리판독의·진단검사의 공급자 — 환자와 무관하지만 환자 태그로 노출된다</summary>
|
||||
public static Func<IReadOnlyDictionary<string, string>> PathologyDoctorSource()
|
||||
{
|
||||
|
||||
@@ -75,7 +75,8 @@ public static class PatientSession
|
||||
PatientIdentity.VitalSourceFor(picked),
|
||||
PatientIdentity.ScheduledSurgerySourceFor(picked),
|
||||
PatientIdentity.PathologyDoctorSource(), PatientIdentity.LabDoctorSource(),
|
||||
PatientIdentity.ExtraStoreSource(), PatientIdentity.ContextStoreSource())
|
||||
PatientIdentity.ExtraStoreSource(), PatientIdentity.ContextStoreSource(),
|
||||
PatientIdentity.BstSourceFor(picked), PatientIdentity.ChartVitalSourceFor(picked))
|
||||
: session;
|
||||
return (tags, new MDataTableRunner(document, Variables()));
|
||||
}
|
||||
|
||||
@@ -239,6 +239,12 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
|
||||
private readonly Dictionary<string, TagValue> deptAtCache = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>BST 공급자(정렬 → 값) — 표가 달라 바이탈 소스와 분리</summary>
|
||||
private readonly Func<string, string>? bstSource;
|
||||
|
||||
/// <summary>차트 전체 기준 바이탈 공급자 — BMI·표준·조정체중이 쓴다</summary>
|
||||
private readonly Func<string, string, string[], string, string>? chartVitalSource;
|
||||
|
||||
private readonly Dictionary<DiagnosisStore.Scope, IReadOnlyList<Dictionary<string, string>>> reviewCache = new();
|
||||
|
||||
public PatientTagResolver(PatientContext context, ITagValueResolver next,
|
||||
@@ -255,7 +261,9 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
Func<IReadOnlyDictionary<string, string>>? pathologyDoctorSource = null,
|
||||
Func<IReadOnlyDictionary<string, string>>? labDoctorSource = null,
|
||||
Func<PatientExtraStore?>? extraStoreSource = null,
|
||||
Func<PatientContextStore?>? contextStoreSource = null)
|
||||
Func<PatientContextStore?>? contextStoreSource = null,
|
||||
Func<string, string>? bstSource = null,
|
||||
Func<string, string, string[], string, string>? chartVitalSource = null)
|
||||
{
|
||||
this.context = context;
|
||||
this.next = next;
|
||||
@@ -274,6 +282,8 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
this.labDoctorSource = labDoctorSource;
|
||||
this.extraStoreSource = extraStoreSource;
|
||||
this.contextStoreSource = contextStoreSource;
|
||||
this.bstSource = bstSource;
|
||||
this.chartVitalSource = chartVitalSource;
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -333,6 +343,10 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
{
|
||||
return deptAtValue;
|
||||
}
|
||||
if (FromBodyDerived(tag) is { } bodyValue)
|
||||
{
|
||||
return bodyValue;
|
||||
}
|
||||
if (Computed.TryGetValue(tag, out var compute))
|
||||
{
|
||||
var made = compute(context);
|
||||
@@ -937,6 +951,147 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
: new TagValue(false, "수술일자 값이 형식에 맞지 않습니다");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BST·BMI·체중 파생 6종(bzDataInterface.vb:12042-12420).
|
||||
///
|
||||
/// BST 는 바이탈과 표가 다르고(E_EmdInf_BST), BMI·표준체중·조정체중은 내원이 아니라
|
||||
/// <b>차트 전체</b> 최신 측정을 쓴다(EmrChtNum — 주석 처리된 EmrComNum 이 그 흔적).
|
||||
/// 비만도만 내원(EmrComNum) 기준이다 — 원문이 그렇다.
|
||||
///
|
||||
/// 표준·조정체중의 성별 계수(여 21·남 22)는 주민번호에서 나온다. 성별을 못 정하면
|
||||
/// 레거시는 계수 0 으로 계산해 <b>"0" 이 종이에 찍힌다</b> — 그대로 옮겼다.
|
||||
/// 비만도는 그 경우 0 나눗셈이라 레거시도 정상 값을 못 만든다 — 사유로 말한다.
|
||||
/// </summary>
|
||||
private TagValue? FromBodyDerived(string tag)
|
||||
{
|
||||
if (tag is not ("OCM_BST" or "OCM_BST_LAST" or "OCM_BMI"
|
||||
or "OCM_표준체중_LAST" or "OCM_조정체중_LAST" or "OCM_비만도"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (extraCache.TryGetValue(tag, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
var value = Compute();
|
||||
extraCache[tag] = value;
|
||||
return value;
|
||||
|
||||
TagValue Compute()
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (tag)
|
||||
{
|
||||
case "OCM_BST":
|
||||
case "OCM_BST_LAST":
|
||||
{
|
||||
if (bstSource is null)
|
||||
{
|
||||
return new TagValue(false, "BST 조회가 연결되지 않았습니다");
|
||||
}
|
||||
var bst = bstSource(tag == "OCM_BST"
|
||||
? "EmdDte ASC, EmdTime ASC" : "EmdDte DESC, EmdTime DESC");
|
||||
return bst.Length > 0
|
||||
? new TagValue(true, bst)
|
||||
: new TagValue(false, "이 내원에 BST 기록(E_EmdInf_BST)이 없습니다");
|
||||
}
|
||||
case "OCM_비만도":
|
||||
{
|
||||
// 내원 기준(EmrComNum) — 키·체중을 따로 최신으로 집는다(:12333-12420)
|
||||
if (vitalSource is null)
|
||||
{
|
||||
return new TagValue(false, "바이탈 조회가 연결되지 않았습니다");
|
||||
}
|
||||
var h = Number(vitalSource("EmdHeight", "EmdHeight",
|
||||
new[] { "EmdHeight" }, "EmdDte DESC, EmdTime DESC"));
|
||||
var w = Number(vitalSource("EmdWeight", "EmdWeight",
|
||||
new[] { "EmdWeight" }, "EmdDte DESC, EmdTime DESC"));
|
||||
if (h is null || w is null)
|
||||
{
|
||||
return new TagValue(false, "이 내원에 키·체중 측정이 없습니다");
|
||||
}
|
||||
var std = StandardWeight(h.Value);
|
||||
if (std is null or 0)
|
||||
{
|
||||
return new TagValue(false, "성별을 판정할 수 없어 표준체중을 만들 수 없습니다");
|
||||
}
|
||||
var pct = Math.Round(w.Value / Math.Round(std.Value, 0) * 100, 1);
|
||||
return new TagValue(true, pct.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
}
|
||||
default:
|
||||
{
|
||||
// 차트 전체 최신(EmrChtNum) — BMI·표준체중·조정체중(:12159-12333)
|
||||
if (chartVitalSource is null)
|
||||
{
|
||||
return new TagValue(false, "차트 바이탈 조회가 연결되지 않았습니다");
|
||||
}
|
||||
if (tag == "OCM_표준체중_LAST")
|
||||
{
|
||||
var h = Number(chartVitalSource("EmdHeight", "EmdHeight",
|
||||
new[] { "EmdHeight" }, "EmdDte DESC, EmdTime DESC"));
|
||||
if (h is null)
|
||||
{
|
||||
return new TagValue(false, "이 차트에 키 측정이 없습니다");
|
||||
}
|
||||
// 성별 불명이면 계수 0 → "0"(레거시 Select Case 의 빈 Case "")
|
||||
var std = StandardWeight(h.Value) ?? 0;
|
||||
return new TagValue(true, Math.Round(std, 1)
|
||||
.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
}
|
||||
if (tag == "OCM_조정체중_LAST")
|
||||
{
|
||||
// 키와 체중이 <b>같은 행</b>에서 나온다(:12291-12303) — 결합식으로 한 번에
|
||||
var pair = chartVitalSource("EmdHeight || '/' || EmdWeight AS HW", "HW",
|
||||
new[] { "EmdWeight", "EmdHeight" }, "EmdDte DESC, EmdTime DESC");
|
||||
var parts = pair.Split('/');
|
||||
var h = parts.Length == 2 ? Number(parts[0]) : null;
|
||||
var w = parts.Length == 2 ? Number(parts[1]) : null;
|
||||
if (h is null || w is null)
|
||||
{
|
||||
return new TagValue(false, "키와 체중이 함께 있는 측정이 없습니다");
|
||||
}
|
||||
var std = StandardWeight(h.Value) ?? 0;
|
||||
var abw = std + (w.Value - std) * 0.25;
|
||||
return new TagValue(true, Math.Round(abw, 1)
|
||||
.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
}
|
||||
// OCM_BMI — 체중과 키를 따로 최신으로(:12168-12216), 반올림 2자리
|
||||
var weight = Number(chartVitalSource("EmdWeight", "EmdWeight",
|
||||
new[] { "EmdWeight" }, "EmdDte DESC, EmdTime DESC"));
|
||||
var height = Number(chartVitalSource("EmdHeight", "EmdHeight",
|
||||
new[] { "EmdHeight" }, "EmdDte DESC, EmdTime DESC"));
|
||||
if (weight is null || height is null || height.Value == 0)
|
||||
{
|
||||
return new TagValue(false, "이 차트에 키·체중 측정이 없습니다");
|
||||
}
|
||||
var meters = height.Value / 100;
|
||||
var bmi = Math.Round(weight.Value / (meters * meters), 2);
|
||||
return new TagValue(true, bmi.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new TagValue(false, $"측정치 조회에 실패했습니다: {ex.GetType().Name}");
|
||||
}
|
||||
}
|
||||
|
||||
// 표준체중 = (키/100)² × (여 21 / 남 22) — 성별 불명이면 null(:12275-12283)
|
||||
double? StandardWeight(double heightCm)
|
||||
=> ResidentNumber.SexOf(residentNumber) switch
|
||||
{
|
||||
"F" => heightCm / 100 * (heightCm / 100) * 21,
|
||||
"M" => heightCm / 100 * (heightCm / 100) * 22,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
static double? Number(string? value)
|
||||
=> double.TryParse((value ?? string.Empty).Trim(),
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var n) ? n : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 입원과·퇴원과 4종(bzDataInterface.vb:6009-6318) — 문맥의 적용일시와 <b>다른 시점</b>의
|
||||
/// 진료과를 묻는다. 입원과는 <b>접수 시점</b>(전과했으면 처음 과), 퇴원과는
|
||||
@@ -1195,7 +1350,7 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
or "PAT_건보_급여_세대주명" or "PAT_협력업체" or "PAT_보호자연락처"
|
||||
or "OCM_FollowUp" or "OCM_외출외박신청일" or "OCM_외출외박종료일"
|
||||
or "OCM_최초내원일" or "OCM_초진일_발병일" or "OCM_신환_초진일"
|
||||
or "OCM_입원일자_낮병동"))
|
||||
or "OCM_입원일자_낮병동" or "OCM_혈액형" or "OCM_LMP"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -1292,6 +1447,15 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
? new TagValue(true, Hyphenate(day[..8]))
|
||||
: new TagValue(false, "낮병동 신청(P_DcpInf) 구간이 이 내원을 포함하지 않습니다");
|
||||
}
|
||||
case "OCM_혈액형":
|
||||
return Wrap(store.BloodType(chtNum), "혈액형 판정(S_BlpInf) 행이 없습니다");
|
||||
case "OCM_LMP":
|
||||
{
|
||||
var comNum = PatientContext.Value(context.ComInf, "ComNum");
|
||||
return decimal.TryParse(comNum, out var m)
|
||||
? Wrap(store.Lmp(m), "임신 정보(O_PrgInf)에 최종 월경일이 없습니다")
|
||||
: new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다");
|
||||
}
|
||||
case "OCM_FollowUp":
|
||||
{
|
||||
// 지금보다 뒤의 내원이 없으면 레거시도 "" 다(:1055) — 사유로 말한다
|
||||
@@ -1453,6 +1617,8 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
"OCM_진료과_그룹코드", "OCM_진료과_영어", "OCM_진료과대외명칭", "OCM_진료과약어명칭",
|
||||
"OCM_입원과", "OCM_입원과_한글명칭", "OCM_퇴원과", "OCM_퇴원과_한글명칭",
|
||||
"OCM_입원의사", "OCM_퇴원의사", "OCM_협진과", "OCM_협진과_협진의",
|
||||
"OCM_혈액형", "OCM_LMP", "OCM_BST", "OCM_BST_LAST",
|
||||
"OCM_BMI", "OCM_표준체중_LAST", "OCM_조정체중_LAST", "OCM_비만도",
|
||||
"OCM_입원시간", "OCM_입원일시", "OCM_입원일시_영문", "OCM_입실시간",
|
||||
"OCM_의사퇴원예고일시", "OCM_퇴원예정일시", "OCM_퇴원시간", "OCM_퇴원일시",
|
||||
"OCM_퇴원예고일시", "OCM_재원일수", "OCM_퇴원예고재원일수", "OCM_입원일자_낮병동",
|
||||
|
||||
Reference in New Issue
Block a user