수술 8종을 옮긴다 — 그리고 이 시험 DB 에는 O_OprInf 가 없다는 것을 알았다. 51종 → 59종

## GetOprInfDT(bzDataInterface.vb:13203-13363)의 이식

상병과 달리 병원 분기가 없다(Select Case 0회). 바인드는 내원번호 하나 —
접수일자 조건은 원문에서 이미 주석 처리된 죽은 코드다.
SQL 133줄을 원문에서 기계 추출했다(O_OprInf 축, 수술명·부위·상병·집도의·마취의 LISTAGG).

소비 태그 8종이 전부 <b>첫 행에서 컬럼 하나</b>를 집는다:
  OCM_OPNAME · OPRCODNAM · DX · 수술상병(OkdNam) · 수술부위(OprRegionName)
  수술_OprPatETC · 수술집도의(OprDtrNam) · 수술마취의사(AneUidCod)

원문 Catch 는 ErrorMessageBox 를 띄운다 — 조회 함수의 대화상자 부작용은 옮기지 않고
던져서 해석기가 사유로 바꾼다. 수술 조회도 상병과 같은 지연 정책이다
(수술 태그가 없는 서식이 다수라 환자를 붙일 때 무조건 읽지 않는다).

## 검증의 한계를 정확히 적는다

--db-patient ㉘ 이 O_OprInf 에서 내원을 못 찾았고, 처음엔 "행이 없다"로 적었다.
가려 보니 <b>ORA-00942 — 이 시험 DB(SRCH_TEST)에 표 자체가 없다.</b>
즉 수술 태그는 여기서 실행 검증이 불가능하고 운영 DB 에서만 확인할 수 있다.
SKIP 문구를 그 사실대로 고쳤다 — "확인했다"고 적을 뻔한 것을 진단이 막았다.
런타임에서는 해석기가 예외를 잡아 "수술 조회에 실패했습니다"로 말한다.

## 게이트

