diff --git a/src/SheetMe.Data/Stores/PatientExtraStore.cs b/src/SheetMe.Data/Stores/PatientExtraStore.cs
index 3d0a220..dcaec54 100644
--- a/src/SheetMe.Data/Stores/PatientExtraStore.cs
+++ b/src/SheetMe.Data/Stores/PatientExtraStore.cs
@@ -704,6 +704,43 @@ public sealed class PatientExtraStore
"SELECT UDF_GETAGE(PatBthDay, :a) FROM P_PatInf WHERE PatChtNum = RPAD(:c, 10)",
("a", day8), ("c", chtNum));
+ ///
+ /// 수술 집도의의 진료과/그룹진료과 이름(OCM_수술집도의_진료과 :3157 · _그룹진료과 :3194) —
+ /// 수술 신청의 집도의(S_OprTyp OprTyp='DTRUIDCOD')를 오늘 기준 사용자·부서 마스터로 푼다.
+ /// 원문 특이점 그대로: OprStt 필터가 없고(취소 수술 포함), 유효기간이 수술일이 아니라
+ /// 오늘 기준이다. 최신 수술(OprKey DESC) 첫 행으로 접는다(원문 Rows(0)).
+ ///
+ public string SurgeonDepartment(decimal comNum, bool groupName)
+ => Scalar(
+ "SELECT v FROM (SELECT "
+ + (groupName ? "DtlCodNam" : "DepKorNam") + " v FROM S_OprInf"
+ + " INNER JOIN S_OprTyp ON OprKey = OprKeyNum AND OprComNum = :m AND OprTyp = 'DTRUIDCOD'"
+ + " INNER JOIN M_UidMst ON UidCod = OprCod"
+ + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN UidStrDte AND UidEndDte"
+ + " INNER JOIN M_DepMst ON DepCod = UidDepCod"
+ + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN DepStrDte AND DepEndDte"
+ + (groupName
+ ? " INNER JOIN M_DtlMst ON DepGrpCod = DtlCod AND DtlTblCod = 'GDPTBL'"
+ + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN DtlStrDte AND DtlExpDte"
+ : string.Empty)
+ + " ORDER BY OprKey DESC) WHERE ROWNUM = 1",
+ ("m", comNum));
+
+ ///
+ /// 수술명 목록(OCM_수술명칭, :3520 Case Else) — 확정 수술의 수술코드명(OprTyp='OPRCOD')
+ /// 을 수술일 순으로. GNBEDRO·HIMCHAN_BP 의 OPNAME 포함 분기는 미이식.
+ /// 결합(첫 행 뒤 공백, 이후 CRLF)은 해석기가 원문대로 한다.
+ ///
+ public IReadOnlyList SurgeryNames(decimal comNum)
+ => Rows(
+ "SELECT OprCodNam FROM S_OprInf"
+ + " INNER JOIN S_OprTyp ON OprKeyNum = OprKey"
+ + " WHERE OprComNum = :m AND OprStt = 'E' AND OprTyp = 'OPRCOD'"
+ + " ORDER BY OprDte",
+ ("m", comNum))
+ .Select(r => r.TryGetValue("OprCodNam", out var v) ? v : string.Empty)
+ .ToArray();
+
/// 스칼라 1칸 — 0행이거나 NULL 이면 빈 문자열. 잔여 공백은 걷는다(문맥 행과 같은 규칙)
private string Scalar(string sql, params (string Name, object Value)[] binds)
{
diff --git a/src/SheetMe.Data/Stores/SurgeryStore.cs b/src/SheetMe.Data/Stores/SurgeryStore.cs
index cff38b6..e629873 100644
--- a/src/SheetMe.Data/Stores/SurgeryStore.cs
+++ b/src/SheetMe.Data/Stores/SurgeryStore.cs
@@ -8,7 +8,7 @@ namespace SheetMe.Data.Stores;
/// 상병과 달리 병원 분기가 없다(Select Case 0회). 바인드는 내원번호 하나뿐이고
/// 접수일자 조건은 원문에서 이미 주석 처리되어 있다(:sAcpDte — 죽은 코드).
///
-/// SQL 은 원문에서 기계 추출했다(133줄 그대로). O_OprInf 를 축으로 수술명·부위·상병·
+/// SQL 은 원문에서 기계 추출했다(133줄 그대로). S_OprInf(수술 신청)를 축으로 수술명·부위·상병·
/// 집도의·마취의를 LISTAGG 로 접는 큰 조회이고, 소비 태그 8종이 전부
/// 첫 행에서 컬럼 하나를 집는다 — 그래서 행 목록만 돌려주고 선택은 해석기가 한다.
///
@@ -165,12 +165,31 @@ ORDER BY OprComNum, OprDte DESC, OprDayNum ASC
#region Methods
/// 이 내원의 수술 행들 — 없으면 빈 목록
public List> Surgeries(decimal comNum)
+ => Surgeries(comNum, mainDiagnosisOnly: false);
+
+ ///
+ /// 가 true 면 상병 집계를 주상병(OPRVAR1='0')로
+ /// 한정한다 — 레거시 GetOprInfDT_OKDMain(bzDataInterface.vb:13364)과
+ /// GetOprInfDT 의 유일한 실질 차이가 이 조인 조건 한 줄이다
+ /// (OKDMain 판의 수술부위·비고 컬럼 부재와 정렬 약화는 옮기지 않는다 —
+ /// 그 판으로 부위 태그를 부르면 레거시도 컬럼 예외였다).
+ ///
+ public List> Surgeries(decimal comNum, bool mainDiagnosisOnly)
{
using var connection = new OracleConnection(connectionString);
connection.Open();
using var command = connection.CreateCommand();
command.BindByName = true;
- command.CommandText = Query;
+ var sql = mainDiagnosisOnly
+ ? Query.Replace("AND OKDNAM.OprTyp = 'OKD'",
+ "AND OKDNAM.OprTyp = 'OKD' AND OKDNAM.OPRVAR1 = '0'")
+ : Query;
+ if (mainDiagnosisOnly && !sql.Contains("OPRVAR1", StringComparison.Ordinal))
+ {
+ // 치환이 빗나가면 주상병 필터 없는 값이 조용히 나간다 — 빈 값보다 나쁘다
+ throw new InvalidOperationException("주상병 필터 치환이 원문과 맞지 않습니다");
+ }
+ command.CommandText = sql;
command.Parameters.Add(new OracleParameter("iComNum", comNum));
var rows = new List>();
diff --git a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs
index 4470512..25185a1 100644
--- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs
+++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs
@@ -2242,18 +2242,21 @@ public static class DbSmoke
}
// ㉘ 수술 — 원문 기계 추출 SQL 이 실제로 돈다.
- var oprOwner = FirstComNum(conn, "O_OprInf", "OprComNum", "OprComNum");
+ // 처음엔 O_OprInf 로 소유자를 찾아 늘 SKIP 이었다 — 실제 조회
+ // (GetOprInfDT)의 축은 S_OprInf(수술 신청)다. 표를 바로잡으니
+ // 이 시험 DB 에서도 수술 태그가 실검증된다.
+ var oprOwner = FirstComNum(conn, "S_OprInf", "OprComNum", "OprComNum");
if (oprOwner is null)
{
// "행이 없다"와 "표가 없다/조회가 죽었다"를 가른다 —
// FirstComNum 은 예외를 삼키므로 이 줄 없이는 못 가른다.
foreach (var line in ExplainPeriod(conn, 0,
- "SELECT COUNT(*) OPRCNT FROM O_OprInf WHERE :k = 0"))
+ "SELECT COUNT(*) OPRCNT FROM S_OprInf WHERE :k = 0"))
{
- lines.Add($" [진단] O_OprInf: {line}");
+ lines.Add($" [진단] S_OprInf: {line}");
}
lines.Add("SKIP ㉘ 수술 — 확인 불가. 위 [진단] 줄이 원인이다:"
- + " ORA-00942 면 이 시험 DB 에 O_OprInf 가 아예 없는 것이고,"
+ + " ORA-00942 면 이 시험 DB 에 S_OprInf 가 아예 없는 것이고,"
+ " 수술 태그는 운영 DB 에서만 검증할 수 있다");
}
else
diff --git a/src/SheetMe.Designer/Services/PatientTagResolver.cs b/src/SheetMe.Designer/Services/PatientTagResolver.cs
index 9df558e..cad9c66 100644
--- a/src/SheetMe.Designer/Services/PatientTagResolver.cs
+++ b/src/SheetMe.Designer/Services/PatientTagResolver.cs
@@ -1716,6 +1716,8 @@ public sealed class PatientTagResolver : ITagValueResolver
"ETC_승인의사", "OCM_수술소독간호사",
"OCM_적용진료과", "OCM_적용진료의사", "OCM_전과", "OCM_간호정보조사지_과거력",
"OCM_영상촬영이력",
+ "OCM_수술주상병", "OCM_수술집도의_진료과", "OCM_수술집도의_그룹진료과",
+ "OCM_수술명칭", "OCM_수술진단명", "OCM_수술처치",
};
///
@@ -2246,6 +2248,66 @@ public sealed class PatientTagResolver : ITagValueResolver
}
return new TagValue(true, text.ToString());
}
+ case "OCM_수술주상병":
+ {
+ // GetOprInfDT_OKDMain(:13364) — 상병 집계를 주상병(OPRVAR1='0')로
+ // 한정한 수술 조회의 첫 행 OkdNam. 이 시험 DB 엔 O_OprInf 계열이
+ // 없어 운영 DB 에서만 값 검증이 된다(수술 8종과 동일).
+ var comNum = PatientContext.Value(context.ComInf, "ComNum");
+ if (!decimal.TryParse(comNum, out var m))
+ {
+ return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다");
+ }
+ if (contextStoreSource is null)
+ {
+ return new TagValue(false, "수술 조회가 연결되지 않았습니다");
+ }
+ var rows = new SurgeryStore(
+ Services.ConfigService.Current.ConnectionString)
+ .Surgeries(m, mainDiagnosisOnly: true);
+ if (rows.Count == 0)
+ {
+ return new TagValue(false, "확정(OprStt='E')된 수술 신청이 없습니다");
+ }
+ var okd = rows[0].TryGetValue("OKDNAM", out var v) ? v : string.Empty;
+ return okd.Length > 0
+ ? new TagValue(true, okd)
+ : new TagValue(false, "수술 행에 주상병(OPRVAR1='0') 표기가 없습니다");
+ }
+ case "OCM_수술집도의_진료과":
+ case "OCM_수술집도의_그룹진료과":
+ {
+ var comNum = PatientContext.Value(context.ComInf, "ComNum");
+ if (!decimal.TryParse(comNum, out var m))
+ {
+ return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다");
+ }
+ return Wrap(store.SurgeonDepartment(m, tag == "OCM_수술집도의_그룹진료과"),
+ "집도의(DTRUIDCOD)와 오늘 기준 부서가 짝이 되는 수술이 없습니다");
+ }
+ case "OCM_수술명칭":
+ {
+ var comNum = PatientContext.Value(context.ComInf, "ComNum");
+ if (!decimal.TryParse(comNum, out var m))
+ {
+ return new TagValue(false, "이 내원의 내원번호를 읽지 못했습니다");
+ }
+ var names = store.SurgeryNames(m);
+ if (names.Count == 0)
+ {
+ return new TagValue(false, "확정 수술의 수술코드명(OPRCOD)이 없습니다");
+ }
+ // 원문 결합(:3546-3556): 각 행 뒤 공백 1칸, 행 사이 CRLF(마지막에도 공백)
+ return new TagValue(true, string.Join("\r\n", names.Select(n => $"{n} ")));
+ }
+ case "OCM_수술진단명":
+ // PURME 전용(:3066) — 그 밖의 병원은 Case 미매칭으로 항상 빈 값이다
+ return new TagValue(false,
+ "PURME 전용 태그입니다 — 그 밖의 병원에서는 레거시도 항상 빈 값입니다");
+ case "OCM_수술처치":
+ // SUSS·YJRCH 전용(:3474) — 그 밖의 병원은 Case 미매칭으로 항상 빈 값이다
+ return new TagValue(false,
+ "SUSS·YJRCH 전용 태그입니다 — 그 밖의 병원에서는 레거시도 항상 빈 값입니다");
case "OCM_간호정보조사지_과거력":
{
var comNum = PatientContext.Value(context.ComInf, "ComNum");
@@ -2491,6 +2553,8 @@ public sealed class PatientTagResolver : ITagValueResolver
"ETC_승인의사", "OCM_수술소독간호사",
"OCM_적용진료과", "OCM_적용진료의사", "OCM_전과", "OCM_간호정보조사지_과거력",
"OCM_영상촬영이력",
+ "OCM_수술주상병", "OCM_수술집도의_진료과", "OCM_수술집도의_그룹진료과",
+ "OCM_수술명칭", "OCM_수술진단명", "OCM_수술처치",
"OCM_입원시간", "OCM_입원일시", "OCM_입원일시_영문", "OCM_입실시간",
"OCM_의사퇴원예고일시", "OCM_퇴원예정일시", "OCM_퇴원시간", "OCM_퇴원일시",
"OCM_퇴원예고일시", "OCM_재원일수", "OCM_퇴원예고재원일수", "OCM_입원일자_낮병동",