부가정보 2차분 20종 — 165종 → 185종 (워크플로 조사 라운드 4·5)
병렬 조사(읽기 11 + 반박 검증 11)로 39종을 분석하고 그중 20종을 이식: 값 18종 — OCM_발병일(:8125, 발병시작일 최신 고정), OCM_당일식이 (:12620, PIVOT 원문 유지·"아침 : X " 결합), OCM_알러지+조영제/약물/ 음식/기타(:14941-15140, LISTAGG CRLF — 카테고리는 한글 리터럴), OCM_알러지_List(:15142 — " - " 구분자가 한 번 켜지면 안 꺼지는 스티키 결함 보존), OCM_외출예상시작/종료일시(:15197/:15231 — 같은 행의 양끝, 원문부터 결정적), ETC_건강보험증번호(:15269 — PisInsCod=1 숫자 리터럴 유지), ETC_재해발생일(:15323), ETC_산재지정번호(:15727 — 진료과→병원 HspWrkNum, 문맥 시점), OCM_감염분류_감염정보(:14084), OCM_퇴원분석_수술처치명(:13920 — 후행 개행까지 원문), OCM_병실인실 (:7065, 최신 입실 고정), OCM_병실차액(:7103 — 원문부터 첫 입실 결정적), OCM_VITAL접수일시_LAST(:12996 — 시각만 정렬하는 원문 유지), OCM_진료과_전화번호(:2773 — 진료과 행 재사용, 왕복 0), OCM_전과일시/ 전과일자(:12985/:12990 — 문맥 진료 행). 사유 완결 2종 — OCM_감염정보(:14122 원문 SQL 의 바인드 콜론 누락 → 상시 ORA-00904 → 빈 값. 고치면 없던 값이 생기므로 사유로 안내), OCM_Cosign_의사명(:14812 — 저장 기록 키(EmrKey) 의존, 미리보기 부재). 검증이 잡은 레거시 결함 3건 기록: 감염정보 콜론 누락, 알러지_List 스티키 구분자, 진료기간_진료과 SQL B 의 sDepCod 미바인드(다음 배치). - dotnet test 338/338 · --edit-smoke 실패 0 (이름 검사 185종) - --db-patient ①~㉟ 전건 통과 — ㉟ 신설: 새 조회 12종 실행 확인 (O_OkdInf·O_ImlInf·O_OutAdmInf·S_AlgInf·S_IcnInf·S_DssOpn 전부 존재) - --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
a33229b5e1
commit
7bae9b9057
@@ -2417,6 +2417,62 @@ public static class DbSmoke
|
||||
Check("㉞ 회신 완료 협진이 있는 내원에서는 협진과가 나온다",
|
||||
consults.Count > 0, "O_CstInf 조인이나 CstSttFlg 를 의심할 것");
|
||||
}
|
||||
|
||||
// ㉟ 부가정보 2차분 — 새 자기 SQL 12종이 예외 없이 돈다.
|
||||
// O_ 계열 표(O_OkdInf·O_ImlInf·O_OutAdmInf)가 이 시험 DB 에
|
||||
// 없으면 개별 [진단] 줄로 남기고 판정은 실행 가능한 것만 본다.
|
||||
try
|
||||
{
|
||||
var probes = new (string Probe, Func<string> Run)[]
|
||||
{
|
||||
("알러지", () => extra.Allergy(extraOwner, null).Length.ToString()),
|
||||
("알러지목록", () => extra.AllergyRows(extraOwner).Count.ToString()),
|
||||
("보험증번호", () => extra.InsuranceCertNumber(extraOwner).Length.ToString()),
|
||||
("재해발생일", () => extra.AccidentDate(extraOwner).Length.ToString()),
|
||||
("감염정보", () => extra.InfectionInfo(extraOwner).Length.ToString()),
|
||||
("발병일", () => owner2 is null ? "-" : extra.OnsetDate(extraOwner, owner2.Value).Length.ToString()),
|
||||
("당일식이", () => owner2 is null ? "-" : extra.TodayMeals(owner2.Value).Count.ToString()),
|
||||
("외출예상", () => extra.ExpectedOuting(extraOwner, false).Length.ToString()),
|
||||
("퇴원분석", () => owner2 is null ? "-" : extra.DischargeAnalysisOperations(owner2.Value).Count.ToString()),
|
||||
("병실인실", () => owner2 is null ? "-" : extra.RoomBedCount(owner2.Value).Length.ToString()),
|
||||
("병실차액", () => owner2 is null ? "-" : extra.RoomExtraCharge(owner2.Value).Length.ToString()),
|
||||
("산재지정번호", () => extra.IndustrialAccidentNumber("0506", "20260819").Length.ToString()),
|
||||
};
|
||||
var shapes2 = new List<string>();
|
||||
var failed = new List<string>();
|
||||
foreach (var (probe, run) in probes)
|
||||
{
|
||||
try
|
||||
{
|
||||
shapes2.Add($"{probe} {run()}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var first = ex.Message.Split('\n')[0];
|
||||
// 표 자체가 없는 것(ORA-00942)은 시험 DB 한계다
|
||||
if (first.Contains("ORA-00942"))
|
||||
{
|
||||
shapes2.Add($"{probe} [표없음]");
|
||||
}
|
||||
else
|
||||
{
|
||||
failed.Add($"{probe}: {first}");
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.Add($" 부가정보2: {string.Join(" · ", shapes2)}");
|
||||
foreach (var f in failed)
|
||||
{
|
||||
lines.Add($" [진단] {f}");
|
||||
}
|
||||
Check("㉟ 부가정보 2차분 조회가 예외 없이 돈다", failed.Count == 0,
|
||||
"위 [진단] 줄의 컬럼·조인 이름을 의심할 것");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Check("㉟ 부가정보 2차분 조회가 예외 없이 돈다", false,
|
||||
$"{ex.GetType().Name}: {ex.Message.Split('\n')[0]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -114,6 +114,11 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
["OCM_입원내원일시"] = c => Stamp(Com(c, "ComAcpDtm")),
|
||||
["OCM_입원내원일자"] = c => Hyphenate(Head(Com(c, "ComAcpDtm"), 8)),
|
||||
|
||||
// 전과 일시·일자 — 문맥 진료 행의 시작일시(bzDataInterface.vb:12985/12990).
|
||||
// 레거시는 행 유무 검사 없이 Substring 해 없으면 예외→노란색이다 — 여기는 빈 값.
|
||||
["OCM_전과일시"] = c => Stamp(PatientContext.Value(c.CodInf, "CodStrDtm")),
|
||||
["OCM_전과일자"] = c => Hyphenate(Head(PatientContext.Value(c.CodInf, "CodStrDtm"), 8)),
|
||||
|
||||
// 내원유형 — 입원/응급실/외래(bzDataInterface.vb:2896-2918).
|
||||
// 응급 여부는 내원행의 ComEmgYon 이다. 그 밖의 환자구분은 레거시도 "" 다.
|
||||
["OCM_내원유형"] = c => Com(c, "ComPatTyp") switch
|
||||
@@ -585,6 +590,9 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
return DeptColumn("DepOfcNam", "진료과 마스터에 대외 명칭이 비어 있습니다");
|
||||
case "OCM_진료과약어명칭":
|
||||
return DeptColumn("DepBrfNam", "진료과 마스터에 약어 명칭이 비어 있습니다");
|
||||
case "OCM_진료과_전화번호":
|
||||
// 같은 행의 전화번호 컬럼(:2773-2822) — 시점 판정도 파생 4종과 동일
|
||||
return DeptColumn("DepTelNum", "진료과 마스터에 전화번호가 비어 있습니다");
|
||||
// ── 요양기관 12종 — 같은 조인의 다른 컬럼(bzDataInterface.vb:15656-16709) ──
|
||||
// 접미·결합도 본문 그대로: 병원장 = 병원명+"장", 귀하 = +"장 귀하",
|
||||
// 전화_팩스 = "Tel : … FAX : …", 영문판은 "+82 - " 접두가 붙는다.
|
||||
@@ -965,7 +973,8 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
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_비만도"))
|
||||
or "OCM_표준체중_LAST" or "OCM_조정체중_LAST" or "OCM_비만도"
|
||||
or "OCM_VITAL접수일시_LAST"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -996,6 +1005,22 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
? new TagValue(true, bst)
|
||||
: new TagValue(false, "이 내원에 BST 기록(E_EmdInf_BST)이 없습니다");
|
||||
}
|
||||
case "OCM_VITAL접수일시_LAST":
|
||||
{
|
||||
// 가장 늦은 접수 '시각'(EmrAdpTim, 시분 4자리)을 HH:MM 으로(:12996-13028).
|
||||
// 정렬 키가 시각뿐이라 날짜와 무관하게 하루 중 가장 늦은 시각이 나온다 —
|
||||
// 원문이 그렇다(SQL 의 SUBSTR 은 1-기반).
|
||||
if (vitalSource is null)
|
||||
{
|
||||
return new TagValue(false, "바이탈 조회가 연결되지 않았습니다");
|
||||
}
|
||||
var time = vitalSource(
|
||||
"SUBSTR(EmrAdpTim, 1, 2) || ':' || SUBSTR(EmrAdpTim, 3, 2) AS ADPTIM",
|
||||
"ADPTIM", new[] { "EmrAdpTim" }, "EmrAdpTim DESC");
|
||||
return time.Length > 0
|
||||
? new TagValue(true, time)
|
||||
: new TagValue(false, "이 내원에 접수 시각이 있는 바이탈 기록이 없습니다");
|
||||
}
|
||||
case "OCM_비만도":
|
||||
{
|
||||
// 내원 기준(EmrComNum) — 키·체중을 따로 최신으로 집는다(:12333-12420)
|
||||
@@ -1339,18 +1364,32 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
return new TagValue(true, ((end - start).Days + 1).ToString());
|
||||
}
|
||||
|
||||
/// <summary><see cref="FromExtra"/> 가 담당하는 태그들 — 전부 자기 SQL 이거나 그 조합</summary>
|
||||
private static readonly HashSet<string> ExtraTags = new(StringComparer.Ordinal)
|
||||
{
|
||||
"PAT_실제생년월일", "PAT_국적", "PAT_장애등급", "PAT_건보세대주명",
|
||||
"PAT_건보_급여_세대주명", "PAT_협력업체", "PAT_보호자연락처",
|
||||
"OCM_FollowUp", "OCM_외출외박신청일", "OCM_외출외박종료일",
|
||||
"OCM_최초내원일", "OCM_초진일_발병일", "OCM_신환_초진일",
|
||||
"OCM_입원일자_낮병동", "OCM_혈액형", "OCM_LMP",
|
||||
"OCM_발병일", "OCM_당일식이",
|
||||
"OCM_알러지", "OCM_알러지_조영제", "OCM_알러지_약물", "OCM_알러지_음식", "OCM_알러지_기타",
|
||||
"OCM_알러지_List",
|
||||
"OCM_외출예상시작일시", "OCM_외출예상종료일시",
|
||||
"ETC_건강보험증번호", "ETC_재해발생일", "ETC_산재지정번호",
|
||||
"OCM_감염분류_감염정보", "OCM_감염정보",
|
||||
"OCM_퇴원분석_수술처치명", "OCM_Cosign_의사명",
|
||||
"OCM_병실인실", "OCM_병실차액",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 환자 부가정보 7종 — 태그별 자기 SQL(<see cref="PatientExtraStore"/>).
|
||||
/// 환자 부가정보 — 태그별 자기 SQL(<see cref="PatientExtraStore"/>).
|
||||
/// 전부 지연 + 태그별 캐시다: 이 태그가 없는 서식이 대부분이라 미리 당기지 않고,
|
||||
/// 같은 태그가 한 장에 여러 번 있어도 왕복은 한 번이다.
|
||||
/// </summary>
|
||||
private TagValue? FromExtra(string tag)
|
||||
{
|
||||
if (tag is not ("PAT_실제생년월일" or "PAT_국적" or "PAT_장애등급" or "PAT_건보세대주명"
|
||||
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_LMP"))
|
||||
if (!ExtraTags.Contains(tag))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -1449,6 +1488,156 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
}
|
||||
case "OCM_혈액형":
|
||||
return Wrap(store.BloodType(chtNum), "혈액형 판정(S_BlpInf) 행이 없습니다");
|
||||
case "OCM_발병일":
|
||||
{
|
||||
var comNum = PatientContext.Value(context.ComInf, "ComNum");
|
||||
if (!decimal.TryParse(comNum, out var m))
|
||||
{
|
||||
return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다");
|
||||
}
|
||||
var onset = store.OnsetDate(chtNum, m);
|
||||
return onset.Length >= 8
|
||||
? new TagValue(true, Hyphenate(onset[..8]))
|
||||
: new TagValue(false, "주상병과 짝이 되는 발병 기록(O_OnsInf)이 없습니다");
|
||||
}
|
||||
case "OCM_당일식이":
|
||||
{
|
||||
var comNum = PatientContext.Value(context.ComInf, "ComNum");
|
||||
if (!decimal.TryParse(comNum, out var m))
|
||||
{
|
||||
return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다");
|
||||
}
|
||||
// 끼니별로 "아침 : X " 를 이어붙인다 — 뒤 공백 1칸까지 원문 그대로(:12646-12656)
|
||||
var meals = store.TodayMeals(m);
|
||||
if (meals.Count == 0)
|
||||
{
|
||||
return new TagValue(false, "오늘 등록된 입원 식이(O_ImlInf)가 없습니다");
|
||||
}
|
||||
var text = string.Empty;
|
||||
foreach (var (label, column) in new[]
|
||||
{ ("아침", "아침_MGDTYP"), ("점심", "점심_MGDTYP"), ("저녁", "저녁_MGDTYP") })
|
||||
{
|
||||
var meal = PatientContext.Value(meals, column);
|
||||
if (meal.Length > 0)
|
||||
{
|
||||
text += $"{label} : {meal} ";
|
||||
}
|
||||
}
|
||||
return text.Length > 0
|
||||
? new TagValue(true, text)
|
||||
: new TagValue(false, "오늘 식이 행은 있는데 세 끼니가 전부 비어 있습니다");
|
||||
}
|
||||
case "OCM_알러지":
|
||||
case "OCM_알러지_조영제":
|
||||
case "OCM_알러지_약물":
|
||||
case "OCM_알러지_음식":
|
||||
case "OCM_알러지_기타":
|
||||
{
|
||||
// 카테고리 리터럴은 레거시가 한글로 하드코딩했다(:15018 등)
|
||||
var category = tag switch
|
||||
{
|
||||
"OCM_알러지_조영제" => "조영제",
|
||||
"OCM_알러지_약물" => "약물",
|
||||
"OCM_알러지_음식" => "음식",
|
||||
"OCM_알러지_기타" => "기타",
|
||||
_ => (string?)null,
|
||||
};
|
||||
return Wrap(store.Allergy(chtNum, category),
|
||||
"코드마스터(ALLERGY)에 매칭되는 알러지 기록이 없습니다");
|
||||
}
|
||||
case "OCM_알러지_List":
|
||||
{
|
||||
var rows = store.AllergyRows(chtNum);
|
||||
if (rows.Count == 0)
|
||||
{
|
||||
return new TagValue(false, "알러지 기록(S_AlgInf)이 없습니다");
|
||||
}
|
||||
// 원문 결합(:15160-15178): "(분류) 종류[ - 증상]" 을 CRLF 로.
|
||||
// 구분자 " - " 는 한 번 켜지면 꺼지지 않는다(레거시 스티키 결함) —
|
||||
// 뒤 행의 증상이 비어도 " - " 가 붙는다. 그대로 옮겼다.
|
||||
var separator = string.Empty;
|
||||
var list = new System.Text.StringBuilder();
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var kind = PatientContext.Value(row, "AlgAlgTyp");
|
||||
var symptom = PatientContext.Value(row, "AlgAlgSpm");
|
||||
if (kind.Length > 0 && symptom.Length > 0)
|
||||
{
|
||||
separator = " - ";
|
||||
}
|
||||
if (list.Length > 0)
|
||||
{
|
||||
list.Append("\r\n");
|
||||
}
|
||||
list.Append($"({PatientContext.Value(row, "AlgAlgCtg")}) {kind}{separator}{symptom}");
|
||||
}
|
||||
return new TagValue(true, list.ToString());
|
||||
}
|
||||
case "OCM_외출예상시작일시":
|
||||
case "OCM_외출예상종료일시":
|
||||
return Wrap(store.ExpectedOuting(chtNum, tag == "OCM_외출예상종료일시"),
|
||||
"오늘 이후 시작하는 외출·외박 신청(O_OutAdmInf)이 없습니다");
|
||||
case "ETC_건강보험증번호":
|
||||
return Wrap(store.InsuranceCertNumber(chtNum), "건보 자격(P_PisInf) 행이 없습니다");
|
||||
case "ETC_재해발생일":
|
||||
return Wrap(store.AccidentDate(chtNum), "산재 자격(P_PisInf, PisInsCod=4) 행이 없습니다");
|
||||
case "ETC_산재지정번호":
|
||||
{
|
||||
// 진료과·시점은 문맥의 것 — 레거시 시점 분기(퇴원/현재/접수)와 같은 판정이다
|
||||
var depCod = PatientContext.Value(context.CodInf, "CodDepCod");
|
||||
if (depCod.Length == 0)
|
||||
{
|
||||
return new TagValue(false, "이 내원의 진료과 코드를 읽지 못했습니다");
|
||||
}
|
||||
return Wrap(store.IndustrialAccidentNumber(depCod, context.AdpDtm),
|
||||
"요양기관 행에서 산재지정번호(HspWrkNum)를 찾지 못했습니다");
|
||||
}
|
||||
case "OCM_감염분류_감염정보":
|
||||
return Wrap(store.InfectionInfo(chtNum), "오늘 유효한 감염 기록(S_IcnInf)이 없습니다");
|
||||
case "OCM_감염정보":
|
||||
// 원문 SQL(:14146)의 차트번호 바인드에 콜론이 빠져 있어(IcnChtNum = sChtNum)
|
||||
// 실행마다 ORA-00904 → 오류창 → 빈 값이다 — 고치면 없던 값이 갑자기 생긴다
|
||||
return new TagValue(false,
|
||||
"레거시 결함(바인드 콜론 누락)으로 항상 빈 값인 태그입니다 — OCM_감염분류_감염정보를 쓰세요");
|
||||
case "OCM_퇴원분석_수술처치명":
|
||||
{
|
||||
var comNum = PatientContext.Value(context.ComInf, "ComNum");
|
||||
if (!decimal.TryParse(comNum, out var m))
|
||||
{
|
||||
return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다");
|
||||
}
|
||||
var operations = store.DischargeAnalysisOperations(m);
|
||||
if (operations.Count == 0)
|
||||
{
|
||||
return new TagValue(false, "퇴원분석 수술·처치(S_DssOpn) 행이 없습니다");
|
||||
}
|
||||
// 원문은 AppendLine 결합이라 마지막 줄 뒤에도 개행이 남는다(:13950) — 그대로
|
||||
var text = new System.Text.StringBuilder();
|
||||
foreach (var row in operations)
|
||||
{
|
||||
text.Append(PatientContext.Value(row, "OpnNam")).Append("\r\n");
|
||||
}
|
||||
return new TagValue(true, text.ToString());
|
||||
}
|
||||
case "OCM_Cosign_의사명":
|
||||
// 레거시는 저장된 기록 키(moShtInfoBiz.EmrKey)로 확인자(E_RcdInf)를 찾는다(:14834).
|
||||
// 미리보기는 저장된 기록이 아니라 그 키가 없다 — _Refer 계열과 같은 이유로 완결.
|
||||
return new TagValue(false,
|
||||
"저장된 기록에서만 값이 됩니다 — 미리보기에는 기록 키(EmrKey)가 없습니다");
|
||||
case "OCM_병실인실":
|
||||
{
|
||||
var comNum = PatientContext.Value(context.ComInf, "ComNum");
|
||||
return decimal.TryParse(comNum, out var m)
|
||||
? Wrap(store.RoomBedCount(m), "입실 이력과 병실 마스터가 짝이 되는 행이 없습니다")
|
||||
: new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다");
|
||||
}
|
||||
case "OCM_병실차액":
|
||||
{
|
||||
var comNum = PatientContext.Value(context.ComInf, "ComNum");
|
||||
return decimal.TryParse(comNum, out var m)
|
||||
? Wrap(store.RoomExtraCharge(m), "병실 등급·산정 마스터가 짝이 되는 행이 없습니다")
|
||||
: new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다");
|
||||
}
|
||||
case "OCM_LMP":
|
||||
{
|
||||
var comNum = PatientContext.Value(context.ComInf, "ComNum");
|
||||
@@ -1619,6 +1808,12 @@ public sealed class PatientTagResolver : ITagValueResolver
|
||||
"OCM_입원의사", "OCM_퇴원의사", "OCM_협진과", "OCM_협진과_협진의",
|
||||
"OCM_혈액형", "OCM_LMP", "OCM_BST", "OCM_BST_LAST",
|
||||
"OCM_BMI", "OCM_표준체중_LAST", "OCM_조정체중_LAST", "OCM_비만도",
|
||||
"OCM_VITAL접수일시_LAST", "OCM_발병일", "OCM_당일식이",
|
||||
"OCM_알러지", "OCM_알러지_조영제", "OCM_알러지_약물", "OCM_알러지_음식", "OCM_알러지_기타",
|
||||
"OCM_알러지_List", "OCM_외출예상시작일시", "OCM_외출예상종료일시",
|
||||
"ETC_건강보험증번호", "ETC_재해발생일", "ETC_산재지정번호",
|
||||
"OCM_감염분류_감염정보", "OCM_감염정보", "OCM_퇴원분석_수술처치명", "OCM_Cosign_의사명",
|
||||
"OCM_병실인실", "OCM_병실차액", "OCM_진료과_전화번호", "OCM_전과일시", "OCM_전과일자",
|
||||
"OCM_입원시간", "OCM_입원일시", "OCM_입원일시_영문", "OCM_입실시간",
|
||||
"OCM_의사퇴원예고일시", "OCM_퇴원예정일시", "OCM_퇴원시간", "OCM_퇴원일시",
|
||||
"OCM_퇴원예고일시", "OCM_재원일수", "OCM_퇴원예고재원일수", "OCM_입원일자_낮병동",
|
||||
|
||||
Reference in New Issue
Block a user