수술 일정 6종을 옮긴다 — 100종 → 106종

S_OprInf(수술 접수 일정 — GetOprInfDT 의 E_OprInf 축 큰 조회와 다른 표) 를
OprComNum + OprStt='E' 로 거르고 OprKey 순으로 한 행:

- ETC_수술일자_1_몇년/_1_몇년2자리/_2_몇월/_3_몇일 — DESC(마지막 수술, '수술실 요청' 주석 그대로), OprDte 자름
- OCM_수술일자_마지막수술 — DESC, "9999-99-99" 형식
- OCM_수술일자 — Else 갈래는 ASC(첫 수술). PURME(주사오더 O_OdrInf 조회)·
  BSGH·HIMCHAN_*·GJHNSS(DESC) 분기는 미이식 안내 — Else 로 뭉개면
  그 병원에서 첫/마지막이 뒤바뀐 날짜가 조용히 찍힌다.

## 게이트

- dotnet test 338/338 · --edit-smoke 실패 0(이름 검사 106종)
- --db-patient 전건 통과 · --db-render P062 md5 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-19 16:07:27 +09:00
co-authored by Claude Opus 5
parent e065c5a645
commit 4dad271e9c
5 changed files with 103 additions and 3 deletions
@@ -226,6 +226,22 @@ public sealed class PatientContextStore
("d", code), ("y", date[..8]));
}
/// <summary>
/// 수술 일정(S_OprInf) 한 행 — <c>OCM_수술일자</c>·<c>ETC_수술일자_*</c> 계열이 쓴다.
/// <c>WHERE OprComNum = :c AND OprStt = 'E' ORDER BY OprKey</c> —
/// 무인자(Else 갈래)는 ASC(첫 수술), ETC_ 계열과 마지막수술은 DESC('수술실 요청' 주석 그대로).
/// GetOprInfDT(E_OprInf 축 큰 조회)와 <b>다른 표</b>다 — 이쪽은 수술 접수 일정이다.
/// </summary>
public IReadOnlyDictionary<string, string> ScheduledSurgery(decimal comNum, bool latestFirst)
{
using var connection = new OracleConnection(connectionString);
connection.Open();
return Row(connection,
"SELECT * FROM (SELECT * FROM S_OprInf WHERE OprComNum = :c AND OprStt = 'E'"
+ $" ORDER BY OprKey{(latestFirst ? " DESC" : string.Empty)}) WHERE ROWNUM = 1",
("c", comNum));
}
/// <summary>
/// 건물관리번호로 주소 — 레거시 <c>GetZipInfoByBuildingNumber</c>(clsCommonLib.vb:16600).
///
@@ -161,6 +161,15 @@ public static class PatientIdentity
: new VitalStore(connection).One(context.ComNum, expr, pick, notNull, order);
}
/// <summary>수술 일정(S_OprInf) 공급자 — 최신순 여부별 1회</summary>
public static Func<bool, IReadOnlyDictionary<string, string>> ScheduledSurgerySourceFor(PatientContext context)
{
var connection = ConfigService.Current.ConnectionString;
return latest => connection.Length == 0
? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
: new PatientContextStore(connection).ScheduledSurgery(context.ComNum, latest);
}
/// <summary>수술 행 공급자 — 상병과 같은 정책(필요할 때 1회, 실패는 던져 사유로)</summary>
public static Func<IReadOnlyList<Dictionary<string, string>>> SurgerySourceFor(PatientContext context)
{
@@ -72,7 +72,8 @@ public static class PatientSession
PatientIdentity.DiagnosisSourceFor(picked),
PatientIdentity.SurgerySourceFor(picked),
PatientIdentity.ReviewDiagnosisSourceFor(picked),
PatientIdentity.VitalSourceFor(picked))
PatientIdentity.VitalSourceFor(picked),
PatientIdentity.ScheduledSurgerySourceFor(picked))
: session;
return (tags, new MDataTableRunner(document, Variables()));
}
@@ -190,6 +190,11 @@ public sealed class PatientTagResolver : ITagValueResolver
private readonly Dictionary<string, string> vitalCache = new(StringComparer.Ordinal);
/// <summary>수술 일정 공급자 — (최신순?) 별 1회 캐시</summary>
private readonly Func<bool, IReadOnlyDictionary<string, string>>? scheduledSurgerySource;
private readonly Dictionary<bool, IReadOnlyDictionary<string, string>> scheduledSurgeryCache = new();
private readonly Dictionary<DiagnosisStore.Scope, IReadOnlyList<Dictionary<string, string>>> reviewCache = new();
public PatientTagResolver(PatientContext context, ITagValueResolver next,
@@ -201,7 +206,8 @@ public sealed class PatientTagResolver : ITagValueResolver
Func<DiagnosisStore.Scope, IReadOnlyList<Dictionary<string, string>>>? diagnosisSource = null,
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<string, string, string[], string, string>? vitalSource = null,
Func<bool, IReadOnlyDictionary<string, string>>? scheduledSurgerySource = null)
{
this.context = context;
this.next = next;
@@ -215,6 +221,7 @@ public sealed class PatientTagResolver : ITagValueResolver
this.surgerySource = surgerySource;
this.reviewSource = reviewSource;
this.vitalSource = vitalSource;
this.scheduledSurgerySource = scheduledSurgerySource;
}
#endregion
@@ -254,6 +261,10 @@ public sealed class PatientTagResolver : ITagValueResolver
{
return vitalValue;
}
if (FromScheduledSurgery(tag) is { } scheduleValue)
{
return scheduleValue;
}
if (Computed.TryGetValue(tag, out var compute))
{
var made = compute(context);
@@ -745,6 +756,66 @@ public sealed class PatientTagResolver : ITagValueResolver
new[] { "EmrAdpTim" }, "EmrAdpTim DESC"),
};
/// <summary>
/// 수술 일정(S_OprInf) 6종 — 해당 태그가 아니면 null.
/// ETC_수술일자_* 와 마지막수술은 <b>DESC(마지막)</b>, OCM_수술일자는 Else 갈래가 ASC(첫).
/// OCM_수술일자의 병원 분기(PURME 주사오더 / BSGH·HIMCHAN_*·GJHNSS DESC)는 미이식 안내.
/// </summary>
private TagValue? FromScheduledSurgery(string tag)
{
var latest = tag switch
{
"ETC_수술일자_1_몇년" or "ETC_수술일자_1_몇년2자리"
or "ETC_수술일자_2_몇월" or "ETC_수술일자_3_몇일"
or "OCM_수술일자_마지막수술" => true,
"OCM_수술일자" => false,
_ => (bool?)null,
} is { } l ? l : (bool?)null;
if (latest is null)
{
return null;
}
var hsp = UserSession.Current.HspCod;
if (tag == "OCM_수술일자"
&& (hsp is "PURME" or "BSGH" or "GJHNSS"
|| hsp.StartsWith("HIMCHAN_", StringComparison.Ordinal)))
{
return new TagValue(false, $"이 병원({hsp})의 수술일자 분기는 아직 옮기지 않았습니다");
}
if (scheduledSurgerySource is null)
{
return new TagValue(false, "수술 일정 조회가 연결되지 않았습니다");
}
if (!scheduledSurgeryCache.TryGetValue(latest.Value, out var row))
{
try
{
row = scheduledSurgerySource(latest.Value);
}
catch (Exception ex)
{
return new TagValue(false, $"수술 일정 조회에 실패했습니다: {ex.GetType().Name}");
}
scheduledSurgeryCache[latest.Value] = row;
}
var oprDte = row.TryGetValue("OprDte", out var d) ? d.Trim() : string.Empty;
if (row.Count == 0 || oprDte.Length == 0)
{
return new TagValue(false, "이 내원에 수술 일정이 없습니다");
}
var value = tag switch
{
"ETC_수술일자_1_몇년" => Head(oprDte, 4),
"ETC_수술일자_1_몇년2자리" => oprDte.Length >= 4 ? oprDte.Substring(2, 2) : string.Empty,
"ETC_수술일자_2_몇월" => oprDte.Length >= 6 ? oprDte.Substring(4, 2) : string.Empty,
"ETC_수술일자_3_몇일" => oprDte.Length >= 8 ? oprDte.Substring(6, 2) : string.Empty,
_ => Hyphenate(Head(oprDte, 8)),
};
return value.Length > 0
? new TagValue(true, value)
: new TagValue(false, "수술일자 값이 형식에 맞지 않습니다");
}
private TagValue? FromVital(string tag)
{
// 머리둘레 두 종은 레거시 결함이다 — SELECT 는 EmdHc AS HC 인데 Item("BP") 를 읽어
@@ -813,6 +884,8 @@ public sealed class PatientTagResolver : ITagValueResolver
"OCM_맥박", "OCM_맥박_LAST", "OCM_호흡", "OCM_호흡_LAST", "OCM_SPO2", "OCM_SPO2_LAST",
"OCM_혈압", "OCM_혈압_LAST", "OCM_혈압_BPS", "OCM_혈압_BPD", "OCM_VITAL접수일시_LAST",
"OCM_머리둘레", "OCM_머리둘레_LAST",
"OCM_수술일자", "OCM_수술일자_마지막수술", "ETC_수술일자_1_몇년",
"ETC_수술일자_1_몇년2자리", "ETC_수술일자_2_몇월", "ETC_수술일자_3_몇일",
};
/// <summary>
@@ -229,7 +229,8 @@ public partial class PreviewWindow : Window
PatientIdentity.DiagnosisSourceFor(picked),
PatientIdentity.SurgerySourceFor(picked),
PatientIdentity.ReviewDiagnosisSourceFor(picked),
PatientIdentity.VitalSourceFor(picked));
PatientIdentity.VitalSourceFor(picked),
PatientIdentity.ScheduledSurgerySourceFor(picked));
// 러너를 <b>새로</b> 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아
// 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다.
fields = new MDataTableRunner(designer.Document,