diff --git a/src/SheetMe.Data/Stores/PatientContextStore.cs b/src/SheetMe.Data/Stores/PatientContextStore.cs
index 43f1066..eb500fd 100644
--- a/src/SheetMe.Data/Stores/PatientContextStore.cs
+++ b/src/SheetMe.Data/Stores/PatientContextStore.cs
@@ -218,9 +218,12 @@ public sealed class PatientContextStore
}
using var connection = new OracleConnection(connectionString);
connection.Open();
+ // HspStrDte(개원일)는 퇴원 계열 태그의 분기 재료(HspCfg.HspStrDte 대응),
+ // ServerToday 는 "오늘" 폴백·재원일수 계산용 서버 날짜 — 단말 시계를 쓰지 않는다
return Row(connection,
"SELECT * FROM (SELECT HspInsNam, HspOwnNam, HspTelNum, HspFaxNum,"
- + " HspKorAdr, HspEngNam, HspEngAdr, HspRgnNum, HspInsNum FROM M_DepMst"
+ + " HspKorAdr, HspEngNam, HspEngAdr, HspRgnNum, HspInsNum, HspStrDte,"
+ + " TO_CHAR(SYSDATE, 'YYYYMMDD') AS ServerToday FROM M_DepMst"
+ " INNER JOIN M_HspMst ON DepHspCod = HspCod AND :y BETWEEN HspStrDte AND HspEndDte"
+ " WHERE TRIM(DepCod) = :d AND :y BETWEEN DepStrDte AND DepEndDte) WHERE ROWNUM = 1",
("d", code), ("y", date[..8]));
diff --git a/src/SheetMe.Data/Stores/PatientExtraStore.cs b/src/SheetMe.Data/Stores/PatientExtraStore.cs
index 166c6e7..813c3c4 100644
--- a/src/SheetMe.Data/Stores/PatientExtraStore.cs
+++ b/src/SheetMe.Data/Stores/PatientExtraStore.cs
@@ -181,6 +181,21 @@ public sealed class PatientExtraStore
+ " ORDER BY ComAcpDtm) WHERE ROWNUM = 1",
("c", chtNum), ("a", acpDtm), ("p", depCod2));
+ ///
+ /// 낮병동 입원일자(OCM_입원일자_낮병동, :7260) — 이 내원의 접수일시를 포함하는
+ /// 낮병동 신청(P_DcpInf, DcpAcpStt='A') 구간의 시작일. 원문 Rows(0) 은 구간이
+ /// 겹치면 임의 행이었다 — 시작일 최신 행으로 고정한다.
+ ///
+ public string DayCareAdmissionDate(decimal comNum)
+ => Scalar(
+ "SELECT v FROM (SELECT DcpStrDte v FROM P_ComInf"
+ + " INNER JOIN P_DcpInf ON ComChtNum = DcpChtNum"
+ + " AND ComAcpDtm BETWEEN DcpStrDte AND DcpEndDte || '2359'"
+ + " AND DcpAcpStt = 'A'"
+ + " WHERE ComNum = :m AND ComAcpStt <> 'OC' AND ComDayCar = 'Y'"
+ + " ORDER BY DcpStrDte DESC) WHERE ROWNUM = 1",
+ ("m", comNum));
+
/// 스칼라 1칸 — 0행이거나 NULL 이면 빈 문자열. 잔여 공백은 걷는다(문맥 행과 같은 규칙)
private string Scalar(string sql, params (string Name, object Value)[] binds)
{
diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs
index bcf751a..581c32f 100644
--- a/src/SheetMe.Designer/Services/PatientTagResolver.cs
+++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs
@@ -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, "수술일자 값이 형식에 맞지 않습니다");
}
+ ///
+ /// 입·퇴원 일시와 재원일수 12종(bzDataInterface.vb:7152-7940, 전부 Case Else 갈래).
+ ///
+ /// MsgBox 갈래는 옮기지 않는다. 레거시는 퇴원 전 환자에서 "퇴원일자를 현재로
+ /// 설정하시겠습니까?"를 조회 도중에 묻는다 — 조회 함수의 대화 상자는 부작용이므로
+ /// 사유로 말한다. 다만 개원일(HspStrDte) 분기로 그 질문 자체를 건너뛰는 병원이
+ /// 있어(:7573 — 2020-09-01 이후 개원이면 무조건 "") 그 분기는 그대로 옮겼다.
+ /// 재원일수의 HspCod >= "KBJY" 는 등호가 아니라 크기비교(레거시 오타로
+ /// 보인다) — 그 갈래도 MsgBox 라 사유로 흡수되므로 따로 흉내내지 않는다.
+ ///
+ 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);
+ }
+
+ /// YYYYMMDDHHMM 의 시각만 — "HH:MM". 12자리가 안 되면 빈 값
+ private static string Time4(string value)
+ => value.Length >= 12 ? $"{value[8..10]}:{value[10..12]}" : string.Empty;
+
+ /// YYYYMMDDHHMM → "YYYY/MM/DD HH:MM". 12자리가 안 되면 빈 값
+ 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;
+
+ ///
+ /// 재원일수 — (끝 날짜 - 시작 날짜) + 1 (레거시 DateDiff(Day)+1).
+ /// 날짜를 못 읽으면 레거시는 예외→"" 였다 — 여기서는 사유로 말한다.
+ ///
+ 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());
+ }
+
///
/// 환자 부가정보 7종 — 태그별 자기 SQL().
/// 전부 지연 + 태그별 캐시다: 이 태그가 없는 서식이 대부분이라 미리 당기지 않고,
@@ -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_요양기관명칭_병원명",