워크플로 조사(읽기 5 + 반박 검증 5)로 전송 계층과 태그 17종을 훑었고,
검증이 조사서의 "FTP" 단정을 반박해 실체가 드러났다: 전송 수단은 단일
프로토콜이 아니라 DB 설정(M_DtlMst 'EMR_FileTransfer' Type)이 고르는
5갈래이고, 새 진단 --db-filecfg 로 확인한 우리 사이트 값은 Type=1 =
FileServer = ActiveX OCX(AxBITSFTLib.AxbitSFT)다. OCX 내부 프로토콜은
코드에 없고 서드파티·ActiveX 는 배포 금지라 <b>다운로드는 재현할 수 없다</b>.
그래서 받지 않고 찾는다: ①레거시 EMR 이 같은 단말에 이미 받아 둔 캐시
({설치경로}\Tmp\DownLoadedFiles\Signature\ — 레거시가 쓰는 그 폴더),
②DB 경로 자체가 열리는 경우(이 병원 저장 형태는 드라이브 절대경로).
둘 다 없으면 사유 → 종이는 빈칸(어제 결정). 판정은 순수 함수
SignatureImagePath 로 두고 테스트 7건(캐시 우선·직접 경로·둘 다 없음·
빈 경로·설치경로 없음·UNC).
경로 조회는 전부 순수 SQL 이라 그대로 이식(SignatureStore): 집도의
(DTRUIDCOD)·마취(수술 ANEUIDCOD / 부서 — 개원일 2021-03-01 분기)·
영상의학(XRAY)·진단검사(LAB)·외출외박 승인자(병동/원무)·병동 수간호사·
병원로고(M_HspMst.HspLgoPth)·병원직인(주석과 달리 DB_REGISTRY 의
'직인경로'). 로그인 사용자 3종은 세션 행에 UidImgPth 를 얹어 왕복 0,
담당의·진단서 의사는 프리페치 행 재사용.
레거시 결함 보존 2건: ETC_병리판독의사싸인은 SELECT 에 UidNam 만 있고
UidImgPth 를 읽어 항상 예외→빈 값(:17434), OCM_Cosign_싸인은 저장 기록
키(EmrKey) 의존이라 미리보기에서 값이 될 수 없다 — 둘 다 사유로 완결.
렌더: PictureBox 가 자리표시자만 그리던 것을 실제 이미지로. 파일을
잠그지 않고(OnLoad) 읽으며, 인쇄가 SizeMode 를 무시하고 사각형에 늘려
그리는 레거시 동작에 맞춰 기본 Stretch=Fill(Zoom 만 비율 유지).
WithTagValue 는 이미지류에서 Text 대신 경로 속성을 채운다.
분류도 정정 — 싸인 계열이 Image(글자만 보여 준다)에서 PatientContext 로.
- dotnet test 354/354 (경로 판정 7건 추가) · --edit-smoke 실패 0
- --db-patient ①~㊲ 전건 통과 — ㊲ 신설: 9개 경로 조회 실행,
마취과 의사 싸인 경로가 실값 29자로 나온다
- --db-filecfg 신설(값 없이 모드만): Type=1 · E_FilInf 102만행 ·
UidImgPth 157행 · 경로 모양 드라이브 절대경로
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OCM_재원일수_단입법(bzDataInterface.vb:7947 — 재원일수와 같은 분기
구조인데 +1 이 없다(끝날 미포함). MsgBox 갈래·개원일 분기 처리 동일),
OCM_재원일수_낮병동(:8083 — 낮병동 신청 구간 일수 +1, 입원일자_낮병동
과 같은 조건·결정 규칙), ETC_로그인사용자_병동_수간호사(:15507 —
직급 코드표(PSTCOD)의 '수간호사/간호수선생' 코드로 로그인 부서
재직자를 찾는다. 원문 두 쿼리 모두 정렬 없는 Rows(0) — 코드·사용자
코드 순으로 고정하고 한 왕복으로 접음. 원문의 부서 코드 문자열
연결은 바인드로 정상화).
- dotnet test 342/342 · --edit-smoke 실패 0 (이름 검사 257종)
- --db-patient ①~㊱ 전건 통과 (낮병동구간·수간호사 실행 확인 추가)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
공통 조회(퇴원약DT, bzDataInterface.vb:10482 — 퇴원의약 오더를
일시·코드·이름 순, 1회 투여량은 SQL 계산) 하나에 파생 6종
(코드/명칭/용량/횟수/일수/용법 — 열 하나를 줄단위로, 마지막 뒤에도
개행, 용법은 LEFT JOIN 미매칭이면 빈 줄) + 고정폭 조립 2종:
OCM_퇴원약(:10617) — PadRight 를 "채울 개수"로 오용한 원문 결함
그대로(실제 의미는 총폭이라 정렬이 어긋난다), 코드가 20바이트를
넘으면 음수 인자 예외 → 사유(레거시는 오류창+빈 값). SATCH 비고
괄호 분기 미이식. OCM_퇴원약_New(:10733) — 바이트 고정폭(코드 10·
명칭 37), 용량·횟수·일수 패딩이 잘라낸 값이 아닌 원본 전체 바이트로
계산되는 결함 보존(5자 이상이면 음수 예외 → 사유).
행이 없으면 SRCH 만 "해당없음." 을 찍는다(:10704/:10864) — 우리
병원 분기라 그대로 값이 된다. CP949 바이트 길이는 ASCII 1·그 외 2
근사(레거시 LenK 대응), 바이트 절단은 문자 경계라 레거시의 반각
깨짐("?") 재절단 분기가 필요 없다(폭 최대 1바이트 차이 — 주석 기록).
- dotnet test 342/342 · --edit-smoke 실패 0 (이름 검사 250종)
- --db-patient ①~㊱ 전건 통과 (㉟ 에 퇴원약·수술집도의과·수술명칭 실행 확인 추가)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OCM_수술주상병(bzDataInterface.vb:13148 + GetOprInfDT_OKDMain :13364 —
기존 수술 조회와의 실질 차이가 상병 집계의 주상병 한정(OPRVAR1='0')
한 줄이라 SurgeryStore 에 mainDiagnosisOnly 를 얹었다. 치환이
빗나가면 필터 없는 값이 조용히 나가므로 가드로 던진다),
OCM_수술집도의_진료과/_그룹진료과(:3157/:3194 — 집도의(DTRUIDCOD)를
오늘 기준 마스터로 푸는 원문 특이점(OprStt 무필터·SYSDATE 유효기간)
그대로, 최신 수술 첫 행 고정), OCM_수술명칭(:3520 Case Else —
OPRCOD 를 수술일 순으로, 각 행 뒤 공백 1칸 + CRLF 결합. GNBEDRO·
HIMCHAN_BP 의 OPNAME 포함 분기 미이식).
사유 완결 2종: OCM_수술진단명(:3066 — PURME 전용, 그 외 병원은
Case 미매칭으로 항상 빈 값), OCM_수술처치(:3474 — SUSS·YJRCH 전용).
㉘ 정정: 수술 검증이 늘 SKIP 이던 원인은 소유자 탐색을 O_OprInf 로
한 것 — 실제 조회(GetOprInfDT)의 축은 S_OprInf(수술 신청)다.
표를 바로잡자 이 시험 DB 에서 수술 행 1건·OPNAME 실값이 검증됐다
(SurgeryStore 의 낡은 주석도 함께 정정).
- dotnet test 342/342 · --edit-smoke 실패 0 (이름 검사 242종)
- --db-patient ①~㊱ 전건 통과 — ㉘ 이 처음으로 실값 PASS
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PAT_나이(bzDataInterface.vb:373 Case Else — 통합 대표행의 AgeMonth
파생 컬럼과 같은 식을 SQL 로: UDF_GETAGE_FROMRESNUM(복호화, 적용일시),
dtCommonLib.vb:3725 확인. CGCH 분기·암호화 N 갈래 미이식),
PAT_나이_세(:417 — 산정코드 24* 는 UDF_GETAGE(PatBthDay), 그 밖은
AgeCheck), PAT_성별_나이(:359 — "성별/나이". HIMCHAN_BP·BSYD 분기
미이식), PAT_나이_개월수(:430 — 월 경계 -1, 기존 MonthsSince 재사용),
개월·일 4종(:436-508 — 30일 나눗셈, 새 DaysSince 로 분해).
AgeCheck(clsCommonLib.vb:665-974)를 ResidentNumber.AgeOf 로 이식 —
세기 접두 규칙(1·2→19, 3·4→20, 5·6→출생2자리<20 이면 20xx 아니면
19xx+월일 0101 강제, 7·8→서버 연도 2자리 비교, 그 외→18xx. REDCROSS·
BSYD·BSGH 특례 미이식), 만나이(생일 당일 차감 없음 — 2024-09-25 이후
전 병원 규칙), 1세 이하는 고정 365일. 테스트 5건 — 기대값 하나가
틀려 있었다(출생 2자리 "20"은 <20 이 아니라 1920년으로 강제 — 구현이
맞고 판정을 고침). 개월·일 4종의 기준일은 레거시가 PC 시계(Now)라
서버 오늘로 바꿈(의도적 이탈 — 단말마다 값이 갈리면 안 된다).
PAT_주민번호_Blind/SexTyp_Blind/SexTyp_BlindStar(:105-186 Case Else —
앞6 " - " 뒤 XXXXXXX / 성별1+XXXXXX / 성별1+******. SYBS 의 J-계열
서식 전체 노출 분기 미이식).
대조군 교체: PAT_나이가 이식되면서 ⑬·edit-smoke 의 "안 옮긴 태그"
대조군이 깨졌다(의도된 동작) — ETC_수술실간호사로 교체.
㊱ 신설: 나이 UDF 2종이 실값을 돌려준다(접두 없이 동작 확인).
- dotnet test 342/342 (AgeOf 5건 추가) · --edit-smoke 실패 0
- --db-patient ①~㊱ 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OCM_영상촬영이력(bzDataInterface.vb:13968) — 영상의학(XRAY) 접수 완료
오더의 일자·이름(30자 절단). 원문 결합 그대로: 한 줄에 2건, 줄 안은
" / "(공백 5), 줄 사이 CRLF, 각 건은 날짜+공백7+이름.
원문 정렬이 8자 절단 별칭이라 같은 날 안 순서가 임의였다 — 원본
일시(OdrDtm)로 정렬해 고정(상위 순서 동일).
- dotnet test 338/338 · --edit-smoke 실패 0 (이름 검사 214종)
- --db-patient ①~㉟ 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OCM_적용진료과(bzDataInterface.vb:5659) — 통합 대표행의
AdpDepCod/AdpDtm 은 진료행 CodDepCod 와 적용일시의 별칭임을 확인
(dtHISOperatingInfo.vb:3699 — CodDepCod AdpDepCod, CodDtrCod
AdpDtrCod). 원문이 적용일시 12자리 전체를 8자리 유효기간과 비교하는
특이점 그대로. OCM_적용진료의사(:3939 — AdpDtrCod + 적용일 8자 →
UidNam, 기존 UserNameAt 재사용).
OCM_전과(:12924 + GetCodInfDT With_Join, dtCommonLib.vb:2777) —
진료행 전부(CodMtiSeq='0', 이름 조인은 오늘 기준·DepWrkTyp IN
('D','A'), SYBS·NYHY 의 'B' 추가 미이식)를 과가 바뀔 때만 "과명 /
시작 ~ 끝" 으로 AppendLine. 원문의 DefaultView 필터·정렬은 순회에
반영 안 되지만 SQL 이 같은 조건·순서(ORDER BY CodStrDtm)라 실질
무해였고, 무기한(2999-12-31) 판정은 Substring(1,8) 결함으로 절대
참이 안 되어 실제 값이 찍힌다 — 판정 없이 그대로.
새 사실: GetCodInfDT 는 항상 ORDER BY CodStrDtm(오름차순)이 있다 —
문맥 로드의 "최신순 1행" 결정과 다른 방향이지만 적용일시 구간에
행이 여럿 겹치는 비정상 데이터에서만 갈린다(기존 결정 유지, 기록만).
OCM_간호정보조사지_과거력(:13030 + GetNurseEmrDT :13523 +
GetNurseShtCod :13770) — 서식 우선순위(S113→S202→S163, SRCH 갈래)를
CASE 정렬로 접어 레거시 4왕복을 1왕복으로. 병력 결합 " ,"(공백+콤마)
와 무병력 시에도 붙는 개행까지 원문 그대로. GCRCH·SRH·GNBEDRO 의
서식 코드 분기는 미이식.
- dotnet test 338/338 · --edit-smoke 실패 0 (이름 검사 213종)
- --db-patient ①~㉟ 전건 통과 (적용진료과 실값 5자)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이름은 "부서번호"지만 값은 로그인 사용자 부서의 전화번호다
(bzDataInterface.vb:17287-17313 — M_DepMst.DepTelNum, SYSDATE 기준
M_UidMst 조인). 우리 로그인 조회가 이미 같은 시점·같은 조인으로
부서 행을 붙이고 있어 컬럼 하나(NVL(d.DepTelNum,' '))만 얹었다 —
왕복이 늘지 않는다. HisUser.DepPhone → UserField.DepPhone.
- dotnet test 338/338 · --edit-smoke 실패 0
- --db-patient ①~㉞ 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OCM_혈액형(bzDataInterface.vb:11783 — S_BlpInf.BlpAboTyp, 갱신일시
최신 고정), OCM_LMP(:11813 — O_PrgInf.PrgLmpDte, 같은 표를 읽는
임신주기의 갱신일시 최신순으로 고정), OCM_BST/BST_LAST(:12042/:12710 —
바이탈과 다른 표 E_EmdInf_BST, VitalStore 를 표 인자화),
OCM_BMI(:12159 — 차트 전체 최신 체중/키², 반올림 2자리),
OCM_표준체중_LAST(:12234 — (키/100)²×여21·남22),
OCM_조정체중_LAST(:12284 — 표준+(실제-표준)×0.25, 키·체중이 같은 행),
OCM_비만도(:12333 — 이것만 내원 기준, 체중/Round(표준,0)×100).
BMI·표준·조정은 내원(EmrComNum)이 아니라 차트(EmrChtNum) 기준이다 —
주석 처리된 EmrComNum 이 그 흔적. 성별 불명이면 표준·조정은 레거시
그대로 "0" 이 찍히고, 비만도는 0 나눗셈이라 사유로 말한다.
OCM_임신주기는 사용자별 레지스트리 설정(DB_REGISTRY, UidCod 컬럼에
UidNam 을 비교하는 수상한 조건 포함) 의존이라 보류 — 별도 조사 대상.
- dotnet test 338/338 · --edit-smoke 실패 0 (이름 검사 164종)
- --db-patient ①~㉞ 전건 통과 (S_BlpInf·O_PrgInf·E_EmdInf_BST 실행 확인)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OCM_협진과(bzDataInterface.vb:6360 Case Else — 회신 완료 CstSttFlg='G'
협진의 회신 과, DISTINCT + 시작일 순 앞 2건을 ", " 결합. GNBEDRO 의
이중 DISTINCT 분기는 미이식), OCM_협진과_협진의(:6437 — M_UidMst 조인
+ 앞 3건을 "과[의사]" 결합), OCM_입원의사(:6499 — 접수 시점 담당의
코드 → 접수일자 기준 UidNam), OCM_퇴원의사(:6583 — 퇴원일시가 비면
바로 빈 값, ILV 면 퇴원 시점·아니면 현재 시점 담당의 → 퇴원일자
기준 이름. 원문의 "퇴원일시 빈 값" If 갈래는 위의 조기 반환 탓에
도달 불가한 데드코드라 옮기지 않았다).
CodInfAt(진료과+담당의 코드 행)·UserNameAt·Consults 를 신설.
이 시험 DB 에 O_CstInf 가 15,224행 있어 ㉞(회신 완료 협진이 있는
내원 → 협진과 실값)를 신설했다 — 2건 결합 확인.
- dotnet test 338/338 · --edit-smoke 실패 0 (이름 검사 156종)
- --db-patient ①~㉞ 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
진료과 파생 4종(bzDataInterface.vb:5556-6007): 그룹코드("한글명(그룹
코드)" 결합 — 행이 있으면 조각이 비어도 결합), 영어(DepEngNam),
대외명칭(DepOfcNam), 약어명칭(DepBrfNam). 전부 프리페치된 진료과
행(M_DepMst SELECT *)의 다른 컬럼이라 왕복이 늘지 않는다. 레거시의
시점 판정(퇴원→퇴원시점·재원→현재·외래→접수)은 문맥 적용일시와 같은
규칙이다 — 자격이 퇴원보다 먼저 끝난 희귀 케이스만 갈린다(주석).
입원과 2종(:6009-6110)은 접수 시점, 퇴원과 2종(:6162-6318)은
퇴원(ILV)/현재 시점의 진료과 — 문맥과 다른 시점이라
DepartmentCodeAt(GetCodInfDT 대응, 시작일시 최신 1행 고정)과
DischargeDepartment(P_ComInf 에 날짜 조건만으로 M_DepMst 를 조인하는
원문 모양 그대로)를 신설했다. 퇴원과 본체의 If/Else 동일 쿼리(낮병동
주석과 달리 복붙 결함 — 퇴원일시 비면 0행→빈)도 그대로 보존,
한글명칭만 접수일시 폴백이 실제로 있다.
OCM_진료과: 개원일(HspStrDte)이 요양기관 행에 생겨 레거시 분기
(2022-07-01 이후 개원 + 대외명칭 있음 → 대외명칭)를 그대로 복원 —
전에는 개원일이 없어 DepKorNam 으로 고정했었다.
- dotnet test 338/338 · --edit-smoke 실패 0 (이름 검사 152종)
- --db-patient ①~㉝ 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OCM_FollowUp(bzDataInterface.vb:1028 — 지금 이후 취소 아닌 내원.
원문의 ROWNUM 은 ORDER BY 보다 먼저 걸려 임의 행이었다 — 인라인뷰
정렬로 "가장 가까운 미래 내원"에 고정), OCM_외출외박신청일/종료일
(:1883/:1936 — 오늘 포함 CowSlpOut='Y' 구간의 시작/끝. 0행이면
서버 현재시각이 나오는 레거시 폴백을 SQL NVL 로 그대로 옮겼다 —
㉛에서 12자 폴백 확인), OCM_최초내원일(:1988 Case Else — 이 쿼리는
원문부터 인라인뷰 정렬이 있어 결정적. KIMEYE 분기 미이식),
OCM_초진일_발병일(:2040)·OCM_신환_초진일(:2138 — 진료과 앞 2자를
레거시는 GetCodInfDT 임의 행에서 뽑았지만 우리는 문맥의 진료 행에서).
"지금/오늘"은 전부 서버 시각(SYSDATE) — 레거시 SystemDateTime 과
같은 의미이고 단말 시계에 좌우되지 않는다.
- dotnet test 338/338 · --edit-smoke 실패 0 (이름 검사 131종)
- --db-patient ①~㉝ 전건 통과 — ㉝ 신설: 재진 있는 차트로 실값 검증
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PAT_실제생년월일(bzDataInterface.vb:226 — P_PatEtcInf.PatEtcBirDte,
8자리·연월일 조각이 0 이 아닐 때만, YYYYMMDD 원문 그대로),
PAT_국적(:639 — M_DtlMst NATCOD), PAT_장애등급(:687 — P_pdsoInf
PdsoFlg='E', PdsoGrd 두 번째 글자), PAT_건보세대주명(:732 — P_PISINF
⨝P_CoiInf, PisInsCod='1'), PAT_건보_급여_세대주명(:780 — 자격
(코드·순번) 직접 지정; _Refer 갈래는 미리보기 미설정이라 자기 내원만),
PAT_협력업체(:837 — ComCoopHsp→M_DtlMst, 원문대로 DtlTblCod 없음),
PAT_보호자연락처(:881 — PatEtcGrdnPhn IS NOT NULL).
새 PatientExtraStore 에 자기 SQL 을 모았다. 차트번호는 문맥이 Trim
하므로 CHAR 컬럼은 RPAD(:c,10) 복원 매칭 — ㉜ 가 실값으로 증명한다.
P_pdsoInf 만 VARCHAR2 라 IN (:c, RPAD(:c,10)) 양쪽을 본다
(㉛ [조사]: 이 DB 는 패딩 행 0 — 운영 DB 가 다를 수 있어 유지).
ORDER BY 없는 Rows(0)/RowNum 1 은 전부 정렬 명시로 고정(갱신일시·
시작일 최신) — 장애등급의 P_ComInf 조인은 행만 곱해서 EXISTS 로 대체.
해석기는 지연 + 태그별 캐시(이 태그 없는 서식이 대부분).
- dotnet test 338/338 · --edit-smoke 실패 0 (이름 검사 125종)
- --db-patient ①~㉜ 전건 통과 — ㉛ 7종 실행, ㉜ 국적 실값 검증 신설
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ETC_병리판독의사명/전문의번호(bzDataInterface.vb:17318/:17515 — M_UidMst
UidDtrYon='Y' AND UidDepCod='TLAB' + 유효기간), ETC_진단검사의사명/
전문의번호(:17550/:17655 — M_DepMst DepGrpCod='LAB' 조인 + UidLicNum
IS NOT NULL). 환자와 무관한 원내 의사 조회지만 레거시가 환자 태그로
노출하므로 PatientTagResolver 에 둔다(lazy 소스 + 캐시 — 이 태그가
없는 서식이 대부분이라 프리페치하지 않는다).
의도적 이탈 하나: 원문엔 ORDER BY 가 없어 TLAB 의사가 둘이면
이름과 전문의번호가 서로 다른 사람이 될 수 있었다(태그마다 따로
조회하므로). ORDER BY UidCod + 1행 고정으로 두 태그가 반드시 같은
사람을 가리키게 했다 — 담당의 때 정한 결정 규칙의 세 번째 적용.
- dotnet test 338/338 · --edit-smoke 실패 0 (이름 검사 110종)
- --db-patient ①~㉚ 전건 통과 — ㉚ 신설: 두 조회가 예외 없이 돈다
(이 시험 DB 는 TLAB·LAB 의사 0행 — 태그는 사유로 완결되는 정상 경로)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
## 세션 조회 확장(UserContextStore)
M_UidMst 행에서 UidLicNum·UidSpcLic 를 더 읽고, 직종명은 레거시 그대로
DtsDtlCod='JOBCOD' 만으로 M_DtsMst 를 조인해(bzDataInterface.vb:17944 —
DtsTblCod 조건이 없다) DtsCodNam 을 가져온다. HisUser 에 세 필드 추가.
## 태그 5종 + Dead 1종
- ETC_로그인_의사면허번호(UidLicNum) · 전문의번호(UidSpcLic) · 직종(DtsCodNam)
- ETC_로그인_근무부서 = 세션 부서 한글명. 레거시는 개원일(HspStrDte>=20220701)로
DepOfcNam 을 가르지만 개원일 미상 결정에 따라 DepKorNam 갈래다(기존 결정과 동일).
- ETC_로그인_근무부서_사용자명 = "부서-이름". 레거시는 UidNam 으로 걸러
동명이인에서 남의 부서가 나올 수 있는데 세션 사용자 행이라 결정적이다(주석 기록).
- ETC_로그인_직급 → Dead. 값은 UidNam 을 담고 조건은 UidCod 로 걸어(:17262·17272)
실무상 늘 빈칸이던 태그다. 고치면 없던 값이 갑자기 채워지므로 합의 전까지 레거시 유지.
레거시의 면허·전문의번호 조회는 유효기간 없이 M_DepMst 를 불필요하게 조인해
ORDER BY 없이 첫 행을 집는 비결정 조회였다(조사 문서 risk) — 세션 행(유효기간 검증
완료)에서 읽는 쪽이 결정적이고 값 원천(M_UidMst 같은 행)은 같다.
## 게이트
- dotnet test 338/338 · --edit-smoke 실패 0(이름 검사 100종)
- --db-patient 전건 통과 · --db-render P062 md5 불변
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
## 바이탈(측정치) 17종
E_EmdInf_VITAL ⨝ E_EmrInf(삭제 제외) 를 내원으로 거르고 해당 컬럼이 빈 값이 아닌
행을 측정일시 순으로 한 건 집는다 — 무인자는 첫 측정(ASC), _LAST 는 마지막(DESC).
키/몸무게/체온/맥박/호흡/SPO2 (±LAST) · 혈압/혈압_LAST(BPS||'/'||BPD) ·
혈압_BPS/_BPD(단독값이지만 널 필터는 둘 다 — 레거시 그대로) · VITAL접수일시_LAST(HH:MM).
태그별 (식·컬럼·필터·정렬)을 전부 본문에서 확인해 표로 박았다(:11508-13046).
조회는 태그가 물을 때 한 값씩, 태그별 1회 캐시.
## 머리둘레 2종은 레거시가 고장이다
SELECT 는 EmdHc AS HC 인데 Item("BP") 를 읽는다(:11744, 12581) —
행이 있으면 예외 → MessageBox → "". 레거시에서 한 번도 값이 나온 적 없는 태그다.
그대로 빈 값 + "레거시 결함(컬럼명 불일치)으로 항상 빈 값이던 태그" 사유로 둔다.
고쳐서 값을 내면 레거시와 달라진다 — 고칠지는 별도 결정.
## 실DB 확인
--db-patient ㉙ 신규: 내원 6004487 에서 첫/마지막 몸무게 2자.
표 부재 가능성도 수술과 같은 방식으로 가른다(ORA-00942 명시).
## 게이트
- dotnet test 338/338 · --edit-smoke 실패 0(이름 검사 95종)
- --db-patient ①~㉙ 전건 통과 · --db-render P062 md5 불변
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
## OCM_STFDTR
담당의 조회를 코드 컬럼만 일반화했다(CodDtrCod → CodStfDtr 파라미터).
시각 갈래는 담당의와 같다(bzDataInterface.vb:4026-4067).
코드가 비어 있으면 레거시도 즉시 "" — 진료과 행이 돌아오면 주치의 행이 아니므로 비운다.
## _Refer 계열은 "안 옮긴 것"이 아니었다
참조 내원은 EMR 실행 화면(ActionTag — fmCodLstActionTag.vb:54 등)에서
<b>사용자가 지정하는 값</b>이고, 레거시 서식생성기 미리보기(TestPatientSetting)는
그것을 설정하지 않는다. 즉 <b>레거시 미리보기에서도 _Refer 태그는 전부 빈 값</b>이다.
지금까지 그 태그들이 "환자 태그이지만 아직 옮기지 않았습니다"로 나왔는데 그건 틀린 사유다 —
옮길 것이 없는 게 아니라, 이 화면에는 참조 내원이라는 개념 자체가 없다.
사유를 갈랐다: "참조 내원 태그입니다 — 참조 내원은 EMR 실행 화면에서 지정되며,
레거시 미리보기에서도 빈 값입니다". _Refer ~40종의 표시가 이것으로 바뀐다.
이로써 <b>서식생성기 미리보기 기준의 레거시 호환</b>에서 _Refer 계열은 완결이다 —
레거시가 빈 값인 자리는 빈 값(+정확한 사유)이 맞는 이식이다.
## 게이트
- dotnet test 338/338 · --edit-smoke 실패 0(이름 검사 76종)
- --db-patient 전건 통과 · --db-render P062 md5 불변
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bkd_상병쿼리(bzDataInterface.vb:14625-14811)의 이식. DiagnosisStore 와 구조가 같고
표만 B_OkdInf(심사분)다. SQL 52줄 원문 기계 추출(블록1 UNION 블록2, 주/부 필터는
각 블록의 RN 접기 앞). 병원 분기는 KIMEYE 하나 — 그 병원만 미이식 안내.
태그 4종(본문 확인): 심사_주상병명/코드는 첫 행, 심사_부상병명/코드는 줄바꿈 결합
(가로형이 없다).
## 게이트
- dotnet test 338/338 · --edit-smoke 실패 0(이름 검사 63종)
- --db-patient 전건 통과 (B_OkdInf 도 이 시험 DB 에 없을 수 있어 수술과 같은 취급 —
런타임에서 해석기가 예외를 사유로 바꾼다)
- --db-render P062 md5 불변
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
## GetOprInfDT(bzDataInterface.vb:13203-13363)의 이식
상병과 달리 병원 분기가 없다(Select Case 0회). 바인드는 내원번호 하나 —
접수일자 조건은 원문에서 이미 주석 처리된 죽은 코드다.
SQL 133줄을 원문에서 기계 추출했다(O_OprInf 축, 수술명·부위·상병·집도의·마취의 LISTAGG).
소비 태그 8종이 전부 <b>첫 행에서 컬럼 하나</b>를 집는다:
OCM_OPNAME · OPRCODNAM · DX · 수술상병(OkdNam) · 수술부위(OprRegionName)
수술_OprPatETC · 수술집도의(OprDtrNam) · 수술마취의사(AneUidCod)
원문 Catch 는 ErrorMessageBox 를 띄운다 — 조회 함수의 대화상자 부작용은 옮기지 않고
던져서 해석기가 사유로 바꾼다. 수술 조회도 상병과 같은 지연 정책이다
(수술 태그가 없는 서식이 다수라 환자를 붙일 때 무조건 읽지 않는다).
## 검증의 한계를 정확히 적는다
--db-patient ㉘ 이 O_OprInf 에서 내원을 못 찾았고, 처음엔 "행이 없다"로 적었다.
가려 보니 <b>ORA-00942 — 이 시험 DB(SRCH_TEST)에 표 자체가 없다.</b>
즉 수술 태그는 여기서 실행 검증이 불가능하고 운영 DB 에서만 확인할 수 있다.
SKIP 문구를 그 사실대로 고쳤다 — "확인했다"고 적을 뻔한 것을 진단이 막았다.
런타임에서는 해석기가 예외를 잡아 "수술 조회에 실패했습니다"로 말한다.
## 게이트
- dotnet test 338/338 · --edit-smoke 실패 0(이름 검사 59종)
- --db-patient ①~㉗ 통과, ㉘ SKIP(표 부재 명시)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
## Okd_상병쿼리(bzDataInterface.vb:14160-14623)의 이식
구조: 블록1(M_KcdMst 와 맞는 상병, INNER JOIN) UNION 블록2(마스터에 없는 코드 —
LEFT JOIN 후 KcdCod IS NULL). 주/부 필터(OkdDspSeq='1' / <>'1')가 <b>각 블록에,
ROW_NUMBER 접기 앞에</b> 붙는다. 그래서 전체(T)를 받아 클라이언트에서 가르면 안 된다 —
같은 코드가 주·부 양쪽에 있으면 RN 접기가 한쪽을 지워 결과가 갈린다.
갈래(T/M/S)별로 따로 조회하되, 태그가 물을 때 한 번만 읽고 캐시한다.
SQL 은 손으로 다시 치지 않고 <b>VB 원문에서 기계 추출했다</b>(Select Case 를
Case Else 경로로 시뮬레이션, AppendLine 페이로드만 수집) — 55줄이 공백까지 원문 그대로다.
## 병원 분기를 얼버무리지 않는다
원문은 쿼리 자체가 6갈래(KIMEYE·NYJB·SYBS·HIMCHAN_CW·SPHH·Else)로 갈리고,
이름 규칙이 JEGG(배제 접미), 코드 규칙이 BSGH(KcdSeqCod)에서 또 갈린다.
이 DB(SRCH)의 Case Else 만 옮겼고, 그 병원들에서는 <b>미이식이라고 말한다</b> —
Else 로 뭉개면 그 병원 서식에 다른 모양의 상병이 조용히 찍힌다.
NYJB 갈래에는 사용자 코드를 SQL 에 <b>문자열로 잇는</b> 주입 구멍도 있다(:14235) —
옮길 때 바인드로 바꿔야 한다는 것을 조사 문서가 이미 적어 뒀다.
## 태그 11종 (전부 본문에서 결합 규칙 확인)
상병명_가로/세로 · 영어_가로/세로 · 코드_가로/세로 전체(T)
부상병코드 · 부상병POA 부(S)
주상병명 · 주상병코드 · 주상병POA 주(M) 첫 행
가로 = ", " 구분, 세로 = 줄바꿈. 한글명에만 확→(확진)·의→(의증) 접미.
주상병명은 접미 없이 KcdKorNam(:10126), 주상병코드는 KcdElcCod(:9744).
## 실DB 확인 (--db-patient ㉕~㉗ 신규)
O_OkdInf 에 행을 가진 내원을 찾아 걸었다(행 없는 내원으로 판정하는 함정을 이미 두 번 밟았다).
내원 6005266: 전체 1행 = 주 1 + 부 0, 한글명 11자. 주/부 필터 분해가 전체와 일치한다.
## 게이트
- dotnet test 338/338 · --edit-smoke 실패 0(이름 검사 51종)
- --db-patient ①~㉗ 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
## conflict 6건 (통합 계획의 "고칠 것")
값이 바뀌는 것 둘:
- ETC_현재년도 — "2026" → "2026년"(bzDataInterface.vb:16923 의 + "년").
형제 ETC_현재일시_1_년 은 접미가 없어 둘을 갈랐다(ClockFormat.YearSuffixed 신설).
"2026" 을 단정하던 테스트도 고쳤다.
- ETC_로그인_사용자ID_사용자명 — 레거시(:17930)는 빈 값 검사 없이 항상 결합한다.
한쪽이 비어도 "U01." 이 그대로 나간다. 조건을 붙였던 것이 레거시와 달랐다.
행 선택이 바뀌는 것 둘:
- ETC_로그인_사용자연락처 / 원내연락처 — 전에는 M_UidMst 를 조건 없이 첫 행.
이력이 여러 행인 사용자에서 옛 연락처가 나올 수 있었다.
유효기간(SYSDATE 일자 BETWEEN)으로 고르고, 0행이면 레거시 폴백(:10641)처럼
조건 없이 다시 찾는다.
의도적 차이로 기록한 것 둘(코드 주석):
- 유효기간 판정 일자 — 레거시는 로그인 시점, 여기는 SYSDATE(자정 넘긴 세션에서만 갈림).
- Trim — 레거시는 무가공, 여기는 걷는다(CHAR 잔여 공백이 종이에 찍히는 쪽이 더 이상하다).
## new 48종 중 재료 재사용으로 되는 10종
워크플로 매핑 상세(docs/new48-mappings.json)를 붙여 보니 46종이 needs-lookup 인데,
그중 10종은 <b>이미 든 행의 다른 컬럼</b>이거나 문맥 계산이었다:
- 담당의 행 재사용 3: OCM_담당의사_영문(UidEngNam) · 면허번호(UidLicNum) · 전문의번호(UidSpcLic)
— 조회 갈래가 OCM_담당의사와 문자 그대로 같고 집는 컬럼만 다르다(:4094·4119·4144)
- CowInf 재사용 3: OCM_병실베드(CowBedCod) · 병동_병실 · 병동_병실_베드(" / " 결합, 행 없으면 "")
- 문맥 계산 1: OCM_내원유형(입원/응급실/외래 — ComPatTyp·ComEmgYon)
- User 계열 3: OCM_User진료과 · _한글명칭 · User진료의사명.
로그인 사용자가 의사(UidDtrYon=Y)면 사용자의 과·이름, 아니면 환자의 것.
HisUser 에 DoctorYn·DepOfficialName 을 추가했다(세션 조회 SELECT 확장).
남은 new 는 새 조회가 필요한 세 무리다: 상병(Okd_상병쿼리) · 수술(GetOprInfDT) ·
심사(Bkd_상병쿼리). 다음 배치.
## 게이트
- dotnet test 338/338 (현재년도·ID.사용자명 판정 갱신)
- --edit-smoke 실패 0 (이름 검사 40종)
- --db-patient ①~㉔ 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
사용자가 서식에서 빈칸으로 확인한 자리들이다(B·C 배치).
## 본문에서 확인한 것
- OCM_병동 / OCM_병실 (bzDataInterface.vb:6718, 6801)
문맥의 CowInf 행에서 <b>코드값 그대로</b>다(CowWadCod·CowRomCod).
마스터로 이름을 찾지 않는다 — 레거시가 코드를 그대로 찍는다.
외래는 CowInf 가 0행이라 자연히 빈 값(레거시도 외래·비낮병동은 "").
- OCM_진료과 (:5437-5495)
M_DepMst 를 CodDepCod + 접수일자 BETWEEN 으로 찾는다.
유효기간 일자는 입원·외래 무관하게 <b>접수일자</b>다(:5445).
이름 선택: NH 는 DepOfcNam, 그 밖은 개원일 2022-07-01 이후 + DepOfcNam 존재 시
DepOfcNam, 아니면 DepKorNam. 개원일은 세션에 없어 빈 값 결정(기존과 동일)이라
그 갈래는 DepKorNam 으로 간다.
- ETC_요양기관명칭_병원명 (:15792-15860)
M_DepMst ⨝ M_HspMst(DepHspCod=HspCod) 에서 HspInsNam.Trim.
진료과 코드로 병원을 찾는 구조다(다부지 구성).
판정 일자가 진료과와 다르다 — ILV 는 퇴원일시·재원 중은 지금·외래는 접수일시.
문맥의 적용일시가 정확히 그 판정이라 AdpDtm 을 그대로 쓴다.
원본 마스터 조회에 ORDER BY 가 없어 담당의 때와 같은 규칙(최신순 1행)으로 고정했다.
## 왕복은 환자당 두 번 추가
진료과·요양기관명을 환자를 붙일 때 한 번씩 읽는다(주민번호·주소·담당의와 같은 정책).
미리보기 창과 Ctrl+P 인쇄(PatientSession.ResolversFor) 둘 다 같은 재료를 받는다.
## 실DB 확인
--db-patient ㉓·㉔ 신규: 진료과 코드 '0506' → 44열·한글명 5자, 요양기관명 6자.
코드가 이름이 되는 것까지 확인해야 태그가 값을 낸다.
## 게이트
- dotnet test 338/338 · --edit-smoke 실패 0(이름 검사 30종)
- --db-patient ①~㉔ 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
23종 → 26종. 조회 둘(P_CodInf 선택 + M_UidMst)이 새로 붙었다.
## 원칙을 처음 깬다
지금까지는 "레거시가 이상해 보여도 그대로 옮긴다"였다
(주민번호의 DateValidCheck 가 그랬다 — 조건이 항상 참이지만 그대로 뒀다).
여기서는 그럴 수 없다. GetCodInfDT 는 ORDER BY 없이 CodMtiSeq='0' 만 걸고
호출부가 Rows(0) 을 집는데, 이 DB 에서 그런 내원이 <b>25,488건</b>이고 최대 41행이다.
41행 중 첫 행은 사실상 무작위다 — <b>재현할 결정적 동작이 존재하지 않는다.</b>
그래서 적용일시로 거르고 최신순 1행을 집는다(문맥의 CodInf 와 같은 규칙).
레거시도 이 시각을 넘기기는 한다 — 마스터 유효기간 조인에만 쓸 뿐이고
P_CodInf 의 CodStrDtm/CodEndDtm 은 WHERE 에 없다.
"그 시점의 진료과"를 뽑으려던 의도가 반쯤만 구현된 것으로 보인다.
근거는 docs/TAG-PORTABLE-48.md 에 남겼다.
## 폴백을 빠뜨리지 않았다
GetUidMst(clsCommonLib.vb:10615)는 유효기간으로 먼저 찾고
<b>0행이면 코드만으로 다시 찾는다</b>. 그 폴백을 빼면 유효기간이 지난 의사가
통째로 빈칸이 된다. 두 번째 조회까지 옮겼다.
## 세 갈래 시각
입원 · ComPrgStt='ILV' → 퇴원일시
입원 · 그 밖 → 문맥의 적용일시
외래 → 접수일시
담당의 유효기간을 볼 날짜도 갈린다(입원은 진료과 행의 CodEndDtm, 외래는 접수일자).
## 또 같은 함정을 밟을 뻔했다
㉒ 를 처음 돌렸을 때 0열이 나왔다. 쿼리 문제로 보였지만 아니었다 —
시험 내원(1988년)에 P_CodInf 행이 아예 없었다. 앞서 "내원 0건인 환자로 판정하려 한" 것과
같은 함정이다. 행을 가진 내원을 찾아 걸도록 고쳤다: 내원 5000323 → 52열, 이름 3자.
## 게이트
- dotnet test 336/336
- --edit-smoke 실패 0 — 이름 검사가 26종 전부 실제 태그임을 확인
- --db-patient ①~㉒ 전건 통과 (㉒ 담당의 조회 신규)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
19종 → 23종. M_ZipMst 를 건물관리번호(PatZipBdg)로 찾는 조회 하나가 추가됐다.
## 결합식을 그대로 옮긴다
레거시(clsCommonLib.vb:16600)는 DECODE 로 빈 조각을 건너뛰며 구분자를 붙인다.
시도·시군구·읍면·도로명·지하여부·건물번호 본번/부번·건물명이 각각 다른 규칙으로 이어진다.
손으로 다시 짜면 주소가 미묘하게 달라지므로 SQL 을 그대로 가져왔다(바인드는 유지).
## 눈으로 보면 오타 같은 것 셋
PAT_도로명주소 도로명주소 + " " + 상세 (공백 있음)
PAT_지번주소 지번주소 + 상세 (공백 <b>없음</b>)
PAT_영문도로명주소 상세 + " " + 영문주소 (순서가 <b>뒤바뀐다</b>)
레거시가 그렇게 찍는다. 맞춰야 같은 종이가 된다.
그리고 PAT_우편번호 가 돌려주는 것은 <b>우편번호 컬럼이 아니라 구역번호</b>다.
조회는 둘 다 가져오지만 레거시가 쓰는 것은 구역번호다.
주소를 못 찾으면 빈 값이 아니라 <b>상세주소만</b> 돌려준다 — 레거시와 같다.
## 왕복은 환자당 한 번
주소 태그가 넷이라 태그마다 읽으면 왕복이 넷이 된다.
주민번호와 같이 환자를 붙이는 시점에 한 번 읽어 해석기에 넘긴다.
읽기에 실패해도 던지지 않는다 — 상세주소만으로 값을 만들 수 있고 레거시도 그렇게 한다.
## 게이트
- dotnet test 336/336
- --edit-smoke 실패 0 — 이름 검사가 23종 전부 실제 태그임을 확인
- --db-patient ①~㉑ 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"검색 및 선택하면 시간이 너무 오래걸려" — 체감이 아니라 숫자였다.
진단에 시간을 재는 줄을 먼저 넣었다. 어디가 느린지 숫자 없이 고치면 엉뚱한 곳을 만진다.
## 실측 (--db-patient)
성명 검색 5,025ms → 441ms
ListVisits 5,000ms 내외 → 1~45ms
문맥 읽기 557ms (그대로 — 병목이 아니었다)
## 검색 — 보이지도 않는 값에 5초를 쓰고 있었다
최종내원일시와 재원여부를 파생표 두 개로 붙였는데 둘 다 P_ComInf 를 <b>통째로 GROUP BY</b>
한 뒤에야 조인된다. 조건에 맞는 환자가 몇 명이든 내원 테이블 전체를 집계했다.
그리고 최종내원일시는 <b>목록 열에 없다</b> — 차트번호·성명·주민번호·휴대전화·비고뿐이다.
아무도 보지 않는 값을 위해 5초를 쓰고 있었다. 뺐다.
재원여부는 비고 배지에 쓰므로 남기되, 행 상한을 먼저 걸고 <b>돌아온 환자에 대해서만</b>
따로 읽는다(FillInpatient). 왕복이 하나 늘지만 집계 범위가 전체 → 최대 200명으로 줄어든다.
차트번호는 바인드로 넘긴다 — 이어 붙이면 레거시의 주입 구멍을 되살린다.
## 내원 조회 — 원인을 한 번 잘못 짚었다
마스터 셋(M_DepMst·M_UidMst·M_InsMst)을 ROW_NUMBER 파생표로 접은 것이 문제라고 보고
상관 스칼라(MAX ... KEEP DENSE_RANK FIRST)로 바꿨다. 결과는 같고 정렬이 사라지는데
<b>시간은 그대로 5초였다.</b>
진짜 원인은 WHERE 였다 — <c>TRIM(A.ComChtNum) = :c</c> 는 컬럼에 함수를 씌워
인덱스를 못 타고 P_ComInf 전건을 스캔한다. 내원이 한 건인 환자도 5초가 걸린 이유다.
LIKE 접두로 인덱스 범위 스캔을 살리고, 그것만으로는 '123' 이 '1234' 까지 잡으므로
TRIM 등호를 함께 둬서 넘친 행을 걸러 낸다. 5,000ms → 1ms.
측정을 안 했으면 마스터 조인만 고치고 "고쳤다"고 보고했을 것이다.
그 변경도 유지한다(정렬이 사라지는 것은 이득이다) — 다만 그것이 원인은 아니었다.
## 게이트
- --db-patient ①~㉑ 전건 통과(판정 23건 유지 — 결과가 달라지지 않았다)
- dotnet test 336/336 · --edit-smoke 실패 0
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
문서(docs/RESNUM-DECRYPT.md)의 1→2→3 을 실행. 태그 7종 → 11종.
## 이 DB 가 암호화 병원이다
진단이 읽은 설정: 전체=Y 주민번호=Y.
앞 커밋에서 PAT_주민번호 를 뺀 판단이 맞았다 —
PatResNum 을 그대로 썼으면 <b>암호문이 종이에 찍혔다</b>.
## 앱은 키를 갖지 않는다
복호화는 오라클 패키지가 한다.
SELECT SUBSTR(:e,1,7) || CRYPTO_AES256.DEC_AES(SUBSTR(:e,8,256)) FROM DUAL
바인드 변수이고 접근 통제는 DB 권한이다. SCHILDREN 갈래(UDF_GETDECRESNUM_damo)도 함께 옮겼다.
앞 7자리가 평문이라 SUBSTR(...,1,7) 을 이어 붙이는 것을 빠뜨리면 앞자리가 사라진다.
## 설정을 못 읽으면 값을 만들지 않는다
"암호화 아님"으로 가정하면 PatResNum 을 평문처럼 써서 암호문을 찍는다.
PatientIdentity 는 예외 시 빈 문자열을 돌려주고 사유만 로그에 남긴다 — 값은 남기지 않는다.
주민번호를 만드는 자리를 한 곳으로 좁혀 뒀다(만드는 경로가 여럿이면 어디서 새는지 추적할 수 없다).
## 순수 계산은 표로 고정했다 (테스트 13건)
보정 ①(기관기호, SRCH·JINJU): 앞 6자리를 PatBthDay 로 갈아 끼운다.
원본은 If Not DateValidCheck(Substring(1,6)) 으로 감싸 있지만 <b>그 조건은 항상 참이다</b> —
DateValidCheck 는 길이가 8·12·14·17 이 아니면 무조건 False 인데 6자리를 넘긴다.
여기서 "고쳐서" 진짜 날짜 검사를 넣으면 레거시와 다른 번호가 나온다(이 DB 가 SRCH 다).
이식은 동작을 옮기는 일이지 바로잡는 일이 아니다 — 그 사실을 테스트로 못 박았다.
보정 ②(외국인): 7번째 자리 0/9 를 PatBthDay 세기로 6/8·5/7 로. 국적코드가 있어야 걸린다.
성별: 7번째 자리 mod 2. PAT_성별 은 CoiCalCod 가 "24" 로 시작하면 PatSexTyp 을 먼저 쓴다.
## 추측하면 걸린다
M_DtsMst 컬럼을 DtsTabCod·DtlCod 로 추측해 ORA-00904 를 맞았다.
실제는 DtsTblCod·DtsDtlCod·DtsCod 다(dtCommonLib.vb:1136).
## 확인한 것과 못 한 것을 갈라 적는다
진단은 HIS 사용자 문맥 없이 도므로 병원코드가 비고, SRCH 기관기호 보정 갈래는
이 경로로 확인되지 않는다. 통과로 세지 않고 [미확인] 줄로 적는다 —
그 갈래는 순수 함수 테스트가 덮는다.
진단 리포트에 <b>값을 쓰지 않는다</b> — 길이와 "전부 숫자인가"만 적는다. 리포트는 파일로 남는다.
## 아직 안 한 것
나이(AgeCheck)는 병원별 규칙과 의료급여 전산관리번호 규약이 붙어 별도 작업이다.
개인정보 접근 로그(C_PdrInf)에 쓸지는 운영 DB 쓰기라 승인 전까지 하지 않는다.
## 게이트
- dotnet test 336/336 (주민번호 판정 13건 신규)
- --db-patient ①~㉑ 전건 통과 (⑲ 설정 · ⑳ 13자리 · ㉑ 성별)
- --edit-smoke 실패 0 (이름 검사가 파생 태그 4종까지 본다)
- --dialog-shots FAIL 0
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
전 커밋까지 환자 조회·시각 판정·권한 규칙은 다 있었지만 화면에 붙은 것이
하나도 없었다. 미리보기를 열면 환자 선택이 없고, 태그는 전부 사유만 찍혔다.
## 만든 사슬
PatientPickerDialogView(모달) → PatientContextStore(문맥 5행)
→ PatientTagResolver(태그 → 컬럼) → PreviewWindow.UsePatient → 종이
모달로 둔 이유: 30초 안에 끝나는 자족적인 일이고 권한 문이 걸린 동작이다.
사이드 패널로 두면 실제 환자 정보를 꺼내는 창이 항상 열려 있게 되는데
이 창은 열려 있는 것 자체가 비용이다.
환자와 내원을 한 화면에 둔다. 고르는 대상은 환자가 아니라 내원이다 —
문맥 5행이 전부 ComNum + AdpDtm 으로 읽히므로 환자만 골라서는 아무것도 못 읽는다.
기준 시점(마지막/처음)을 화면에 노출한다. 입원 내원의 진료과·병실·자격 값을
실제로 바꾸는 값이라 숨기면 값이 왜 다른지 설명할 수 없다.
문맥 읽기를 창 안에서 한다. 닫고 나서 읽으면 실패를 미리보기 창에서 알려야 하고
그때는 이미 고른 것이 사라진 뒤다.
## 매핑은 확인된 6종만
PAT_이름 / PAT_차트번호 / PAT_주민번호 / PAT_휴대전화 / PAT_전화번호 / PAT_주소.
전부 레거시 조회 SQL 의 SELECT 목록에서 컬럼을 직접 확인한 것이다.
PAT_나이·PAT_성별은 넣지 않았다 — UDF_GETAGE·UDF_GETSEX 를 거치는 값이라
컬럼을 짐작해 넣으면 값이 조용히 틀린다. 종이에 그럴듯한 값이 찍히면
아무도 의심하지 않으므로 빈칸보다 나쁘다.
안 옮긴 환자 태그는 "아직 옮기지 않았습니다"라고 말한다.
"DB 접속이 없습니다"로 뭉개면 환자를 골랐는데도 그 사유가 나와 접속을 의심하게 된다.
## 문맥 조회를 한 번에 다섯 번 읽는다
레거시는 5행을 프로퍼티 접근 시점에 하나씩 지연 로딩한다(각각 별도 왕복).
태그마다 다시 읽으면 태그 40개짜리 서식에 왕복 40회가 된다.
## 개인정보
- 감사 로그는 고른 직후 남긴다 — 종이에 값이 찍히는 것과 무관하게 조회는 이미 일어났다.
- 로그에 환자 이름을 남기지 않는다(내원번호로 추적 가능하고, 로그가
개인정보를 들고 있으면 로그 자체가 위험물이 된다).
- 진단 리포트도 값 대신 길이만 적는다.
- 권한 문은 수정(TK_MODIFY)과 같은 범위다. 넓히면 서식생성기가 환자 조회 도구가 된다.
- 화면에서 감사 기록 사실을 말한다.
## 게이트가 잡은 것 셋
① --db-patient 가 "전건 통과"인데 ⑧~⑬ 이 통째로 SKIP 이었다.
첫 환자에게 내원이 0건이라 문맥 조회를 한 번도 안 돌린 채 초록불이었다.
내원 있는 환자를 30명까지 훑고, 못 찾으면 SKIP 이 아니라 FAIL 로 바꿨다.
② 그렇게 찾은 내원에서 CodInf·CoiInf·CowInf 가 셋 다 0행이었다.
0 을 적어 두는 것으로는 "조건이 어긋났다"와 "행이 없다"를 못 가른다.
조건 없는 원본 행을 함께 찍게 하니 후자였다(1988년 내원, 행 자체가 없음).
③ 그러면 기간 조회 세 갈래는 아직 한 번도 행을 돌려준 적이 없다.
안 돌아본 갈래는 통과한 갈래가 아니므로, 각 표에서 직접 내원을 찾아 들어가
BETWEEN·MtiSeq 조건이 실제로 행을 뽑는 것까지 확인하게 했다(⑮).
--edit-smoke 판정 7건 추가. 전부 대조군이 있다 —
환자 없이 같은 종이를 찍어 사유가 나오는 것을 먼저 확인한 뒤 값으로 바뀌는 것을 본다.
미리보기 창 배선도 따로 본다(붙이면 값, 떼면 사라짐). 그래서 고르는 일과
붙이는 일을 나눠 두었다 — 모달에 묻어 두면 "골랐는데 종이가 그대로"를 자동으로 못 잡는다.
## 게이트
- dotnet test 312/312
- --edit-smoke 실패 0 (환자·미리보기 판정 7건 추가)
- --dialog-shots FAIL 0 (08b-patient-picker 추가)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 (변화 없음)
- --db-smoke 1,271건 diff 0
- --db-patient ①~⑮ 전건 통과 (기간 조회 3갈래 실제 통과 확인)
- --db-modify-smoke S999 8/8 (⑤ 는 E_SctMst 0행으로 여전히 SKIP)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
미리보기 레거시 호환 계획 A3 의 조회 계층. --db-patient 로 실DB 검증까지.
<b>레거시에서 옮기지 않은 것 셋</b>(dtPatVisitList.vb:42, 133):
① <b>바인드 변수가 하나도 없다.</b> 검색어를 SQL 문자열에 그대로 잇고 따옴표 이스케이프도 없다 —
성명 칸에 작은따옴표를 넣으면 그대로 SQL 이 된다. 전부 OracleParameter 로 바꿨고
와일드카드는 SQL 리터럴이 아니라 <b>파라미터 값</b>에 붙인다.
② <b>행 제한이 없다.</b> ROWNUM/FETCH 어디에도 없어서 성명 한 글자면 전 환자가 클라이언트로 온다.
ROWNUM 상한 200 을 건다.
③ <b>TRIM 정규화가 없다.</b> CHAR 고정폭 잔여 공백에 걸린다.
<b>컬럼을 다 가져오지 않는다.</b> 레거시 SELECT 는 도로명·지번 주소를 CASE 로 조립하고
M_ZipMst 를 조인해 화면 12열을 채운다. 미리보기 환자 선택에 필요한 것은 사람을 특정할 최소한이다 —
개인정보는 덜 읽는 쪽이 낫다. 주민번호도 UDF_GetMaskedResNum 결과만 읽는다
(레거시는 암호화 병원에서 복호화한 평문을 그리드에 그대로 띄운다).
내원 목록의 <b>금액 6열은 옮기지 않았다</b> — 레거시도 값을 받지 못해 화면에서 늘 비어 있다.
기간 PK 마스터(M_DepMst·M_UidMst·M_InsMst)는 ROW_NUMBER … Rn = 1 로 접는다.
안 접으면 같은 내원이 여러 번 뜬다 — 검사 ⑦ 이 그것을 본다.
<b>UID 는 Oracle 내장 함수다.</b> 의사 마스터 별칭을 Uid 로 두었더니 ORA-00909(인수 개수 오류)가 났다 —
Uid.Nam 을 함수 호출로 파싱한다. 컴파일은 통과했고 실DB 에서만 드러났다.
--db-patient 가 없었으면 환자를 고르는 화면에서 처음 터졌을 것이다.
실측(테스트 접속): 성명 '김' 200건(상한), 네 갈래 전부 예외 없음,
빈 검색어 0건, 주민번호 1글자 0건, 따옴표 주입 0건, 상한 동작, 내원 중복 없음 — 7/7.
게이트: 테스트 303/303, --edit-smoke 0실패, --dialog-shots FAIL 0,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.
<b>남은 것</b>: 환자 선택 UI, ComNum→문맥 5행(P_PatInf/P_ComInf/P_CodInf/P_CoiInf/P_CowInf)과
적용일시(AdpDtm) 결정 규칙, 그리고 그 문맥을 쓰는 태그 해석기.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ModifyDesign 은 운영 테이블에 쓰는데 한 번도 실행해 본 적이 없었다.
저장 경로에는 이 검증이 있고(RunSaveSmoke) 수정 경로에만 없었다 —
"코드는 맞아 보이는데" 상태이고, 이 저장소에서 그건 여러 번 틀렸다.
--db-modify-smoke <서식코드> <리포트>. 단정 8건:
① 활성 SdgKey 가 바뀌지 않는다 — EMR 이 참조하는 키 유지가 이 동작의 존재 이유다
② 활성 행이 새 디자인으로 갱신된다 (문구를 실제로 바꿔서 본다 —
무변경 재저장으로는 "갱신됐다"를 증명할 수 없다)
③ 이력 행이 하나 생기고, SdgDelYon='Y' 이고, <b>옛</b> XML 을 담는다
(저장과 방향이 반대다. 뒤집혔으면 ③-c 가 잡는다)
④ 감사 컬럼이 활성 행에 기록된다 — 레거시는 이걸 이력 행에 썼다(버그, 미복제)
⑤ E_SctMst 를 건드리지 않는다
+ 원복 2건
S999 실측: SdgKey 52451 유지, 버전 511 → 512 → 511, 감사 202608120855 → 202608180917.
<b>DB 를 원상태로 되돌린다.</b> 활성 행의 XML·감사값을 원문으로 UPDATE 하고
이 진단이 만든 이력 행을 지운다. DELETE 에 SdgDelYon='Y' 를 함께 걸어
<b>SdgKey 를 잘못 짚어도 활성 행은 어떤 경우에도 지워지지 않게</b> 했다.
<b>⑤ 가 공허하게 통과하고 있었다.</b> S999·P062 둘 다 E_SctMst 가 0건이라
"0 → 0" 을 비교했는데 PASS 로 찍혔다. 읽는 사람은 검증됐다고 믿는다.
행이 0건이면 SKIP 으로 적고 "행이 있는 서식으로 다시 돌려야 한다"를 남긴다.
이번 세션에만 같은 함정에 네 번 걸렸다 — 대조군 없는 0 은 증거가 아니다.
<b>남은 것</b>: ⑤ 를 실제로 재려면 E_SctMst 행이 있는 서식이 필요하다.
테스트 접속에서 아직 못 찾았다.
게이트: --db-modify-smoke 8/8(⑤ SKIP), 테스트 298/298, --edit-smoke 0실패,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
레거시 TK_MODIFY('기록지 수정'). SheetMe 에는 저장만 있었고 이것이 없었다.
DbSmoke.cs:1072 가 그 부재를 이미 적어 뒀다 — 실측 ShtCneYon='Y' 40건, 현역 19건.
<b>먼저 내 앞선 설명을 고친다.</b> 나는 수정이 ShtCneYon 제자리 갱신 경로라고 했는데 틀렸다.
수정 경로는 ShtCneYon 을 <b>읽지도 않는다</b>. 그 컬럼은 저장이 버저닝이냐 제자리냐만 가른다.
저장과 수정은 이력 방향이 반대다.
저장: 옛 행을 SdgDelYon='Y' 로 내리고 <b>새 행이 활성</b>이 된다(SdgKey 바뀜)
수정: <b>활성 행을 제자리에서 갱신</b>하고 옛 XML 을 담은 새 행이 이력이 된다(SdgKey 유지)
SdgKey 가 유지되므로 EMR 이 참조하던 키가 그대로다 — 그게 이 동작의 존재 이유다.
E_SctMst 를 건드리지 않는다(레거시 :170-174 주석 처리). 그것이 "글자만 수정 가능"이라는
경고문의 기술적 근거다 — 기존 매핑 행은 SdgKey 가 그대로라 유효하지만 새 컨트롤은 행이 안 생긴다.
<b>레거시 버그 둘은 복제하지 않는다.</b>
① 감사 컬럼이 뒤집혀 있었다 — 활성 행에는 아무것도 안 쓰고 <b>옛 디자인을 담은 이력 행</b>이
현재 사용자·시각을 받았다(:162-163). 이력 패널이 "누가 언제 이 버전을 만들었는가"를 거꾸로 보여 준다.
활성 행에 기록하고 이력 행은 원래 값을 지킨다.
② Rows(0) 을 개수 검사보다 먼저 읽어 활성 행이 없으면 IndexOutOfRange 였다(:150).
먼저 확인하고 "수정 대신 저장을 쓰세요"로 안내한다.
<b>새 컨트롤은 막는다.</b> 레거시는 산문으로만 경고하고 막지 않았다 —
경고문을 읽지 않으면 그대로 번진다. SctMstXmlWalker 로 문서의 컨트롤 이름을 뽑아
활성 SdgKey 의 E_SctMst 행과 대조한다. 판정 근거를 저장이 행을 만드는 규칙과 같은 것으로 통일해서,
"행이 원래 안 생기는 종류"를 새 컨트롤로 오인하지 않는다.
<b>권한 게이트는 유지한다.</b> 레거시가 이 버튼만 전산실에 묶어 둔 이유가 위와 같다
(frmSheetDesigner.vb:1029-1060, 주석 '전산실만 사용가능'). 저장 버튼에는 그런 분기가 없다.
규칙을 Core 의 순수 함수로 옮겨 표로 고정했다 — 병원·부서 조합이 세 갈래라
화면에서 즉석 판정하면 어느 갈래가 왜 막혔는지 확인할 방법이 없다.
막힌 이유도 갈래마다 다르게 말한다("권한 없음"만으로는 누구에게 물어야 할지 모른다).
<b>HspStrDte 는 세션에 없다.</b> 레거시는 그 값으로 "2025-05-01 이후 개원 병원은 사내 계정만"을 가른다.
빈 문자열로 두면 그 갈래가 안 걸리고 부서 규칙(EDPS)으로 떨어진다 — 레거시의 다수 경로와 같다.
없는 값을 지어내 더 조이지 않고, 이 사실을 코드와 테스트에 적어 뒀다.
게이트: 테스트 298/298(신규 7), --edit-smoke 0실패, --dialog-shots FAIL 0 대조군 4/4,
--modal-check 0실패, --cleartype 11/11, --maxrect 0실패, --scale-budget 5/5, 빌드 경고 0,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.
<b>실DB 수정 경로는 아직 안 돌려 봤다</b> — 운영 테이블에 쓰는 동작이라 --db-* 진단으로
왕복 검증을 붙이는 것이 다음 일이다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
앞 커밋의 판정표를 화면에 붙였다. 우측 정보 패널에 '이 태그의 값' 한 칸.
'미리보기'라 부르지 않는다. 실제로 값이 나오는 것은 384종 중 16종뿐이라
그렇게 이름 붙이면 나머지 368종이 고장 난 것처럼 보인다. 여기서 보여 주는 것은
값이거나 사유이고, 사유 쪽이 대부분이며 실은 그쪽이 더 쓸모 있다 —
ETC_요양기관명칭_병원명(실사용 3위)이 왜 여기서 값이 안 나오는지는 지금 어디서도 알 수 없다.
TagPreviewStore 를 만들었다. 왕복 2회로 끝난다 — 서버 시각 1회, M_UIDMST 1행 1회.
세션에 이미 있는 것(사용자 코드·이름) 말고 없는 것은 연락처 두 칸뿐이라 그 줄만 더 읽는다.
재료는 창당 한 번만 읽는다: 태그를 고를 때마다 치면 목록을 화살표로 훑는 동안
초당 몇 번씩 왕복한다.
지킨 것:
· 고정 SQL 만. 사용자 입력이 SQL 에 닿는 경로를 만들지 않는다 — 이 창은 임의 쿼리를 돌리는 곳이 아니다.
· CommandTimeout 5초. 이 저장소에 CommandTimeout 이 0건이었는데 ODP.NET 기본은 무제한이다.
· 실패는 삼킨다. 미리보기가 안 되는 것과 태그를 못 고르는 것은 다른 일이다.
· DB 접속이 없으면 줄 자체를 감춘다 — 진단 스크린샷이 DB 없이 이 창을 띄우므로
이 가드가 없으면 거기서 늘 '값을 읽지 못했습니다'가 찍힌다(--dialog-shots 57파일 그대로).
· 값이 비는 두 갈래를 가른다. '읽지 못했다'와 '이 계정에 값이 없다'는 다른 일이다.
ServerClock 을 internal 에서 public 으로 열었다(다른 변경 없음).
게이트: 테스트 277/277, --edit-smoke 0실패, --modal-check 2/2, --dialog-shots 57파일,
--db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
색칠만으로는 편집기라 하기 어렵다. 실제로 쿼리를 칠 때 손이 가는 곳을 메웠다.
■ 자동완성 (Ctrl+Space / 치는 대로)
문맥을 보고 후보를 바꾼다(SqlCompletion — Core 순수 함수라 테스트로 고정했다):
· << 직후 → 치환 변수 51종. 이 편집기에만 있는 문법이라 외워서 칠 수 없다.
고르면 알맹이를 넣고 닫는 >> 를 붙여 준다.
· FROM/JOIN 뒤 → 테이블·뷰 이름(운영 스키마에서 2,494건)
· 별칭. 뒤 → 그 테이블의 컬럼. 별칭은 같은 쿼리 FROM 절에서 푼다
(FROM P_PatMst p → p. 는 P_PatMst 의 컬럼).
· 그 밖 → 예약어·함수·테이블
일반 위치에서 한 글자도 안 쳤으면 목록을 띄우지 않는다 — 아무 데서나 뜨면 방해가 된다.
테이블 목록은 처음 한 번만 읽고, 컬럼은 테이블별로 캐시한다(매번 조회하면 입력이 끊긴다).
DB 가 없으면 예약어·함수·변수만 나오고 나머지는 조용히 빠진다.
별칭 자리에 예약어가 오면 별칭으로 보지 않는다 — FROM P_PatMst WHERE 에서 WHERE 를
별칭으로 잡으면 엉뚱한 테이블의 컬럼을 제안하게 된다.
■ 검증 실행 (F5)
저장 전에 실제로 돌려 본다. 치환 변수는 디자인 시점에 값이 없으므로 런타임과 같은 방식으로
빈 문자열로 바꾼 뒤 실행한다(clsMDataTable.vb:67-71). 목적은 데이터 확인이 아니라
테이블·컬럼 오타와 구문 오류를 배포 전에 잡는 것이다 — 지금까지는 EMR 런타임에서야
처음 실행됐고 실패해도 빈 catch 가 삼켰다.
성공하면 컬럼 목록과 표본 행(최대 20)을 보여 주고, 실패하면 ORA 메시지를 그대로 띄운다.
안전 장치: SELECT/WITH 로 시작하는 조회만 실행한다. 운영 DB 에 붙을 수 있는 도구이고
편집 중인 텍스트를 그대로 실행하므로 DML·DDL 이 섞이면 되돌릴 수 없다.
앞쪽 주석을 걷어내고 판정하므로 /* SELECT */ DELETE ... 같은 우회도 막힌다.
--db-trial 진단을 신설해 실DB 로 확인했다(10/10 통과):
정상 조회 통과 · 없는 테이블 ORA-00942 · 없는 컬럼 ORA-00904 · 구문 오류 ORA-00936 ·
앞 주석 뒤 정상 통과 · INSERT/DELETE/DROP/주석우회 전부 차단 · 빈 쿼리 차단.
■ 편집 기본기
따옴표·괄호 자동 닫기(선택이 있으면 그 선택을 감싼다) — SQL 에서 짝을 빠뜨리면
런타임에서 조용히 실패하는 종류다.
앞서 넣은 줄번호·Tab 들여쓰기·Enter 자동 들여쓰기·Ctrl+/ 주석 토글은 그대로.
단위 테스트 15건 추가(문맥 판정 6종·별칭 풀기 4종·키워드 별칭 배제·후보 정렬·내장 목록).
회귀: 테스트 211/211, 편집 스모크 실패 0, 검증 실행 점검 10/10,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
레거시 대비 전수조사에서 나온 출시 차단 9건 중, 하루 안에 끝나는 국소 수정 7건.
저장 데이터를 망가뜨리는 ③④ 를 먼저 고쳤다.
③ 페이지 복제·추가가 루트 이름을 중복 발급했다
Clone 이 Root.Id 까지 베끼는데 자식만 재발급해서 한 서식에 MDesignerHost1 이 둘 생겼다.
AddPage 도 '페이지 수 + 1' 로 지어서 루트가 1·3 인 2장짜리 서식에서 곧바로 3 이 겹쳤다.
E_SctMst 동기화는 SctObjNam 사전이라 둘째 루트가 첫째 루트 행에 매핑되고,
그 페이지 자식들의 SctParObj 가 없는 ObjID 를 가리킨 채 남는다.
→ 두 경로 모두 RenamePageRoot 로 이름과 Name 속성을 함께 새로 받는다.
④ 이름 채번이 빈 번호를 메웠다
TextBox3 을 지우고 새로 놓으면 다시 TextBox3 이 나왔다. 이름은 저장된 답변
(E_SctDta.SctObjNam)이 붙는 열쇠라서, 지운 칸의 과거 입력이 새 칸에 그대로 나타난다.
액션 대상과 CalcBox 수식도 이름으로 서로를 가리키므로 조용히 오배선된다.
→ 레거시 clsNameCreationService 규약대로 '사용 중 최대번호 + 1'.
숫자가 아닌 접미사(TextBoxTotal)는 번호로 보지 않는다.
① 탭순서 적용이 문서 전체를 밀었다
한 페이지를 편집해도 전 페이지 컨트롤이 999999+i 로 초기화됐다.
레거시는 페이지마다 별도 디자인 서피스라 다른 페이지는 건드리지 않는다.
→ 초기화 범위를 이번에 클릭한 페이지로 한정.
② 이력본이 열린 상태에서 현재본을 열면 이력 탭이 활성화됐다
FindOpenDbDocument 가 ShtCod 만 보고 있었다. 사용자는 현재본을 고친다고 믿은 채
읽기 전용 스냅샷을 편집하게 된다. → HistorySdgKey is null 조건 추가.
⑤ 레거시에 없는 태그 3종을 고를 수 있었다
PAT_BMI · PAT_IBW · PAT_IBW_퍼센트 는 bzDataInterface.vb:922/957/970 에서
주석 처리된 채로 남아 있어 런타임에 해석되지 않는다. 고르면 배포 후에야 빈칸으로 드러난다.
→ 카탈로그에서 제거(값은 자유 입력이라 기존 서식에는 영향 없다).
⑥ 신규 서식 등록이 ShtHspYon·ShtExpDte 를 비웠다
--db-shtmst 진단을 신설해 실측한 결과 운영 E_ShtMst 1,737행 중 두 컬럼이
NULL 인 행은 0건이다. ShtExpDte 는 dtEMRLoader.vb:1229 가
NVL(ShtExpDte,' ')='29991231' 로 못박아 거르고, 여러 조회가
sysdate BETWEEN ShtAdpDte AND ShtExpDte 를 쓴다 — NULL 이면 BETWEEN 이 거짓이라
등록은 성공했는데 EMR 어디에서도 안 보이는 서식이 된다(29991231 이 1,718행 98.9%).
ShtHspYon 은 기록지정보·자동생성 목록이 'Y' 로 거르며, 디자이너가 만든 서식
169건 중 160건이 'Y'. → 두 값을 INSERT 에 넣는다.
⑦ 빈 선택 상태의 Ctrl+드래그가 예외로 죽었다
Ctrl/Shift 클릭은 선택을 바꾸지 않고 업에서 토글하므로 주선택이 빈 채 드래그가
시작될 수 있는데, BeginMove 가 Selection.Primary! 를 그대로 넘겨
RootAncestorOf 가 즉시 역참조했다. → 앵커를 Primary ?? 첫 선택으로 완화.
편집 스모크 9건 추가(루트 이름 유일 2 · 채번 4 · 탭순서 페이지 격리 1 · 빈 선택 드래그 1 · 태그 3종 부재 1).
채번 검사는 '가운데 번호'를 지운다 — 최대 번호를 지우면 최대값 자체가 내려가므로
같은 이름이 다시 나오는 것이 레거시와 같은 정상 동작이다.
회귀: 테스트 124/124, 편집 스모크 169건 실패 0,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일(HEAD 대비 md5 일치).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
사용자 지적: "서식 목록 구분없이 하나로 다 나오는거 불편한데 묶여서 나와야해".
[기준 선정] 후보 컬럼 7종을 실DB로 전수 집계해(신규 --db-cls) ShtClsCod 를 택했다. 이것만이
판정 3기준을 통과한다 — 그룹 24종, 최대 그룹 269건(16%), 빈값 1건(0.06%). 나머지는 전부 한쪽
쏠림이다: ShtTyp D 79% / ShtPatTyp C 92% / ShtLstTyp NONREC 88% / ShtDepCls 빈값 74%.
의미상으로도 ShtClsCod 만 '분류'이고 나머지는 동작 규칙이다(ShtWrtTyp=1장 제약, ShtUsrTyp·
ShtPatTyp=작성 게이트, ShtOprTyp=.NET 클래스명, ShtDspSeq=정렬키).
무엇보다 이건 사용자가 이미 아는 축이다. 레거시에서 서식생성기를 여는 화면(기록지정보 fmShtMst)
상단이 `[ 분류별 전체 ]` + 분류별 탭이고, EMR 기록지 트리 1단 헤더도 같은 값이다. 재교육 비용 0.
[라벨] 코드→한글명은 M_DtlMst 의 DtlTblCod='EMR_SHEET_GROUP' 이 소유한다. 소스 어디에도 매핑표가
없고 병원별 마스터라(HspCod 분기가 레거시에 다수) 하드코딩하면 다른 병원에서 즉시 오답이 된다.
런타임 조인만 쓴다. 실측: 마스터 24행 ↔ 서식 24코드 완전 일치, 고아 코드 0건.
[조인 방향] E_ShtMst 드라이빙 + LEFT JOIN 이다. 레거시 EMR 트리(bizSheetList.vb)는 M_DtlMst 를
드라이빙으로 써서 분류 미지정 서식이 목록에서 통째로 사라지는데, 열기 목록에서 그건 "그 서식을
영영 못 여는" 장애다. 기록지정보 화면 방식(dtShtMst.vb:75-81)을 따랐다.
[조인 증식 방어] M_DtlMst 는 PK 에 DtlStrDte 가 들어가 같은 코드가 기간별 다중행일 수 있다.
그대로 조인하면 서식이 목록에 두 번 뜬다. 실측상 현재 증식 0건이지만 그건 데이터 운이지 쿼리가
막은 게 아니므로, ROW_NUMBER 로 적용일 최신 1행만 택하게 했다.
[그룹 순서] LPAD(DtlDspSeq,4,'0') 우선, 그다음 코드순, 미분류는 맨 뒤. 병원이 정한 표시순서가
1순위여야 레거시 분류 탭과 같은 자리에 뜬다. 이 병원은 DspSeq 가 전부 비어 있어 결과적으로
코드순(A~X)이지만, 채워 쓰는 병원에서 어긋나지 않는다. LPAD 는 레거시의 2자리 대신 4자리 —
'100' 과 '99' 가 뒤집히는 것을 막는다.
[상한 300 → 2,000] 종전 300 상한은 정렬이 분류순으로 바뀌면서 성격이 달라진다. 코드순 300 은
고르게 잘렸지만 분류순 300 은 뒤쪽 분류가 통째로 사라져 "그 분류가 없는 것"처럼 보인다.
모집단 1,629건이라 이제 전량이 온다. 도달 시 하단에 잘렸다고 명시한다(조용한 절단 금지).
[분류명도 검색 대상] 헤더에서 본 '간호기록'을 그대로 쳤는데 0건이면 배신감이 크다.
LIKE 토큰에 DtlCodNam 을 추가했다.
[접기 상태] 기본 접힘 + 검색 시 자동 펼침. 다만 레거시는 서식이 꽉 찬 평면 목록으로 시작하므로
접힌 첫 화면을 낯설어하는 사용자가 있다 — 전체 여닫기 토글을 검색칩 옆에 두고, 그 선택을
%LocalAppData%\SheetMe\prefs.json 에 보존한다(신규 UserPrefs). 매 실행마다 다시 펴지 않아도 된다.
[WPF 함정] 그룹핑을 걸면 WPF 는 기본적으로 가상화를 끈다 — 1,600여 항목이 한꺼번에 실체화돼
탭 전환이 눈에 띄게 멈춘다. IsVirtualizingWhenGrouping + VirtualizationMode=Recycling 필수.
그룹 키를 문자열이 아니라 SheetGroupViewModel 인스턴스로 둔 것도 의도다 — CollectionViewSource 가
값 동등성으로 묶으므로 코드당 같은 인스턴스를 주면(SheetGroupRegistry) 헤더에서
Name.IsExpanded 를 TwoWay 로 바인딩해 접기 상태를 VM 이 소유할 수 있다.
[열기 대화상자] 같은 ListSheets 를 쓰는 SheetOpenDialogView 는 그리드라 트리 대신 '분류' 열을
추가했다(같은 정보, 그 화면에 맞는 형태).
[진단 모드 신설] --db-cls (분류 후보 분포 + 라벨 마스터 + 조인 증식 검사 + 고아 코드 + 모집단),
--db-tables <패턴> (이름 모르는 마스터 테이블 탐색). 둘 다 읽기 전용.
검증: 테스트 124/124, edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0,
**종이 렌더 P062 픽셀 대조 차이 0**.
실 UI: 24분류 한 화면 · 의사기록 펼침 266건 · '경과기록' 45건/6분류 자동 펼침 ·
'간호기록' 204건/4분류(분류명 매칭 동작 확인).
별건 보고: 목록 모집단에 폐기 서식(ShtUseYon='N')이 452건(28%) 섞여 그룹 건수를 부풀린다.
레거시 서식생성기도 이 필터를 걸지 않으므로 이번엔 동작을 바꾸지 않았다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
[사용자 컨텍스트] 감사 컬럼에 Windows 계정명이 들어가던 6지점을 HIS UidCod 로 교체했다.
레거시 이력 화면(ucSheetHistory.vb:51)이 SdgUidCod 를 수정자로 표시하므로 지금까지는
감사 추적이 끊겨 있었다.
- UserContextStore — M_UidMst 단독 조회로 인증 판정(레거시 bzLogIn 자동로그인과 동일 의미론,
비밀번호를 보지 않고 행 존재 + 유효기간만 확인). 부서/병원은 LEFT JOIN best-effort 표시용.
적용일자는 서버 시각을 쓴다 — 단말 시계가 밀리면 유효 계정이 거부될 수 있다.
- UserSession — 정적 set-once. 생성자 주입을 택하지 않은 이유는 진단 모드(--db-*)가
ViewModel 그래프 밖이라 주입으로는 감사 기록 지점에 닿을 수 없기 때문이다. 대신 읽는 곳을
App 초기화 / FormDesignDataBusiness / MainViewModel 표시 세 군데로 제한했고, 저장소는
지금처럼 uid 를 파라미터로만 받는다. 진단 모드는 "SMOKE" 식별자를 쓴다.
- StartupArguments — 레거시 규약 "UidCod,ComNum,ShtCod" 관용 파싱(PadRight 후 split) 이식.
ComNum 은 레거시에서도 선언·대입뿐인 죽은 인자라 보존만 하고 쓰지 않는다.
- App.OnStartup 재구성 — 진단 플래그 12종을 RunDiagnostic 으로 분리(각 분기가 종료 코드를
반환하도록 정리), 세션 확정 후 MainView 표시. 기동 인자에 ShtCod 가 있으면 PendingSheetCode
로 넘겨 빈 새 문서를 만들지 않고 그 서식만 연다(열기 실패 시에는 빈 문서로 폴백).
- 인자로 받은 UidCod 조회에 실패하면 기동을 중단한다(레거시 :892 와 동형). 인자 없이 단독
실행하면 미인증으로 계속하되 DB 쓰기를 차단하고, 개발 편의는 His:DevUidCod 설정으로 뺐다.
[권한 게이트] E_ShtMst.ShtUsrDesYon — 'Y' 또는 미설정이면 허용, 그 외만 거부(레거시
frmSheetDesigner.vb:71-84 규칙, 거부 문구도 그대로). 레거시는 게이트가 열기 진입점마다
흩어져 있어 이력 노드 클릭·MessageQueue 두 경로로 우회가 가능했는데, SheetMe 는 모든 열기가
OpenDbSheet 로 수렴하므로 거기 한 곳이면 구조적으로 우회가 불가능하다. 추가로 이력 열람과
DB 저장에도 넣었다 — 후자는 레거시에 아예 없던 구멍이다(열기 이후 플래그가 꺼질 수 있고,
파일에서 연 문서를 DB 에 저장하는 경로는 열기 게이트를 거치지 않는다).
목록에서 거르지 않고 잠금 아이콘으로 표시한다(안 보이면 사용자가 진단할 수 없다).
--db-gate 진단 신설 — 실측 결과 차단되는 디자인 보유 서식 0건(N 은 1건뿐이고 디자인 없음).
[서버 시각] ServerClock 신설(SELECT SYSTIMESTAMP 왕복 1회로 8/12/17자리 확보, 포맷은
invariant 로 C# 에서 — NLS 의존 제거). 감사 정확성뿐 아니라 ResolveActiveSdgKey 가
ORDER BY SdgUpdDtm 으로 활성 버전을 고르기 때문에, 시계가 뒤로 밀린 단말이 저장하면
활성 버전 판정이 뒤집힐 수 있었다. Reorder 는 루프 밖 1회로 바꿔 한 번의 순서 변경이
행마다 다른 시각을 남기지 않게 했다. 실패 시 폴백하지 않는다.
RecordWordStore.Remove 에 auditUid 인자 추가 — 남길 컬럼은 없지만(런타임 조회 쿼리에
ShtStt 필터가 없어 논리 삭제로 바꾸면 지운 문구가 계속 노출된다) "모든 쓰기 API 는 uid 를
명시한다"는 규약을 컴파일 타임에 강제한다. 상용구 대화상자는 uid 를 생성자로 받고,
미인증이면 읽기 전용으로 연다.
검증: 사전 실측으로 M_UidMst/M_DepMst/M_HspMst 접근 확인(2단계 최대 리스크 해소, 유효
사용자 539명). 테스트 70/70, edit-smoke 실패 0(기동 인자 파싱 8건 추가), 왕복 1,271건
diff 0/예외 0, db-save-smoke 제자리 갱신 통과, db-word-smoke 통과.
DB 직접 확인: P163 저장분 SdgUidCod='SMOKE', SdgUpdDtm 이 서버 시각과 일치
(변경 전 저장분 S999 는 'Msystech' 로 대비됨).
남은 항목: 단일 인스턴스 + Named Pipe IPC 는 미구현. 기동 인자·게이트가 먼저 필요했고,
바로가기 운영에서는 중복 실행 억제가 덜 급하다. 다중 사용자 병행 운영 전에 처리한다.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ShtCneYon='Y' 서식은 기존 이름 행을 continue 로 건너뛰기만 해서, 라벨 문구를
고쳐도 항목사전(SctObjTxt)이 옛 문구로 남았다. 이 값은 외부인터페이스·환자전달사항·
알람이 리터럴로 매칭하는 조회 키라 조용한 데이터 불일치가 된다.
레거시 bzSaveSheetDesignNControlInfo.vb:378 과 동일하게 SctObjTxt 만 덮어쓴다:
- SctObjTyp/SctParObj/SctObjSeq/SctObjID 는 갱신하지 않음(SctKey 보존 의미론)
- 삭제된 컨트롤의 잔존 행도 지우지 않음(레거시 동일)
- SctKey 기준 UPDATE — 동명 중복 오염 데이터에서 다중 행 덮어쓰기 방지
- Spread AsTemplate 항목 행은 ObjName 이 항목 텍스트라 컨트롤 이름과 분리해 매칭.
레거시의 매 저장 중복 INSERT 버그는 복제하지 않는다(런타임이 개수를 세지 않음).
--db-save-smoke 에 문구 변경 검증 추가 — 무변경 재저장으로는 이 경로를 증명할 수
없으므로 실제로 Text 를 바꿔 SctObjTxt 반영·SctKey 보존을 확인하고 원복한다.
검증: P163(ShtCneYon=Y) 2회 연속 통과(행 6/6 안정, SctKey 보존, 원복 확인),
S999(버저닝) diff 0 + 이전 버전 SdgDelYon='Y', 전수 왕복 1,271건 diff 0/예외 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
레거시 서식생성기(VB.NET WinForms) 대체용 C#/.NET 10 WPF 디자이너.
기준선: 실DB 활성 디자인 1,271건 왕복 의미론 diff 0 / 예외 0, 단위 테스트 49/49.
이 커밋에 함께 포함된 자격증명 분리:
- appsettings.json 을 __HOST__/__PASSWORD__ 플레이스홀더로 전환
- 실접속 정보는 appsettings.Development.json 으로 분리(.gitignore 제외,
csproj Debug 조건부 복사라 Release 산출물에 실리지 않음)
- ConfigLoader 를 환경변수 > Development > appsettings 순 레이어링으로 변경,
미치환 플레이스홀더는 '미설정'으로 간주
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>