- dotnet test 338/338 · --edit-smoke 실패 0(이름 검사 59종)
- --db-patient ①~㉗ 통과, ㉘ SKIP(표 부재 명시)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-19 15:22:16 +09:00
co-authored by Claude Opus 5
parent 0da3305a75
commit 460f6c4a91
6 changed files with 293 additions and 3 deletions
@@ -109,6 +109,15 @@ public static class PatientIdentity
: new DiagnosisStore(connection).Diagnoses(context.ComNum, scope);
}
/// <summary>수술 행 공급자 — 상병과 같은 정책(필요할 때 1회, 실패는 던져 사유로)</summary>
public static Func<IReadOnlyList<Dictionary<string, string>>> SurgerySourceFor(PatientContext context)
{
var connection = ConfigService.Current.ConnectionString;
return () => connection.Length == 0
? new List<Dictionary<string, string>>()
: new SurgeryStore(connection).Surgeries(context.ComNum);
}
/// <summary>
/// 이 내원의 진료과 마스터 행 — 못 읽으면 빈 사전.
/// 진료과 코드는 문맥의 CodInf(적용일시로 고른 행)에서, 유효기간 일자는
@@ -68,7 +68,8 @@ public static class PatientSession
PatientIdentity.ResidentNumberOf(picked), PatientIdentity.AddressOf(picked),
PatientIdentity.DoctorOf(picked), PatientIdentity.DepartmentOf(picked),
PatientIdentity.HospitalNameOf(picked),
PatientIdentity.DiagnosisSourceFor(picked))
PatientIdentity.DiagnosisSourceFor(picked),
PatientIdentity.SurgerySourceFor(picked))
: session;
return (tags, new MDataTableRunner(document, Variables()));
}
@@ -174,11 +174,17 @@ public sealed class PatientTagResolver : ITagValueResolver
private readonly Dictionary<DiagnosisStore.Scope, IReadOnlyList<Dictionary<string, string>>> diagnosisCache = new();
/// <summary>수술 행 공급자 — 상병과 같은 이유로 물을 때 한 번만 읽는다</summary>
private readonly Func<IReadOnlyList<Dictionary<string, string>>>? surgerySource;
private IReadOnlyList<Dictionary<string, string>>? surgeryCache;
public PatientTagResolver(PatientContext context, ITagValueResolver next,
string residentNumber = "", IReadOnlyDictionary<string, string>? address = null,
IReadOnlyDictionary<string, string>? doctor = null,
IReadOnlyDictionary<string, string>? department = null, string hospitalName = "",
Func<DiagnosisStore.Scope, IReadOnlyList<Dictionary<string, string>>>? diagnosisSource = null)
Func<DiagnosisStore.Scope, IReadOnlyList<Dictionary<string, string>>>? diagnosisSource = null,
Func<IReadOnlyList<Dictionary<string, string>>>? surgerySource = null)
{
this.context = context;
this.next = next;
@@ -188,6 +194,7 @@ public sealed class PatientTagResolver : ITagValueResolver
this.department = department ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
this.hospitalName = hospitalName;
this.diagnosisSource = diagnosisSource;
this.surgerySource = surgerySource;
}
#endregion
@@ -215,6 +222,10 @@ public sealed class PatientTagResolver : ITagValueResolver
{
return diagnosisValue;
}
if (FromSurgery(tag) is { } surgeryValue)
{
return surgeryValue;
}
if (Computed.TryGetValue(tag, out var compute))
{
var made = compute(context);
@@ -503,6 +514,54 @@ public sealed class PatientTagResolver : ITagValueResolver
: new TagValue(false, "상병 행은 있는데 그 값이 비어 있습니다");
}
/// <summary>
/// 수술 8종 — 해당 태그가 아니면 null. 전부 <b>첫 행에서 컬럼 하나</b>다
/// (bzDataInterface.vb:13099-13202 및 :3109 — 각 함수가 Rows(0).Item(...) 을 집는다).
/// GetOprInfDT 는 병원 분기가 없어 미이식 병원 안내가 필요 없다.
/// </summary>
private TagValue? FromSurgery(string tag)
{
var column = tag switch
{
"OCM_OPNAME" => "OPNAME",
"OCM_OPRCODNAM" => "OPRCODNAM",
"OCM_DX" => "DX",
"OCM_수술상병" => "OkdNam",
"OCM_수술부위" => "OprRegionName",
"OCM_수술_OprPatETC" => "OprPatEtc",
"OCM_수술집도의" => "OprDtrNam",
"OCM_수술마취의사" => "AneUidCod",
_ => null,
};
if (column is null)
{
return null;
}
if (surgerySource is null)
{
return new TagValue(false, "수술 조회가 연결되지 않았습니다");
}
if (surgeryCache is null)
{
try
{
surgeryCache = surgerySource();
}
catch (Exception ex)
{
return new TagValue(false, $"수술 조회에 실패했습니다: {ex.GetType().Name}");
}
}
if (surgeryCache.Count == 0)
{
return new TagValue(false, "이 내원에 등록된 수술이 없습니다");
}
var value = surgeryCache[0].TryGetValue(column, out var v) ? v.Trim() : string.Empty;
return value.Length > 0
? new TagValue(true, value)
: new TagValue(false, $"수술 행은 있는데 {column} 값이 비어 있습니다");
}
/// <summary>주민번호에서 나온 값 — 비면 <b>왜</b> 비었는지 말한다</summary>
private TagValue Wrap(string value) => value.Length > 0
? new TagValue(true, value)
@@ -523,6 +582,8 @@ public sealed class PatientTagResolver : ITagValueResolver
"OCM_상병명_가로", "OCM_상병명_세로", "OCM_상병명_영어_가로", "OCM_상병명_영어_세로",
"OCM_상병코드_가로", "OCM_상병코드_세로", "OCM_부상병코드", "OCM_부상병POA",
"OCM_주상병명", "OCM_주상병코드", "OCM_주상병POA",
"OCM_OPNAME", "OCM_OPRCODNAM", "OCM_DX", "OCM_수술상병", "OCM_수술부위",
"OCM_수술_OprPatETC", "OCM_수술집도의", "OCM_수술마취의사",
};
/// <summary>