입·퇴원 일시와 재원일수 13종 — 131종 → 144종

FromStay 12종(bzDataInterface.vb:7152-7940, 전부 Case Else 갈래):
입원시간(HH:MM)·입원일시(이름과 달리 날짜만 — SJRCH·BSYD 만 시각
포함)·입원일시_영문·입실시간(ComTrsDtm 슬래시 12자)·의사퇴원예고일시
(ComSchLev)·퇴원예정일시(ComLevSch)·퇴원시간·퇴원일시·퇴원예고일시
(ILV→ComLevDtm / ILP→ComLevSch)·재원일수·퇴원예고재원일수
(DateDiff(Day)+1). FromExtra 에 입원일자_낮병동(:7260 — P_DcpInf
구간 매칭, 시작일 최신 행 고정).

MsgBox 갈래("퇴원일자를 현재로 설정하시겠습니까?")는 조회 중 대화
상자라 옮기지 않고 사유로 말한다. 그 질문을 건너뛰는 개원일 분기
(HspStrDte>=20200901 → 빈, 외래 <20190501 → 오늘)는 그대로 옮겼다 —
개원일과 서버 오늘을 요양기관 행에 컬럼 2개로 추가(HspStrDte,
ServerToday). 재원일수의 HspCod >= "KBJY" 는 등호가 아닌 크기비교
(레거시 오타로 보임) — MsgBox 갈래라 사유로 흡수된다.

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-19 16:56:54 +09:00
co-authored by Claude Fable 5
parent ec47c98bdd
commit 975aff4211
3 changed files with 154 additions and 2 deletions
@@ -315,6 +315,10 @@ public sealed class PatientTagResolver : ITagValueResolver
{
return extraValue;
}
if (FromStay(tag) is { } stayValue)
{
return stayValue;
}
if (Computed.TryGetValue(tag, out var compute))
{
var made = compute(context);
@@ -891,6 +895,120 @@ public sealed class PatientTagResolver : ITagValueResolver
: new TagValue(false, "수술일자 값이 형식에 맞지 않습니다");
}
/// <summary>
/// 입·퇴원 일시와 재원일수 12종(bzDataInterface.vb:7152-7940, 전부 Case Else 갈래).
///
/// <b>MsgBox 갈래는 옮기지 않는다.</b> 레거시는 퇴원 전 환자에서 "퇴원일자를 현재로
/// 설정하시겠습니까?"를 조회 도중에 묻는다 — 조회 함수의 대화 상자는 부작용이므로
/// 사유로 말한다. 다만 개원일(HspStrDte) 분기로 그 질문 자체를 건너뛰는 병원이
/// 있어(:7573 — 2020-09-01 이후 개원이면 무조건 "") 그 분기는 그대로 옮겼다.
/// 재원일수의 <c>HspCod >= "KBJY"</c> 는 등호가 아니라 <b>크기비교</b>(레거시 오타로
/// 보인다) — 그 갈래도 MsgBox 라 사유로 흡수되므로 따로 흉내내지 않는다.
/// </summary>
private TagValue? FromStay(string tag)
{
if (tag is not ("OCM_입원시간" or "OCM_입원일시" or "OCM_입원일시_영문" or "OCM_입실시간"
or "OCM_의사퇴원예고일시" or "OCM_퇴원예정일시" or "OCM_퇴원시간" or "OCM_퇴원일시"
or "OCM_퇴원예고일시" or "OCM_재원일수" or "OCM_퇴원예고재원일수"))
{
return null;
}
var inpatient = Com(context, "ComPatTyp") == "I";
var acp = Com(context, "ComAcpDtm");
var prg = Com(context, "ComPrgStt");
var lev = Com(context, "ComLevDtm");
var sch = Com(context, "ComLevSch");
var openDate = PatientContext.Value(hospital, "HspStrDte");
var today = PatientContext.Value(hospital, "ServerToday");
// 레거시가 사용자에게 묻는 자리(:7580 등) — 자동 값을 만들지 않는다
static TagValue Ask() => new(false,
"레거시는 퇴원 전 환자에서 이 값을 사용자에게 물어 정합니다 — 자동으로 만들지 않습니다");
// 개원일·서버 오늘이 필요한 갈래에서 요양기관 행이 없으면 판정을 못 한다
TagValue? NeedFacility() => openDate.Length == 0 || today.Length == 0
? new TagValue(false, "요양기관 행(개원일)을 읽지 못해 퇴원 전 분기를 판정할 수 없습니다")
: null;
switch (tag)
{
case "OCM_입원시간":
return Ok(inpatient ? Time4(acp) : string.Empty);
case "OCM_입원일시":
// 이름과 달리 날짜만 나온다(:7203 — SJRCH·BSYD 만 시각 포함, 여기는 Else)
return Ok(inpatient ? Hyphenate(Head(acp, 8)) : string.Empty);
case "OCM_입원일시_영문":
return Ok(inpatient ? Reorder(Head(acp, 8), 4, 6, 0) : string.Empty);
case "OCM_입실시간":
return Ok(SlashStamp(Com(context, "ComTrsDtm")));
case "OCM_의사퇴원예고일시":
return Ok(inpatient && Com(context, "ComSchLev").Length > 0
? Stamp(Com(context, "ComSchLev")) : string.Empty);
case "OCM_퇴원예정일시":
return Ok(inpatient && sch.Length > 0 ? Stamp(sch) : string.Empty);
case "OCM_퇴원시간":
return prg == "ILV" ? Ok(Time4(lev))
: inpatient ? Ask()
: Ok(string.Empty);
case "OCM_퇴원일시":
return prg == "ILV" ? Ok(Hyphenate(Head(lev, 8)))
: inpatient
? NeedFacility() ?? (string.CompareOrdinal(openDate, "20200901") >= 0
? Ok(string.Empty) : Ask())
: NeedFacility() ?? Ok(string.CompareOrdinal(openDate, "20190501") < 0
? Hyphenate(today) : string.Empty);
case "OCM_퇴원예고일시":
return prg == "ILV" ? Ok(Hyphenate(Head(lev, 8)))
: prg == "ILP" ? Ok(Hyphenate(Head(sch, 8)))
: inpatient
? NeedFacility() ?? (string.CompareOrdinal(openDate, "20200901") >= 0
? Ok(string.Empty) : Ask())
: NeedFacility() ?? Ok(string.CompareOrdinal(openDate, "20190501") < 0
? Hyphenate(today) : string.Empty);
case "OCM_재원일수":
return prg == "ILV" ? StayDays(acp, lev)
: inpatient
? NeedFacility() ?? (string.CompareOrdinal(openDate, "20200901") >= 0
? StayDays(acp, today) : Ask())
: NeedFacility() ?? StayDays(acp, today);
default: // OCM_퇴원예고재원일수
return prg == "ILV" ? StayDays(acp, lev)
: prg == "ILP" ? StayDays(acp, sch)
: inpatient
? NeedFacility() ?? (string.CompareOrdinal(openDate, "20200901") >= 0
? Ok(string.Empty) : Ask())
: NeedFacility() ?? StayDays(acp, today);
}
static TagValue Ok(string value) => new(true, value);
}
/// <summary>YYYYMMDDHHMM 의 시각만 — "HH:MM". 12자리가 안 되면 빈 값</summary>
private static string Time4(string value)
=> value.Length >= 12 ? $"{value[8..10]}:{value[10..12]}" : string.Empty;
/// <summary>YYYYMMDDHHMM → "YYYY/MM/DD HH:MM". 12자리가 안 되면 빈 값</summary>
private static string SlashStamp(string value)
=> value.Length >= 12
? $"{value[..4]}/{value[4..6]}/{value[6..8]} {value[8..10]}:{value[10..12]}"
: string.Empty;
/// <summary>
/// 재원일수 — (끝 날짜 - 시작 날짜) + 1 (레거시 DateDiff(Day)+1).
/// 날짜를 못 읽으면 레거시는 예외→"" 였다 — 여기서는 사유로 말한다.
/// </summary>
private static TagValue StayDays(string from, string to)
{
if (from.Length < 8 || to.Length < 8
|| !DateTime.TryParseExact(from[..8], "yyyyMMdd", null,
System.Globalization.DateTimeStyles.None, out var start)
|| !DateTime.TryParseExact(to[..8], "yyyyMMdd", null,
System.Globalization.DateTimeStyles.None, out var end))
{
return new TagValue(false, "재원일수를 만들 날짜를 읽지 못했습니다");
}
return new TagValue(true, ((end - start).Days + 1).ToString());
}
/// <summary>
/// 환자 부가정보 7종 — 태그별 자기 SQL(<see cref="PatientExtraStore"/>).
/// 전부 지연 + 태그별 캐시다: 이 태그가 없는 서식이 대부분이라 미리 당기지 않고,
@@ -901,7 +1019,8 @@ public sealed class PatientTagResolver : ITagValueResolver
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_신환_초진일"
or "OCM_입원일자_낮병동"))
{
return null;
}
@@ -986,6 +1105,18 @@ public sealed class PatientTagResolver : ITagValueResolver
}
case "PAT_보호자연락처":
return Wrap(store.GuardianPhone(chtNum), "부가정보에 보호자연락처가 없습니다");
case "OCM_입원일자_낮병동":
{
var comNum = PatientContext.Value(context.ComInf, "ComNum");
if (!decimal.TryParse(comNum, out var m))
{
return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다");
}
var day = store.DayCareAdmissionDate(m);
return day.Length >= 8
? new TagValue(true, Hyphenate(day[..8]))
: new TagValue(false, "낮병동 신청(P_DcpInf) 구간이 이 내원을 포함하지 않습니다");
}
case "OCM_FollowUp":
{
// 지금보다 뒤의 내원이 없으면 레거시도 "" 다(:1055) — 사유로 말한다
@@ -1144,6 +1275,9 @@ public sealed class PatientTagResolver : ITagValueResolver
"PAT_건보_급여_세대주명", "PAT_협력업체", "PAT_보호자연락처",
"OCM_FollowUp", "OCM_외출외박신청일", "OCM_외출외박종료일",
"OCM_최초내원일", "OCM_초진일_발병일", "OCM_신환_초진일",
"OCM_입원시간", "OCM_입원일시", "OCM_입원일시_영문", "OCM_입실시간",
"OCM_의사퇴원예고일시", "OCM_퇴원예정일시", "OCM_퇴원시간", "OCM_퇴원일시",
"OCM_퇴원예고일시", "OCM_재원일수", "OCM_퇴원예고재원일수", "OCM_입원일자_낮병동",
"PAT_우편번호", "PAT_도로명주소", "PAT_지번주소", "PAT_영문도로명주소",
"OCM_담당의사", "OCM_담당의사_담당의사코드", "OCM_담당의사_연락처",
"OCM_진료과", "ETC_요양기관명칭_병원명",