using Oracle.ManagedDataAccess.Client; namespace SheetMe.Data.Stores; /// /// 환자 부가정보 태그들의 자기 SQL — 레거시 bzDataInterface.vb 의 함수 본문에서 /// 기계적으로 옮겼다(실제생년월일 :226 · 국적 :639 · 장애등급 :687 · 건보세대주명 :732 · /// 건보_급여_세대주명 :780 · 협력업체 :837 · 보호자연락처 :881). /// /// 차트번호 매칭. 레거시는 PatChtNum(CHAR 10) 원문 — 패딩 포함 — 을 바인드한다. /// 우리 문맥은 행을 저장할 때 Trim 하므로(PatientContextStore.Row), CHAR 컬럼은 /// RPAD(:c,10) 으로 패딩을 되살려 비교한다(인덱스도 산다). P_pdsoInf 만 VARCHAR2 라 /// 저장값의 패딩 여부를 확정할 수 없어 두 형태 모두 본다(IN). /// /// Rows(0) 결정 규칙. 원문은 전부 ORDER BY 없는 첫 행이다. 여기서는 /// 담당의 때 정한 규칙대로 정렬을 명시해 한 행으로 고정한다 — 각 메서드 주석에 근거를 남겼다. /// public sealed class PatientExtraStore { #region Member Fields private readonly string connectionString; #endregion #region Constructors public PatientExtraStore(string connectionString) { this.connectionString = connectionString; } #endregion #region Methods /// /// 실제생년월일(P_PatEtcInf.PatEtcBirDte, YYYYMMDD 원문) — 없으면 빈 문자열. /// 원문은 SELECT * 의 Rows(0) 인데 이 표는 차트번호가 사실상 유일키(환자 부가정보 마스터)다. /// 만에 하나 다중이면 갱신일시 최신 행 — 마지막으로 저장한 값이 현재 값이라는 뜻이다. /// public string ActualBirthDate(string chtNum) => Scalar( "SELECT PatEtcBirDte FROM (SELECT PatEtcBirDte FROM P_PatEtcInf" + " WHERE PatEtcChtNum = RPAD(:c, 10) ORDER BY PatEtcUpdDtf DESC) WHERE ROWNUM = 1", ("c", chtNum)); /// 보호자연락처(P_PatEtcInf.PatEtcGrdnPhn) — 원문의 IS NOT NULL 조건 그대로 public string GuardianPhone(string chtNum) => Scalar( "SELECT PatEtcGrdnPhn FROM (SELECT PatEtcGrdnPhn FROM P_PatEtcInf" + " WHERE PatEtcChtNum = RPAD(:c, 10) AND PatEtcGrdnPhn IS NOT NULL" + " ORDER BY PatEtcUpdDtf DESC) WHERE ROWNUM = 1", ("c", chtNum)); /// /// 국적 한글명 — P_PatInf.PatNatCod 를 M_DtlMst(NATCOD) 로 푼다. /// P_PatInf 는 차트번호당 1행, 조인 키(DtlTblCod+DtlCod)도 유일이라 정렬이 필요 없다. /// public string Nationality(string chtNum) => Scalar( "SELECT DtlCodNam FROM P_PatInf" + " INNER JOIN M_DtlMst ON DtlCod = PatNatCod AND DtlTblCod = 'NATCOD'" + " WHERE PatChtNum = RPAD(:c, 10)", ("c", chtNum)); /// /// 장애등급 — P_pdsoInf(PdsoFlg='E') 의 PdsoGrd. 호출부가 두 번째 글자를 뽑는다(레거시 그대로). /// /// 원문(:701-710)은 P_ComInf 를 INNER JOIN 해 행을 내원 수만큼 곱해 놓고 RowNum 1 로 /// 접는다 — 조인의 실효는 "내원이 있는 환자인가" 확인뿐이라 EXISTS 로 바꿨다(의미 동일). /// ORDER BY 없는 RowNum 1 은 등급 변경 이력이 있으면 임의 행이다 — 주석의 의도 /// ("현재 작성일 기준 등급", 2017-10-25 푸르메 요청)에 맞게 시작일 최신 행으로 고정한다. /// public string DisabilityGrade(string chtNum) => Scalar( "SELECT PdsoGrd FROM (SELECT PdsoGrd FROM P_pdsoInf" + " WHERE PdsoChtNum IN (:c, RPAD(:c, 10)) AND PdsoFlg = 'E'" + " AND EXISTS (SELECT 1 FROM P_ComInf WHERE ComChtNum = RPAD(:c, 10))" + " ORDER BY PdsoStrDte DESC, PdsoSeq DESC) WHERE ROWNUM = 1", ("c", chtNum)); /// /// 건보 세대주명 — P_PISINF(PisInsCod='1') 를 이 내원의 자격(P_CoiInf.CoiInsSeq)과 맞춘다. /// 자격 이력이 여러 건이면 원문은 임의 행 — 시작일 최신 행으로 고정한다. /// public string HouseholderName(string chtNum, decimal comNum) => Scalar( "SELECT Pis1SedaejuNm FROM (SELECT Pis1SedaejuNm FROM P_PISINF" + " INNER JOIN P_CoiInf ON CoiInsSeq = PisInsSeq AND CoiComNum = :m" + " WHERE PisChtNum = RPAD(:c, 10) AND PisInsCod = '1'" + " ORDER BY PisStrDte DESC) WHERE ROWNUM = 1", ("m", comNum), ("c", chtNum)); /// /// 세대주명(건보·급여) — 문맥 자격의 (보험코드, 순번)으로 P_PISINF 를 직접 짚는다. /// 레거시(:780-)는 참조 내원이 있으면 그쪽 자격을 쓰지만 미리보기는 참조를 설정하지 /// 않으므로(TestPatientSetting) 자기 내원 갈래만 옮겼다. /// public string HouseholderNameByInsurance(string chtNum, string insSeq, string insCod) => Scalar( "SELECT Pis1SedaejuNm FROM (SELECT Pis1SedaejuNm FROM P_PISINF" + " WHERE PisChtNum = RPAD(:c, 10) AND PisInsSeq = :s AND PisInsCod = :d" + " ORDER BY PisStrDte DESC) WHERE ROWNUM = 1", ("c", chtNum), ("s", insSeq), ("d", insCod)); /// /// 협력업체 한글명 — 내원행의 ComCoopHsp 를 M_DtlMst 로 푼다. /// 원문(:850-854)은 DtlTblCod 조건 없이 DtlCod 만으로 조인한다 — 같은 코드가 다른 /// 코드표에도 있으면 다중행이다. 레거시 조인을 그대로 두되 테이블코드 순 첫 행으로 고정한다. /// public string Cooperator(decimal comNum) => Scalar( "SELECT DtlCodNam FROM (SELECT DtlCodNam FROM P_ComInf" + " INNER JOIN M_DtlMst ON ComCoopHsp = DtlCod" + " WHERE ComNum = :m ORDER BY DtlTblCod) WHERE ROWNUM = 1", ("m", comNum)); /// /// 퇴원 이후 다음 외래 방문(OCM_FollowUp, :1028) — 지금보다 뒤의 취소 아닌 내원 중 /// 가장 가까운 것의 접수일시 12자리. 원문의 ROWNUM 은 ORDER BY 보다 먼저 걸려 /// 임의 행이었다 — 인라인뷰로 정렬을 먼저 걸어 "가장 가까운 미래 내원"으로 고정한다. /// "지금"은 서버 시각이다(레거시 SystemDateTime 과 같은 의미). /// public string FollowUp(string chtNum) => Scalar( "SELECT v FROM (SELECT ComAcpDtm v FROM P_ComInf" + " WHERE ComChtNum = RPAD(:c, 10)" + " AND ComAcpDtm > TO_CHAR(SYSDATE, 'YYYYMMDDHH24MI')" + " AND ComAcpStt <> 'OC' ORDER BY ComAcpDtm) WHERE ROWNUM = 1", ("c", chtNum)); /// /// 외출·외박 구간의 한쪽 끝(OCM_외출외박신청일/종료일, :1883/:1936) — 오늘을 포함하는 /// 외박 허용(CowSlpOut='Y') 병실 행의 CowStrDtm/CowEndDtm. /// 0행이면 서버 현재시각을 돌려준다 — 특이하지만 레거시가 그렇다 /// (기록이 없으면 "지금"이 종이에 찍힌다). P_RblInf 곱으로 다중행이면 원문은 /// 임의 행이었다 — 시작일시 최신 행으로 고정한다. /// public string LeaveOfAbsence(decimal comNum, bool endSide) { var column = endSide ? "CowEndDtm" : "CowStrDtm"; // "오늘"은 서버 날짜다(레거시 SystemDate 와 같은 의미) — 바인드 대신 SYSDATE 로 쓴다 return Scalar( "SELECT NVL(MAX(v), TO_CHAR(SYSDATE, 'YYYYMMDDHH24MI')) FROM (" + $"SELECT v FROM (SELECT {column} v FROM P_CoiInf" + " INNER JOIN P_ComInf ON ComNum = CoiComNum AND ComPatTyp = 'I' AND ComAcpStt <> 'IC'" + " INNER JOIN P_CowInf ON CowComNum = CoiComNum" + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') || '2359' BETWEEN CowStrDtm AND CowEndDtm" + " INNER JOIN P_RblInf ON RblComNum = CoiComNum AND CoiMtiSeq = RblMtiSeq" + " AND RblStrDte BETWEEN CoiStrDte AND CoiEndDte" + " WHERE CoiComNum = :m AND RblDtrCod = '-' AND RblNxtNum = 0 AND RblIncTyp = 'M'" + " AND CowSlpOut = 'Y'" + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN CoiStrDte AND CoiEndDte" + " ORDER BY CowStrDtm DESC) WHERE ROWNUM = 1)", ("m", comNum)); } /// /// 최초 내원일(OCM_최초내원일, :1988 Case Else) — 이 내원 이전의 취소 아닌 재진(ComNfsTyp='2') /// 중 가장 이른 접수일시. 원문이 인라인뷰 안에 ORDER BY 를 걸어 이 쿼리는 원래 결정적이다. /// P_CodInf 조인은 원문 그대로(진료 행이 있는 내원만) — 곱행이 생겨도 값이 같은 컬럼이라 무해. /// KIMEYE 분기(초재진 무구분, ComNfsTyp 우선 정렬)는 옮기지 않았다. /// public string FirstVisit(string chtNum, string acpDtm) => Scalar( "SELECT v FROM (SELECT ComAcpDtm v FROM P_ComInf" + " INNER JOIN P_CodInf ON ComNum = CodComNum" + " WHERE ComChtNum = RPAD(:c, 10) AND ComAcpDtm <= :a" + " AND ComAcpStt <> 'OC' AND ComNfsTyp = '2'" + " ORDER BY ComAcpDtm) WHERE ROWNUM = 1", ("c", chtNum), ("a", acpDtm)); /// /// 진료과별 초진일(OCM_초진일_발병일 :2040 · OCM_신환_초진일 :2138) — 최초 내원일에 /// 진료과 앞 2자 조건이 붙는다. 신환 포함이면 ComNfsTyp IN ('1','2'), 아니면 '2' 만. /// public string FirstVisitByDepartment(string chtNum, string acpDtm, string depCod2, bool includeNew) => Scalar( "SELECT v FROM (SELECT ComAcpDtm v FROM P_ComInf" + " INNER JOIN P_CodInf ON ComNum = CodComNum" + " WHERE ComChtNum = RPAD(:c, 10) AND ComAcpDtm <= :a" + (includeNew ? " AND ComAcpStt <> 'OC' AND ComNfsTyp IN ('1','2')" : " AND ComAcpStt <> 'OC' AND ComNfsTyp = '2'") + " AND SUBSTR(CodDepCod, 1, 2) = :p" + " 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)); /// /// 혈액형(OCM_혈액형, :11783) — S_BlpInf.BlpAboTyp. 원문 Rows(0) 에 ORDER BY 가 없다 — /// 갱신일시 최신 행으로 고정한다(마지막 판정이 현재 혈액형이다). /// BlpChtNum 은 VARCHAR2 라 저장 형태를 확정할 수 없어 두 형태 모두 본다. /// public string BloodType(string chtNum) => Scalar( "SELECT v FROM (SELECT BlpAboTyp v FROM S_BlpInf" + " WHERE BlpChtNum IN (:c, RPAD(:c, 10))" + " ORDER BY BlpUpdDtm DESC NULLS LAST) WHERE ROWNUM = 1", ("c", chtNum)); /// /// 최종 월경일(OCM_LMP, :11813) — O_PrgInf.PrgLmpDte. 원문 Rows(0) 에 ORDER BY 가 없다 — /// 같은 표를 읽는 임신주기(:11845)가 갱신일시 최신순을 쓰므로 그 정렬로 고정한다. /// public string Lmp(decimal comNum) => Scalar( "SELECT v FROM (SELECT PrgLmpDte v FROM O_PrgInf" + " WHERE PrgComNum = :m ORDER BY PrgUpdDtf DESC) WHERE ROWNUM = 1", ("m", comNum)); /// /// 발병일(OCM_발병일, :8125) — 대표 주상병(OkdSeq=1, OkdStt='E')과 오늘 유효한 /// 발병 기록(O_OnsInf)의 시작일. 원문 Rows(0) 에 ORDER BY 가 없다 — /// 발병 시작일 최신 행으로 고정한다. /// public string OnsetDate(string chtNum, decimal comNum) => Scalar( "SELECT v FROM (SELECT OnsStrDte v FROM O_OkdInf" + " INNER JOIN O_OnsInf ON OkdKcdCod = OnsKcdCod" + " AND OnsChtNum = RPAD(:c, 10)" + " AND (OnsDelDtf IS NULL OR OnsDelDtf = ' ')" + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN OnsStrDte AND OnsEndDte" + " WHERE OkdComNum = :m AND OkdSeq = 1 AND OkdStt = 'E'" + " ORDER BY OnsStrDte DESC) WHERE ROWNUM = 1", ("c", chtNum), ("m", comNum)); /// /// 당일 식이(OCM_당일식이, :12620) — 오늘(SYSDATE) 입원 식이의 아침/점심/저녁 PIVOT 행. /// PIVOT 이 내원 단위로 접으므로 결과는 최대 1행 — 원문 그대로 결정적이다. /// public IReadOnlyDictionary TodayMeals(decimal comNum) => Row( "WITH ComIml AS (SELECT A01.ImlMgdTyp MgdTyp, B01.MgdKorNam MgdNam, A01.ImlComNum" + " FROM O_ImlInf A01 INNER JOIN M_MgdMst B01 ON MgdCod = ImlMgdCod" + " WHERE ImlDte = TO_CHAR(SYSDATE, 'YYYYMMDD') AND ImlPatTyp = 'I' AND ImlComNum = :m)" + " SELECT * FROM ComIml" + " PIVOT (MAX(MgdNam) AS MgdTyp FOR MgdTyp IN ('B' 아침, 'L' 점심, 'D' 저녁))", ("m", comNum)); /// /// 알러지(OCM_알러지·_조영제·_약물·_음식·_기타, :14941-15140) — 오늘 유효한 코드마스터 /// (ALLERGY)에 매칭되는 행을 SQL LISTAGG 로 CRLF 결합. category 가 null 이면 전체. /// 집계 쿼리라 항상 1행 — 매칭 0건이면 NULL → 빈 문자열이다(원문과 같은 귀결). /// public string Allergy(string chtNum, string? category) { var filter = category is null ? string.Empty : " AND AlgAlgCtg = :g"; var binds = category is null ? new (string, object)[] { ("c", chtNum) } : new (string, object)[] { ("c", chtNum), ("g", category) }; return Scalar( "SELECT LISTAGG(AlgAlgCtg || '-' || CASE WHEN NVL(AlgAlgSpm, ' ') <> ' '" + " THEN AlgAlgTyp || ' (증상 : ' || AlgAlgSpm || ')' ELSE AlgAlgTyp END," + " CHR(13) || CHR(10)) WITHIN GROUP (ORDER BY DtsDtlCod, DtsDspSeq)" + " FROM S_AlgInf INNER JOIN M_DtsMst ON DtsTblCod = 'ALLERGY'" + " AND AlgAlgTyp = DtsCodNam" + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN DtsStrDte AND DtsExpDte" + " WHERE AlgChtNum IN (:c, RPAD(:c, 10))" + " AND (AlgDelFlg IS NULL OR AlgDelFlg <> 'D')" + filter, binds); } /// /// 알러지 원장 행들(OCM_알러지_List, :15142) — 코드마스터 조인 없이 전부, /// 갱신일시 오름차순(원문 ORDER BY 그대로). 결합은 해석기가 원문 결함까지 흉내낸다. /// public IReadOnlyList> AllergyRows(string chtNum) => Rows( "SELECT AlgAlgCtg, AlgAlgTyp, AlgAlgSpm FROM S_AlgInf" + " WHERE AlgChtNum IN (:c, RPAD(:c, 10))" + " AND (AlgDelFlg IS NULL OR AlgDelFlg <> 'D') ORDER BY AlgUpdDtf ASC", ("c", chtNum)); /// /// 외출 예상 일시(OCM_외출예상시작/종료일시, :15197/:15231) — 오늘 0시 이후 시작하는 /// 외출·외박 신청 중 가장 이른 것. 원문부터 인라인뷰 정렬 + ROWNUM 이라 결정적이다. /// 두 태그가 같은 행의 양 끝을 읽는다(정렬 키는 시작 쪽). /// public string ExpectedOuting(string chtNum, bool endSide) => Scalar( "SELECT " + (endSide ? "NVL(OutAdmEndDtm, OutAdmExpEndDtm)" : "NVL(OutAdmStrDtm, OutAdmExpStrDtm)") + " FROM (SELECT * FROM O_OutAdmInf" + " WHERE OutAdmChtNum IN (:c, RPAD(:c, 10))" + " AND OutAdmStrDtm >= TO_CHAR(SYSDATE, 'YYYYMMDD') || '0000'" + " ORDER BY NVL(OutAdmStrDtm, OutAdmExpStrDtm)) WHERE ROWNUM = 1", ("c", chtNum)); /// /// 건강보험증번호(ETC_건강보험증번호, :15269) — 건보(PisInsCod=1) 자격의 최신 순번 행. /// 원문의 숫자 리터럴 비교(PisInsCod = 1)와 TO_NUMBER 정렬을 그대로 옮겼다. /// public string InsuranceCertNumber(string chtNum) => Scalar( "SELECT v FROM (SELECT Pis1AsylmSym v FROM P_PisInf" + " WHERE PisInsCod = 1 AND PisChtNum = RPAD(:c, 10)" + " ORDER BY TO_NUMBER(PisInsSeq) DESC) WHERE ROWNUM = 1", ("c", chtNum)); /// 재해발생일(ETC_재해발생일, :15323) — 산재(PisInsCod=4) 자격의 최신 순번 행. YYYYMMDD 원문 그대로 public string AccidentDate(string chtNum) => Scalar( "SELECT v FROM (SELECT Pis4AcdDte v FROM P_PisInf" + " WHERE PisInsCod = 4 AND PisChtNum = RPAD(:c, 10)" + " ORDER BY TO_NUMBER(PisInsSeq) DESC) WHERE ROWNUM = 1", ("c", chtNum)); /// /// 산재지정번호(ETC_산재지정번호, :15727) — 진료과로 병원을 찾아 HspWrkNum. /// 요양기관 행과 같은 조인이라 다중이면 한 행으로 고정한다. /// public string IndustrialAccidentNumber(string depCod, string day) => depCod.Trim().Length == 0 || day.Trim().Length < 8 ? string.Empty : Scalar( "SELECT v FROM (SELECT HspWrkNum v 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", depCod.Trim()), ("y", day.Trim()[..8])); /// /// 감염 분류·정보(OCM_감염분류_감염정보, :14084) — 오늘 유효한 감염 기록을 /// "분류 앞 2자-상세명" 으로 LISTAGG CRLF 결합. 집계라 항상 1행이다. /// (OCM_감염정보 :14122 는 원문 SQL 의 바인드 콜론이 빠져 항상 오류→빈 값 — /// 해석기가 사유로 말하고 여기서는 구현하지 않는다.) /// public string InfectionInfo(string chtNum) => Scalar( "SELECT LISTAGG(SUBSTR(DtlCodNam, 1, 2) || '-' || DtsCodNam, CHR(13) || CHR(10))" + " WITHIN GROUP (ORDER BY DtsDspSeq)" + " FROM S_IcnInf" + " INNER JOIN M_DtlMst ON DtlCod = IcnIcnCod AND DtlTblCod = 'INFECTION'" + " INNER JOIN M_DtsMst ON DtsCod = IcnDtlCod" + " WHERE IcnChtNum IN (:c, RPAD(:c, 10))" + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN IcnStrDte AND IcnEndDte", ("c", chtNum)); /// /// 퇴원분석 수술·처치명 목록(OCM_퇴원분석_수술처치명, :13920) — "코드 - 이름" 을 /// 순번 순으로. 결합은 해석기가 한다(원문은 AppendLine 이라 마지막 뒤에도 개행이 남는다). /// public IReadOnlyList> DischargeAnalysisOperations(decimal comNum) => Rows( "SELECT DssOpnCod || ' - ' || DssOpnNam AS OpnNam FROM S_DssOpn" + " WHERE DssComNum = :m" + " AND (DssOpnCod IS NOT NULL AND DssOpnCod <> ' ')" + " AND (DssOpnNam IS NOT NULL AND DssOpnNam <> ' ')" + " ORDER BY DssOpnSeq", ("m", comNum)); /// /// 병실 인실(OCM_병실인실, :7065) — 입실 이력과 병실 마스터의 배정 침상 수. /// 원문 Rows(0) 에 ORDER BY 가 없다 — 전실했으면 임의 병실이었다. 최신 입실로 고정한다. /// public string RoomBedCount(decimal comNum) => Scalar( "SELECT v FROM (SELECT RomAsgBed v FROM P_CowInf" + " INNER JOIN M_RomMst ON RomCod = CowRomCod" + " AND CowStrDtm BETWEEN RomStrDte AND RomEndDte || '2359'" + " WHERE CowComNum = :m ORDER BY CowStrDtm DESC) WHERE ROWNUM = 1", ("m", comNum)); /// /// 병실 차액 구분(OCM_병실차액, :7103) — 첫 입실 병실 등급의 건보 산정 구분(SgaHthIns). /// 원문이 CowStrDtm 오름차순 첫 행이라 결정적이다 — 그대로 옮겼다. /// public string RoomExtraCharge(decimal comNum) => Scalar( "SELECT v FROM (SELECT SgaHthIns v FROM P_ComInf" + " INNER JOIN P_CowInf ON CowComNum = ComNum" + " INNER JOIN M_RomMst ON CowRomCod = RomCod" + " AND CowStrDtm BETWEEN RomStrDte AND RomEndDte || '2359'" + " INNER JOIN M_RgdMst ON CowGrdCod = RgdCod" + " AND CowStrDtm BETWEEN RgdStrDte AND RgdEndDte || '2359'" + " INNER JOIN M_SgaMst ON RgdAmtCod = SgaCod" + " AND CowStrDtm BETWEEN SgaStrDte AND SgaEndDte || '2359'" + " WHERE ComNum = :m ORDER BY CowStrDtm ASC) WHERE ROWNUM = 1", ("m", comNum)); /// /// 계산유형 이름(OCM_계산유형, :5133) — 자격의 보험·계산 코드로 M_CalMst.CalNam. /// 원문 Rows(0) 에 ORDER BY 가 없다 — 시작일 최신 행으로 고정한다. /// public string CalculationTypeName(string insCod, string calCod, string strDte) => Scalar( "SELECT v FROM (SELECT CalNam v FROM M_CalMst" + " WHERE CalInsCod = :i AND CalCod = :l AND :d BETWEEN CalStrDte AND CalEndDte" + " ORDER BY CalStrDte DESC) WHERE ROWNUM = 1", ("i", insCod), ("l", calCod), ("d", strDte)); /// 보험유형 이름(OCM_보험유형, :5186) — M_InsMst.InsNam. 같은 규칙으로 고정 public string InsuranceTypeName(string insCod, string strDte) => Scalar( "SELECT v FROM (SELECT InsNam v FROM M_InsMst" + " WHERE InsCod = :i AND :d BETWEEN InsStrDte AND InsEndDte" + " ORDER BY InsStrDte DESC) WHERE ROWNUM = 1", ("i", insCod), ("d", strDte)); /// /// 수납일시(OCM_수납일시, :5287) — 자격에 걸린 수납청구(P_RblInf, RblCalNxt=0)의 /// 수납(P_RcpInf) 갱신일시 12자리. 원문은 두 쿼리 모두 ORDER BY 없는 Rows(0) — /// 청구·수납 다건이면 임의였다. 최신 청구의 최신 수납으로 고정하고 한 왕복으로 접었다. /// public string ReceiptStamp(decimal comNum, string mtiSeq, string strDte) => Scalar( "SELECT v FROM (SELECT RcpUpdDtf v FROM P_RcpInf WHERE RcpRblNum IN (" + " SELECT n FROM (SELECT RblNum n FROM P_RblInf" + " WHERE RblComNum = :m AND RblMtiSeq = :s AND RblStrDte = :d AND RblCalNxt = 0" + " ORDER BY RblNum DESC) WHERE ROWNUM = 1)" + " ORDER BY RcpUpdDtf DESC) WHERE ROWNUM = 1", ("m", comNum), ("s", mtiSeq), ("d", strDte)); /// /// 최신 퇴원약 오더 일시(OCM_진료기간 SRCH 갈래, :2299-2318) — 인라인뷰 정렬이 /// 원문부터 있어 결정적이다. 필터(처방의약·숨김 제외 등)는 원문 그대로. /// public string DischargeMedicationOrderDate(decimal comNum) => Scalar( "SELECT v FROM (SELECT OdrDtm v FROM O_OdrInf" + " WHERE OdrComNum = :m AND OdrStt = 'OE'" + " AND (OdrDscMed IS NOT NULL AND OdrDscMed <> ' ')" + " AND (OdrHidden IS NULL OR OdrHidden <> 1)" + " AND OdrGrpSub = 0" + " AND (OdrAutCod IS NULL OR OdrAutCod = '-')" + " AND (OdrSimEdt IS NULL OR OdrSimEdt NOT IN ('I','E'))" + " ORDER BY OdrDtm DESC) WHERE ROWNUM = 1", ("m", comNum)); /// /// 외래 진료기간 시작(OCM_진료기간 외래 갈래, :2325-2345) — 이 내원 이하의 취소 아닌 /// 최초 접수일시. OCM_최초내원일과 달리 진료행 조인·재진 필터가 없다(원문 그대로 — /// ComNfsTyp='N' 조건은 주석 처리돼 있어 옮기지 않는다). /// public string TreatmentPeriodStart(string chtNum, string acpDtm) => Scalar( "SELECT v FROM (SELECT ComAcpDtm v FROM P_ComInf" + " WHERE ComChtNum = RPAD(:c, 10) AND ComAcpDtm <= :a AND ComAcpStt <> 'OC'" + " ORDER BY ComAcpDtm) WHERE ROWNUM = 1", ("c", chtNum), ("a", acpDtm)); /// 외래 진료기간 끝(:2352-2372) — 시작 이후 오늘(2359)까지의 최신 접수일시 public string TreatmentPeriodEnd(string chtNum, string strDte) => Scalar( "SELECT v FROM (SELECT ComAcpDtm v FROM P_ComInf" + " WHERE ComChtNum = RPAD(:c, 10) AND ComAcpDtm >= :s" + " AND ComAcpDtm <= TO_CHAR(SYSDATE, 'YYYYMMDD') || '2359' AND ComAcpStt <> 'OC'" + " ORDER BY ComAcpDtm DESC) WHERE ROWNUM = 1", ("c", chtNum), ("s", strDte)); /// /// 응급증상 오더의 산정 여부(OCM_응급증상여부_YES/NO, :12748/:12830) — 응급 내원에 /// 걸린 응급의료관리료 오더 중 한 행의 OdrInsYon. 행이 없으면 null(레거시 $NoCheck$ — /// 체크 상태를 정하지 않는다). 원문 Rows(0) 에 ORDER BY 가 없다 — 오더일시 최신으로 /// 고정한다. 코드 목록은 레거시 GetDtlMst("EMGEMGCOD") 그대로: 코드표에 있으면 그것, /// 없으면 하드코딩 6종(레거시 폴백). /// public string? EmergencySymptomInsYon(decimal comNum) { var codes = Rows("SELECT DtlCod FROM M_DtlMst WHERE DtlTblCod = 'EMGEMGCOD' ORDER BY DtlCod") .Select(r => r.TryGetValue("DtlCod", out var v) ? v : string.Empty) .Where(v => v.Length > 0) .ToArray(); if (codes.Length == 0) { codes = new[] { "V1200", "V1300", "V1400", "V1500", "AC103", "AC105" }; } var placeholders = string.Join(", ", codes.Select((_, i) => $":g{i}")); var binds = new List<(string, object)> { ("m", comNum) }; for (var i = 0; i < codes.Length; i++) { binds.Add(($"g{i}", codes[i])); } var value = Scalar( "SELECT v FROM (SELECT OdrInsYon v FROM S_EmgInf OCMEMG" + " LEFT JOIN S_EmgIcmInf ICMEMG ON OCMEMG.EmgComNum = ICMEMG.EmgOcmNum" + " INNER JOIN O_OdrInf ON CASE WHEN NVL(ICMEMG.EmgComNum, 0) > 0" + " THEN ICMEMG.EmgComNum ELSE OCMEMG.EmgComNum END = OdrComNum" + " INNER JOIN O_OdsInf ON OdrKey = OdsOdrKey AND OdsStt = 'OE'" + " INNER JOIN M_SgaMst ON OdrCod = SgaCod" + " AND OdrDtm BETWEEN SgaStrDte AND SgaEndDte || '2359'" + " WHERE OCMEMG.EmgComNum = :m" + " AND OdrStt = 'OE' AND (OdrHidden IS NULL OR OdrHidden <> 1)" + $" AND OdrElcCod IN ({placeholders})" + " ORDER BY OdrDtm DESC, OdrKey DESC) WHERE ROWNUM = 1", binds.ToArray()); // 빈 문자열은 "행은 있는데 값이 NULL"과 "행이 없다"를 못 가른다 — COUNT 로 가른다 if (value.Length > 0) { return value; } var count = Scalar( "SELECT COUNT(*) FROM S_EmgInf OCMEMG" + " LEFT JOIN S_EmgIcmInf ICMEMG ON OCMEMG.EmgComNum = ICMEMG.EmgOcmNum" + " INNER JOIN O_OdrInf ON CASE WHEN NVL(ICMEMG.EmgComNum, 0) > 0" + " THEN ICMEMG.EmgComNum ELSE OCMEMG.EmgComNum END = OdrComNum" + " INNER JOIN O_OdsInf ON OdrKey = OdsOdrKey AND OdsStt = 'OE'" + " INNER JOIN M_SgaMst ON OdrCod = SgaCod" + " AND OdrDtm BETWEEN SgaStrDte AND SgaEndDte || '2359'" + " WHERE OCMEMG.EmgComNum = :m" + " AND OdrStt = 'OE' AND (OdrHidden IS NULL OR OdrHidden <> 1)" + $" AND OdrElcCod IN ({placeholders})", binds.ToArray()); return count == "0" ? null : string.Empty; } /// /// 전과 내역(OCM_전과내역, :1103) — 진료 행 전부를 과명·구간과 함께. /// 원문 조인 그대로: 과 조인이 담당의 조인의 병원 코드에 종속돼(DepHspCod=Usr.UidHspCod) /// 의사 매칭이 빠지면 과명도 빈다. 정렬(CodStrDtm)이 원문부터 있어 결정적이다. /// public IReadOnlyList> TransferHistory(decimal comNum) => Rows( "SELECT DepKorNam AS DepNam, Cod.CodStrDtm, Cod.CodEndDtm FROM P_CodInf Cod" + " LEFT OUTER JOIN M_UidMst Usr ON Usr.UidCod = CodDtrCod AND Usr.UidDtrYon = 'Y'" + " AND Cod.CodEndDtm BETWEEN Usr.UidStrDte AND Usr.UidEndDte || '2359'" + " LEFT OUTER JOIN M_DepMst Dep ON DepCod = CodDepCod AND DepHspCod = Usr.UidHspCod" + " AND DepWrkTyp = 'D' AND Cod.CodEndDtm BETWEEN DepStrDte AND DepEndDte || '2359'" + " WHERE CodComNum = :m AND CodMtiSeq = '0' ORDER BY CodStrDtm", ("m", comNum)); /// /// 청구 진료과목 이름(OCM_진단서청구진료과목_로그인, :4790) — 진료과의 청구코드 /// (DepBilCod)를 코드표(BDPTBL)로 푼다. 기준일은 오늘(원문 SystemDate). /// 원문 Rows(0) 무가드 — 한 행으로 고정한다. /// public string BillingDepartmentName(string depCod) => depCod.Trim().Length == 0 ? string.Empty : Scalar( "SELECT v FROM (SELECT DtlCodNam v FROM M_DepMst" + " INNER JOIN M_DtlMst ON DepBilCod = DtlCod AND DtlTblCod = 'BDPTBL'" + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN DtlStrDte AND DtlExpDte" + " WHERE TRIM(DepCod) = :d" + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN DepStrDte AND DepEndDte)" + " WHERE ROWNUM = 1", ("d", depCod.Trim())); /// /// 당뇨식 칼로리(OCM_당뇨식칼로리_List, :3569) — 당뇨식 중 최신 식이일의 칼로리 1건. /// 원문부터 인라인뷰 정렬 + ROWNUM 이라 결정적이다(식이일 동률만 모호). /// public string DiabetesMealCalorie(string chtNum) => Scalar( "SELECT ImlCal FROM (SELECT ImlCal FROM O_ImlInf" + " INNER JOIN P_ComInf ON ComNum = ImlComNum AND ComChtNum = RPAD(:c, 10)" + " INNER JOIN M_MgdMst ON MgdCod = ImlMgdCod" + " WHERE MgdKorNam LIKE '%당뇨%' AND TO_NUMBER(ImlCal) > 0" + " ORDER BY ImlDte DESC) WHERE ROWNUM = 1", ("c", chtNum)); /// /// 수술 신청의 수술일자(OCM_수술일자 Case Else :2977 · OCM_수술일자_마지막수술 :2999) — /// 확정(OprStt='E') 수술의 OprDte. 본체는 첫 수술(OprKey ASC), 마지막수술은 최신 /// (OprKey DESC). PURME(마취 오더)·BSGH/HIMCHAN/GJHNSS(DESC 뒤집기) 분기는 미이식. /// public string SurgeryDate(decimal comNum, bool latestFirst) => Scalar( "SELECT v FROM (SELECT OprDte v FROM S_OprInf" + " WHERE OprComNum = :m AND OprStt = 'E'" + $" ORDER BY OprKey {(latestFirst ? "DESC" : "ASC")}) WHERE ROWNUM = 1", ("m", comNum)); /// /// 외래 실진료일 목록(OCM_외래실진료일 :1217 · _과목별 :1503) — 신환('1') 첫 접수 /// (없으면 전체 첫 접수)부터 마지막 접수까지, 지정 진료과의 접수일시 전부(시간순). /// exactDepartment=false 면 과 앞 2자 매칭(본체), true 면 전체 코드 일치(과목별). /// 원문 세 선행 쿼리 모두 인라인뷰 정렬 + ROWNUM 이라 결정적이다. /// 본체 원문의 종료일 쿼리는 차트번호를 문자열 연결로 넣지만 값이 같아 바인드로 정상화했다. /// public IReadOnlyList OutpatientVisitDays(string chtNum, string acpDtm, string depCod, bool exactDepartment) { var first = Scalar( "SELECT v FROM (SELECT ComAcpDtm v FROM P_ComInf" + " WHERE ComChtNum = RPAD(:c, 10) AND ComAcpDtm <= :a" + " AND ComAcpStt <> 'OC' AND ComNfsTyp = '1'" + " ORDER BY ComAcpDtm) WHERE ROWNUM = 1", ("c", chtNum), ("a", acpDtm)); if (first.Length < 8) { first = Scalar( "SELECT v FROM (SELECT ComAcpDtm v FROM P_ComInf" + " WHERE ComChtNum = RPAD(:c, 10) AND ComAcpStt <> 'OC'" + " ORDER BY ComAcpDtm ASC) WHERE ROWNUM = 1", ("c", chtNum)); } if (first.Length < 8) { return Array.Empty(); } var last = Scalar( "SELECT v FROM (SELECT ComAcpDtm v FROM P_ComInf" + " WHERE ComChtNum = RPAD(:c, 10) AND ComAcpStt <> 'OC'" + " ORDER BY ComAcpDtm DESC) WHERE ROWNUM = 1", ("c", chtNum)); var depFilter = exactDepartment ? " AND CodDepCod = :p" : " AND SUBSTR(CodDepCod, 1, 2) = :p"; return Rows( "SELECT ComAcpDtm FROM P_ComInf" + " INNER JOIN P_CodInf ON ComNum = CodComNum" + " WHERE ComChtNum = RPAD(:c, 10)" + " AND ComAcpDtm >= :s || '0000' AND ComAcpDtm <= :e || '9999'" + depFilter + " AND ComAcpStt <> 'OC' ORDER BY ComAcpDtm", ("c", chtNum), ("s", first[..8]), ("e", last.Length >= 8 ? last[..8] : string.Empty), ("p", depCod)) .Select(r => r.TryGetValue("ComAcpDtm", out var v) ? v : string.Empty) .Where(v => v.Length >= 8) .ToArray(); } /// /// 적용 진료과 이름(OCM_적용진료과, :5659) — 통합 대표행의 AdpDepCod/AdpDtm 은 /// 진료행(CodDepCod)과 적용일시의 별칭이다(dtHISOperatingInfo.vb:3699). /// 원문은 적용일시 12자리 전체를 8자리 유효기간과 비교한다(절단 없음) — 그대로. /// public string AppliedDepartmentName(string depCod, string adpDtm) => depCod.Trim().Length == 0 || adpDtm.Trim().Length == 0 ? string.Empty : Scalar( "SELECT v FROM (SELECT DepKorNam v FROM M_DepMst" + " WHERE TRIM(DepCod) = :d AND :y BETWEEN DepStrDte AND DepEndDte)" + " WHERE ROWNUM = 1", ("d", depCod.Trim()), ("y", adpDtm.Trim())); /// /// 전과 목록(OCM_전과, :12924 — GetCodInfDT(With_Join, 전체) 대응, dtCommonLib.vb:2777-2832). /// 원문의 이름 조인(오늘 기준 유효기간, DepWrkTyp IN ('D','A') — SYBS·NYHY 의 'B' 추가는 /// 미이식)과 정렬(CodStrDtm 오름차순) 그대로. 원문이 DefaultView 에 건 필터·정렬은 /// 순회에 반영되지 않지만 SQL 이 이미 같은 조건·순서라 실질 무해했다. /// public IReadOnlyList> TransferList(decimal comNum) => Rows( "SELECT DepKorNam AS DepNam, Cod.CodDepCod, Cod.CodStrDtm, Cod.CodEndDtm FROM P_CodInf Cod" + " LEFT OUTER JOIN M_UidMst Usr ON Usr.UidCod = CodDtrCod AND Usr.UidDtrYon = 'Y'" + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN Usr.UidStrDte AND Usr.UidEndDte" + " LEFT OUTER JOIN M_DepMst Dep ON DepCod = CodDepCod AND DepHspCod = Usr.UidHspCod" + " AND DepWrkTyp IN ('D','A')" + " AND TO_CHAR(SYSDATE, 'YYYYMMDD') BETWEEN DepStrDte AND DepEndDte" + " WHERE CodComNum = :m AND CodMtiSeq = '0' ORDER BY CodStrDtm", ("m", comNum)); /// /// 간호정보조사지의 과거력 행(OCM_간호정보조사지_과거력 :13030 + GetNurseEmrDT :13523 + /// GetNurseShtCod :13770) — 서식 우선순위(S113 성인 → S202 소아 → S163 산모, SRCH 갈래)로 /// 고른 기록의 병력 항목 6칸을 PIVOT 한 행. 레거시는 서식 확인 3왕복 + 본조회였다 — /// 우선순위를 CASE 정렬로 접어 한 왕복이다. 기록이 없으면 빈 사전. /// public IReadOnlyDictionary NurseHistoryRow(decimal comNum) => Row( "WITH Sht AS (SELECT v FROM (SELECT EmrShtCod v FROM E_EmrInf" + " WHERE EmrComNum = :m AND EmrShtCod IN ('S113','S202','S163')" + " AND (EmrSttFlg IS NULL OR EmrSttFlg <> 'D')" + " ORDER BY CASE EmrShtCod WHEN 'S113' THEN 1 WHEN 'S202' THEN 2 ELSE 3 END)" + " WHERE ROWNUM = 1)," + " Data AS (SELECT SctObjNam, EmdRecord, ShtCod FROM E_EmrInf" + " INNER JOIN E_ShtMst ON ShtCod = EmrShtCod" + " INNER JOIN E_EmdInf ON EmdEmrKey = EmrKey" + " INNER JOIN E_SctMst ON SctKey = EmdItmKey AND SctObjNam IN" + " ('chkMedHis_1','chkMedHis_2','chkMedHis_3','chkMedHis_4','chkMedHis_5','txtMedHis')" + " WHERE EmrComNum = :m AND EmrShtCod = (SELECT v FROM Sht)" + " AND (EmrSttFlg IS NULL OR EmrSttFlg <> 'D'))" + " SELECT * FROM (SELECT EmdRecord, SctObjNam, ShtCod FROM Data)" + " PIVOT (MIN(EmdRecord) FOR SctObjNam IN ('chkMedHis_1' AS 당뇨, 'chkMedHis_2' AS 고혈압," + " 'chkMedHis_3' AS 간염, 'chkMedHis_4' AS 결핵, 'chkMedHis_5' AS 병력기타," + " 'txtMedHis' AS 병력기타내용))", ("m", comNum)); /// /// 영상 촬영 이력(OCM_영상촬영이력, :13968) — 영상의학(XRAY) 접수 완료 오더의 /// 일자·이름(30자 절단, 원문 SUBSTR). 원문 정렬은 8자 절단 별칭이라 같은 날 안 순서가 /// 임의였다 — 원본 일시로 정렬해 고정한다(상위 순서는 동일). /// public IReadOnlyList> RadiologyHistory(decimal comNum) => Rows( "SELECT SUBSTR(OdrDtm, 1, 8) AS OdrDay, SUBSTR(OdrNam, 1, 30) AS OdrNam FROM O_OdrInf" + " WHERE OdrComNum = :m AND OdrAcpDep = 'XRAY' AND OdrStkStt = 'OE'" + " ORDER BY OdrDtm DESC", ("m", comNum)); /// /// 나이(개월 표기 포함) — 통합 대표행의 AgeMonth 파생 컬럼과 같은 식 /// (dtCommonLib.vb:3725 — UDF_GETAGE_FROMRESNUM(복호화 주민번호, 적용일시)). /// PAT_나이의 Case Else 갈래가 이 컬럼을 그대로 돌려준다(CGCH 분기 미이식). /// 암호화 N 병원 갈래(PatResNum 직접)는 미이식 — 이 DB 는 암호화 Y 다. /// public string AgeMonthText(string chtNum, string adpDtm) => Scalar( "SELECT UDF_GETAGE_FROMRESNUM(UDF_GetDecResNum(PatResEnc), :a) FROM P_PatInf" + " WHERE PatChtNum = RPAD(:c, 10)", ("a", adpDtm), ("c", chtNum)); /// /// 정수 나이 — 통합 대표행의 AGE 파생 컬럼과 같은 식 /// (dtCommonLib.vb:3712 — UDF_GETAGE(PatBthDay, 적용일 8자)). /// PAT_나이_세의 산정코드 24* 갈래가 이 컬럼을 쓴다. /// public string AgeYearsUdf(string chtNum, string day8) => Scalar( "SELECT UDF_GETAGE(PatBthDay, :a) FROM P_PatInf WHERE PatChtNum = RPAD(:c, 10)", ("a", day8), ("c", chtNum)); /// 스칼라 1칸 — 0행이거나 NULL 이면 빈 문자열. 잔여 공백은 걷는다(문맥 행과 같은 규칙) private string Scalar(string sql, params (string Name, object Value)[] binds) { using var connection = new OracleConnection(connectionString); connection.Open(); using var command = connection.CreateCommand(); command.BindByName = true; command.CommandText = sql; foreach (var (name, value) in binds) { command.Parameters.Add(new OracleParameter(name, value)); } var result = command.ExecuteScalar(); return result is null or DBNull ? string.Empty : result.ToString()!.Trim(); } /// 행 하나를 컬럼명(대문자) → 값 사전으로 — 없으면 빈 사전 private IReadOnlyDictionary Row(string sql, params (string Name, object Value)[] binds) { var rows = Rows(sql, binds); return rows.Count > 0 ? rows[0] : new Dictionary(StringComparer.OrdinalIgnoreCase); } /// 여러 행 — 각 행은 컬럼명 → Trim 값 사전 private IReadOnlyList> Rows( string sql, params (string Name, object Value)[] binds) { using var connection = new OracleConnection(connectionString); connection.Open(); using var command = connection.CreateCommand(); command.BindByName = true; command.CommandText = sql; foreach (var (name, value) in binds) { command.Parameters.Add(new OracleParameter(name, value)); } var rows = new List>(); using var reader = command.ExecuteReader(); while (reader.Read()) { var row = new Dictionary(StringComparer.OrdinalIgnoreCase); for (var i = 0; i < reader.FieldCount; i++) { row[reader.GetName(i)] = reader.IsDBNull(i) ? string.Empty : reader.GetValue(i).ToString()!.Trim(); } rows.Add(row); } return rows; } #endregion }