Commit Graph
100 Commits
Author SHA1 Message Date
MsystechandClaude Opus 5 510663c5fc 태그를 11종 → 19종으로. "전부"의 규모를 먼저 재 봤다
"나머지도 전부 옮기자" — 추측하지 않고 386종을 본문 모양으로 분류했다.

## 실제 분포 (bzDataInterface.vb 386종)

  자기 SQL 을 든 것        232  각각 별도 이식 — 본문 SQL 을 읽고 바인드로 옮기고 실DB 로 확인
  MessageBox 를 띄우는 것    48  대화형이라 미리보기에서 <b>돌 수 없다</b>
  문맥만 읽는 것             48  옮길 수 있는 것
  기타(다른 라이브러리 호출) 55  하나씩 봐야 한다
  HspCod 로 갈리는 것         3  병원별 분기

즉 "전부"는 태그 늘리기가 아니라 <b>232건의 데이터 접근 이식</b>이다.
48종 목록은 docs/TAG-PORTABLE-48.md 에 본문째로 뽑아 뒀다 — 다음 작업이 기계적이 된다.

## 이번에 옮긴 8종

순수 형식 가공만 하는 것들. Computed 표(태그 → 문맥 함수)를 새로 뒀다 —
컬럼 하나로 안 되는 것(두 컬럼을 보거나 잘라 붙이는 것)을 담는다.

  PAT_생년월일_영문 · PAT_생년월일_영문_일월년도   자리만 바꿔 붙인다
  PAT_연락처                                      휴대전화가 있으면 그것, 없으면 자택
  PAT_연락처_뒷4자리 · PAT_휴대전화번호_뒷4자리    -, . 을 걷고 뒤 4자리
  OCM_외래내원일자 · OCM_입원전환일자             ComAcpDtm·ComTrsDtm 앞 8자리
  OCM_입원전환일시                                12자리를 일시 형태로

자리수가 모자라면 <b>빈 값</b>이다. 잘라서 그럴듯하게 만들지 않는다.

## 이번에 뺀 것과 이유

  PAT_우편번호 · PAT_도로명주소 · PAT_지번주소 · PAT_영문도로명주소
    → GetZipInfoByBuildingNumber_Simple 조회가 먼저 필요하다(왕복 추가)
  OCM_담당의사 계열
    → GetCodInfDT + GetUidMst 두 조회를 거치고 입원/외래로 갈린다
  OCM_외래내원일시
    → 별도 헬퍼(외래내원일시)의 형식을 아직 안 봤다

## 게이트

- dotnet test 336/336
- --edit-smoke 실패 0 — 이름 검사가 19종 전부 실제 태그임을 확인
  (지어낸 이름 셋이 조용히 앉아 있던 적이 있어서 이 검사가 있다)
- --db-patient ①~㉑ 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 11:27:33 +09:00
MsystechandClaude Opus 5 222d1987d5 환자 검색·내원 조회를 재 보고 고친다 — 5초 두 곳이 각각 441ms·1ms 가 됐다
"검색 및 선택하면 시간이 너무 오래걸려" — 체감이 아니라 숫자였다.
진단에 시간을 재는 줄을 먼저 넣었다. 어디가 느린지 숫자 없이 고치면 엉뚱한 곳을 만진다.

## 실측 (--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>
2026-08-19 11:23:49 +09:00
MsystechandClaude Opus 5 4bdd583adb 주민번호 복호화를 붙인다 — 이 DB 는 실제로 암호화되어 있었다
문서(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>
2026-08-19 11:15:24 +09:00
MsystechandClaude Opus 5 34c53e3d69 주민번호 복호화 경로를 찾아 문서로 남긴다 — 앱에 키를 넣지 않아도 된다
환자 태그의 다음 관문. 성별(277건)·나이(204건)가 전부 복호화된 주민번호를 선행 조건으로 삼는다.

## 가장 중요한 발견

복호화가 <b>DB 안에서</b> 일어난다(dtCommonLib.vb:6960).

  SELECT SUBSTR(:enc,1,7) || CRYPTO_AES256.DEC_AES(SUBSTR(:enc,8,256)) FROM DUAL

바인드 변수이고 키는 오라클 패키지 안에 있다 — 접근 통제가 DB 권한이라는 뜻이고
그것이 있어야 할 자리다. <b>앱은 키를 갖지 않는다.</b>
이 성질을 깨는 이식(키를 상수로 옮기는 등)은 하지 않는다.
앞 7자리는 평문이고 8자리부터만 암호화되어 있다.

병원 SCHILDREN 만 UDF_GETDECRESNUM_damo 로 갈린다(2023-10-14 DAMO 전환).

## 어떤 병원에서 켜지는가

M_DtlMst(HSPCFG/PersonalDataEnc) 가 Y 일 때만
M_DtsMst(HSPCFG/PersonalDataEnc/ResNum) 를 한 번 더 본다(clsCommonLib.vb:5835-5841).
N 이면 P_PatInf.PatResNum 이 평문이고, Y 면 그 컬럼은 쓰지 말고 PatResEnc 를 복호화해야 한다.
앞 커밋에서 PAT_주민번호 를 뺀 이유가 정확히 이것이다.

## 복호화 뒤 보정이 더 있다

SRCH·JINJU 는 주민번호에 기관기호가 들어간 경우를 PatBthDay 로 고친다.
<b>이 개발 DB 가 SRCH 다 — 살아 있는 갈래다.</b>
외국인은 7번째 자리 0/9 를 PatBthDay 세기에 따라 6/8 또는 5/7 로 바꾼다.
둘 다 순수 문자열 계산이라 표로 고정할 수 있다.

## 성별은 지금 옮길 수 있다

7번째 자리 mod 2(0=F, 1=M). 암호화 여부로 갈라지지만 두 갈래 본문이 같다.
단 PAT_성별 에는 CoiCalCod 가 "24" 로 시작하면 PatSexTyp 을 쓰는 선행 분기가 있고,
CoiCalCod 는 P_CoiInf 라 이미 문맥에 들어와 있다.

## 나이는 지금 옮기지 않는다

AgeCheck(clsCommonLib.vb:735)는 7번째 자리로 세기를 정하는데 갈래가 많고
REDCROSS 별도 규칙과 의료급여 전산관리번호 규약이 붙는다. 보험코드까지 결과를 바꾼다.
원본 주석이 <b>1901~1919년생 외국인과 2001년 이후 보호기관 영아는 원리상 구분 불가</b>라고
명시한다(20080821).

절반만 옮기면 종이에 그럴듯한 나이가 찍힌다 — 아무도 의심하지 않으므로 빈칸보다 나쁘다.
사례 표를 먼저 만드는 별도 작업으로 둔다.

## 개인정보 접근 로그

레거시에 C_PdrInf 전용 경로가 있다(clsCommonLib.vb:28279).
주민번호를 복호화하기 시작하면 가장 민감한 필드를 다루는 것이므로 기록 범위를 정해야 한다.
그 표에 쓰는 것은 운영 DB 쓰기라 사용자 승인이 필요하다 —
정하기 전에는 파일 로그에 사실만 남기는 쪽이 안전하다.

코드 변경 없음(문서만). 게이트는 앞 커밋 상태 그대로다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 11:03:49 +09:00
MsystechandClaude Opus 5 128aedc34f 환자 태그 매핑을 레거시 함수 본문으로 다시 세운다 — 셋은 존재하지 않는 이름이었다
"환자 태그이지만 아직 옮기지 않았습니다 엄청 많은데?" — 맞다. 그리고
옮겼다고 적어 둔 것도 절반이 거짓이었다.

## 지어낸 이름 셋

PAT_휴대전화 · PAT_전화번호 · PAT_주소 는 <b>실존하지 않는 태그</b>다.
컬럼 이름만 보고 태그 이름을 추측해 표에 넣었고, 이름이 안 맞으면 매칭이 안 되므로
값도 안 나오고 오류도 안 난다 — 화면에는 "아직 안 옮겼습니다"로 보인다.
실제 이름은 PAT_휴대전화번호 · PAT_자택전화번호 이고 PAT_주소 는 없다.

그래서 진단에 이름 검사를 넣었다 — 옮긴 이름이 전부
LegacyTagCatalog.DataInterfaceTags 에 있는지 본다. 대조군(없는 이름이 실제로 걸리는지)도 함께 둔다.

## PAT_주민번호 는 빼야 했다

레거시는 PatResNum_DEC(복호화 컬럼)를 읽는다(bzDataInterface.vb:87).
P_PatInf 의 PatResNum 을 그대로 쓰면 <b>암호화 병원에서 암호문이 종이에 찍힌다</b>.
빈칸보다 나쁜 값이라 표에서 뺐다.

## 실제로 맞게 돌던 것은 2종이었다

PAT_이름 · PAT_차트번호. 앞서 "6종"이라고 보고한 것은 틀렸다.

## 이번에는 함수 본문을 읽고 넣었다

기계적으로 옮길 수 있는 것은 <b>본문이 컬럼 하나를 그대로 돌려주는</b> 5종뿐이다
(추출해서 셌다). 거기에 형식이 하나 붙는 생년월일을 더해 7종:

  PAT_이름 · PAT_영문이름 · PAT_휴대전화번호 · PAT_자택전화번호 · PAT_여권번호
  PAT_차트번호(내원의 ComChtNum — moPatInfoBiz.ChtNum 이 그것이다, :70)
  PAT_생년월일(PatBthDay → YYYY-MM-DD, :188-198)

차트번호를 PatInf 에서 ComInf 로 고쳤고, 진단의 가상 문맥도 같이 고쳤다 —
가상 문맥이 옛 매핑을 쓰고 있으면 실제와 다른 것을 검사한다.

## 왜 나머지가 많이 남는가 (노력 부족이 아니다)

- PAT_성별/한글성별 : CoiCalCod 가 "24" 로 시작하면 PatSexTyp, 아니면
  GetSexAge(PatResNum_DEC) 로 주민번호에서 뽑는다(:283-316) — 복호화가 선행 조건이다.
- PAT_나이 계열 : 병원 코드로 갈라지고 CGCH 는 자기 SQL 을 들고 있다(:373-).
- 그 밖 238종 : 함수 본문에 자기 SQL 이 있다.

즉 다음 관문은 태그를 더 옮기는 것이 아니라 <b>주민번호 복호화</b>다.
그것이 풀리면 성별 계열이 따라 열린다. 짐작으로 채우면 종이에 그럴듯한 값이 찍히고,
그건 아무도 의심하지 않는다.

## 게이트

- dotnet test 323/323
- --edit-smoke 실패 0 (이름 검사 + 대조군 2건 신규)
- --db-patient ①~⑱ 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:56:42 +09:00
MsystechandClaude Opus 5 5ecc7b514a 환자 선택을 제목줄로 올린다 — 한 번 고르면 앱 전체가 그 환자를 본다
테마 버튼 오른쪽에 user 아이콘. 전에는 미리보기 창 안에만 있어서
쿼리 편집처럼 환자 값이 필요한 다른 화면에서는 고를 방법이 없었다.

## 방향을 뒤집었다

전에는 미리보기 창이 환자를 들고 있고 세션에 <b>밀어 올렸다</b>.
그러면 제목줄에서 바꿀 때 창이 모르고, 세션과 창이 어긋나는 경로가 생긴다.

이제 모든 진입점이 PatientSession 에만 쓰고, 창들은 Changed 를 <b>듣는다</b>.
제목줄·미리보기·쿼리 편집 어디서 바꿔도 나머지가 따라온다.
미리보기 창은 열 때 세션의 환자를 물려받는다 —
창을 열 때마다 초기화하면 "앱 전체 적용"이 거짓이 된다.

## 아이콘이라 상태를 색으로 알린다

늘 보이는 자리라 글자를 둘 폭이 없다. 고르면 강조색, 이름은 툴팁에 둔다 —
제목줄에 환자 이름을 상시 노출하지 않는다.

이미 고른 상태에서 누르면 <b>메뉴</b>가 뜬다(바꾸기 / 해제).
"누르면 해제"로 두면 늘 보이는 아이콘이라 실수로 지운다.

## 말뿐인지 값인지 확인했다

제목줄 아이콘은 세션만 건드린다. 열려 있는 창이 그것을 듣지 않으면
아이콘은 켜지는데 종이는 그대로다 — "설정했는데 안 먹는다"가 된다.
그래서 판정은 <b>창의 메서드를 부르지 않고</b> 세션에만 써서 종이가 따라오는지 본다.

구독을 떼는지도 본다. 정적 이벤트라 안 떼면 창이 회수되지 않고
죽은 창이 계속 그리려 든다 — 닫은 뒤 세션을 흔들어 예외 없이 지나가는지 확인한다.

## 게이트

- dotnet test 323/323
- --edit-smoke 실패 0 (전체 적용 판정 3건 신규)
- --dialog-shots FAIL 0 · --query-popup 실패 0
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:47:24 +09:00
MsystechandClaude Opus 5 1baf805f02 흐림의 진짜 원인은 TextFormattingMode 였다 — 색 번짐만 재서 두 번 놓쳤다
전역 TextBlock 스타일이 TextOptions.TextFormattingMode="Ideal" 을 박고 있었다
(DesignerTheme.xaml:40). Ideal 은 WPF 기본값이고 글리프를 픽셀 격자에 맞추지 않아
11~14px UI 글자가 눌린 듯 흐려 보인다.

## 왜 두 번이나 못 잡았나

<b>ClearType 과 다른 축이다.</b> Ideal 이든 Display 든 색 번짐은 똑같이 생긴다 —
그래서 색 번짐을 세는 --cleartype 으로는 원리상 구분이 안 된다.
표면을 늘려 가며(카드 제목 → 본문 → 목록) 전부 "켜짐"을 확인했고 게이트는 초록불이었는데
사용자 눈에는 계속 흐렸다. 측정하고 있던 것이 물어야 할 것이 아니었다.

이번에는 픽셀이 아니라 <b>속성 값</b>으로 잠근다 —
G·H·I 세 표면에서 GetTextFormattingMode() == Display 를 확인한다.

## 화면과 종이를 상속으로 가른다

- 창(ThemedWindow)에 Display. 상속 속성이라 TextBlock·TextBox·버튼·표 글자에 모두 닿는다.
- 종이는 Ideal. PageView 의 용지 Grid 와 PrintService 의 캔버스가 각각 명시하고
  그 안의 글자가 상속한다. 인쇄 지면 자릿수가 바뀌면 레거시와 다른 종이가 된다.

전역 TextBlock 스타일에서는 이 속성을 <b>빼야</b> 했다.
스타일 세터는 상속값을 이기므로, 거기에 값을 박으면 종이 안의 글자까지 그 값이 되어
용지 Grid 가 정한 Ideal 을 덮는다. 지금은 두 쪽 다 상속으로만 정한다.

## 종이가 끌려가지 않았는지 두 겹으로 확인

- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
- --edit-smoke 에 '종이 글자는 Ideal 을 유지한다' 를 추가.
  md5 는 서식 하나만 보므로 속성 값으로도 못 박았다.

## 게이트

- dotnet test 323/323
- --cleartype 실패 0 (색번짐 3표면 + Display 판정 3건 신규)
- --edit-smoke 실패 0 · --query-popup 실패 0 · --dialog-shots FAIL 0
- --db-render md5 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:41:43 +09:00
MsystechandClaude Opus 5 bbbc7f5a08 요약에서 환자를 두 번 적지 않고, 흐린 글씨를 표면 단위로 잡는다
## ① 요약이 환자를 두 번 적고 있었다

아래 버튼이 이미 "환자 해제 — 강한(0001381872) · 외래 2019-03-28 09:22 · 기준 …" 을
그대로 보여 주는데 요약도 같은 것을 다시 적었다. 두 줄로 늘어 결과 표를 밀어내고,
화면에 환자 이름이 필요 이상으로 여러 번 남았다.

환자가 있을 때는 침묵한다 — 버튼이 이미 말한다.
없을 때만 "변수 NULL — 구문만 검사" 를 남긴다(그 결과는 값이 아니라는 뜻이고,
그건 버튼 글자로는 알 수 없다). 빈 조각은 걸러 낸다 — 안 걸러 내면
"성공 · · 경고" 처럼 가운뎃점이 떠 있는다.

## ② 앞 수정이 부족했다 — 표면마다 경계가 따로 있다

카드 DockPanel 에만 힌트를 걸어 두고 고쳤다고 했는데 정작 SQL 글자는 계속 뭉개졌다.
본문은 SqlHighlightLayer 가 그리고 그 사이에 또 다른 중간 서피스가 있으며,
치환 변수 목록은 ListBox 템플릿 안에 ScrollViewer 를 품고 있다 —
카드에 건 힌트는 그 <b>바깥</b>이라 듣지 않는다(진단 표본 I 가 정확히 그 경우다).

- SQL 편집 영역: 불투명 B.Surface 를 가진 안쪽 Border 에 힌트
- 검증 결과 창(TrialPane): 배경 + 힌트
- 치환 변수 목록: ItemsPanel 을 불투명하게 만들어 ScrollViewer <b>안쪽</b>에 힌트(표본 J).
  VirtualizingStackPanel 을 쓴다 — StackPanel 로 바꾸면 변수 120개가 전부 실체화된다.

## 게이트가 초록불인데 화면만 틀린 것을 또 만들지 않기 위해

앞 커밋에서 실제 창 표본을 넣었지만 <b>카드 제목만</b> 쟀다.
제목은 켜졌고(150) 본문은 꺼져 있었는데 게이트는 통과했다 — 같은 함정을 한 번 더 밟았다.
사람이 실제로 들여다보는 표면을 다 재도록 바꿨다:

  G 카드 제목        색번짐    150
  H SQL 본문         색번짐  8,114
  I 치환 변수 목록   색번짐 22,047

대조군(B Effect·C1 힌트를 Effect 요소에·I 힌트 바깥 ScrollViewer)은 모두 0 이므로
이 숫자들은 뜻이 있다.

## 게이트

- dotnet test 323/323
- --cleartype 실패 0 (실제 창 표본 1개 → 3개)
- --query-popup 실패 0 · --edit-smoke 실패 0 · --dialog-shots FAIL 0
  (변수 목록 그룹 표시 유지 확인 — ItemsPanel 교체가 그룹을 깨지 않았다)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:36:38 +09:00
MsystechandClaude Opus 5 52f1e30437 쿼리 편집에서 환자를 고를 수 있게 하고, 카드 안 글씨를 다시 선명하게 만든다
"아직도 안됨, 그리고 글씨도 흐리게보이고" — 둘 다 맞다.

## ① 안 되는 게 아니라 고를 수가 없었다

ORA-00936 은 사라졌고 검증 실행은 성공한다. 그런데 0행이다.
화면이 이유를 말하고 있었다 — "환자를 고르지 않아 변수를 NULL 로 두고 구문만 검사".
그런데 <b>환자 선택이 미리보기 창 안에만 있었다.</b>
쿼리를 고치는 사람은 값 확인을 할 방법이 없고, 늘 0행이면 그건 "안 된다"로 읽힌다.
사유를 정확히 말해도 고칠 수단이 없으면 안내가 아니다.

검증 실행 옆에 '환자 선택...' 을 뒀다. 고르면 치환 변수에 실제 값이 들어가
구문뿐 아니라 값까지 확인된다. 세션 하나를 공유하므로 미리보기와 같은 환자다.
이미 고른 상태에서는 버튼이 '환자 해제 — <이름>' 이 된다 —
버튼 하나에 고르기·바꾸기·해제를 다 넣으면 무엇이 일어날지 예측할 수 없다.

## ② 카드 그림자가 ClearType 을 끄고 있었다

이 세션에서 미뤄 둔 항목이다. Card 스타일이 Effect(그림자)를 걸고,
Effect 는 하위 트리 전체를 중간 서피스로 보내 ClearType 을 끈다 —
13px 한글이 회색 안티에일리어싱으로 뭉개진다.

힌트는 Effect 가 걸린 Border 에 걸면 듣지 않는다(--cleartype 표본 C1 이 그것이다).
안쪽의 불투명 배경 요소에 걸어야 한다(C2). 카드 안 DockPanel 에 배경과 힌트를 줬다.

## 기법이 듣는 것과 걸어 둔 것은 다른 일이다

--cleartype 은 그동안 <b>합성 도형</b>만 쟀다. A~J 전부 통과했는데도
실제 창에는 기법을 안 걸어 둬서 글씨가 계속 뭉개졌다 —
게이트가 초록불인 채로 사용자가 보는 화면만 틀려 있었다.

그래서 실제 쿼리 편집 창을 띄워 카드 제목을 재는 표본 G 를 넣었다.
색번짐 150 / 글자픽셀 188 → 켜짐. 고치기 전 조건은 표본 B(Effect + 힌트 없음)와
같고 그건 0 이다. 창은 화면 안(0,0)에 띄운다 — BitBlt 은 실제로 그려진 픽셀만 읽으므로
화면 밖에 두면 아무것도 못 잰다.

## 게이트

- dotnet test 323/323
- --cleartype 실패 0 (A~J + G(실제 창) 신규)
- --query-popup 실패 0 · --edit-smoke 실패 0 · --dialog-shots FAIL 0
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:16:32 +09:00
MsystechandClaude Opus 5 8964c662f3 검증 실행이 치환 변수를 지우던 것을 고친다 — 그 쿼리들이 통과할 수 없었다
select * from P_PATINF where PATCHTNUM = <<…bzPatientInfo.ChtNum>>
→ ORA-00936: 누락된 표현식

원인은 오라클이 아니라 우리 쪽이었다. 검증 실행이 <<…>> 를 전부 빈 문자열로
지운 뒤 보냈다 — "WHERE PATCHTNUM = " 가 된다.
런타임이 값 없을 때 그렇게 한다는 이유였지만(clsMDataTable.vb:67-71),
그 결과 <b>치환 변수를 값 자리에 쓴 쿼리는 검증 실행을 통과할 수가 없었다.</b>
정작 이 기능이 존재하는 이유가 그런 쿼리들이다.

## 두 갈래로 나눴다

환자를 골랐으면 <b>실제 값</b>으로 치환한다 — 구문뿐 아니라 값까지 확인된다.
안 골랐으면 빈 자리를 NULL 로 채운다. 따옴표 안이든 밖이든 문법이 성립하고
0행이 나오므로 테이블·컬럼 오타와 구문 오류는 그대로 걸린다.

자리표시는 QuerySubstitution.Apply 의 선택 인자다(기본은 레거시와 같이 빈 문자열).
실행 경로(미리보기)는 쓰지 않는다 — 거기서는 레거시와 같은 SQL 이어야 한다.
내역(Hits)에는 실제 판정을 남긴다. 자리표시로 채운 것을 "값이 있었다"로 적으면
화면이 "값이 나왔다"고 말하게 되고, 그게 이 기능에서 가장 위험한 거짓말이다.

## 환자를 세션으로 올렸다

환자는 미리보기 창에서 고르는데 검증 실행도 같은 환자로 돌아야 한다.
창마다 따로 들고 있으면 같은 서식을 두 화면에서 서로 다른 환자로 보게 되고
그건 화면으로 구분할 수 없다. PatientSession 하나만 둔다(디스크에 쓰지 않는다).

## 무엇으로 돌렸는지 말한다

요약 첫 줄이 "환자 …의 실제 값으로 실행" 또는 "환자를 고르지 않아 변수를 NULL 로
두고 구문만 검사"다. 이 줄이 없으면 0행을 보고 "이 환자에게 자료가 없다"로 잘못 읽는다.

따옴표 밖에 놓인 변수도 짚는다 — 값이 원문 그대로 박히므로(clsMDataTable.vb:113)
문자 컬럼과 비교하려면 서식 쪽에서 '<<…>>' 로 감싸야 한다. 이 배선의 가장 흔한 실수다.
질문에 딸려 온 쿼리가 정확히 그 경우였다.

## 대조군이 함정을 잡았다

'따옴표로 감싸면 경고가 없다'가 처음에 통과했는데, 경고가 <b>아예 안 떠서</b>
양쪽이 다 "없음"이었다. 원인은 내 가드다 — 토큰이 문장 끝에 오면 건너뛰게 써 놨고,
WHERE 절 마지막이 가장 흔한 자리다. 경계를 '따옴표 없음'으로 보도록 고쳤다.
대조군 없이 긍정 판정만 있었으면 이 기능은 조용히 아무것도 안 하고 있었을 것이다.

## 게이트

- dotnet test 323/323
- --query-popup 실패 0 (⑨-b 5건 신규: 구문 검사가 돌고 · 자리가 비지 않고 ·
  근거를 밝히고 · 따옴표 밖을 짚고 · 감싸면 안 짚는다)
- --edit-smoke 실패 0 · --dialog-shots FAIL 0
- --db-patient ①~⑱ 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:08:07 +09:00
MsystechandClaude Opus 5 3ef1937902 '새 서식'을 없앤다 — 코드 없는 문서는 그린 뒤에 저장이 막힌다
서식은 항상 목록에서 골라 연다는 결정.

## 없앤 진입점 넷

- 파일 메뉴 '새 서식(N)'
- Ctrl+N
- 탭 흐름 옆 '+' 버튼(DesignerTheme.xaml)
- 빈 화면의 '새 서식' 버튼

시작 시 빈 문서를 만들던 것도 없앴다. 그게 실제 이유다 —
코드 없는 문서는 DB 에 저장할 수 없고(FormId 가 "NewSheet"), 사용자는
한참 그려 넣은 뒤에야 그 사실을 알게 된다. 만들 수 있게 두는 것 자체가 함정이었다.

이제 기동 인자에 서식 코드가 없으면 아무것도 열지 않고 서식 목록 탭을 띄운다.
빈 화면 안내도 '서식 목록에서 열기' 하나로 줄였다(F4 · Ctrl+O 병기).

CreateNew 와 파일 저장·열기 코드는 남긴다 — 접속 없이 도는 진단이 그 경로를 쓴다.
UI 진입점만 없앤 것이고, 이는 'DB에서 열기' 창을 지울 때와 같은 방식이다.
NewFileCommand 를 쓰던 --dialog-shots 는 AttachBlankForDiagnostics 로 바꿨다
(이름에 '진단 전용'을 박아 UI 에서 다시 부르지 못하게 했다).

## 없애자마자 드러난 것

문서가 아예 없는 상태가 <b>기본</b>이 되니 속성 패널에 정렬 바와 간격값이
그대로 남아 있었다. CurrentDesigner.Inspector 가 null 이면 안쪽 Visibility
바인딩이 전부 실패하고, WPF 는 <b>실패한 바인딩을 기본값(Visible)으로 떨군다</b> —
Collapsed 가 아니다. 선택이 없다는 안내와 정렬 바가 동시에 떠 있었다.

열린 서식이 없으면 인스펙터를 접는다. 그리고 이 회귀는 값으로 잡히지 않으므로
시각 게이트에 대조군을 넣었다 —
00-main-default(문서 있음 → 인스펙터 보임) 대 00d-main-empty(문서 없음 → 접힘).
대조군 없이 빈 화면만 찍으면 "원래 그랬던 것"과 구분되지 않는다.

## 게이트

- dotnet test 323/323
- --edit-smoke 실패 0
- --dialog-shots FAIL 0 (00d-main-empty 추가)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 09:27:58 +09:00
MsystechandClaude Opus 5 9bff3dd5f6 MDataTable 을 미리보기에서 실제로 돌린다 — 태그만 채우면 반쯤 빈 것을 못 본다
"태그와 MDataTable도 환자 선택하면 해당 정보로 조회되는지 확인가능해야" 한다는 요구.
확인해 보니 미리보기가 DataTableViewModel 을 아예 건너뛰고 있었다(PrintService:87,143).
서식의 값 상당수가 태그가 아니라 이 관으로 오는데(운영 실측 Rows 형 1,624건)
그쪽은 미리보기에 존재하지 않았다.

## 레거시 구조를 먼저 확인했다

TK_PREVIEW 는 meLoadMode 를 Runtime 으로 바꾸고 TestPatientSetting() 으로
환자 선택 창을 띄운다(frmSheetDesigner.vb:452-462). 고르는 것만으로는 화면이 안 바뀐다 —
moPatInfoBiz 는 필드에 담기기만 하고(:634-674), 서식을 다시 열어야
ucLoadSheetBase(moWrkInfoBiz, moPatInfoBiz, …, EN_LoadType.Edit) 로 로드되면서
그때 태그와 MDataTable 이 실행된다(:216). 탭마다 별도 인스턴스라
환자를 바꿔도 이미 열린 탭은 옛 환자를 계속 들고 있다.

SheetMe 는 1단계다. UsePatient 가 해석기를 갈고 즉시 다시 그린다.
창이 하나뿐이라 잔상도 없다. 이 차이는 의도한 것이다.

## 치환 엔진(QuerySubstitution)

clsMDataTable.ConvertQuery 를 옮겼다. 한 칸이라도 다르면 미리보기가 운영과
다른 SQL 을 돌린다 — "미리보기에서는 나왔는데 실제로는 안 나온다"가 되고
사람이 확인했다고 믿고 넘어가므로 미리보기가 없는 것보다 나쁘다.

처음에 갈래 하나를 반대로 만들었다. 대문자 Item 처럼 정규식이 안 맞는 경우를
"토큰이 남는다"로 단정했는데, GetPropertyInfo 는 정규식 실패에 ""를 돌려주고(:118)
호출부가 그걸 빈 문자열로 치환한다(:66-70) — 즉 지워진다.
토큰이 남는 갈래는 <b>접두어 불일치</b>뿐이다(oBaseObj 가 Nothing 이라 Replace 를 안 한다).
소스를 읽어 고쳤다. 두 갈래를 섞으면 안 되는 이유는 하나는 ORA 구문오류가 되고
다른 하나는 조건이 사라진 SQL 이 조용히 도는 것이라서다.

SQL 에서는 같아지는 것들도 사람에게는 갈라서 말한다 —
Unknown(아직 안 옮긴 속성) / Empty(값이 빔) / NotAVariable(접두어 틀림).
셋은 고칠 곳이 전부 다르다.

## 짐작하지 않는 변수원(PatientQueryVariableSource)

DataRow 접근형이 이 기능의 대부분을 실어 준다 — PatInfDR.item("아무컬럼") 은
SELECT * 결과 사전을 그대로 조회하면 되므로 <b>내가 컬럼을 알 필요가 없다.</b>
짐작할 것이 없으니 조용히 틀릴 일도 없다.
스칼라도 대부분 그 다섯 행의 컬럼 하나라, 컬럼 이름만 적고 있으면 값·없으면 모른다고
답한다. 존재 여부는 DB 가 판정한다. UDF 산출값(Age·Sex)은 넣지 않았다.

## 값이 안 나올 때를 위한 창

종이에는 "[MDataTable1.ALGYON — 조회 결과가 0행입니다]" 한 줄만 나온다.
그것으로는 쿼리가 틀렸는가·치환이 빈 값이 됐는가·이 환자에게 자료가 없는가를 못 가른다.
그래서 데이터소스 창이 <b>치환을 마친 SQL</b>을 그대로 보여 준다.
값이 안 나왔을 때 봐야 하는 것은 결과가 아니라 무엇을 물었는가다.
레거시에는 이걸 볼 수단이 없었다 — 런타임이 빈 catch 로 삼켜 "빈칸"만 남았다.

진단 화면 표본도 <b>실패 상태</b>로 찍는다. 성공 화면만 회귀 대상으로 두면
정작 사람이 오래 들여다보는 화면이 검사에서 빠진다.

## 안전

- 실행은 OracleQueryWorkbench.Trial 을 그대로 쓴다 — SELECT/WITH 문이 이미 거기 있다.
  문을 두 군데 두면 한쪽이 느슨해진다.
- 치환 값은 레거시처럼 원문 그대로 박힌다(그래야 같은 SQL 이다). 따옴표가 섞인 값은
  QuotedValues 로 드러내고 실행은 SELECT/WITH 문이 막는다.
- 데이터소스당 한 번만 실행하고 캐시한다 — 배선 40개면 왕복 40회가 된다.
- 환자를 바꾸면 러너를 새로 만든다. 재사용하면 태그만 바뀌고 표는 옛 환자가 남는데
  그건 아무도 눈치채지 못한다. 서식을 갈아탈 때도 다시 만든다(옛 서식의 쿼리를 쓰게 된다).

## 아직 안 되는 것

Select 형의 필터(DataTable.Select 메모리 문법)는 옮기지 않았다.
무시하고 행 번호만 쓰면 다른 행의 값이 조용히 찍히므로, 값을 내지 않고 사유를 말한다.
운영 다수파인 Rows 형(76%)은 된다.

## 게이트

- dotnet test 323/323 (치환 판정 10건 신규)
- --edit-smoke 실패 0 (배선 판정 7건 추가, 대조군 포함)
- --db-patient ①~⑱ 전건 통과. ⑯ 치환한 SQL 이 실제로 1행을 뽑고,
  ⑱ 대조군은 환자 없이 같은 쿼리가 "WHERE ComNum =  AND ... = ''" 로 깨진다 —
  이 대조가 없으면 ⑯ 은 "쿼리가 원래 환자와 무관했다"와 구분되지 않는다
- --dialog-shots FAIL 0 (08c-datasource-result 추가)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
- --db-smoke 1,271건 diff 0

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 09:08:40 +09:00
MsystechandClaude Opus 5 f50a7cff4b 환자 미리보기에서 부서 문을 뺀다 — 레거시에 없던 제약을 내가 붙였다
개발자 계정으로 환자 선택을 누르면 "전산실 소속만 가능합니다"가 떴다.
PatientPreviewPolicy 가 ModifyPolicy.CanModify 를 그대로 호출하고 있었기 때문이다.

## 근거를 다시 읽었다

레거시가 부서로 가리는 툴은 TK_MODIFY 하나뿐이다(frmSheetDesigner.vb:1030-1060).
TK_ExportXML / TK_ImportXML 은 MSYS 계정 문이고, TK_PREVIEW 에는
가시성 조건이 하나도 걸려 있지 않다 — 서식생성기를 열 수 있으면 쓸 수 있다.

즉 이 제약은 레거시에서 옮긴 것이 아니라 내가 "무게가 비슷하다"고 판단해
붙인 것이다. 사용자가 정한 것도 아니었다 — 환자 접근에 대한 결정은
"전체 검색 + 감사 로그"였고 부서 제한은 거기에 없다.

## 두 일의 무게가 실제로 다르다

수정은 지금 쓰이는 서식을 제자리에서 바꿔 병원 전체에 즉시 영향을 준다.
미리보기는 이미 그 사람이 볼 수 있는 기록을 읽기만 한다.

그리고 이 문은 막아서 더 위험해지는 쪽이었다. 서식을 만드는 사람이
자기 서식을 검증할 수 없으면 값이 틀린 서식이 그대로 운영에 올라간다.
태그가 정확한 값을 뽑는지 확인하는 것이 이 기능을 만든 이유였는데
그 확인을 만든 사람에게서 빼앗고 있었다.

## 바꾼 것

CanPreviewPatient 는 인증만 요구한다 — 미인증이면 감사 로그에 남길 주체가 없고
주체 없는 환자 조회는 남기면 안 되는 일이다. 그것이 유일한 조건이다.

병원·부서 인자는 받아 두고 쓰지 않는다. 병원별 규칙이 생기면 호출부를 고치지 않고
여기서 받을 수 있어야 한다.

안전장치는 막는 것이 아니라 감사 로그다. 그래서 테스트도 무게를 옮겼다 —
클래스 주석의 "감사와 권한이 유일한 장치"를 "감사 로그가 유일한 장치"로 고쳤고,
감사 한 줄의 모양(주체가 있는가·이름이 새지 않는가)이 가장 중요한 판정이 된다.

## 대조군

'수정_권한은_그대로_좁게_남는다' 를 새로 넣었다. 미리보기를 연 것이
수정까지 연 것이 되면 안 되고, 이 대조가 없으면 위 완화가 조용히
ModifyPolicy 까지 번져도 알 수 없다.

게이트: dotnet test 313/313 (미리보기 판정 1건 → 2건으로 갈라짐)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:51:30 +09:00
MsystechandClaude Opus 5 88dab2edcf 환자를 골라 태그에 값이 들어간다 — 데이터 계층만 있던 것을 화면에 붙인다
전 커밋까지 환자 조회·시각 판정·권한 규칙은 다 있었지만 화면에 붙은 것이
하나도 없었다. 미리보기를 열면 환자 선택이 없고, 태그는 전부 사유만 찍혔다.

## 만든 사슬

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>
2026-08-18 15:13:48 +09:00
MsystechandClaude Opus 5 be9889ed4e 적용일시 판정을 표로 고정한다 — 한 칸 어긋나면 태그가 통째로 빈다
미리보기 레거시 호환 계획 A3 의 세 번째 조각. 순수 함수 + 테스트 9건.

<b>왜 이것부터인가.</b> 환자 문맥 5행이 전부 이 시각을 키로 읽힌다 —
진료과·담당의(P_CodInf), 보험 자격(P_CoiInf), 병동·병실(P_CowInf) 모두
:AdpDtm BETWEEN StrDtm AND EndDtm 이다.
시각이 한 칸 어긋나면 조인이 0행이 되고 <b>태그가 통째로 빈 값</b>이 된다 —
값이 틀리는 것이 아니라 아무것도 안 나오고, 사용자는 "미리보기가 고장났다"고 읽는다.

갈래가 여섯이고 각각 경계 조건이 다르다:
  외래 → 접수일시 그대로(자격 구간을 보지 않는다)
  입원 First → 자격 시작과 접수 중 <b>늦은</b> 쪽
  입원 Last, 재원 중(CoiEndDte=29991231) → 지금. 단 입원 전환이 미래면 접수 시각
  입원 Last, 퇴원 → 자격 종료와 퇴원 중 <b>이른</b> 쪽
  호출자 지정(8자리/12자리) → 내원 구간 안으로 <b>가둔다</b>
  해석 불가 → 자동 판정으로 떨어진다(빈 값을 내보내면 조회가 전부 0행이다)

<b>저장소에 묻지 않고 순수 함수로 뺐다.</b> 안에 넣으면 실DB 없이는 한 갈래도 확인할 수 없다.
지금은 여섯 갈래를 전부 표로 고정했다.

시각 비교는 문자열 사전순이다 — 레거시가 그렇게 하고, 고정폭 YYYYMMDDHHMM 은 사전순이 곧 시간순이다.
DateTime 으로 바꾸면 잘못된 값(빈 문자열·공백)에서 예외가 난다.
CHAR 잔여 공백을 흡수한다 — 안 하면 사전순 비교가 어긋난다(검사로 못 박음).

게이트: 테스트 312/312(신규 9), --edit-smoke 0실패, --dialog-shots FAIL 0, 빌드 경고 0,
--db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

<b>남은 것</b>: 이 시각으로 5행을 읽는 PatientContextStore, 환자 선택 UI,
그리고 그 문맥을 쓰는 태그 해석기.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 11:43:34 +09:00
MsystechandClaude Opus 5 1f501df5f7 환자·내원 조회를 붙인다 — 레거시 SQL 을 그대로 옮기지 않고
미리보기 레거시 호환 계획 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>
2026-08-18 10:21:47 +09:00
MsystechandClaude Opus 5 e9d8f7356e 환자 미리보기 접근 규칙을 먼저 세운다 — 레거시에서 옮기지 않을 것들
미리보기 레거시 호환 계획의 A3 첫 조각. DB 조회는 조사 중이라 규칙과 감사부터 세운다.

<b>레거시에서 옮기지 않는 것이 요점이다.</b>
  · 폼이 뜰 때마다 인가코드를 InputBox 로 묻는데 <b>통과값 3개가 소스에 평문</b>이다
    (frmSheetDesigner.vb:913)
  · 환자를 검색조건 없이 <b>운영 전체</b>에서 찾는다(:636)
  · 누가 어느 환자를 열었는지 <b>남기지 않는다</b>(감사 0건)
셋을 그대로 옮기면 신규 앱이 최악을 물려받는다.

검색 범위는 레거시와 같게 두기로 했다(전체 검색 + 감사 로그).
그래서 <b>남기는 쪽</b>이 안전장치의 본체다 — 조회를 막지 않는 대신 흔적을 남긴다.
AppLog.Audit 은 FIXED 레벨이라 어느 로그 설정에서도 남는다.

권한 축은 ModifyPolicy 와 같은 것을 쓴다. 환자 기록을 보는 일은
지금 쓰이는 서식을 제자리에서 고치는 일과 무게가 다르지 않다.
미인증이면 막는다 — 주체가 없으면 감사에 남길 것이 없고, 남길 수 없는 조회는 하지 않는다.

<b>감사 한 줄에 환자 이름은 넣지 않는다.</b> 내원번호로 되짚을 수 있고,
로그는 오래 남고 접근 통제가 DB 보다 느슨하다. 검사로 못 박았다.

게이트: 테스트 303/303(신규 4), --edit-smoke 0실패,
--db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 10:09:10 +09:00
MsystechandClaude Opus 5 5072acb518 태그 사유가 맞아진다 — 'Inital' 은 오타가 아니라 실제 표기다
미리보기 레거시 호환 계획의 A2. 값이 더 나오는 효과는 <b>최대 7건</b>이고,
본체는 "왜 값이 안 나오는지"가 맞아지는 것이다.

<b>① Inital</b> — 분류 규칙이 EndsWith("InitialDatatable") 인데 실제 태그 3종은
_InitalDatatable 이다(LegacyTagCatalog.cs:184, 194, 196). 레거시가 그렇게 적어 두었다.
맞춤법을 고치는 것이 아니라 실재하는 표기를 받는 것이다.
안 받으면 그 셋이 표가 아니라 환자 문맥으로 분류되어,
화면이 "환자 정보가 있어야 값이 나온다"는 <b>틀린 사유</b>를 말한다 —
사용자는 환자를 골라 보고 왜 안 나오는지 계속 찾게 된다.
검사에 "이 표기가 카탈로그에 실재하는가"를 함께 넣었다. 표기가 바뀌면 검사가 헛돈다.

<b>② ETC_건강보험증번호_Refer</b> — 본문 34줄이 전부 주석 처리되고 Return "" 만 남았다
(bzDataInterface.vb:15352). 운영 2건. Dead 표에 없어서 화면이 "환자 정보가 있으면 나온다"고 말했는데,
환자를 골라도 영원히 안 나온다.

<b>앞선 보고를 정정한다</b>: "카탈로그 오분류 45종 122건 회수"는 틀렸다.
재계측하면 최대 40종 7건이고, 근거로 들었던 _병원직인(82건)·_병원로고(22건)는
이름에 '직인'·'로고' 가 들어가 이미 Image 로 분류된다 — 누수가 아니었다.

게이트: 테스트 299/299(신규 1), --edit-smoke 0실패, --dialog-shots FAIL 0,
--db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 10:06:53 +09:00
MsystechandClaude Opus 5 26e93cae94 미리보기에 태그 값이 흐를 관을 판다 — 값 아니면 사유
미리보기 레거시 호환 계획의 A1.

지금까지 미리보기는 태그를 <b>글자 그대로</b> 찍었다 — PAT_이름 이 종이에 그렇게 인쇄됐다.
값이 흐를 관이 아예 없었다.

<b>해석기를 인터페이스로 둔다.</b> 오늘 값이 나오는 것은 서버 시각·로그인 사용자 16종
(운영 사용 579건, 9.1%)뿐이고 나머지 368종은 환자·내원 문맥이 있어야 한다.
그 문맥이 생기면 <b>같은 자리로</b> 더 강한 해석기가 들어온다 — 관을 지금 파 두고 해석기만 갈아 끼운다.
A1 의 값어치는 9.1% 가 아니라 그 관이다.

<b>못 만든 값을 감추지 않는다.</b> 값이 나오면 값을, 안 나오면 사유를 대괄호로 감싸 그린다 —
[PAT_차트번호 — 환자·내원 정보가 있어야 값이 나오는 태그입니다].
빈칸으로 두면 서식이 잘못된 줄 알고, 태그 이름을 그대로 두면 값이 나온 줄 안다.
레거시도 같은 문제를 같은 이유로 다뤘다 — 실패한 컨트롤을 노란색으로 칠했다
(ucLoadSheetBase.vb:2300-2308). 그 장치가 있다는 것 자체가 현장 실패가 흔하다는 방증이다.

주입은 컨테이너 투영과 같은 방식이다 — 모델을 복제해 Text 만 갈아 끼우고 같은 타입의 VM 을 만든다.
<b>캔버스가 쓰는 VM 과 모델은 건드리지 않는다.</b>

DB 는 창당 한 번만 읽는다(재료 왕복 2회). 그리기마다 새로 만들면 다시 그릴 때마다 DB 를 친다 —
레거시가 태그마다 쿼리를 돌려 태그 40개 서식에 왕복 40회를 하는 것과 같은 실수다.

<b>단정은 종이에 찍힌 글자를 본다.</b> VM 속성만 보면 템플릿이 그 값을 쓰는지 알 수 없다 —
배선과 결과는 다른 문제이고, 이 저장소에서 그 둘이 여러 번 갈렸다.
검사용 해석기를 따로 둬 접속 없이도 같은 답이 나오게 했다.

게이트: 테스트 298/298, --edit-smoke 0실패(신규 3), --dialog-shots FAIL 0,
--modal-check 0실패, --cleartype 11/11, --scale-budget 5/5, 빌드 경고 0,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 10:04:49 +09:00
MsystechandClaude Opus 5 a67b6ee4b8 미리보기에 보기 전환을 둔다 — 인쇄될 것만 / 화면 그대로
미리보기 레거시 호환 계획의 P0-3. P0 세 항목이 이걸로 끝난다.

레거시 미리보기와 레거시 인쇄가 서로 다르다 —
미리보기는 PrintOutPut=False 컨트롤을 <b>그대로 보여 주고</b>(그 속성은 PrintMe 안에만 있다,
Label.vb:454-457) 인쇄만 뺀다. SheetMe 는 인쇄 쪽을 골라 테스트로 못 박아 뒀었다.
둘 다 확인해야 하므로 미리보기 창이 보기를 가른다. 기본은 인쇄 기준 —
저장 전에 확인하려는 것이 대개 종이 결과다.

Hidden(디자이너 임시 숨김)과 MDataTable(런타임 비가시)은 보기와 무관하게 항상 뺀다.
레거시 미리보기에서도 안 보이는 것들이다 — 보기가 가르는 것은 Visible/PrintOutPut 뿐이다.
인쇄 경로는 언제나 필터를 건다. 인쇄는 인쇄다.

<b>단정은 "돌아오는가"를 본다.</b> 필터를 켜서 뺀 것이 끄면 다시 나와야 한다 —
안 나오면 전환이 배선만 된 것이다. 컨테이너 안까지 세므로 P0-1 의 투영도 함께 검증된다.

<b>XAML 파싱 중에 이벤트가 발생했다.</b> 토글의 IsChecked="True" 가 파스 시점에 Checked 를 쏘는데,
토글이 트리에서 앞에 있어 그때 PagesHost 는 아직 null 이다 →
InitializeComponent 안에서 NullReferenceException. 가드를 넣고 이유를 적었다.
--edit-smoke 가 잡았다. 없었으면 미리보기를 여는 순간 앱이 죽었을 것이다.

게이트: 테스트 298/298, --edit-smoke 0실패(미리보기 7건), --dialog-shots 글자 2,653개 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 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 09:54:00 +09:00
MsystechandClaude Opus 5 9e3cd088e2 미리보기가 편집을 따라오고, 창이 하나만 뜬다
미리보기 레거시 호환 계획의 P0-2. 환자 값이 들어가기 <b>전에</b> 반드시 고쳐야 하는 항목이다.

<b>① 창이 여러 장 쌓였다.</b> 모덜리스인데 누를 때마다 새로 만들어서,
각각이 서로 다른 시점의 서식을 들고 겹쳐 있었고 어느 것이 지금 것인지 화면으로 구분할 수 없었다.
환자 데이터가 들어가면 그건 개인정보 사고가 된다 — 환자를 B 로 바꿔도 옛 창이 A 를 계속 띄운다.
이제 하나만 뜨고, 이미 있으면 지금 문서로 갈아타 앞으로 온다.

<b>② 생성자에서 한 번만 그렸다.</b> 컨트롤을 추가·삭제하거나 숨김·인쇄출력을 토글해도
창은 옛 그림을 계속 보여 줬다 — 저장 전에 결과를 확인하려고 여는 창이
<b>구조적으로 거짓말</b>을 하고 있었다.
페이지·컨트롤 추가/삭제는 즉시(CollectionChanged), 그 밖의 변경은 창을 누르는 순간(Activated) 다시 그린다.
즉시 갱신이 안 되는 변경이 남아 있으므로 새로 고침 버튼도 함께 뒀다 — 자동에 기대게 하지 않는다.

제목에 서식명을 넣는다. 창이 하나뿐이라 무엇을 보고 있는지 제목이 말해야 한다.

<b>같은 함정에 세 번째로 걸렸다.</b> 단정이 52 → 41 로 나왔다 — 재빌드는 됐는데
새 자식이 아직 실체화되지 않아 요소 수가 오히려 줄어 보인 것이다.
UpdateLayout 을 돌리고 세니 통과했다. WPF 시각 트리를 세는 검사는 <b>반드시</b> 레이아웃을 먼저 돌려야 한다.

진단(--dialog-shots)은 생성자를 직접 부른다 — 단일 인스턴스 등록을 건너뛰어야
샷마다 새 창을 찍을 수 있다. 앱 코드는 ShowFor 만 쓴다는 것을 주석에 못 박았다.

게이트: 테스트 298/298, --edit-smoke 0실패(신규 1), --dialog-shots 글자 2,651개 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 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 09:42:19 +09:00
MsystechandClaude Opus 5 dbb3deeda3 컨테이너 안도 인쇄 필터를 탄다 — Panel 안 컨트롤이 '인쇄 출력'을 지킨다
미리보기 레거시 호환 계획의 P0-1. 오늘 실제로 틀리고 있던 유일한 항목이다.

BuildPageVisual 은 page.Controls <b>최상위만</b> 훑는데, Panel·GroupBox 템플릿이
ItemsSource="{Binding Children}" 로 자식을 스스로 그린다. 그래서 최상위를 아무리 걸러도
컨테이너 안은 그대로 나왔다 —
  ① Panel 안 컨트롤은 '인쇄 출력'을 꺼도 인쇄됐다
  ② 컨테이너 안 MDataTable 은 파란 DB 원통 배지가 종이에 찍혔다
얼마 전 인쇄 필터를 넣으면서 이 구멍을 못 봤다.

<b>먼저 실패시키고 고쳤다.</b> 단정을 넣고 돌리니 요소 17 → 17. 그 red 가 두 번 일했다.

<b>첫 접근이 틀렸고 측정이 잡았다.</b> 컨테이너 VM 에 '인쇄 필터' 플래그를 켰다 끄는 방법을 썼는데
듣지 않았다 — WPF 는 템플릿 자식을 <b>레이아웃 시점</b>에 만드는데 BuildPageVisual 은
프레젠터만 만들어 두고 반환하므로, 자식이 실체화될 때는 이미 finally 가 플래그를 껐다.
플래그 수명을 인쇄 비주얼 전체로 늘리면 그동안 캔버스에서도 자식이 사라진다(VM 공유).
그 접근은 되돌렸다.

<b>택한 방법</b>: 같은 모델로 같은 타입의 VM 을 하나 더 만들고 인쇄될 자식만 담는다.
타입이 같으니 템플릿이 그대로 잡히고, 모델이 같으니 배경·테두리·머리글이 똑같이 나온다.
자식 VM 은 새로 만들지 않고 원본을 담아 해석된 글꼴·색을 유지한다.
<b>캔버스가 쓰는 VM 은 한 글자도 건드리지 않는다</b> — 단정 하나가 그것을 지킨다.
모르는 컨테이너 타입은 투영하지 않고 원본을 그린다(잘못 투영하느니 그 편이 낫다).

중간에 한 번 더 걸렸다: 처음 단정이 2 → 2 를 냈는데 그건 레이아웃을 안 돌려 시각 트리가
아예 없었던 것이다. Measure/Arrange 를 돌리고 나서야 17 → 17 이라는 진짜 실패가 보였다.

또 하나 정정: --db-render P062 는 이 경로를 쓰지 않는다(캔버스 PageView 를 찍는다).
"md5 가 걸린 경로"라고 했던 것은 과했다. 그래도 게이트로 확인했고 md5 는 동일하다.

게이트: 테스트 298/298, --edit-smoke 0실패(신규 2), --dialog-shots 글자 2,649개 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 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 09:32:27 +09:00
MsystechandClaude Opus 5 3af7b3a071 수정 경로를 실DB 로 돌려 본다 — 바꾸고, 확인하고, 원복한다
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>
2026-08-18 09:19:33 +09:00
MsystechandClaude Opus 5 ae2f8f2f61 '수정'을 되살린다 — 지금 쓰이는 버전을 제자리에서 고친다
레거시 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>
2026-08-17 21:17:55 +09:00
MsystechandClaude Opus 5 a386c5504b 열기·저장을 DB 로만 — 'DB에서 열기' 대화상자를 지우고 좌측 목록으로 모은다
<b>먼저 밝힐 것: 내가 "완전 중복"이라고 인용한 판정이 틀렸다.</b>
좌측 목록과 이 대화상자는 같은 조회를 쓰지만(둘 다 ListSheets, WHERE·ORDER BY·상한 동일)
같은 쓸모를 주지 않았다. 지우기 전에 구멍 넷을 좌측에 옮겼다.

<b>① 저장·등록 뒤 재조회 — 가장 컸다.</b>
좌측 목록은 기동 1회와 검색 버튼으로만 채워졌고 저장 뒤 갱신이 없었다.
즉 <b>방금 저장한 서식이 목록에 안 떴다</b>. 대화상자는 열 때마다 재조회해서 그 구멍을 덮고 있었다.
그것을 안 메우고 창만 지웠으면, DB 전용으로 바꾼 직후 신규 서식을 저장한 사용자가
그 서식을 다시 찾지 못했을 것이다.

<b>② 서식명 툴팁</b> — 좌측은 폭이 148px 남짓(자물쇠 붙으면 134, 패널 최소폭이면 80)이라
긴 이름이 잘리는데 전문을 볼 방법이 없었다. 대화상자는 열이 300px 에 드래그로 늘릴 수 있었다.

<b>③ '디자인 없음'과 '서식생성기 사용 꺼짐'</b> 이 좌측에서는 둘 다 같은 회색+Opacity 0.6 이라
화면만 보고 구분할 수 없었다. 툴팁으로 갈랐다.

<b>④ Enter 로 열기</b> — 좌측은 더블클릭 전용이라 키보드로 서식을 열 수 없었다.
대화상자에는 [열기] 버튼이 있어 그 구멍이 가려져 있었다.
검색칸에서 ↓ 로 목록에 내려가는 길도 붙였다.

그 다음 지웠다: SheetOpenDialogView(680×560), OpenFromDbCommand, 파일 메뉴의 파일 열기·
다른 이름으로 저장·DB에서 열기, 빈 화면의 '파일 열기' 버튼, Ctrl+Shift+S.
Ctrl+O 는 이제 서식 목록으로 간다 — 전에는 OS 파일 대화상자였고, 운영 서식은 전부 DB 에 있다.

<b>SaveMode 기본값은 바꾸지 않았다.</b> 조사해 보니 그 규칙은 실재하고 문서·코드 4곳에 있다 —
docs/DEPLOYMENT.md:56 은 "이 기본값을 바꾸지 말 것"이라고 명시한다.
배포된 ini 의 CurrentServer 가 운영 병원 DB 를 가리켜서, 명시적으로 켠 단말에서만
운영 테이블(E_SdgMst/E_SctMst)에 쓰게 하려는 장치다. ini 는 SaveMode 를 담지 않으므로
이 규칙이 병원 단말의 유일한 방어선이다. UI 진입점만 없애고 규칙은 그대로 둔다.
파일 저장·열기 <b>코드</b>도 남긴다 — 접속 없이 도는 --render-smoke 와 --edit-smoke 직렬화 왕복이
그 경로를 쓴다.

대신 화면이 상태를 말하게 했다 — SaveMode=File 단말에서는 저장 메뉴·버튼이 비활성이다.
전에는 항상 활성이고 누른 뒤에야 경고가 떴다.

<b>종료 가드를 고쳤다 — 앱이 안 닫히는 상태를 없앤다.</b>
전에는 저장 실패가 곧 종료 중단이었다. 파일 폴백이 있을 때는 드문 분기였지만
저장이 DB 로만 가면 흔한 경로가 된다 — SaveMode=File 단말이나 접속 불가에서
<b>수정한 탭을 가진 사용자가 앱을 닫을 수 없게</b> 된다. 이제 '버리고 종료'를 묻는다.

게이트 대체물도 함께 옮겼다: --dialog-shots 의 01-sheet-open 을 지우면
서식명 잘림을 잡을 자리가 통째로 사라지므로, 좌측 목록에 표본을 넣은 01-sheet-list 샷으로 바꿨다
(진단 모드는 Loaded 를 안 돌려 목록이 비므로 표본을 직접 주입한다).
검사한 글자 2,531 → 2,649개.

게이트: 테스트 291/291, --edit-smoke 0실패, --dialog-shots 62장 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 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 17:58:12 +09:00
MsystechandClaude Opus 5 2f5c9bf82d 간격을 숫자로 넣는다 — 4px 씩 12왕복에서 한 번으로
여기까지 간격을 정하는 길은 4px 씩 넓게/좁게뿐이었다.
20px 를 32px 로 바꾸려면 3번, 로고 아래 4단 메뉴로 하면 12왕복이고 매번 마우스가 좌상단까지 갔다.
단축키(Alt+[ / Alt+])로 왕복은 없앴지만 여전히 4px 씩이다.

드래그 스냅은 형제 간격을 정확히 계산해 분홍 표식까지 그리는데(SnapSolver),
<b>명령 경로에는 그 값을 넣을 칸이 없었다</b>. 인스펙터에 세로·가로 칸을 붙인다.

<b>둘</b>부터 뜬다 — 라벨과 입력칸 한 쌍이 가장 흔한 경우다.
균등 나누기가 셋을 요구하는 것과 기준이 다르므로 표시 조건을 따로 뒀다(CanSpaceExactly).

<b>첫 항목은 움직이지 않는다.</b> 기준이 흔들리면 같은 값을 두 번 넣어도 자리가 계속 밀린다 —
"24 를 넣었는데 블록이 아래로 기어간다"가 되고, 그건 사용자가 자기 조작을 의심하게 만든다.
검사에서 8 을 두 번 넣어 첫 항목의 Y 가 그대로인지 본다.

<b>값이 갈리면 빈 칸이다.</b> 하나로 보여 주면 그 값이 이미 적용된 것처럼 읽혀서,
사용자는 이미 고른 상태라고 믿고 넘어간다.

음수·NaN 은 무시한다 — 조용히 뒤엉킨 배치가 되는 것보다 아무 일도 안 하는 쪽이 낫다.

실측 6건: 갈린 값 빈 칸 · 24 로 놓으면 24 · 첫 항목 고정 · 재입력 시 안 밀림 · 음수 무시 · NaN 무시.

게이트: 테스트 291/291, --edit-smoke 0실패(신규 6), --dialog-shots 글자 2,531개 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 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 18:04:57 +09:00
MsystechandClaude Opus 5 6d06f0a4c8 태그 피커에서 재진술을 덜어낸다 — 우측 패널 4블록 삭제
4단계 계속.

우측 300px 정보 패널 6블록 중 <b>4개가 왼쪽 목록 행의 재진술</b>이었다.
  이름 — 목록 행 · 하단 선택 칩 · 이 패널 <b>세 곳에 동시에</b>
  분류 — 좌측 분류 열의 선택 강조와 중복
  사용 건수 — 목록 행 배지와 중복
  추천 — 분류('자주 쓰는 것') · 목록 정렬 1순위 · 인스펙터 칩에 이어 <b>네 번째</b>
300px 세로 전체를 쓰면서 새 정보는 두 블록뿐이었다.

남긴 둘은 다른 어디에도 없다 —
'이 태그의 값'(값이 나오면 값, 아니면 왜 안 나오는지)과
'변형'(같은 값을 다른 형식으로 내는 태그).
복사 버튼은 값 블록 머리로 옮겼다 — 태그는 이름이 곧 키라 복사는 계속 쓸모가 있다.

우측 칸 300 → 230, 목록 행 높이 26 → 20.
행 총높이는 38 → 32 다(24 로 가려면 ListBoxItem 패딩을 건드려야 하는데
그 스타일은 서식 목록·레이어와 공용이라 이번에는 두었다).
가용 487px 기준 12.8행 → 15.2행.

<b>주석을 지우다 XAML 을 깨뜨렸고 게이트가 잡았다.</b>
옛 주석의 본문이 닫는 태그만 남은 채 <b>본문 텍스트로</b> 들어가
UIElementCollection 에 문자열을 넣는 XamlParseException 이 됐다.
--dialog-shots 가 태그 피커 두 장을 FAIL 로 표시했다 —
그 게이트가 없으면 태그를 고르러 창을 열 때까지 몰랐을 것이다.
(같은 실행에서 글자 검사 수가 2,465 → 2,305 로 떨어졌는데, 그건 창 두 개가 아예 안 떠서였다.
 고친 뒤 2,465 로 돌아왔다 — 지운 블록은 접힌 상태라 애초에 세지지 않았다.)

게이트: 테스트 291/291, --edit-smoke 0실패, --dialog-shots 글자 2,465개 넘침 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 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 17:59:00 +09:00
MsystechandClaude Opus 5 9b2ae0ea67 태그 행에서 모달 없이 태그를 고른다 — 인라인 완성
4단계 계속. 주 경로에서 1080x700 모달이 사라진다.

칩 상위 8종만으로 운영 사용의 41.7%, 상위 16종으로 61.5% 를 덮는다.
나머지는 이름을 정확히 16자 외워 치거나 모달을 열어야 했고, 자유 입력은 열려 있는데
제안이 없어서 <b>오타가 검증 없이 저장되고 EMR 에서 조용히 빈 칸이 됐다</b>.
같은 인스펙터의 '속성 추가' 행에는 이미 부분일치 제안이 있었다 —
패턴은 있는데 운영 8,032건이 걸린 칸에만 없었다.

<b>새 패턴을 발명하지 않았다.</b> QueryEditorWindow 의 CompletionPopup 에서 배운 것을 그대로 쓴다:
StaysOpen=False 로 두면 팝업이 마우스를 캡처해 항목 클릭을 '바깥 클릭'으로 먹어 버린다
(눌러도 입력이 안 되고 사라진다 — 그 함정을 이미 한 번 겪었다).
AllowsTransparency 도 켜지 않는다 — 레이어드 창이 되면 ClearType 이 꺼져 13px 한글이 뭉개진다
(진단 cleartype 의 D 표본). 대신 안쪽에 ClearTypeHint 를 건다.

<b>칩이 사라지는 기준을 바꿨다.</b> 전에는 ValueText.Length == 0 이라 값이 정해지면 칩이 사라졌다.
그래서 잘못 고른 태그를 바꿀 때 도움이 끊겼다 — 태그를 고치는 일은 처음 고르는 일만큼 잦은데
고치는 쪽만 맨손이었다. 이제 기준은 '값이 있는지'가 아니라 <b>'타이핑 중인지'</b>다.
타이핑을 시작하면 아래 완성 목록이 같은 일을 더 정확하게 하므로 칩을 접는다.
그 옛 규칙을 고정하던 검사 하나를 새 규칙으로 고쳤다 — 의도한 변경이다.

완성 목록은 입력 중인 글자를 <b>인자로</b> 받는다. 태그 행의 Text 바인딩이
UpdateSourceTrigger=LostFocus 라 타이핑 중에는 ValueText 가 아직 옛 값이고,
그것으로 목록을 만들면 한 글자 뒤처진다.
순위는 이 타입에서 자주 쓰이는 순서 → 운영 사용 건수 → 이름이다.
알파벳 순으로 두면 상위 10종(전체 사용의 47.5%)이 목록 아래로 흩어진다.
검색은 TagSearch 를 그대로 쓴다 — 별칭·초성이 여기서도 듣는다('환자명'으로도 뜬다).

<b>Popup.IsOpen 은 기본이 TwoWay 다.</b> 읽기 전용 속성에 바인딩하면 XamlParseException 이고,
그 실패가 --dialog-shots 를 통째로 죽였다(종료코드 3). Mode=OneWay 를 명시한다.
게이트가 아니면 인스펙터를 띄우는 화면에서만 터졌을 것이다.

실측 14건: 값이 있어도 칩 유지 · 타이핑 전 미표시 · 타이핑 시 칩 접힘 · 별칭 적용 ·
자주 쓰는 것이 위 · 10줄 상한 · ↑↓ 순환 · Enter 확정과 커밋 · 확정 후 닫힘/칩 복귀 ·
정확히 하나면 안 가림 · Esc 가 값을 보존.

게이트: 테스트 291/291, --edit-smoke 0실패(신규 14), --dialog-shots 글자 2,465개 넘침 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 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 17:43:53 +09:00
MsystechandClaude Opus 5 a869e5a5bc 태그 검색이 사람 말을 받는다 — '사인'으로 서명 태그 17종이 나온다
4단계 시작.

사용자가 태그를 못 찾은 이유는 접두어(PAT_·ETC_)가 아니라 <b>어휘</b>였다.
검색은 부분 문자열과 초성만 봤고, 카탈로그 표기는 레거시가 정한 것이다.
실제로 세어 보니 흔한 말이 전부 0건이었다 —
  환자명 0건 (표기는 이름)        날짜 0건 (일자, 21건)
  사인 0건 (싸인, 17건)           주민등록번호 0건 (주민번호)
  오늘 0건 (현재, 11건)           도장·신장·남녀·성명 0건
서명 태그가 전부 레거시 오표기 '싸인' 이라, 의료진이 '사인'을 치면
"결과 없음"만 나오고 회복 경로가 없었다.

입력어를 카탈로그 표기로 옮기는 표 34항을 넣었다.
<b>태그마다 설명을 지어내는 것이 아니다</b> — 오른쪽 값은 전부 카탈로그에 실재하는 표기다.
없는 내용을 만들지 않는다는 규칙은 그대로다.

<b>표만 검사하면 부족하다.</b> "환자명 → 이름" 매핑이 옳다는 것만 확인되고
그 표기가 카탈로그에 실재하는지는 확인되지 않는다 — 없는 표기로 옮기면 여전히 0건이다.
그래서 실제 카탈로그 384종을 대상으로 건수를 단정하고,
별칭이 가리키는 표기가 카탈로그에 있는지도 따로 단정한다(그 검사가 없으면
"오늘 → 없는말" 같은 표를 적어도 0건인 채로 통과한다).

<b>테스트 가정을 한 번 틀렸다.</b> "'사인' 과 '싸인' 이 같은 건수"라고 적었는데
17 대 16 으로 깨졌다 — 별칭은 대칭이 아니다.
'싸인' 질의는 별칭 '서명' 까지 타서 한 건을 더 잡는다.
재야 할 것은 "'싸인' 이라고 적힌 태그를 '사인' 으로 다 찾는가" 이므로 원문으로 세도록 고쳤다.

화면에는 별칭으로 찾아 줬다는 사실을 적는다 — "17개 — 이 카탈로그는 '싸인' 으로 적혀 있습니다".
결과만 내놓으면 사용자는 다음에도 자기 말이 통했다고 오해하고,
결과가 비었을 때 왜 비었는지도 모른다.

게이트: 테스트 291/291(신규 6), --edit-smoke 0실패, --dialog-shots 넘침 0,
--modal-check 0실패, --cleartype 11/11, --maxrect 0실패, --scale-budget 5/5, 빌드 경고 0,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 17:35:41 +09:00
MsystechandClaude Opus 5 d72ab3ce1e 인스펙터가 손을 놓지 않는다, 그리고 저장 확인이 사람 말을 한다
3단계 계속.

<b>① 선택이 바뀌면 편집하던 칸과 스크롤이 사라졌다.</b>
선택 집합이 바뀔 때마다 Rows.Clear() 로 행을 통째로 다시 만들기 때문이다.
하루 수백 건을 다루는 사람에게 그건 "다음 컨트롤로 넘어가면 손이 처음으로 돌아간다"는 뜻이다 —
라벨 20개의 글꼴을 차례로 고치는 일이 20번의 스크롤·클릭이 된다.

행을 키 기준 갱신으로 바꾸는 쪽이 더 근본적이지만 <b>이번에는 하지 않는다</b>.
이 패널은 회귀 기록이 가장 많은 곳이고(MainView.xaml:321-322, LayerPanelView.xaml:127-129),
실제 아픔은 재구성 비용이 아니라 포커스·스크롤 소실이다
(--scale-budget 이 뷰모델 작업은 마이크로초 단위임을 보여 준다).
그래서 재구성은 그대로 두고 포커스와 스크롤만 되돌린다. 행은 라벨로 찾는다 —
타입이 달라도 같은 이름의 행(글꼴·정렬·표시)은 같은 일을 한다.

Rebuild 를 감쌌다. 본문에 이른 return 이 여러 갈래 있어서 끝에 이벤트를 두면
도는 경로와 안 도는 경로가 갈리고, 그러면 <b>어떤 선택 변경에서만</b> 포커스가 안 돌아온다 —
그런 결함은 재현 조건을 찾기 전까지 "가끔 그런다"로만 보인다.
복원은 DispatcherPriority.Loaded 로 미룬다. 즉시 부르면 컨테이너가 아직 없어 빈 트리를 뒤진다
(이 세션에서 같은 함정을 두 번 밟았다).
사용자가 그 사이 다른 칸을 눌렀으면 포커스를 빼앗지 않는다.

<b>② F4 로 인스펙터에 들어간다.</b> 인스펙터로 포커스를 보내는 키가 저장소에 하나도 없었다.
캔버스에서 Tab 을 누르면 좌측 패널·문서 탭·플로팅 바를 다 지나야 인스펙터에 닿았다.
커맨드로 만들지 않는다 — 그러면 뷰모델이 뷰의 포커스를 알아야 한다.

<b>③ 저장 확인 문면을 두 층으로 갈랐다.</b>
이 대화상자는 세 부류가 다 보는 유일한 위험 지점인데(사내 인력·병원 전산팀·의료진),
본문에 E_SdgMst/E_SctMst, SdgDelYon='Y', ShtCneYon='Y' 가 그대로 노출되어 있었다.
사내 인력에게는 정확한 정보이고, 전산팀에게는 읽을 수 없는 문장이고, 간호부에게는 공포다.
위층은 사람 말("이 서식의 새 버전을 만듭니다. 지금 쓰이는 버전은 이력으로 남습니다"),
테이블·컬럼은 접힌 상세로 내렸다.

게이트: 테스트 285/285, --edit-smoke 0실패, --dialog-shots 글자 2,465개 검사 넘침 0,
--modal-check 0실패, --scale-budget 5/5, --maxrect 0실패, --cleartype 11/11, 빌드 경고 0,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 17:30:11 +09:00
MsystechandClaude Opus 5 7e38ba654a 모달 인구를 줄이고 배치 명령에 손잡이를 붙인다
3단계 계속.

<b>① Ctrl+S 가 DB 문서를 디스크에 쓰고 있었다.</b>
Ctrl+S 는 SaveFileCommand 에 걸려 있고 그건 항상 파일 저장이다.
DB 에서 연 서식에서 누르면 OS 저장 대화상자가 뜨고 XML 이 디스크에 쓰였다 —
사용자는 저장했다고 믿지만 <b>DB 에는 아무것도 안 들어간다</b>.
종료 가드는 이미 IsFromDb 로 갈라 준다(MainViewModel.cs:365). Ctrl+S 만 안 갈라 주고 있었다.

<b>② 저장 완료 알림 모달을 없앤다.</b> 정보량이 0 이었다 —
SdgKey 를 바로 위에서 상태바에 넣고 같은 값을 모달로 한 번 더 보여 주는 것이었다.
하루 30회 저장하는 사람에게 그건 Enter 를 30번 치는 의식이고, 그때마다 화면이 어두워졌다.
실패는 계속 모달로 알린다 — 조용히 넘기면 저장된 줄 알고 넘어간다.

<b>③ 스크림의 기본을 뒤집었다.</b> 전에는 모든 ShowDialog 에 무조건 붙어서
태그 하나 고르는 데도 앱이 암전했다. 하루 수백 번이면 신호가 아니라 소음이다.
전역 후크는 그대로 둔다 — 호출부 17곳 중 하나만 빠져도 그 대화상자만 다르게 동작하고
그건 눈으로 전수 확인해야만 안다. 대신 창이 스스로 자격을 밝혀야 깔리게 했다
(ModalScrim.DimBehind). 새 대화상자가 잊으면 암전이 안 되는데, 이제 그쪽이 안전한 기본값이다.
자격을 밝힌 것: 물음(되돌릴 수 없는 쓰기의 확인)·오류(진행이 정의되지 않는 차단)·서식 신규 등록.

--modal-check 에 ⑤ 를 더했다: <b>자격을 밝히지 않은 창은 어두워지지 않는다</b>.
①~④ 만으로는 "깔린다"만 확인되고 "안 깔려야 할 때 안 깔린다"는 확인되지 않는다.

<b>④ 배치 명령에 단축키와 우클릭 메뉴를 붙였다.</b>
전에는 단축키가 0개였고 우클릭 메뉴가 앱 전체에 하나도 없었다.
정렬 6종·같은 크기·간격이 전부 로고 아래 4단 메뉴 안에만 있어서,
20px→32px 간격 조정이 4px 씩 12왕복이고 매번 마우스가 화면 좌상단까지 갔다.
자리는 Figma 와 같게 둔다(Alt+A/D/W/S/H/V) — 이미 그 손버릇을 가진 사람이 있다.
균등 Alt+Shift+H/V, 같은 크기 Alt+Shift+W/S, 간격 Alt+[ / Alt+].

<b>Alt 조합에 함정이 있다.</b> WPF 는 Alt+문자를 Key.System 으로 싸서 넘기고
실제 키는 SystemKey 에 있다. e.Key 만 보면 Alt 단축키가 하나도 안 걸리는데,
그 실패는 "눌러도 아무 일이 없다"로만 보여서 원인을 찾기 어렵다.

우클릭 메뉴에 InputGestureText 를 적었다 — 단축키를 모르는 사람이 거기서 배운다.

게이트: 테스트 285/285, --edit-smoke 0실패, --modal-check 0실패(신규 1),
--dialog-shots 넘침 0(대조군 4/4), --scale-budget 5/5, --maxrect 0실패,
--cleartype 11/11, 빌드 경고 0,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 17:18:57 +09:00
MsystechandClaude Opus 5 d6052610b9 루프를 키보드로 닫는다 — Tab 다음, Shift+Tab 이전, Enter 안, Esc 밖
이 도구의 일은 "컨트롤 하나 골라 값 하나 바꾸고 다음으로"의 반복인데,
<b>그 루프가 마우스 없이는 한 바퀴도 돌지 않았다.</b> 선택을 옮길 키가 하나도 없었다 —
캔버스 키맵 18개 중 선택 이동 0, 편집 시작 0. 화살표는 선택이 아니라 컨트롤을 민다.
Label 59,997 + TextBox 28,574 의 문구를 고치는 일이 전부 더블클릭에 묶여 있었다.

<b>읽는 순서로 돈다 — z-order 가 아니다.</b> 계획서는 z-order 순회를 적었지만,
Tab 을 누르는 이유는 "서식을 위에서 아래로 훑으며 문구를 고치는 것"이다.
기록지는 위→아래·왼쪽→오른쪽으로 읽으므로 그 순서가 손에 맞는다.
z-order 는 겹침을 정하는 축이라 화면상 순서와 무관하게 튄다.
같은 줄로 볼 만큼 가까우면(8px = 그리드 두 칸) X 로 가른다.
EMR 런타임 입력 순서인 ApplyTabOrder 와는 다른 개념이다 — 그건 저장되는 값이다.

<b>범위는 형제다.</b> 컨테이너 안으로는 Enter, 밖으로는 Esc.
전체를 한 줄로 이으면 패널 안 라디오 15,026개를 지나야 다음 항목에 닿는다.

Esc 가 이제 한 겹만 나간다. 전에는 항상 선택을 통째로 풀어서,
패널 자식을 고른 뒤 Esc 를 누르면 부모로 돌아가는 대신 선택이 사라졌다.
드래그 중 Esc 는 그대로 드래그 취소다.

선택이 옮겨지면 RevealControl 로 화면에 데려온다 —
레이어 목록 화살표 이동에 이것이 없어서 화면 밖 컨트롤을 눈 없이 편집할 수 있었다.

PreviewKeyDown 에 붙인다. KeyDown 이면 WPF 포커스 관리자가 Tab 을 먼저 먹어
포커스가 캔버스 밖(좌측 패널·문서 탭·플로팅 바)으로 나간다.

검사는 배선이 아니라 <b>한 바퀴가 도는지</b>를 본다 — 9건:
읽는 순서 · 끝에서 처음으로 되돌아옴 · 역방향 · 컨테이너 안 · 밖 · 나갈 곳 없음 · 편집 시작.
컨테이너는 GroupSelection 이 아니라 문서에 직접 만든다 —
그 경로에는 거절 조건(라디오·z 불연속)이 있어 막히면 순회 대신 그룹 규칙을 재게 된다
(실제로 처음에 그렇게 막혔다).

게이트: 테스트 285/285, --edit-smoke 0실패(신규 9), --dialog-shots 넘침 0(대조군 4/4),
--scale-budget 5/5, --maxrect 0실패, --cleartype 11/11, --modal-check 0실패, 빌드 경고 0,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 17:07:09 +09:00
MsystechandClaude Opus 5 1b49f07dd9 미리보기가 거짓말을 멈춘다, 그리고 9번째 컨트롤이 1번째를 덮지 않는다
3단계 시작 — 실사용 버그 두 건.

<b>① 미리보기·인쇄가 인쇄 필터를 무시하고 있었다.</b>
레거시 런타임은 인쇄에서 이렇게 걸러낸다 —
  If Me.Visible = False OrElse mbPrintOutPut = False Then Return False
그 문장이 ControlRegistry.cs:17 에 인용까지 되어 있는데, 렌더 경로가 두 키를 아무도 읽지 않았다.
대신 걸러낸 것은 ControlElement.Hidden 뿐이고 그건 [JsonIgnore] 디자이너 전용 플래그다
(주석에도 "레거시 Visible 과 별개"라고 적혀 있다).
그래서 인스펙터에서 '인쇄 출력'을 꺼도 미리보기에 그대로 나왔다 —
서식을 저장하기 전에 결과를 확인할 유일한 수단이 거짓말을 하고 있었다.

<b>키 이름이 타입마다 다르다.</b> 라벨만 소문자 visible 이다
(Label.vb 가 Shadows Property visible 로 Control.Visible 을 가리고 직렬화기가 그림자를 쓴다).
운영에서 소문자 58,864건(99.7%) 대 대문자 196건이다.
그래서 관용으로 둘 다 읽지 않는다 — 타입이 선언한 키를 카탈로그에서 찾아 그것만 읽는다.
둘 다 읽으면 대문자 Visible=False 인 라벨이 우리 인쇄에서만 빠지고, 그건 호환이 아니라 새 차이다.
모르는 값과 없는 키는 인쇄한다 — 내용을 조용히 빼는 것이 조용히 넣는 것보다 나쁘다.

캔버스는 그대로 다 보여 준다. 편집 중인 것을 못 보면 고칠 수 없다.
--db-render 는 이 경로를 쓰지 않아 종이 렌더 md5 는 불변이다(확인했다).

<b>② 팔레트 즉시 배치가 9번째부터 겹쳤다.</b>
(page.Controls.Count % 8) * 16 이라 두 가지가 틀렸다 —
9번째가 1번째와 정확히 같은 자리에 떨어지고, 하나 지우고 다시 놓으면 지운 자리로 갔다.
단조 증가 카운터로 바꿨다. 계단은 종이를 벗어나기 전에 옆으로 비킨다.

<b>순수 함수 테스트만으로는 배선을 증명하지 못한다.</b>
PrintFilter 단위 테스트 8건은 판정이 옳다는 것만 말한다. 렌더 경로가 그 판정을 부르는지는
다른 문제이고, 이 앱에서 "코드는 맞아 보이는데 화면은 다른" 일이 이번 세션에만 세 번 있었다.
그래서 --edit-smoke 에서 BuildPageVisual 의 자식 수를 실제로 세는 검사 3건을 더했다.

게이트: 테스트 285/285(신규 8), --edit-smoke 0실패(신규 5),
--dialog-shots 넘침 0(대조군 4/4), --scale-budget 5/5, --maxrect 0실패,
--cleartype 11/11, --modal-check 0실패, 빌드 경고 0,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 17:01:46 +09:00
MsystechandClaude Opus 5 d55ca39e6a 운영 규모 예산을 잰다 — 그리고 2단계 근거가 사라졌다
재설계 1단계 마지막. --scale-budget [출력] [컨트롤수], 기본 1067.

--edit-smoke 는 컨트롤 3개 합성 문서에서 배선만 본다. 운영 최대는 1,067개인데
그 규모의 숫자가 CI 에 하나도 없었다. 없으면 재설계가 새 상호작용을 얹었을 때
재설계가 드러낸 느림을 재설계 탓으로 뒤집어쓴다.

실측(최상위 1,067 + 중첩 자식 533, 마퀴로 1,067개 전부 잡힘):
  호버(선택 0)        평균 0.01ms  최악 0.02ms
  드래그(선택 1)      평균 1.41ms  최악 7.49ms
  드래그(선택 500)    평균 2.70ms  최악 9.73ms
  마퀴 전체 선택      7.29ms
  되돌리기 1회        7.74ms
예산: 프레임당 16ms(60fps), 한 번 하는 조작 1000ms. 전부 통과.

<b>이 숫자가 재설계 계획의 2단계를 무효로 만든다.</b>
계획은 "좌표 캐시"를 3단계(상호작용) 앞에 필수로 두었고 근거는
"PageOf 가 O(N) 이고 O(N) 루프 안에 있다 — 드래그 프레임당 약 107만 회 참조 비교,
마퀴 전체 선택 한 번에 5~6백만 회"였다.
연산 횟수 산술은 맞다. 틀린 것은 그 횟수에 <b>연산당 비용을 곱하지 않은 것</b>이다 —
참조 비교는 1ns 수준이라 5백만 회가 곧 5ms 이고, 실제로 마퀴가 7.29ms 로 그 예측과 맞는다.
즉 분석은 옳았고 결론("느리다")만 틀렸다.
좌표 캐시는 지금 근거가 없으므로 뺀다. 필요해지면 이 게이트가 먼저 알려 준다.

<b>규모를 두 번 틀리게 잡았다.</b> 처음엔 자식 절반을 최상위에서 빼서 N=534 를 재고 있었다.
O(N) 원시연산이 보는 N 은 최상위 개수라 그 상태로는 최악을 못 잰다.
'만들려던 수와 다르면 실패'라는 가드가 그것을 잡았다.

재지 못하는 것을 클래스 주석에 적었다 — 여기 도는 것은 뷰모델 계층뿐이고
WPF Measure/Arrange·바인딩 재평가·렌더가 빠져 있다.
줌이 LayoutTransform 이라 눈금마다 서브트리를 다시 재는 비용,
Undo 뒤 시각 트리를 다시 세우는 비용은 이 숫자에 없다.

게이트: --scale-budget 5/5, 테스트 277/277, --dialog-shots 넘침 0(대조군 4/4),
--edit-smoke 0실패, --maxrect 0실패, --cleartype 11/11, --modal-check 0실패, 빌드 경고 0,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 16:55:33 +09:00
MsystechandClaude Opus 5 4150ebe1d7 메인 화면을 게이트에 넣고, 최소 크기를 단일 출처로, 창 크기를 기억한다
재설계 1단계 계속.

<b>메인 셸이 게이트에 아예 없었다.</b> 사용자가 하루 종일 보는 화면만 자동 검증 밖이었다.
넣고 나서 글자 31개만 검사되는 것을 보고 빈 껍데기를 찍고 있다는 것을 알았다 —
컨트롤을 놓고 하나를 골라 인스펙터·레이어를 채우니 81~95개가 된다.
세 크기·탭으로 찍는다: 기본 1440x920, 현장 최대화 1920x1032, 도구 상자 탭.
Loaded 커맨드(서식 목록 DB 조회)는 진단 모드에서 돌지 않게 막았다 —
게이트 결과가 접속 상태에 따라 달라지면 안 된다.

<b>게이트가 게이트의 오류를 잡았다.</b> 메인 화면을 넣자 세로잘림 30건이 떴는데 전부 오탐이었다.
DesiredSize 는 Margin 을 포함하고 RenderSize 는 포함하지 않아서, 세로 Margin 이 있는
TextBlock 이 예외 없이 걸린 것이다(도구 상자 그룹 머리가 "필요 27 > 실제 14" 였고 그 13 이 Margin).
Margin 을 빼고 비교하도록 고쳤다. 이 검사기에서 측정이 나를 정정한 세 번째다.

<b>--maxrect 가 최소 크기를 베껴 두고 있었다.</b> ProbeMinWidth=940 / ProbeMinHeight=640 /
ProbeBorder=6 은 MainView.xaml 의 복사본이라, 최소 크기를 바꾸면 이 검사가
<b>옛 값을 계속 검사하며 계속 통과한다</b>. 실제 MainView 를 만들어 XAML 이 정한 값을 읽는다.

<b>창 크기·위치·상태·두 패널 폭을 기억한다.</b> UserPrefs 가 Dictionary&lt;string,bool&gt; 이라
숫자를 못 담던 것이 원인이었다. JsonElement 로 바꿔 이미 깔린 prefs.json 이 살아남게 했다 —
string 이나 double 로 바꾸면 역직렬화가 터져 기존 취향이 통째로 초기화된다.

복원값은 반드시 클램프한다. 이 앱은 WindowStyle=None 이라 제목 표시줄을 직접 그리므로,
저장된 위치가 지금 화면에 없으면(회사에서 두 번째 모니터, 집에서 노트북) 메뉴·저장·닫기가
전부 손에 닿지 않고 마우스로 되돌릴 방법이 없다.
기본 크기로는 이 문제가 안 나지만, 저장을 시작하는 순간 복원되는 것은 사용자가 만든 값이다.
닫기를 물렀을 때는 저장하지 않는다 — 조작하지 않은 값이 남는다.

게이트: 테스트 277/277, --dialog-shots 대조군 4/4 · 글자 2,465개 검사 · 넘침 0건,
--maxrect 0실패(MainView 실측 940x640 테두리 6), --cleartype 11/11,
--edit-smoke 0실패, --modal-check 0실패, 빌드 경고 0,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 16:50:24 +09:00
MsystechandClaude Opus 5 862657cf2f 글자가 잘리면 CI 가 실패하게 만든다 — 사람이 그림 57장을 보는 대신
재설계 1단계. 화면 픽셀은 하나도 안 옮긴다.

지금까지 이 저장소에서는 글자가 잘렸다는 이유로 실패할 수 있는 것이 하나도 없었다.
테스트 277건은 Core·Data 만 참조해 XAML 을 한 줄도 로드하지 않고,
--dialog-shots 는 PNG 56장을 만들면서 단정하는 것은 창 배경색과 스타일 존재 여부 둘뿐이다.
그래서 이번 세션에만 세 건이 눈으로만 잡혔다 — "232 로는 이름이 잘린다",
"220 이면 마지막 토글이 잘린다", "0 크기 Grid 가 자식을 폭 0 으로 재서 글자가 사라진다".

글자를 담은 요소만 본다. WPF 에서 DesiredSize > RenderSize 는 정상인 경우가 많아
전부 훑으면 수백 건이 떠서 게이트가 무용지물이 된다.

측정이 검사 방식을 두 번 정정했다.

① "Width 를 좁게 주고 말줄임표를 끄면 잘린다"고 놓았는데 아니었다.
   TextTrimming=None 이면 TextBlock 은 준 Width 를 지키지 않고 자연폭을 그대로
   ActualWidth 로 보고한다(Width=40 인데 ActualWidth=213.3).
   즉 WPF 에서 말줄임표 없는 NoWrap 글자는 잘리는 것이 아니라 <b>옆을 침범한다</b>.
   그래서 자기 ActualWidth 와 비교하는 대신 <b>자르는 조상의 사각형</b>과 비교한다.
   자르는 조상이 없으면 위반으로 세지 않는다 — 겹침은 다른 종류의 결함이고
   이 검사기가 판정할 수 있는 것이 아니다.

② 0 폭 칸에 갇힌 글자는 ActualWidth 가 0 이 아니라 9.6 이었다(말줄임표 글리프 몫).
   대조군 기대값을 그에 맞게 고쳤다.

자가 점검을 함께 넣었다. 위반 0건은 "화면이 깨끗하다"와 "검사기가 고장났다"를
구분해 주지 않고, 보고서에서 그 둘은 똑같이 생긴다. 이번 세션에 그 함정에 두 번 빠졌다 —
--maxrect 가 기본값 0 을 기대값 0 과 비교해 공허하게 통과했고,
--modal-check 가 Loaded 보다 먼저 재서 언제나 0 을 셌다.
그래서 일부러 잘린 대조군 4종을 넣고, 검사한 글자 수를 함께 적는다.

실측: 대조군 4/4 · 창 57개에서 글자 1,936개 검사 · 넘침 0건.

기동 화면 밖 문제는 최우선에서 내렸다 — 현장 단말이 1920x1080 이상뿐이라는 답을 받았다.
1440x920 을 1920x1032 에 CenterScreen 하면 Left 240 / Top 56 으로 양수다.
MaximizeToWorkArea.cs:251 의 1366x768@125% 주석은 현장 기술이 아니라 방어적 가정이었다.
같은 이유로 A4 맞춤은 50% 가 아니라 77% 이고, 화면 예산 회수는 급한 일이 아니다.

게이트: 테스트 277/277, --dialog-shots 대조군 4/4 넘침 0, --cleartype 11/11,
--edit-smoke 0실패, --modal-check 0실패, --maxrect 0실패,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 16:33:57 +09:00
MsystechandClaude Opus 5 280594362a Effect·팝업이 ClearType 을 끄고 있었다 — 글자가 흐린 이유
사용자 지적이 맞았다. Effect 가 걸린 요소는 하위 트리가 중간 서피스로 렌더링되면서
ClearType 이 꺼지고, 글자가 회색 안티에일리어싱으로 그려진다. 13px 한글이 특히 뭉개진다.

저장소는 이 사실을 이미 알고 있었다 — MessageDialogView.xaml:18 과 TagPickerDialogView.xaml:11 이
"AllowsTransparency 를 켜면 ClearType 이 꺼져 13px 한글이 뭉개진다"고 적어 두었다.
그런데 그 판단이 <b>창에만</b> 적용되고 Effect 와 Popup 에는 적용되지 않았다.
ClearTypeHint 는 저장소 전체에 0 곳이었다.

추론으로 끝내지 않았다. RenderTargetBitmap 으로는 이 질문에 답할 수 없다 —
그 자체가 알파를 가진 중간 서피스라서 Effect 가 있든 없든 양쪽 다 회색으로 나오고,
검사가 통과하면서 아무것도 증명하지 못한다. 그래서 진짜 화면을 BitBlt 으로 떠서
글리프의 채널 어긋난 픽셀(ClearType 은 R≠G≠B, 회색 AA 는 R=G=B)을 셌다.

--cleartype 표본 11종 실측:
  A 아무것도 없음            색번짐 1115  ← 대조군. 이게 0 이면 판정 불가로 끝낸다
  B Effect                    색번짐    0  ← 지적한 그대로
  C1 힌트를 Effect 요소 자신에게  색번짐    0  ← 듣지 않는다
  C2 힌트를 안쪽 불투명 배경에    색번짐 1344  ← 여기가 맞는 자리
  D 레이어드 팝업              색번짐    0  ← 두 번째 원인
  E 팝업 + 힌트                색번짐 1115  ← AllowsTransparency 를 끌 필요가 없다
  I 힌트 바깥에 ScrollViewer   색번짐    0  ← ScrollViewer 도 중간 서피스를 연다
  J 힌트를 ScrollViewer 안쪽에  색번짐 1115
  F 앱 콤보 드롭다운(실제 템플릿) 색번짐    0 → 수리 후 1115

두 번 헛짚었고 두 번 다 측정이 정정했다.
CornerRadius 를 의심했으나 아니었고, ScrollViewer 가 범인이었다.
그리고 F 를 처음 통과시켰을 때는 선택된 콤보 항목의 파란 배경(B.Sel, 채널 차이 61)이
색번짐으로 잡힌 것이었다 — 색번짐 4359 인데 글자픽셀은 1075 였다.
그래서 배경이 유채색이면 판정을 거부하는 장치를 넣었다.

규칙: 힌트는 Effect·ScrollViewer·레이어드 팝업 <b>안쪽</b>의 불투명 배경 요소에 건다.

적용한 곳 — 글자가 많은 순:
  콤보 드롭다운(DesignerTheme) · 메뉴 하위팝업(배치·정렬 명령이 다 여기 있다) ·
  색 팝오버 · 컨트롤 추가 플라이아웃 · 줌 플라이아웃

남긴 곳: 플로팅 바(글자는 줌 퍼센트뿐), QueryEditorWindow 의 Card 스타일
(Effect 가 DataGrid·편집기의 중첩 ScrollViewer 를 감싸서 힌트를 넣을 자리가 여럿이다 —
그림자를 빼는 쪽이 맞아 보이고, 그건 디자인 결정이라 따로 다룬다).

게이트: --cleartype 11/11, 테스트 277/277, --edit-smoke 0실패, --modal-check 0실패,
--maxrect 0실패, --dialog-shots 57파일, --db-smoke 1271건 diff 0,
--db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일(종이 렌더 불변).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 16:11:23 +09:00
MsystechandClaude Opus 5 b0d2e9b470 가림막이 대화상자 위에 있었다 — z순서를 명시적으로 내린다
두 번째 오진이다. 지난번에는 가림막이 아예 안 깔렸고, 고친 뒤에는 <b>대화상자 위에</b> 깔렸다.
그래서 대화상자까지 같이 흐려져 글자가 눌려 보였다.

원인: 가림막과 대화상자가 같은 창을 소유자로 갖는 형제라, 나중에 띄운 가림막이 위에 온다.
Activate() 로는 안 된다 — 활성 창을 바꾸는 것과 z순서를 바꾸는 것은 다른 일이다.
SetWindowPos 로 가림막을 대화상자 바로 아래에 꽂는다.

두 번 다 "코드를 읽어서는 맞아 보이는데 화면은 다른" 경우였다.
그래서 --modal-check 에 z순서 검사를 더했다 — 창 개수만 세면 위에 있는지 아래에 있는지 모른다.
GetWindow(GW_HWNDNEXT) 로 대화상자 다음 창이 가림막인지 본다.

실측: 떠 있는 동안 가림막 1개 · 대화상자 아래에 있음 · 닫으면 0개.

게이트: 테스트 277/277, --edit-smoke 0실패, --modal-check 3/3(1건 신규),
--dialog-shots 57파일, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:06:08 +09:00
MsystechandClaude Opus 5 7e0e201acb 태그 값 줄 — 값이 나오면 값을, 아니면 왜 안 나오는지를
앞 커밋의 판정표를 화면에 붙였다. 우측 정보 패널에 '이 태그의 값' 한 칸.

'미리보기'라 부르지 않는다. 실제로 값이 나오는 것은 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>
2026-08-14 15:03:01 +09:00
MsystechandClaude Opus 5 836a3e843a 태그마다 값을 만들 수 있는지, 없다면 왜인지 — 판정표
'예시 값' 을 진짜로 보여 주기로 했는데, 조사해 보니 서식생성기에서 값이 나오는 태그는
384종 중 16종(4%)뿐이다. 나머지는 환자·내원 문맥이 있어야 한다.

그런데 조사에서 값보다 쓸모 있는 것이 나왔다 — <b>왜 안 되는지</b>다.
지금은 어디서도 알 수 없다:
· ETC_요양기관명칭_병원명(실사용 3위, 303건)이 환자 내원을 타고 병원을 푼다는 것
· 값이 아니라 표를 돌려주는 태그 15종(콤보 채우기용이라 한 줄 값이 없다)
· 파일서버 이미지 경로를 돌려주는 태그 9종(싸인·직인)
· 본문이 Return "" 인 죽은 태그 1종
· 태그가 아니라 레거시 내부 함수 8종 — 우리 카탈로그가 Public Function 을 전수 추출해
  섞여 들어왔다. 고르면 값이 안 나오는데 레거시는 Try 안에서 조용히 삼킨다.

그 판정을 TagPreviewCatalog 에 담았다. <b>표로 박는다</b> — 판정 근거는 레거시 함수 본문이
환자·내원 객체를 참조하는지 여부이고, 그건 태그 이름에도 접두어에도 없다.
ETC_현재일자 와 ETC_요양기관명칭_병원명 은 같은 접두어인데 한쪽만 만들 수 있다.
이름 규칙으로 추정하면 절반만 맞는다. 규칙이 확실한 둘(…List·…InitialDatatable / 싸인·직인)만
접미어·낱말로 가르고 나머지는 실측을 옮겨 적었다.

값 계산도 함께 넣었다. 서버 시각 11종은 레거시 Data2Format 과 같은 형식으로 찍고
(영문은 MM-DD-YYYY 재배치), 로그인 사용자 5종은 항목을 골라 잇는다.
ID·이름을 잇는 태그는 한쪽이 비면 점만 남은 값을 만들지 않는다.

단위 테스트 10건. 이 표가 틀리면 화면이 사용자에게 거짓말을 한다 —
값이 나오는 태그를 '안 된다'고 하거나, 안 나오는 태그에 빈 칸을 띄운다.
판정 근거가 레거시 함수 본문이라 코드로는 다시 확인할 수 없어 여기가 유일한 안전망이다.

아직 화면에 붙이지 않았다. 값 줄은 DB 조회(서버 시각·M_UidMst)가 필요하고
그 통로를 만드는 것이 다음 단계다. 판정과 계산은 순수 함수라 그 전에 확정해 둔다.

게이트: 테스트 277/277(신규 10), --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>
2026-08-14 14:54:37 +09:00
MsystechandClaude Opus 5 b2bdc7fa41 태그 선택 — 목업 구조로. 다만 없는 정보는 만들지 않는다
1080x700 3단(분류 / 결과 / 태그 정보), 큰 제목 + 부제, 검색칩,
바닥에 선택 칩과 버튼. 목업의 뼈대를 그대로 가져왔다.

가져오지 않은 것과 그 이유:

· 설명 한 줄 — 384건 전부 없다. 원천(bzDataInterface)에도 주석이 0줄이라 사람이 써야 한다.
  LLM 으로 지어내면 안 된다: 태그가 실제로 무엇을 꺼내는지는 VB 함수 본문의 SQL 을 읽어야 알고,
  틀린 설명은 없는 설명보다 나쁘다.
· 예시 값 — 값은 런타임에 HIS DB 에서 환자 문맥을 받아 계산된다. '강남성심병원' 같은 더미를 넣으면
  사용자가 그것을 실제 반환값으로 믿는다. 실제 조회 가능 여부는 별도 조사 중이다.
· 최근 사용 일시 — 저장소가 없다. 만들어도 첫 실행에 384칸이 전부 빈다.
· 태그 키 절 — 태그는 이름이 곧 키다. 같은 문자열을 두 번 그리는 대신
  우측 큰 이름을 고정폭으로 두고 복사 버튼을 붙였다.
· 2단 소분류(기관·문서 > 기본 정보) — 데이터는 1단이다. 억지로 가르면 1개짜리 버킷 25개가 나오고,
  손으로 배당하면 새 태그가 자동 분류되지 않는 부채가 남는다.
· 상단 가로 칩 필터 줄 — 좌측 분류 트리와 같은 필터를 두 번 그린다. 두 곳이 갈라지면
  어느 쪽이 진실인지 알 수 없다. 필터의 단일 출처는 좌측 열이다.
· ⌘K 칩 — Windows 앱이고, 창이 열리면 생성자가 이미 검색칸에 포커스를 준다.

바꿔 넣은 것:

· '관련 필드' → '변형'. 없는 개념을 흉내내는 대신 지금 데이터로 계산되는 진짜 관계를 쓴다 —
  마지막 밑줄 앞이 같은 태그들(PAT_주민번호 → _Dash · _Blind). TagSearch.VariantsOf 신설.
  밑줄이 하나뿐이면 그건 접두어 경계라 제외한다(안 그러면 PAT_ 전체가 변형으로 딸려 온다).
· '자주 사용' 배지 → '추천'. 그런 임계값은 코드에 없다. 있는 것은 컨트롤 타입별 큐레이션
  (LegacyTagUsageCatalog.SuggestFor)뿐이라 '자주 사용'이라 부르면 근거를 왜곡한다.
· 사용 횟수에 스냅샷 표기를 붙였다. 숫자만 크게 띄우면 실시간 집계로 읽힌다.
· 상단 칩 줄 자리에는 좌측과 겹치지 않는 것 하나만 — '미사용 숨기기'.
  384종 중 227종이 미사용이라 값이 크다. 분류와 겹쳐서 걸린다.

창 껍데기는 OS 크롬을 유지했다. 목업의 '오른쪽 위 X + 둥근 카드'는 WindowStyle=None 이 필요한데
이 앱 대화상자 10종이 전부 OS 크롬이라 이 창만 바꾸면 규약이 두 벌이 되고,
AllowsTransparency 는 ClearType 이 꺼져 13px 한글이 뭉개져 금지돼 있다.
둥근 모서리와 그림자는 DWM 이 이미 그린다.

게이트: 테스트 267/267, --edit-smoke 0실패, --modal-check 2/2, --maxrect 10/10,
--query-popup 17/17, --db-smoke 3000 diff 0·예외 0,
--db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일, --dialog-shots 57파일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 14:32:09 +09:00
MsystechandClaude Opus 5 6ae05b73cd 가림막이 실제로는 안 깔리고 있었다 — 판정 시점을 고친다
직전 커밋(d9c1eaa)은 코드만 멀쩡했고 화면에는 아무 일도 일어나지 않았다.

ShowDialog() 는 창을 먼저 보이고(그때 Loaded 가 온다) <b>그다음에</b> 모달 루프에 들어간다.
그래서 Loaded 에서 읽는 ComponentDispatcher.IsThreadModal 은 아직 false 다 —
가드에 걸려 매번 그냥 반환했다. 모달 프레임이 돌기 시작한 뒤로 판정을 미룬다.
한 프레임 늦게 깔리는 것은 눈에 띄지 않는다.

이런 종류는 코드를 다시 읽어서는 안 잡힌다. 열어 봐야 안다.
그래서 --modal-check 를 만들었다: 대화상자를 실제로 ShowDialog 로 띄우고
그 순간 가림막 창이 화면에 존재하는지 센 뒤 닫는다.

이 진단을 만들면서 두 번 헛다리를 짚었고, 둘 다 검사가 스스로를 속인 경우다.
· BeginInvoke 로 세면 그 호출이 Loaded 보다 먼저 큐에 들어가 가림막을 거는 콜백보다
  앞서 실행된다 — 항상 0 이 나온다. 타이머로 바꿨다.
· 진단 분기는 OnStartup 의 전역 배선보다 먼저 반환하므로 진단 모드에는 그 훅이 없다.
  검사가 같은 배선을 직접 걸어야 프로덕션과 같은 것을 잰다.

실측: Loaded 시점 IsThreadModal=False / 모달 루프 안 True / 떠 있는 동안 가림막 1개 / 닫으면 0개.

게이트: 테스트 267/267, --edit-smoke 0실패, --maxrect 10/10, --dialog-shots 57파일,
--db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일, --modal-check 2/2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 14:20:02 +09:00
MsystechandClaude Opus 5 d9c1eaa293 모달 뒤를 어둡게 — 대화상자가 떠 있다는 것이 보이게
WPF 의 ShowDialog 는 입력만 막고 화면은 그대로 둔다. 대화상자가 앱과 같은 밝기로 떠 있으면
"이게 떠 있는 동안은 뒤를 못 만진다"가 안 읽혀서, 뒤를 클릭했다가 아무 반응이 없으면
멈춘 것으로 오해한다.

창이 뜨는 순간을 전역으로 잡아 모달일 때만 반투명 검정 창을 뒤에 깐다.
호출부마다 고치지 않은 이유는 ShowDialog 가 17곳이고 앞으로 더 늘기 때문이다 —
한 곳이라도 빠지면 그 대화상자만 다르게 동작하는데 그건 눈으로 전수 확인해야만 알 수 있다.
제목 표시줄 색을 거는 자리(WindowChromeTheme)와 같은 곳에 나란히 걸었다.

모달 판정은 ComponentDispatcher.IsThreadModal 이다. 진단 모드는 창을 모달이 아니라
화면 밖에 그냥 띄우므로 이 경로를 안 탄다 — --dialog-shots 57파일 그대로다.

가림막은 뒤 창의 이동·크기·상태 변화를 따라간다. 안 따라가면 창을 옮겼을 때
어두운 사각형만 제자리에 남는다. 최대화된 창은 Left/Top 이 '복원했을 때의 값'이라
그대로 쓰면 엉뚱한 데 깔리므로 화면 좌표를 직접 물어 DIP 로 되돌린다.

대화상자가 겹쳐 뜨면 그만큼 쌓이고, 각자 닫힐 때 제 것만 걷는다.

게이트: 테스트 267/267, --edit-smoke 0실패, --query-popup 17/17, --maxrect 10/10,
--db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일,
--dialog-shots 57파일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 13:48:56 +09:00
MsystechandClaude Opus 5 893d81a40e 패널이 선택 단위 — 자식을 집어도 묶음째 움직인다
패널로 감싸 놓고 정작 자식을 클릭하면 자식만 잡혀서 그 하나만 끌려갔다. 그룹처럼 안 움직인다.

이제 컨트롤을 클릭하면 최상위 컨테이너까지 거슬러 올라가 그것을 고른다.
패널이 잡히므로 끌면 자식들이 부모를 따라 함께 움직이고, 자식의 상대좌표는 그대로 남는다.
마퀴로 자식 하나가 걸려도 같은 규칙이라 선택 단위가 한 가지로 유지된다.

안으로 들어가려면 더블클릭한다. 한 번 들어가면 그 자식이 선택된 상태이므로
다시 더블클릭하면 예전처럼 글자 편집이다. 최상위 컨트롤은 들어갈 곳이 없으니
첫 더블클릭이 바로 글자 편집으로 간다(기존 동작 그대로).

이 규칙은 그룹용 패널뿐 아니라 사용자가 직접 만든 패널에도 똑같이 걸린다.
둘을 구분할 방법이 없기도 하고, 구분하면 같은 모양이 다르게 동작해 더 헷갈린다.
디자인 도구의 관례(프레임을 클릭하면 프레임, 들어가려면 더블클릭)와도 맞는다.

--edit-smoke 에 3건 추가: 자식을 집으면 패널이 잡히는가, 끌면 패널째 움직이고 자식 상대좌표가
그대로인가, 더블클릭이 안으로 들어가는가.

게이트: 테스트 267/267, --edit-smoke 0실패, --query-popup 17/17, --maxrect 10/10,
--db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 13:33:31 +09:00
MsystechandClaude Opus 5 0e30aa39c6 그룹을 진짜 패널로 — 저장하면 살아남는다
지금까지 그룹은 GroupId 라는 편집 전용 표시였고, 저장 포맷에 담을 자리가 없어 다시 열면 풀려 있었다.
레거시도 같았다(clsDesignTimeGroupService.vb:3-4 — "디자인 시에만 쓰는 임시 그룹, 저장 시 Group 정의 없이
컨트롤만 저장됨"). 다만 우리는 레이어 폴더로, 이름까지 붙이는 1급 개념으로 보여 줘서
사라질 때의 배신감이 더 컸다.

묶으면 실제 Panel 을 만들어 멤버를 그 자식으로 옮긴다. 패널은 레거시에 실재하는 컨트롤이라
저장·재열기·레거시 디자이너 재저장 전부에서 그대로 살아남는다.
검증: EMR 런타임의 5개 소비 경로(태그 채움·MDataTable·저장·탭순서·인쇄)가 전부 컨테이너를 재귀하고,
운영 1,273건 중 492건이 이미 컨테이너 안에 컨트롤을 담고 동작 중이다. XML 스키마도 원래 구조다.

그 대신 그룹이 '문서를 바꾸는 조작'이 된다. 조사에서 나온 부작용 넷을 가드로 막는다.

· 라디오는 묶지 않는다. 레거시 RadioButton 에는 GroupName 이 없어 <b>부모가 곧 배타 단위</b>다.
  '택1' 문항에서 일부만 묶으면 그 일부가 독립 집합이 되어 EMR 에서 답이 둘 선택된다.
  캔버스에는 아무 변화도 안 보이고 어떤 게이트도 못 잡는 종류라 아예 막았다.

· 멤버 사이에 남이 끼어 있으면 묶지 않는다. WinForms 에서 형제는 남의 자식 사이에 낄 수 없어
  멤버가 반드시 연속 z 블록이 되는데, 사이에 낀 것이 앞뒤로 밀려 쌓임 순서가 바뀐다.
  샘플 1장 실측에서 '이웃 둘 묶기' 35쌍 중 5쌍(14%)이 실제로 바뀌었다.

· 패널은 멤버 bbox 에 정확히 맞춘다. WinForms 패널은 자식을 하드 클리핑하는데 인쇄 경로에는
  클리핑이 0건이다 — 넘치면 화면에서만 사라지고 인쇄에는 나오는 컨트롤이 만들어진다.

· 좌표를 감쌀 때 정수로 확정한다. 저장 포맷이 정수만 쓰는데 부모 원점과 자식 상대좌표를
  따로 반올림하면 Location 과 LocationOnBase 가 축당 1px 어긋난 파일이 나온다.

탭 순서를 컨테이너 단위로 고쳤다. TabIndex 는 Windows 표준 탐색이라 컨테이너 범위인데,
문서 평면에 0,100,200… 을 뿌리고 있어서 패널 안의 것들이 저희끼리만 겨루고 탭이 그 패널을
통째로 지나친 뒤에야 들어갔다. 이제 부모별로 매기고 패널 자신에게는 제 자식 중 가장 이른 순번을 준다.

Ctrl+Shift+G 는 패널 풀기가 된다 — 자식을 최상위로 되돌리고 좌표를 절대값으로 복원하며
패널이 있던 z 자리에 그대로 끼워 넣어 쌓임 순서를 보존한다.
<b>레거시 디자이너에는 이 조작이 없다</b> — 거기서 패널을 지우면 자식까지 함께 지워진다.

기존 문서는 건드리지 않는다(새로 묶을 때만 패널이 생긴다). 그래서 왕복 검사와 렌더는 무사하다 —
직전 판단에서 "즉시 파괴"라고 적었던 것은 과장이었고, 여기서 정정한다.

--edit-smoke 의 그룹 검사를 다시 썼다. 폴더 행(LayerGroupViewModel) 기반 단언 9건은
그 표현이 사라져 삭제했고, 패널 구조·가드 2종·풀기 복원으로 8건을 새로 넣었다.
그룹 검사는 별도 문서에서 돈다 — 메인 designer 의 최상위 구성이 바뀌면 뒤따르는 Find() 들이
전부 무너진다(실제로 그렇게 깨져서 분리했다).

게이트: 테스트 267/267, --edit-smoke 0실패, --query-popup 17/17, --maxrect 10/10,
--db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

남은 것: 레이어 패널에서 '그룹용 패널'과 사용자가 만든 패널이 똑같이 보인다.
구분이 필요하면 별건으로 다룬다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 13:09:59 +09:00
MsystechandClaude Opus 5 d474937837 레이어 검색을 접어 두고 머리로 올린다
검색칩이 항상 한 줄을 먹고 있었다. 컨트롤이 몇 개뿐인 서식이 대부분이라 그 줄은 대개 빈 상자다.
돋보기·필터·필터해제를 '레이어' 글씨 오른쪽으로 올리고, 검색바는 돋보기를 눌러야 나오게 했다.

돋보기를 끌 때 검색어를 지운다. 상자가 접힌 채 필터만 살아 있으면 목록이 통째로 비어 보이는데
원인을 찾을 단서가 화면에 하나도 없다 — 열어 봐야 비로소 "아, 검색어가 남아 있었구나"가 된다.

켤 때는 한 틱 뒤에 포커스를 준다. 상자가 실제로 보이게 된 다음이어야 캐럿이 들어간다
(그룹 이름 편집 상자가 같은 이유로 같은 방식을 쓴다).

필터 버튼 둘은 위치만 옮겼다 — 활성 표시(파란 깔때기)와 해제 버튼을 흐리게 두는 규칙은 그대로다.

게이트: 테스트 267/267, --edit-smoke 236건 0실패, --query-popup 17/17, --maxrect 10/10,
--db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일,
--dialog-shots 57파일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:38:11 +09:00
MsystechandClaude Opus 5 6ee4435575 좌측 패널 — 페이지와 레이어를 두 구획으로 가른다
레이어 탭 하나에 페이지 마디와 컨트롤이 한 시퀀스로 섞여 있었다. 페이지를 추가·복제·삭제하는
조작이 컨트롤을 눈으로 찾는 일과 같은 목록에서 경쟁했다. 위는 페이지, 아래는 레이어로 나눈다.

페이지 행을 누르면 그 페이지를 활성화한다 — 캔버스가 거기로 스크롤하고 인스펙터가 용지 속성을 연다.
경로는 이미 있던 ActivatePage 그대로다. 강조는 ListBox 의 IsSelected 가 아니라 IsCurrent 가 그린다
(레이어 행이 Highlight 3상태로 그리는 것과 같은 규율 — 두 강조가 겹치면 어느 쪽이 진짜인지 모른다).

트리는 그 페이지로 좁히지 않는다. 세 가지가 거기 걸려 있다: 레이어 검색이 문서 전체를 훑는 성질,
그룹 강조가 페이지를 넘나드는 멤버까지 집계해야 폴더가 파래지는 것, 그리고 캔버스가 전 페이지를
세로로 이어 그리므로 트리만 한 장으로 좁으면 두 뷰의 1:1 대응이 깨진다는 것.
좁히고 싶으면 페이지 접기가 이미 있고 그건 명시적이다.

트리의 페이지 마디는 장수로 갈린다.
· 한 장이면 아예 없앤다 — 모든 컨트롤이 그 페이지 소속이라 정보가 0 이고,
  최상위가 들여쓰기 없이 바로 보인다. 실사용 문서 대부분이 여기다.
· 두 장 이상이면 남기되 경계 구분자로 줄인다(높이 20, 회색, 개수·복제·삭제 없음).
  통째로 빼면 안 되는 이유가 있다 — 페이지 행이 항상 무강조라서 연속 선택 블록이 페이지 경계에서
  저절로 끊기는데, 빼면 앞 페이지 마지막 컨트롤과 뒷 페이지 첫 컨트롤이 하나의 파란 띠로 붙는다.

들여쓰기 상수를 계산값으로 바꿨다. 페이지 마디가 없으면 들여쓸 이유도 없다 —
페이지 직속 12→0, 그룹 멤버 32→20.

트리 맨 아래 '페이지 추가' 행은 없앴다. 페이지 구획 머리의 + 가 그 자리다.
목록 안에 조작 행이 섞여 있으면 스크롤 끝까지 가야 눌 수 있다.

페이지 목록은 5행까지 보이고 그 뒤로는 목록 안에서만 스크롤한다 — 7장짜리 문서에서도
레이어 트리가 밀려나지 않는다. 트리는 남은 영역을 그대로 차지해 가상화를 유지한다.

이름 변경과 순서 변경은 넣지 않았다. FormPage 에 이름 필드가 없고 유일한 식별자인 Root.Id 는
E_SctMst 사전 매핑에 물려 있어 개명이 위험하다. 페이지 이동 API 도 없다(Pages 조작은 통째 재생성뿐).
표시 전용 이름을 새로 만들면 레거시 XML 에 담을 곳이 없어 왕복 diff 0 불변식과 충돌한다.

새 스타일 3종(SectionHead / SectionHeadText / PageRowItem). 구획 머리의 아래 1px 선은 필수다 —
라이트에서 B.PanelHeader(#FAFAFA)와 B.Panel(#FFFFFF)이 1.02:1 이라 배경만으로는 경계가 안 보인다.

게이트: 테스트 267/267, --edit-smoke 236건 0실패(레이어 단언 5건 갱신),
--query-popup 17/17, --maxrect 10/10, --db-smoke 3000 diff 0·예외 0,
--db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일, --dialog-shots 57파일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:33:57 +09:00
MsystechandClaude Opus 5 e82d520de6 스위치를 앞으로, 글씨를 뒤로
라벨을 72px 칸에 밀어 넣으니 "저장본 재조회" "재조회 무확인" 같은 이름이 두세 줄로 접혀
행이 뚱뚱해졌다. 참/거짓 행만 순서를 뒤집는다 — 스위치가 앞, 글씨가 뒤.

이러면 글씨가 남은 폭을 다 써서 접히지 않고, 스위치끼리 왼쪽에 모여 세로 축이 오히려 또렷해진다.
값을 적는 다른 행(라벨 + 입력)과 순서가 갈리는 것은 감수한다 — 스위치는 값을 '적는' 것이 아니라
'켜고 끄는' 것이라 애초에 다른 종류의 행이다.

라벨을 ToggleButton 의 Content 로 넣었다. 별도 TextBlock 으로 두면 글씨를 눌러도 아무 일이 없는데,
스위치 옆 글씨는 누르면 켜질 것으로 기대되는 자리다. 한 덩어리라 자동화 이름·키보드 조작도
그대로 하나로 유지된다. 호버하면 글씨도 함께 진해져 어디까지가 한 버튼인지 보인다.

'여러 값' 표시는 오른쪽 끝으로 뺐다 — 글씨 뒤에 붙이면 긴 라벨에서 밀려난다.

게이트: 테스트 267/267, --edit-smoke 237건 0실패, --query-popup 17/17, --maxrect 10/10,
--db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일,
--dialog-shots 57파일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:11:24 +09:00
MsystechandClaude Opus 5 a5083fd0bb 인스펙터 리디자인 — 목업대로
우측 속성 패널을 [200] 인스펙터 목업에 맞췄다. 바뀐 것 여섯.

· 탭이 알약에서 밑줄로. 알약은 '고르는 필터'로 읽히는데 탭은 화면을 가르는 것이지 값이 아니다.
  좌측 패널 탭 스타일을 재사용할 수 없었다 — 저쪽은 TabControl 의 키 없는 암시 스타일이고
  템플릿 안에 알약 컨테이너가 박혀 있어 고치면 레이어·팔레트 패널이 통째로 바뀐다.
  미선택도 밑줄 두께 2 를 Transparent 로 잡아 둔다. 안 그러면 고르는 순간 글자가 2px 튄다.
  밑줄과 글자는 같은 토큰(B.AccentText)이다 — B.Accent 는 라이트에서 3.0:1 이라 글자로 못 쓴다.

· 탭 배지를 뺐다. 잃는 게 없다: 개수가 0 인 탭은 RefreshTabCounts 가 탭 자체를 접으므로
  배지가 "0"을 보여 줄 기회가 애초에 없고, 누르기 전에 정확한 개수를 알아야 할 작업이 없다.
  DesignCount/DataCount/BehaviorCount 는 남긴다 — ShowDataTab/ShowBehaviorTab 이 그걸 물고 있다.

· 제목 줄이 생겼다. 타입 아이콘 + 이름 + ⋮. 무엇을 편집 중인지 아이콘으로 즉시 읽힌다.
  타입은 툴팁으로 보냈다(Summary 가 "TextBox — TextBox1" 형태로 이미 그 말을 한다).
  여럿을 골랐어도 타입이 같으면 그 타입 아이콘을 쓴다.

· 참/거짓이 체크박스에서 스위치로. 17px 사각형이 목록 오른쪽에 흩어져 정렬 축이 없었다.
  손잡이는 켬·끔 모두 흰색이다 — 색이 아니라 위치가 상태를 말하게 한다.
  불확정(다중선택 값 불일치)은 가운데다. 꺼짐으로 보이면 조용한 덮어쓰기 사고가 된다.
  미끄러지지 않고 튄다: 이 저장소에는 Storyboard 가 한 곳도 없어 여기만 움직이면 물성이 달라진다.
  ToggleButton 을 유지했으므로 스크린리더가 On/Off/Indeterminate 를 그대로 읽는다.

· 섹션 머리 — 위쪽에 구분선(아래에 그으면 마지막 섹션 밑에 뜬 선이 남는다), 제목을 B.Ink 로 승격,
  개수는 접혔을 때만. 펼쳤으면 개수는 바로 아래가 답이라 잡음이다.
  목업의 오른쪽 ⌃ 는 안 그린다 — 왼쪽 셰브론이 이미 같은 상태를 말하는데 한 행에 같은 정보를
  두 번 그리면 둘이 다른 뜻이라고 읽힌다.

· 라벨 폭 92 → 72, 세그먼트는 가로 꽉. 목업의 56 은 못 준다 — 목업에 없는 데이터·동작 탭에
  11자 라벨("입력 파라미터 컨트롤")이 있고 56 이면 3줄로 접힌다. 72 가 2줄을 지키는 최소값이다.
  줄어든 20px 은 그대로 입력 칸으로 갔다(121 → 141). 태그 제안 칩의 하드코딩 들여쓰기도 같이 옮겼다.

목업에 있지만 넣지 않은 것 둘. 회전 행과 그 옆 자물쇠 — Rotation/Angle 이 Core·레지스트리 전부
0건이다. 레거시 WinForms 컨트롤에 회전이 없다. 모델부터 새로 만들 신규 기능이지 리스킨이 아니다.
자물쇠도 종횡비 잠금이 없는 기능이고, 레이어 잠금과 같은 아이콘을 쓰면 오해가 확정된다.
'자동 줄바꿈'은 그 키가 레지스트리에 없어 실제 키인 '자동 높이'로 둔다.

'선택 정보' 탭은 만들지 않기로 했다(사용자 결정). 탭은 3개 그대로, 밑줄 스타일만 입혔다.
정렬 행은 디자인 탭 상단에 그대로 둔다 — 목업이 그려진 시점보다 나중에 생긴 기능이라
목업의 부재는 '빼라'는 뜻이 아니다.

새 토큰 B.SwitchOff 를 양 테마에 넣었다. 기존 토큰으로는 못 만든다 —
B.Input 은 라이트에서 패널과 같은 색이고, 다크는 입력 테두리가 Transparent 라
36x20 알약이 #262626#151515 = 1.23:1 로 사라진다. 비텍스트 3:1 을 넘긴 값으로 골랐다.
아이콘 ellipsis-vertical / file-text 2종 추가.

게이트: 테스트 267/267, --edit-smoke 237건 0실패, --query-popup 17/17, --maxrect 10/10,
--db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일,
--dialog-shots 57파일(인스펙터 스냅샷은 전부 바뀐다 — 이번 변경의 산출물이다).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:07:06 +09:00
MsystechandClaude Opus 5 7f8808eb44 정렬 기준을 고를 수 있게 + 간격 균등 + 아이콘 되돌리기
"전체 선택 후 오브젝트끼리 정렬 / 페이지에서 정렬" — 둘 다 필요한데 하나만 됐다.
직전 커밋의 규칙은 자동이었다: 여럿이면 무조건 선택 영역, 하나면 무조건 부모/페이지.
여럿을 골라 놓고 종이에 맞추고 싶을 때 방법이 없었다.

기준을 명시적으로 고르게 했다. 인스펙터 정렬 행 아래에 알약 둘 — 왼쪽은 선택에 따라
문구가 바뀐다(여럿이면 '선택 영역', 컨테이너 안의 하나면 '부모'), 오른쪽은 '페이지'.
최상위 컨트롤 하나만 골랐을 때는 이 줄을 아예 접는다 — 그 경우 '선택 영역'도 곧 페이지라
알약 둘이 똑같은 말을 하게 된다(실제로 그렇게 렌더돼서 접기로 했다).

컨테이너 안의 것을 페이지 기준으로 맞추면 부모 밖으로 나간다. 막지는 않는다 —
사용자가 고른 것이고 되돌릴 수 있다 — 대신 '페이지' 알약 툴팁이 그 사실을 말한다.

간격 균등 2개(가로·세로)를 붙였다. 3개 이상 골랐을 때만 나타난다 — 사이가 있어야 나눌 것이 있다.
정렬 6개와 같은 줄에 두면 8칸 276px 가 되어 패널 최소 폭 250 에서 잘리고, WrapPanel 로 흘리면
7+1 로 깨진다(둘 다 렌더로 확인했다). 그래서 '간격' 이라는 제 줄을 준다.

아이콘은 [200] 와이어프레임의 '사각형 하나 + 기준선'에서 원래 Lucide 도형
'길이 다른 막대 둘 + 기준선'으로 되돌렸다 — 후보 4종을 그려 고른 결과다.
막대가 둘이면 '무엇을 무엇에 맞추는가'가 도형만으로 읽히고, 저장소의 다른 아이콘과도 결이 맞는다.
LucideIcons 의 align-obj-* 6개는 직전 커밋 이전 상태로 정확히 복귀했다.

기준 알약은 RadioButton 이다. ToggleButton 둘로 만들면 둘 다 꺼진 상태가 표현 가능해지는데
그건 뜻이 없는 상태다.

게이트: 테스트 267/267, --edit-smoke 237건 0실패(기준 전환 검사 3건 신규),
--query-popup 17/17, --maxrect 10/10, --snap-shots 7종 0실패,
--db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일,
--dialog-shots 57파일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 10:31:24 +09:00
MsystechandClaude Opus 5 26f696ab00 정렬 — 기준은 언제나 '담고 있는 것'. 그리고 속성 패널에 정렬 행
"폼 기준으로 정렬"이 없었다. 정확히는 정렬 자체가 2개 이상에서만 동작했고 기준은 늘 선택 영역이라,
컨트롤 하나를 페이지 가운데 놓을 방법이 메뉴 어디에도 없었다.

규칙을 한 문장으로 바꿨다 — 기준은 그것을 담고 있는 것이다.
선택이 2개 이상이면 선택 영역이 그 자리를 대신하고, 1개면 담고 있는 것 —
컨테이너 안이면 컨테이너, 최상위면 페이지 — 이 기준이 된다. 피그마와 같다.

컨테이너 자식을 페이지 기준으로 맞추지 않는 이유. X/Y 는 부모 상대좌표라 '페이지 왼쪽'에 맞추면
음수가 되어 부모 밖으로 나간다. 디자이너 캔버스는 클리핑을 안 해서 멀쩡히 보이지만
레거시 런타임에서는 부모가 잘라먹는다 — 화면과 인쇄가 갈라지고 눈으로는 못 잡는 종류의 버그다.

같이 고친 것들:

· 페이지를 넘는 선택의 세로 정렬을 거부한다. 다른 종이의 '위 맞춤'에는 뜻이 없고,
  예전에는 실제로 깨졌다 — bbox 는 첫 페이지 월드 Y 인데 offset 은 자기 페이지 OffsetY 라
  vm.Y 가 큰 음수가 되어 컨트롤이 제 종이 위 허공으로 날아갔다. 가로는 모든 페이지의 X 원점이
  0 이라 그대로 허용한다.

· 부모와 자식을 함께 선택한 경우 자식을 대상에서 뺀다. 부모를 옮기면 자식 월드 좌표가 따라
  움직이는데 자식에게도 절대좌표를 대입하면 두 번 옮겨진다(레이어 목록에서 만들 수 있는 선택이다).

· 반올림을 AwayFromZero 로 고정했다. 기본 ToEven 은 322.5→322, 323.5→324 로 방향이 값에 따라
  뒤집힌다 — 폭이 제각각인 라벨 다섯 개를 가운데 맞춤하면 어떤 건 322, 어떤 건 323 에 앉는다.
  (스냅에서 반올림을 뺀 것과 반대로 보이지만 이유가 다르다. 거긴 가이드선이 모서리에서 뜨는 게
   보이기 때문이고, 정렬은 선을 긋지 않는다. 저장 포맷은 어차피 정수다.)

· 변경이 실제로 생길 때만 실행취소를 찍는다. 버튼으로 노출되면 연타되는데, 무조건 찍으면
  빈 되돌리기 단계가 쌓이고 '저장 안 한 변경'으로 표시된다.

산술은 Core/Layout/AlignSolver 로 뺐다. 짧지만 경계가 잔소리 같은 규칙 덩어리라(반올림 방향,
기준보다 넓은 항목, 원점이 큰 둘째 장) 디자이너 쪽에서 검사하면 케이스마다 실행취소 스냅샷을
하나씩 먹는다. 단위 테스트 10건이 그걸 공짜로 고정한다.

속성 패널 정렬 행. [200]SheetMe 의 inspector-wireframe.html 을 따랐다 —
30x26 버튼 6개, radius 6, 사이 2px, 가로 축과 세로 축 사이 8px 틈, 아이콘은 stroke 1.5.
LucideIcons 의 align-obj-* 6개는 등록만 되고 아무 데서도 안 쓰이던 죽은 항목이라 참조 도형으로
갈아 끼웠다(사각형 둘 + 기준선 → 사각형 하나 + 기준선). 좌표는 24격자다 — Build 가 Canvas 를
24x24 로 하드코딩해서, 참조의 16격자 좌표를 그대로 넣으면 아이콘이 좌상단 2/3 크기로 쏠린다.
크기 18 에서 stroke 2 × 18/24 = 1.5px 로 참조와 같은 굵기가 나온다.

버튼 스타일은 새로 만들었다. SegmentToggle 은 치수가 같지만 채워진 상태 토글 룩이고,
정렬은 상태가 아니라 액션이라 눌려 있는 것처럼 보이면 안 된다 — 채움 없이 호버만 남겼다.

표시 조건에 HasSelection 을 쓰지 않았다. 그건 페이지 속성 모드에서도 true 라 정렬할 컨트롤이
없는데 바가 뜬다. 선택 0 에서는 접어야 하는데, 안 접으면 '컨트롤을 선택하면…' 안내문 뒤에
버튼 줄이 그대로 남는다(같은 Grid 에 겹쳐 그려진다).

툴팁이 지금 무엇을 기준으로 정렬하는지 말한다 — "왼쪽 맞춤 — 페이지 기준" / "— Panel1 기준" /
"— 선택 영역 기준". 세로 3버튼은 페이지를 넘는 선택에서 비활성되고 그 이유를 툴팁에 적는다.
아이콘만 있는 버튼이라 AutomationProperties.Name 을 붙였다 — 이 저장소에 버튼 자동화 속성
전례가 없어 새로 여는 관례다.

메뉴는 그대로 두되 툴팁으로 전제를 적었고, '페이지 가로/세로 가운데'는 헤더에 '묶음째'를 붙였다.
선택 1개에서는 CenterH 와 결과가 같아지고, 다중에서만 갈리기 때문이다(그건 상대 배치를 보존한 채
묶음을 옮기고, CenterH 는 각자 가운데로 모은다).

분배(간격 균등) 버튼은 넣지 않았다. 참조 와이어프레임에 없고, 3개 이상에서만 살아서
실사용 대부분(1~2개 선택)에 두 칸이 죽은 채 남아 줄이 고장 난 것처럼 읽힌다.

--dialog-shots 에 선택 상태별 인스펙터 3장(12c/12d/12e)을 더했다. 정렬 바가 선택 0 과
페이지 속성 모드에서 사라지는지는 수치로 못 잡는다. 산출물 51 → 57.
기존 인스펙터 5장은 정렬 행만큼 아래로 밀린다 — 정상 diff 다.

알려진 별건(이번에 안 고침): 정렬 후 Ctrl+Z 하면 선택이 풀린다.
Undo 가 문서를 통째로 갈아끼우며 Selection.Clear 하기 때문이고, 정렬은 '왼쪽 → 아니네 → 가운데'로
여러 번 시도하는 조작이라 버튼으로 노출되면서 드러난다. Id 기반 선택 복원이 필요해 별도로 다룬다.

게이트: 테스트 267/267(신규 10), --edit-smoke 234건 0실패(정렬 검사 11건 신규),
--query-popup 17/17, --maxrect 10/10, --snap-shots 7종 0실패,
--db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일,
--dialog-shots 57파일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 10:17:02 +09:00
MsystechandClaude Opus 5 ba838ae54a 피그마식 스냅 — 여러 선이 동시에, 관련된 것만 잇고, 같은 간격이면 그렇다고 말한다
정렬 스냅 자체는 있었다. 다만 축마다 선 하나만, 그것도 화면 끝에서 끝까지 긋는 무한 점선이라
무엇에 맞춘 것인지가 안 보였다. 판단을 SheetMe.Core 로 옮기고 후보의 출처를 끝까지 들고 간다.

왜 옮겼나. 스냅은 눈으로만 확인되는 종류의 코드다 — 후보 하나, 열거 순서 하나가 바뀌면
붙는 자리가 조용히 달라지고 드래그해 보기 전엔 아무도 모른다. Core/Layout 의 SnapSolver 는
순수 기하이고 단위 테스트 27건이 승자 규칙·가이드 개수·선분 범위·간격 계산을 못 박는다.
SnapEngine 에는 "어떤 사각형이 후보인가"와 좌표 변환만 남았다.

달라진 것들:

· 동시에 맞는 정렬은 전부 보인다. 폭이 같은 형제 옆에 대면 좌·중·우 세 줄이 함께 뜬다.
  보정량은 승자 하나만 쓰되, 그 보정을 적용했을 때 실제로 맞는 후보만 선으로 낸다 —
  |거리|만 같고 부호가 반대인 후보까지 그리면 '붙었다고 그려 놓고 안 붙은' 선이 된다.

· 선이 선분이 됐다. 형제 가이드는 관련된 것들의 합집합 범위까지만, 용지 가이드는 그 용지 전체.
  같은 좌표의 형제 여럿은 한 선으로 합치고 범위만 넓힌다.
  덤으로 대비 문제도 풀렸다 — 예전 분홍(#FF4FA3)은 종이 밖 회색 배경 위에서 2.1:1 로 죽었다.

· 허용 오차가 화면 기준이 됐다. 월드 고정이면 줌 50%에서 두 배로 끈적이고 200%에서 절반으로
  미끄러진다 — 손끝 감각은 화면 거리로 정해진다. 핸들 히트 반경도 같은 병이라 함께 고쳤다
  (줌 300%에서 그림은 8px 인데 판정이 18px 였다). 선 두께도 화면 1px 로 고정한다.

· 승자가 결정적이 됐다. 예전에는 컨트롤 열거 순서가 동률을 갈랐다.
  이제 |거리| → 종류(형제 모서리 > 형제 중심 > 용지 경계 > 용지 중앙) → 좌표 → 모서리 순이다.

· 간격 균등. 형제들이 이미 같은 간격으로 늘어서 있으면 그 리듬에 끼거나 이어 붙는 자리에 붙고,
  같은 값이 된 빈틈 전부에 막대와 수치 칩을 놓는다. 양옆만 재면 "30"이 한 번 뜰 뿐이라
  무엇과 같아졌는지 안 보인다. 같은 축에서 정렬과 동시에 사정권이면 정렬이 이긴다 —
  거리로 겨루게 하면 한 픽셀 차이로 표시가 선↔막대로 튀어 종잡을 수 없다.

· 후보 범위. 컨테이너 자식도 이제 후보다(히트테스트는 재귀하는데 스냅만 최상위를 보던 규칙 불일치).
  대신 끌려가는 컨테이너의 자식은 빼야 한다 — 선택 목록엔 없지만 함께 움직이므로
  빼지 않으면 컨테이너가 제 자식에게 붙어 그 자리에서 굳는다.

· 리사이즈. 후보를 이동 드래그에서만 모으고 있어서, 앱을 켜자마자 핸들을 잡으면 스냅이 없고
  이동을 한 번 한 뒤에는 그때의 낡은 좌표에 붙었다. 리사이즈 시작에도 모으고 놓을 때 버린다.
  모서리 스냅이 '첫 매치'였던 것도 이동과 같은 최근접 규칙으로 통일했다. 가이드도 함께 보인다.

· 정합된 축은 반올림하지 않는다. 형제 폭이 홀수면 중심이 반정수라, 반올림하면 중앙에 맞췄는데
  0.5px 어긋난 채로 놓인다. 무한선일 땐 안 보였지만 선분이 되면 모서리에서 뜬 게 눈에 띈다.
  저장은 LegacyFormat.FormatPair 가 어차피 정수로 쓰므로 포맷은 영향 없다.

성능 가드: 한 줄에 겹치는 형제가 64개 넘으면 간격 계산을 건너뛰고(정렬은 정상),
형제가 2000개 넘으면 간격을 끈다. 후보 정렬은 드래그 시작에 1회, 프레임 경로는 무할당.
가이드 내용이 이전과 같으면 컬렉션을 건드리지 않는다.

신규 진단 --snap-shots — 종이와 오버레이를 겹쳐 드래그 도중을 찍는다.
기존 --render-smoke/--db-render 는 종이만 찍어 오버레이가 안 나왔다. 선이 어디서 어디까지
그어지는지, 몇 개가 동시에 보이는지는 그림으로만 확인된다. 게이트는 함께 남기는 _report.txt 의
수치로 걸고 PNG 는 눈 검증 보조다(픽셀 비교는 테마·글꼴에 흔들려 게이트로 못 쓴다).

게이트: 테스트 257/257(신규 27), --edit-smoke 223건 0실패(스냅 검사 14건 신규),
--query-popup 17/17, --maxrect 10/10, --db-smoke 3000 diff 0·예외 0,
--db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일, --snap-shots 7종 0실패.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 09:23:42 +09:00
MsystechandClaude Opus 5 8a7cc65099 최대화가 작업표시줄을 덮던 것 — 창을 작업영역에 맞춘다
메인 창은 WindowStyle=None + WindowChrome 으로 크롬을 직접 그린다. 그 조합에서 OS 는
최대화 사각형을 작업영역이 아니라 모니터 전체를 기준으로 잡고 리사이즈 프레임만큼 더 부풀린다.
실측으로 창이 (-7,-7)~(1927,1087) — 작업영역 (0,0)~(1920,1032) 를 55px 넘겨
작업표시줄이 통째로 가려졌다. 표준 크롬 창에는 없는 문제라 WindowStyle=None 창에만 건다.

WM_GETMINMAXINFO 훅 하나로 답한다(Services/MaximizeToWorkArea.cs).
레이아웃·XAML·테마는 건드리지 않았다. 최대화 상태에서 콘텐츠가 밀려 보인다면
답은 Margin 보정이 아니라 이 훅의 계산이 틀린 것이다 — 창 사각형이 정확히 작업영역이면 잘릴 곳이 없다.

같이 처리한 것들:

· 최소 크기가 최대화를 이기는 경우. OS 는 최대화 크기를 최소 트래킹 크기까지 되밀어 올린다.
  그 값은 우리 뒤에 도는 WPF 가 MinWidth/MinHeight(940x640 DIP)로 채우므로,
  작업영역이 그보다 작으면 창이 도로 부풀어 작업표시줄을 다시 덮는다 —
  1366x768 을 125% 로 쓰면 최소 높이 800px 에 작업영역 720px 다.
  그 경우에만 WPF 의 처리를 막고 트래킹 크기를 직접 채운다. 화면에 안 들어가는 최소 크기는
  최소 크기 구실을 못 한다. 보통은 handled=false 로 두어 MinWidth/MinHeight 가 그대로 산다.

· 최대화 중 상단 6px 리사이즈 띠. 전에는 창이 화면 밖으로 밀려 있어 그 띠가 안 보였다.
  이제 타이틀바 위로 올라와 닫기 버튼 윗변이 먹혔다(실측 y=1·3·5 에서 HTTOP).
  최대화된 창은 어차피 가장자리로 크기를 못 바꾸므로 그동안 띠를 걷는다. 여백 보정이 아니라
  히트테스트 영역 보정이다 — 창 사각형은 그대로 작업영역이다.

· 자동 숨김 작업표시줄. 자동 숨김이면 작업영역이 모니터 전체와 같아지고, 그대로 꽉 채우면
  셸이 전체 화면 앱으로 보아 가장자리 호버에 반응하지 않는다 — 작업표시줄이 영영 안 나온다.
  네 변을 각각 ABM_GETAUTOHIDEBAREX 로 보고 1px 씩 물러선다. 폴백은 '한 변도 못 찾았을 때'가
  아니라 '그 변을 못 찾았을 때' 돈다(좌측 서드파티 바 하나 때문에 하단 작업표시줄이
  구제받지 못하던 구조였다). 폴백이 쓰는 두 API 는 주 작업표시줄 전용이라
  보조 모니터는 못 덮는다 — 주석을 그렇게 바로잡았다.

· 재부착 가드. 최대화 중에 두 번 붙으면 그때의 0 을 '복원 시 두께'로 기억해
  복원해도 가장자리를 끌 수 없는 창이 됐다.

진단 종료 코드가 죽어 있었다. 기본 ShutdownMode 는 OnLastWindowClose 라, 창을 띄웠다 전부 닫는
진단은 마지막 창이 닫히는 순간 WPF 가 먼저 Shutdown(0) 을 걸고 뒤이은 Shutdown(코드) 인자를
무시한다 — 리포트에 '실패 1건' 이 찍혀도 프로세스는 0 을 돌려줬다(--maxrect 첫 실행이 그랬다).
--edit-smoke·--db-smoke 도 같은 구멍이었다. 진단 분기 진입부에서 OnExplicitShutdown 으로 바꿨다.
같은 자리에서 진단 모드 UI 예외도 모달 대신 로그+Shutdown(3) 으로 돌린다 —
'진단 모드에서는 모달을 띄우지 않는다'는 규칙을 그보다 먼저 등록되는 예외 핸들러가 깨고 있었다.

신규 진단 --maxrect (10건). 기대값을 베껴 적지 않고 성질을 단언한다 —
모니터 안에 들어가는가, 자동 숨김 없는 변은 작업영역과 같은가, 있는 변은 물러섰는가,
작업표시줄과 겹치지 않는가, 최소 크기가 최대화 크기를 넘지 않는가.
전사(轉寫)하면 같은 착각을 양쪽이 공유하고 양보 폭을 1→2 로 바꾸는 순간 거짓 실패가 난다.
보내는 버퍼는 센티넬로 채운다 — 0 으로 두면 기대값이 (0,0) 인 모니터에서 훅이 아무것도
안 써도 통과한다(실제로 한 번 그렇게 통과했다). 창을 실제로 띄워 최대화·복원까지 해
OS 가 답을 지키는지와 StateChanged 배선까지 본다. 그 경로에서 화면이 한 번 번쩍인다.

실측(1920x1080·96DPI·하단 고정 작업표시줄):
  최대화 (-7,-7)~(1927,1087) → (0,0)~(1920,1032), 침범 55px → 0px
  복원 1440x920 유지, 최소 크기 940x640 유지
  최대화 상단 히트테스트 HTTOP → HTCAPTION

자동 숨김·다중 모니터·125%/150% 배율은 이 개발기에서 재현할 수 없다. 그 환경에서 --maxrect 를
한 번 돌려 봐야 한다.

게이트: 테스트 230/230, --edit-smoke 0건, --query-popup 17/17,
--db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일,
--dialog-shots 51개, --maxrect 10/10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 18:52:19 +09:00
MsystechandClaude Opus 5 749de6b566 쿼리 편집기 세 가지 — 한글이 사라지던 것, 줄 번호가 밀리던 것, 클릭이 먹지 않던 것
1) 한글로 입력하면 글자가 안 보였다.
   색칠은 "투명한 TextBox 뒤에 우리가 그린다"로 되어 있다. 그런데 IME 조합 중인 글자는
   아직 TextBox.Text 에 없다 — TextChanged 가 오지 않으니 우리는 그릴 수 없고,
   TextBox 는 제 Foreground(투명)로 그리니 아무것도 안 보인다. 자음·모음을 맞추는
   내내 빈 화면이었고, 확정하고 나서야 나타났다.
   조합이 시작되면 TextBox 에게 잉크색을 돌려주고 색칠 층을 감춘다(ShowRawText).
   확정되면(TextInput) 되돌리고, ESC 로 취소돼 CompositionText 가 비어도 되돌린다.
   조합 도중 포커스를 잃으면 끝 신호가 오지 않으므로 blur 에서도 되돌린다 —
   색 없는 상태로 굳지 않게.
   조합 중 잠깐 단색이 되는 편이 글자가 안 보이는 것보다 낫다.

2) 줄 번호가 실제 줄과 어긋났다.
   번호를 "1\n2\n3…" TextBlock 한 덩어리로 두면 번호의 줄 간격은 TextBlock 조판이,
   본문의 줄 간격은 TextBox 가 정한다. 한글 글꼴 대체가 끼면 조금씩 벌어져
   아래로 갈수록 눈에 띄게 밀렸다.
   LineNumberGutter 를 만들어 각 줄 첫 글자의 y 를 GetRectFromCharacterIndex 로 물어
   그 자리에 번호를 그린다. 스크롤·여백·폰트 대체가 전부 TextBox 기준으로 반영돼 오므로
   구조적으로 어긋날 수 없다. 폭은 가장 큰 번호에 맞춰 잡고 번호는 오른쪽 정렬한다.

3) 자동완성 목록의 항목을 클릭하면 아무것도 안 들어가고 목록만 닫혔다.
   Popup 이 StaysOpen=False 라 마우스를 캡처하고 있었다. 항목을 누른 그 클릭이
   '바깥 클릭'으로 먼저 소비돼 팝업이 닫히고, 항목은 클릭을 받지 못했다.
   StaysOpen=True 로 바꾸고 닫는 일은 코드가 맡는다(커서 이동·포커스 상실·창 비활성 —
   이미 있던 핸들러들). 집는 처리는 팝업 안쪽 Border 에서 터널 단계로 받는다.

진단(--query-popup)에 "목록 항목을 집으면 입력되고 목록이 닫힌다"를 더해 17건.
마우스 라우팅 자체는 화면 없이 흉내 낼 수 없어(Ctrl+Enter 때와 같은 이유)
핸들러가 부르는 경로를 그대로 태워 삽입까지 확인한다. IME 조합은 합성할 수 없어
1) 은 실기 확인이 필요하다.

게이트: 테스트 230/230, --edit-smoke 0건, --db-smoke 3000 diff 0·예외 0,
--db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 17:32:48 +09:00
MsystechandClaude Opus 5 6ce055b00c 쿼리 편집기 — 렌더를 목업과 픽셀 대조해 찾은 11건 수정
목업 사양을 글로 옮긴 뒤, 라이트·다크 렌더 4장과 XAML 을 각각 다른 눈으로 대조하고
그 지적을 다시 반증하는 감사를 돌렸다. 확정된 것만 고쳤다. 대부분 '내가 지정한 색이
전역 스타일에 먹히고 있었다'는 한 가지 뿌리에서 나왔다.

■ 글자색이 지정한 대로 안 나오던 것 (가장 컸다)

'포맷'·'전체 지우기'·'닫기'·+ 가 전부 본문색이었다. 라이트 '전체 지우기' 영역
히스토그램에 파란 픽셀이 0개였다. 원인은 전역 TextBlock 스타일(Foreground=B.Ink)이
ContentPresenter 가 만든 글자에 <b>직접</b> 걸려 Button.Foreground 상속을 이기는 것.
공유 버튼 템플릿이 이미 쓰던 방어(ContentPresenter.Resources 로 TextBlock Foreground 를
버튼에 바인딩)를 툴바·칩 템플릿에도 넣었다.
같은 이유로 파란 채움 칩 '전체' 위 글자가 검정이었다 — 채움도 B.Accent(#0099FF, 흰 글자 3.0:1)에서
B.AccentFill(#0F6FBF, 5.19:1)로 바꿔 양 테마가 함께 해결된다.

■ 사각 편집 상자 안에 파란 둥근 상자가 겹쳐 있던 것

전역 TextBox 템플릿이 BorderThickness=1·CornerRadius=8 을 하드코딩하고 포커스 시 그 테두리를
강조색으로 칠한다. SqlBox 의 BorderThickness=0 은 무시된다.
y=300 가로 스캔에서 구분선(#DCDCDC) 바로 옆 x=59 가 #0099FF 였고, 세로로 훑으면 반경 8 의 호를 그렸다.
사용자가 지적한 '줄칸과 입력칸 사이 선의 라운드'가 이것이었다 — 무테 템플릿(ChipTextBox)으로 바꿨다.

■ 다크에서만 사라지던 것 3가지

· 줄번호 홈통: B.Input 과 B.Chip 이 둘 다 #262626 이라 면이 갈라지지 않았다(1px 선만 남음).
  편집 영역 배경을 B.Surface(#1C1C1C)로 바꿔 홈통이 드러나게 했다.
· 결과/상태 상자와 카드 경계: B.InputBorder 가 다크에서 Transparent 라 테두리 픽셀이 0개였다.
  B.Line2 로 바꿨다.
· 현재 줄 띠: B.Hover(#242424)가 편집 배경(#262626)보다 오히려 어두워 1.02:1 이었다.
  편집기 전용 토큰 B.SqlCurrentLine 을 양 테마에 신설했다(다른 화면의 호버 룩은 그대로).
· 선택된 변수 행: B.Sel 이 다크에서 #3C3C3C 중성 회색이라 '옅은 파랑'이 아니었다.
  공유 토큰은 두고 이 목록에만 B.LayerSubtree 를 준다.

■ 그 밖

· 카드에 그림자가 0px 이었다 — B.CardShadow 를 신설했다(기존 B.FloatShadow 는 팔레트용이라 세다).
· 카드 사이 GridSplitter 의 회색 띠가 목업에 없는 선으로 보였다 — 투명으로.
· 결과 표: 마지막 컬럼 뒤 채움 머리글이 회색이라 이름 없는 컬럼처럼 보였다(Column=null DataTrigger 로 처리).
· 결과 표: 본문 글자가 머리글보다 8px 왼쪽으로 밀려 있었다 — DataGridCell 기본 템플릿이 Padding 을
  쓰지 않아서다. 템플릿을 지정해 여백이 먹게 했다.

회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업·정렬·결과표·단축키 점검 16/16,
종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 17:14:55 +09:00
MsystechandClaude Opus 5 c98e2413b5 쿼리 편집기 — 치수 원복, 툴바 '포맷' 강조
■ 치수 원복

앞서 목업과 나란히 놓고 전체를 키웠는데(a4acfe4), 요청은 크기가 아니라 스타일이었다.
창 1440×960 → 1180×760 을 비롯해 글자·여백·행 높이를 모두 되돌린다.
직전 커밋의 스타일 수정(줄번호 홈통 회색 면 · 구분선 직선화 · 결과 표 가로선)은 유지한다.

■ 툴바 '포맷' 강조

목업에서 '포맷'만 파란 글자 + 밑줄로 그려져 있다. 셋 다 동작이라 '선택된 탭'은 아니지만
문서를 통째로 바꾸는 주 동작이라 그 자리를 늘 표시해 둔다(ToolBtnPrimary).

회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업·정렬·결과표·단축키 점검 16/16.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:47:07 +09:00
MsystechandClaude Opus 5 069d228de3 쿼리 편집기 — 줄번호 홈통·구분선·결과 표 스타일
크기가 아니라 스타일이 달랐던 부분을 고쳤다.

· 줄번호 홈통에 회색 면(B.Chip)을 깔았다. 본문과 같은 흰 면이라 홈통이 있는지 안 보였다.
· 편집 상자의 모서리 라운드를 없앴다. 둥글리면 클리핑 때문에 홈통의 회색 면과
  본문 사이 구분선까지 함께 휘어 보인다 — 지목된 그 선이다. 구분선도 B.Line2 로 또렷하게.
· 결과 표: 바깥 테두리를 표가 아니라 감싸는 상자가 그리게 하고, 격자를 가로선만 남겼다.
  세로선까지 그으면 컬럼이 52개인 조회에서 화면이 격자로 가득 찬다.
  머리글은 회색 면 + 아래·오른쪽 경계만, 행 높이 38, 셀 여백 12.

회귀: 편집 스모크 실패 0, 팝업·정렬·결과표·단축키 점검 16/16, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:41:04 +09:00
MsystechandClaude Opus 5 a4acfe4469 쿼리 편집기 — 목업 치수에 맞춤
앞선 커밋은 배치만 맞고 치수가 전반적으로 작았다. 목업과 나란히 놓고 재서 맞췄다.

창 1180×760 → 1440×960, 바깥 여백 16 → 26, 카드 안 여백 14 → 22, 카드 모서리 10 → 14.
제목 17 → 25, 대상 칩 여백·모서리 확대. 카드 제목 13.5 → 16.
툴바 글자 12.5 → 14, 항목 간격 18 → 32.
편집 글꼴 13.5 → 14.5, 줄번호 홈통 여백 확대.
결과 표 행 높이 30 → 40, 머리글 11.5 → 13, 본문 12 → 13.
치환 변수 열 430 → 500, 검색칸 높이 확대, 갈래 칩 13/여백 14,6/완전 둥글게,
목록 행 설명 12 → 14 · 속성명 10.5 → 12 · + 13 → 17, 행 간격 3 → 6.
하단 버튼 여백 확대.

회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업·정렬·결과표·단축키 점검 16/16,
종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:36:36 +09:00
MsystechandClaude Opus 5 568fbdb0dd 쿼리 편집기 디자인 맞춤 — 그리고 스크롤이 자동완성을 지우던 결함
■ 목업에 맞춘 것

· 툴바(포맷·주석·자동완성)를 탭 모양으로 — 밑줄이 들어오는 글자 버튼.
· '전체 지우기'를 빨강에서 강조색 링크로. 되돌릴 수 있는 동작이라 위험색을 쓸 이유가 없다
  (TextBox 실행취소에 남는다).
· 검증 결과와 상태 줄을 한 상자로 묶고, 결과가 닫혀 있어도 상태 줄은 남게 했다.
· 결과 표에 머리글 면·격자선·행 높이를 줬다. 컬럼이 52개까지 나오는 조회가 있어
  가로 스크롤이 자연스럽게 보여야 한다.
· 하단 버튼을 왼쪽으로 옮기고 여백·글자 크기를 키웠다. 줄 번호는 세로선으로 본문과 가른다.

■ 스크롤이 자동완성 목록을 지우던 결함

디자인 작업 중 --query-popup 이 '포커스를 잃으면 닫힌다'에서 실패했다.
파고 보니 원인은 포커스가 아니라 <b>그 앞 단계</b>였다 — 목록이 아예 안 열리고 있었다.

편집기가 스크롤되면 목록을 닫도록 해 뒀는데, 긴 문서 끝에서 타이핑하면 그 입력 자체가
스크롤을 일으킨다. 그래서 목록이 열리자마자 스크롤 이벤트가 닫아 버렸다.
실사용에서 '긴 쿼리에서만 자동완성이 안 뜬다'로 나타났을 결함이다.

이제 스크롤에는 닫지 않고 <b>새 자리에 다시 놓는다</b>(WPF Popup 은 열린 채로 Placement 를
다시 계산하지 않으므로 닫았다 다시 연다). 걸려 있던 낱말을 커서가 벗어난 경우에만 닫는다.

회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업·정렬·결과표·단축키 점검 16/16,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:31:08 +09:00
MsystechandClaude Opus 5 0e2e2fb0e6 검증 실행에 Ctrl+Enter 추가 — 단축키를 선언으로 옮겼다
Ctrl+Enter 로도 검증이 돌게 했다(F5 는 그대로 남는다).

■ PreviewKeyDown 대신 KeyBinding 으로

처음에는 PreviewKeyDown 에서 Keyboard.Modifiers 를 읽어 처리했는데, 그 경로는 검사할 수가 없다 —
Keyboard.Modifiers 는 <b>실제 키보드 상태</b>에서 나오므로 합성 키 이벤트로는 Ctrl 이 잡히지 않는다.
진단을 쓰다 이걸 발견했고(검사 2건이 계속 실패했다), 단축키를 Window.InputBindings 선언으로 옮겼다.
이제 진단이 등록된 KeyBinding 을 찾아 그 명령을 직접 실행해 배선을 확인한다.

■ 자동완성 Enter 와 뜻이 갈린다

목록이 떠 있을 때 Enter 는 후보 확정이다. Ctrl+Enter 가 그 처리에 먼저 걸리면 검증이 안 돈다.
PreviewKeyDown 의 목록 처리에서 Ctrl+Enter 만 비켜 가게 하고, 실행할 때 목록을 닫는다.

--query-popup 진단에 4건 추가(Ctrl+Enter 등록 · F5 유지 · 실행됨 · 실행하며 목록 닫힘). 현재 16/16.

회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업·정렬·결과표·단축키 점검 16/16,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:19:41 +09:00
MsystechandClaude Opus 5 6c71159dad 쿼리 편집기 — 머리말 배지·안내문·목록 안내상자 제거
화면에서 지목된 세 덩어리를 뺐다.

· 제목 앞 아이콘 배지 — 창 제목이 이미 같은 말을 한다.
· 제목 아래 안내문("작성 시점에 <<변수>> 가 …") — 우측 목록과 자동완성이 그 자리를 대신하고,
  두 줄이 편집 영역을 그만큼 밀어 올리고 있었다.
· 변수 목록 아래 안내 상자(ⓘ 118개 · + 를 누르거나 …) — 개수는 칩에 이미 있고
  삽입 방법은 + 버튼 자체가 보여 준다.

SQL 이 아닌 용도(상용구 편집)에서는 안내문이 제목 역할을 하고 있었으므로,
그 경우 제목 자리에 대상 이름을 넣고 코드 칩은 감춘다.

회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업·정렬·결과표 점검 12/12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:14:18 +09:00
MsystechandClaude Opus 5 f447ae742b 검증 실행 결과에 출력 행이 안 보이던 문제
결과를 ExpandoObject 목록으로 DataGrid 에 넘기고 있었다. DataGrid 의 AutoGenerateColumns 는
항목 <b>타입</b>의 속성을 반사해 열을 만드는데 ExpandoObject 에는 그런 속성이 없다 —
그래서 요약("성공 — 컬럼 2개, 표본 3행")은 맞게 뜨는데 표는 비어 있었다.
DataTable 로 바꿨다. DataGrid 가 직접 알아본다.

같은 컬럼명이 두 번 나오면(별칭 없는 조인에서 흔하다) DataTable 이 예외를 내므로 뒤에 번호를 붙인다.

■ 0행과 실패를 구분한다

구문은 맞는데 조건에 걸리는 데이터가 없을 수 있다. 검증 실행은 치환 변수를 빈 값으로 바꿔
돌리므로(런타임과 같은 방식) 조건이 빡빡하면 0행이 정상이다.
그 경우 표를 감추고 "구문은 정상입니다 — 치환 변수를 빈 값으로 실행하므로 조건에 걸리는 행이
없을 수 있습니다" 를 덧붙인다. 빈 표를 보여 주면 실패한 것처럼 읽힌다.

결과 영역을 190 → 260px 로 키웠다.

■ 이 종류를 다시 놓치지 않도록

--query-popup 진단에 3건을 더 넣었다: 결과 창이 열리는가 · 표에 열이 생기는가 · 행이 채워지는가.
요약만 검사했다면 이번 결함을 그대로 통과시켰을 것이다. 현재 12/12.
--dialog-shots 에도 검증 실행을 마친 상태(03b-query-trial)를 추가해 눈으로도 남는다.

회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업·정렬·결과표 점검 12/12,
검증 실행 점검 10/10, DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:10:27 +09:00
MsystechandClaude Opus 5 3e26af8739 쿼리 편집기 — 한글에서 색과 글자가 어긋나던 문제, 다크 제목 표시줄, 목업 맞춤
■ 한글을 치면 색칠이 글자와 어긋났다 (가장 중요)

색칠 레이어가 글자 위치를 <b>직접 재서</b> 잡고 있었다. 앞부분을 FormattedText 로 측정해
그 폭만큼 오른쪽으로 옮겨 그리는 방식이다. ASCII 에서는 맞았지만 한글에서 무너졌다 —
편집 글꼴(Consolas)에 한글 글리프가 없어 폰트 대체가 일어나는데,
TextBox 의 내부 조판과 이쪽 측정이 같은 글꼴·같은 셰이핑을 고르리라는 보장이 없다.
조금만 달라도 뒤로 갈수록 벌어져 색과 글자가 따로 논다.

이제 <b>TextBox 에게 좌표를 묻는다</b>(GetRectFromCharacterIndex).
여백·스크롤·줄 높이·폰트 대체가 전부 TextBox 기준으로 이미 반영돼 오고,
조각마다 위치를 새로 물으므로 오차가 누적되지 않는다.
줄 높이를 손으로 계산하던 코드도 함께 사라졌다.
편집 글꼴 순서도 한글이 있는 것(D2Coding → 굴림체 → Consolas)으로 바꿔 대체 자체를 줄였다.

--query-popup 진단에 정렬 검사 2건을 넣었다: 한글이 섞인 줄에서 글자 좌표가 뒤로 밀리지 않는가,
마지막 글자 다음 좌표가 캐럿 자리와 이어지는가. 현재 9/9.

■ 다크 모드인데 제목 표시줄만 밝게 남던 문제

ThemedWindow 스타일은 창 <b>안쪽</b>만 칠한다. 제목 표시줄은 OS 가 그리고
기본값은 Windows 의 테마를 따르므로, 앱을 다크로 바꿔도 Windows 가 라이트면 흰 띠가 남는다.

DWM 속성(DWMWA_USE_IMMERSIVE_DARK_MODE)으로 창별로 지정한다. 커스텀 크롬을 직접 그리는 것보다
훨씬 싸고, 최소화·최대화·닫기의 동작과 접근성이 OS 것 그대로 남는다.
창마다 손으로 넣으면 새 대화상자에서 반드시 빠지므로 App 에서 Window 클래스 핸들러로 한 번만 걸었다 —
지금 있는 창과 앞으로 만들 창 모두 자동으로 적용된다.
테마를 바꾸는 순간 이미 떠 있는 창들도 함께 맞춘다(ThemeManager.ApplyChromeToOpenWindows).

■ 목업과 위치 맞춤

앞 배지(아이콘) · 검색칸의 돋보기와 안내 문구 · 안내 상자의 ⓘ 를 넣었다.
갈래 칩 순서는 개수순에서 <b>고정 순서</b>(환자·서식·작업자·컨트롤)로 바꿨다 —
컨트롤 수는 서식마다 달라서 개수순으로 두면 서식을 바꿀 때마다 칩이 자리를 바꿔
손이 기억하지 못한다.

회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업·정렬 점검 9/9, 검증 실행 점검 10/10,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:05:33 +09:00
MsystechandClaude Opus 5 68fc01c6ba 쿼리 편집기 레이아웃 — 카드 두 장, 갈래 칩, 그리고 《컨트롤명》 치환
목업대로 다시 짰다. 색·모서리·간격은 SheetMe 토큰을 그대로 쓴다.

■ 레이아웃

머리말(제목 + 대상 칩) / SQL 편집기 카드 / 치환 변수 카드 / 아래 버튼 줄.
편집기 카드에는 툴바(포맷·주석·자동완성 · 전체 지우기)와 상태 줄(연결 상태 · 글자·줄 수 · 줄,열)을 뒀다.
변수 카드에는 검색 + 갈래 칩 + 접이식 그룹 + 행마다 + 버튼을 뒀다.

+ 버튼을 넣은 이유는 더블클릭이 유일한 삽입 수단이었기 때문이다 — 화면에 드러나지 않는 조작이다.

■ 《컨트롤명》 치환 — 아예 없던 기능

레거시 화면에는 Controls 목록이 따로 있었다. 우리에겐 그 문법 자체가 없었다.
clsMDataTable.ConvertQuery 는 <<…>> 를 모두 치환한 뒤 《 로 다시 쪼개
poAllControlOnSheet(이름).GETVALUE 를 박는다(:78-94). 즉 같은 서식의 다른 컨트롤 값을
쿼리 조건에 넣을 수 있다.

이제 이 서식의 컨트롤이 '컨트롤' 갈래로 목록에 뜨고, 토크나이저도 《》 를 변수로 잡아
색이 붙고 검증에 걸린다. 다만 디자인 시점에는 컨트롤 목록이 없어 런타임이 무조건 "0" 으로
치환한다(:86) — 검증 실행이 통과해도 운영에서는 다른 값이 들어간다는 뜻이라 코드에 적어 뒀다.

■ 형식(String/Int32/DataRow) 표시

레거시가 '목록/형식' 두 열로 보여 주던 정보다. DataRow 인지 아닌지가 곧
'컬럼명을 더 채워야 하는가'를 뜻해서 고를 때 필요하다.

■ 갈래 칩

그룹이 일곱이라 칩으로 다 늘어놓으면 그것대로 목록이 된다.
'값이 어디서 오는가'로 네 갈래(환자·서식·컨트롤·작업자)로 묶고, 같은 칩을 다시 누르면 해제된다.

■ 포맷(Ctrl+Shift+F 상당 — 툴바 버튼)

예약어·함수를 대문자로 바꾸고 주요 절 앞에서 줄을 나누며 괄호 깊이만큼 들여쓴다.
문자열·주석·치환 변수는 손대지 않는다 — 특히 치환 변수는 대소문자가 맞아야 런타임이 찾으므로
대문자로 바꾸면 그 칸이 조용히 빈다. 이 포매터가 저지를 수 있는 가장 나쁜 일이라 테스트로 막았다.
두 번 눌러도 결과가 같다(안정성도 테스트로 고정).

단위 테스트 14건 추가(포매터 11 · 컨트롤 치환 3).
회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업 점검 7/7, 검증 실행 점검 10/10,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 15:53:56 +09:00
MsystechandClaude Opus 5 7ce7506286 자동완성 팝업 — 소멸 규칙과 위치, 그리고 그걸 재는 진단
두 가지 신고를 고쳤다: 위치가 안 맞고, 항목을 고르기 전까지 사라지지 않는다.

■ 사라지지 않던 문제

StaysOpen=True 로 두고 있었다. 편집 포커스를 잃지 않으려고 그렇게 했는데,
그 값은 <b>바깥을 클릭해도 닫히지 않는다</b>는 뜻이다. False 로 바꿨다.

더 큰 원인은 따로 있었다. 목록 갱신을 TextChanged 에서만 불렀다.
커서만 옮기면(방향키·클릭·다음 줄) 아무 일도 일어나지 않아, 옛 문맥의 목록이 그대로 남았다 —
FROM 뒤에서 테이블 목록을 띄운 뒤 다음 줄로 내려가도 그 테이블 목록이 계속 떠 있었다.
스크린샷의 상태가 정확히 이것이다.

이제 SelectionChanged 에서도 판정한다. 목록이 걸린 낱말 범위를 커서가 벗어나면 닫는다.
타이핑에 따른 커서 이동과 순수한 커서 이동을 구분해야 해서(TextChanged 뒤에 SelectionChanged 가
이어서 온다) 타이핑 표시를 Input 우선순위로 내려 두고 판정한다.
포커스를 잃을 때·편집기가 스크롤될 때·창이 비활성될 때도 닫는다.

■ 위치가 안 맞던 문제

셋이 겹쳐 있었다.
1. WPF Popup 은 열려 있는 동안 Placement 를 다시 계산하지 않는다 — 오프셋만 바꾸면 제자리에 머문다.
   기준점이 달라졌을 때만 닫았다 다시 연다.
2. GetRectFromCharacterIndex 가 범위 밖에서 Empty 를 준다 — 캐럿 → 원점 순으로 물러선다.
3. 스크롤 위치에 따라 rect 가 편집 영역을 크게 벗어난다.
   진단으로 재 보니 200줄짜리 문서에서 세로 오프셋이 3,074px 이었다(편집 높이는 494px).
   목록이 창 밖에 떠 있었다는 뜻이다. 편집 영역 안으로 제한했다.

기준점은 커서가 아니라 완성 중인 낱말의 시작이다 — 글자마다 흔들리지 않는다.

■ --query-popup 진단

이 종류는 눈으로만 보면 놓친다(두 번 놓쳤다). 실제 창을 화면 밖에 띄워 7가지를 잰다:
목록이 열리는가 · 같은 낱말을 이어 쳐도 기준점이 그대로인가 · 줄이 바뀌면 내려가는가 ·
커서를 옮기면 닫히는가 · 긴 문서에서도 편집 영역 안에 뜨는가 · 포커스를 잃으면 닫히는가 ·
바깥 클릭으로 닫히는 설정인가. 현재 7/7.

만들면서 결함이 하나 더 드러났다: 프로그램으로 Text 를 넣으면 TextChanged 시점의 커서가
아직 옛 자리라 사용자가 친 것과 같은 상태가 되지 않는다. 진단용 통로를 코드베이스 관례대로
(RemoveSelectedPageForSmoke 와 같은 형태) 열어 두고 그 자리에서 갱신을 부른다.

회귀: 테스트 216/216, 편집 스모크 실패 0, 팝업 점검 7/7, 검증 실행 점검 10/10,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 15:37:05 +09:00
MsystechandClaude Opus 5 f7c90979b9 쿼리 편집기 — 자동완성 위치 버그와 목록 가독성
■ 팝업이 화면 구석으로 날아가던 문제

두 가지가 겹쳐 있었다.
1. WPF Popup 은 열려 있는 동안 Placement 를 다시 계산하지 않는다. 글자를 칠 때마다
   HorizontalOffset/VerticalOffset 만 바꿔서는 처음 열린 자리에 그대로 머문다.
2. GetRectFromCharacterIndex 는 스크롤 밖이나 범위를 벗어난 위치에서 Empty 를 준다.
   그대로 쓰면 좌표가 무한대가 되어 팝업이 화면 밖으로 나간다.

기준점을 커서가 아니라 <b>완성 중인 낱말의 시작</b>으로 바꾸고(글자마다 흔들리지 않는다),
그 기준점이 달라졌을 때만 닫았다 다시 연다. Empty·무한대는 캐럿 → 원점 순으로 물러선다.

■ 별칭 없이 컬럼을 칠 때 아무것도 안 뜨던 문제

  select * FROM P_COMINF where comcht|

점을 찍어야만 컬럼을 제안하고 있었다. 그런데 별칭을 안 쓰는 쪽이 오히려 흔하고,
WHERE 절이야말로 컬럼 이름이 가장 많이 필요한 자리다.
이제 일반 위치에서도 이 쿼리가 참조하는 테이블(FROM/JOIN)의 컬럼을 먼저 제안한다.
FROM 뒤에 키워드가 오면 테이블로 보지 않고, 같은 테이블이 두 번 조인돼도 후보는 한 번만 만든다.

■ 치환 변수 목록 가독성

토큰이 60자가 넘고 앞 40자(M.CMM.HISOperatingInfo.bzPatientInfo.)가 모든 줄에서 똑같았다.
그대로 두 줄로 깔면 정작 다른 부분인 속성명이 오른쪽 끝에서 잘려 무엇이 무엇인지 구분되지 않는다.
설명 + 속성명 한 줄로 바꾸고 전체 토큰은 툴팁으로 옮겼다. 줄 수도 절반이 됐다.
DataRow 형태는 꼬리(.item("컬럼명"))를 빼고 속성명만 남긴다 — 컬럼 채우기라는 사실은 설명이 말해 준다.

■ 현재 줄 강조

긴 쿼리에서 지금 어디를 고치고 있는지 잃지 않도록 커서 줄에 옅은 띠를 깐다.
선택 중일 때는 끈다 — 선택 색과 겹치면 오히려 읽기 어렵다.

단위 테스트 6건 추가(별칭 없는 테이블 참조·중복 조인·FROM 뒤 키워드 배제·
짧은 형태에 클래스 이름 없음·길이 상한·DataRow 속성명).
회귀: 테스트 216/216, 편집 스모크 실패 0, 검증 실행 점검 10/10,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 15:28:10 +09:00
MsystechandClaude Opus 5 79db31cb4d 쿼리 편집기 고도화 — 자동완성·검증 실행·편집 기본기 (Oracle)
색칠만으로는 편집기라 하기 어렵다. 실제로 쿼리를 칠 때 손이 가는 곳을 메웠다.

■ 자동완성 (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>
2026-08-13 15:21:08 +09:00
MsystechandClaude Opus 5 864f11b0e4 치환 변수 전량 이식 — 그리고 이 파일의 주석이 틀렸었다
25개였던 카탈로그를 51개로 늘렸다. 그 과정에서 이 파일이 근거로 삼던 규약 자체가
틀렸다는 것이 드러나 함께 고쳤다.

■ 엔진을 잘못 지목하고 있었다

주석은 bzDesignSheetLoader.ConvertQuery 의
  sReserved.Substring(0, sReserved.LastIndexOf("."))
를 근거로 "DataRow 접근형(...PatInfDR.item("컬럼"))은 접두어를 깨뜨려 반드시 실패한다"고
단정하고 그런 변수를 목록에서 뺐다. 테스트도 그 규칙으로 검사하고 있었다.

실제 엔진은 clsMDataTable.ConvertQuery 다
(C:\MsystechHIS_Ver.2\[003]EMR\[002]UserControl\MDataTable\clsMDataTable.vb).
ucLoadSheetBase 가 이것을 부르고 레거시 쿼리 편집기(fmMDataTable)도 같은 것을 쓴다.
그리고 그 파일에서 LastIndexOf(".") 줄은 주석 처리되어 있다(:32). StartsWith(FullName) 로 대체됐고,
클래스명 뒤 경로는 정규식으로 해석한다(:113):

  ^([a-zA-Z_]\w*)(?:\.item\("([^"]+)"\)|\("([^"]+)"\))?$

즉 DataRow 컬럼 접근이 두 형태로 <b>동작한다</b>. 단 item 은 반드시 소문자다 —
정규식에 IgnoreCase 가 없다. 대문자로 쓰면 값을 못 찾고 빈 문자열로 치환된다.

■ 추가한 것

환자 16 + 외부연계 11 + 서식 13 + 작업자 3 = 스칼라 43종(레거시 세 클래스의 스칼라 전량),
여기에 DataRow 컬럼 채우기 틀 8종(P_PatInf·P_ComInf·P_CodInf·P_CoiInf·P_CowInf·E_ShtMst·E_SdgMst).
컬럼 틀은 삽입하면 '컬럼명' 자리가 선택돼 바로 덮어쓸 수 있다.

다른 biz 객체·컬렉션을 돌려주는 속성(PatientInfoBiz_Refer, SaveSheetInfo, PrintEmrKeyList,
UidMst/HspMst/DepMst, SctMst)은 넣지 않았다. 정규식이 한 단계 경로만 허용해 중첩 접근이 안 되고,
단독으로 쓰면 타입 이름 문자열이 SQL 에 박힌다.

■ 판정을 목록 대조에서 형태 검사로 바꿨다

DataRow 컬럼은 무한히 많아 목록에 담을 수 없다. LegacyQueryVariableCatalog.IsResolvable 이
접두어 StartsWith + 위 정규식으로 판정하고, 편집기 경고도 이것을 쓴다.
목록에 없다고 경고하던 종전 방식이었다면 정상적인 DataRow 사용이 전부 오탐이 됐을 것이다.

■ 토크나이저 결함 — 따옴표 안의 변수를 삼키고 있었다

테스트를 쓰다 발견했다. 엔진은 값에 따옴표를 붙여 주지 않고 Replace 로 원문을 박으므로,
문자열 비교에 쓰려면 SQL 쪽에서 '<<...>>' 로 감싸는 것이 정상 사용법이다.
그런데 토크나이저가 '...' 를 통째로 문자열로 잡아 그 안의 변수를 못 봤다 —
가장 흔한 형태의 변수가 색도 검증도 못 받고 있었다. 문자열 구간 안에서도 <<...>> 를 떼어 내도록 고쳤다.
전 구간 덮기 불변식은 유지된다(테스트로 고정).

단위 테스트 22건 추가/수정(카탈로그 자기일관성·DataRow 2형태·item 대문자 거부·축약 접두어 거부·
중첩 경로 거부·따옴표 안 변수·덮기 불변식). 옛 규칙을 박아 둔 기존 테스트 1건은 실제 규약으로 교체.

회귀: 테스트 196/196, 편집 스모크 실패 0,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 15:10:20 +09:00
MsystechandClaude Opus 5 de46f70035 쿼리 편집기 — 서드파티 없이 만든 구문 강조와 편집 보조
배포본에 서드파티 편집기 DLL 을 넣지 않기로 해서 직접 만들었다.

■ 색칠 방식 — 투명 TextBox 뒤에 그린다

RichTextBox 로 직접 칠하면 입력할 때마다 서식을 다시 입혀야 해서 실행취소 묶음이 깨지고
IME 조합 중 깜빡인다. 대신 편집은 평범한 TextBox 가 그대로 하고(캐럿·선택·실행취소·IME·스크롤이
전부 공짜다) 글자만 투명하게 해서, 뒤에 깔린 SqlHighlightLayer 가 같은 자리에 색을 입혀 그린다.
맞춰야 하는 것은 하나뿐이다 — 글꼴·크기·여백·줄바꿈 설정이 TextBox 와 완전히 같을 것.

성능은 화면에 보이는 줄만 그려서 잡았다. x 위치는 앞부분을 다시 재서 잡는다 —
조각 폭을 누적하면 자간·힌팅 때문에 뒤로 갈수록 한두 픽셀씩 밀린다.

■ 조각내기(SqlTokenizer) — Core 에 순수 함수로

예약어 · 내장함수 · 숫자 · 문자열 · 주석 · 치환변수, 그리고 Broken.
Broken 을 따로 둔 것이 요점이다. 이 쿼리는 EMR 런타임에서 돌고 실패해도 빈 catch 가 삼켜
화면에 아무 표시가 없다. 닫히지 않은 따옴표나 << >> 는 편집기에서 즉시 눈에 띄어야 한다.
Oracle 방언 기준이고, '' 이스케이프를 문자열 끊김으로 오인하지 않는다.
줄바꿈을 넘긴 문자열은 닫는 따옴표를 빠뜨린 것으로 본다 — 안 그러면 뒤 문장 전체가 문자열로 물든다.

조각은 겹치지 않고 전 구간을 덮는다(빈틈이 있으면 그 글자가 안 그려진다). 테스트로 고정했다.

■ 편집 보조

줄 번호 · 글자/줄 수 · Tab·Shift+Tab 들여쓰기(선택 줄 단위) · Enter 자동 들여쓰기 ·
Ctrl+/ 주석 토글(섞인 상태면 전부 붙인다 — 토글이 예측 가능해진다).

저장 전 경고 두 가지를 하단에 띄운다: 닫히지 않은 조각, 그리고 카탈로그에 없는 치환 변수.
후자는 접두어를 임의로 줄여 쓴 경우를 잡는다 — 런타임은 그걸 치환하지 못하고
<<...>> 를 SQL 에 그대로 남겨 ORA 오류를 내며, 그 데이터소스를 참조하는 컨트롤이 전부 공백이 된다.

■ 치환 변수 패널

이름·설명·초성으로 좁히는 검색을 붙였다(ㅊㅌㅂㅎ → 차트번호). Enter 로도 삽입된다.
변수 목록 자체의 전량 이식은 별도 커밋으로 이어간다 — 현재 25개는 레거시 surface 의 절반이다.

SQL 이 아닌 용도(상용구 문구)로 재사용할 때는 색칠을 끈다.
예약어와 겹치는 낱말이 엉뚱하게 물들면 안 된다.

단위 테스트 12건 추가(전 구간 덮기·대소문자·식별자 속 예약어 오검출·'' 이스케이프·
변수 통째 인식·닫히지 않은 조각 2종·줄 넘긴 문자열).
회귀: 테스트 174/174, 편집 스모크 실패 0,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 14:36:00 +09:00
MsystechandClaude Opus 5 146d808aac 태그 피커 재설계 — 찾기(초성·건수)와 둘러보기(분류)를 함께
칩으로 자주 쓰는 것을 앞에 깔았지만, 피커 자체는 여전히 384줄짜리 평면 목록이었다.
목록에 구조가 없으면 무엇이 있는지 알 길이 없어 결국 아는 이름만 반복해 쓰게 된다.

■ 분류 — '무엇이 있는지'

태그 이름에는 이미 구조가 있다(PAT_ 환자 · OCM_ 진료·오더 · ETC_ 기관·문서).
그 구조를 왼쪽 분류 목록으로 드러냈다. 건수와 함께 보이므로 규모가 잡힌다:
환자 40 · 진료·오더 260 · 기관·문서 75 · 그 밖 9.

가상 분류 셋을 위에 뒀다:
  · 자주 쓰는 것 — 이 컨트롤 타입의 실사용 상위(타입마다 다르다)
  · 운영에서 쓰임 157 — 384종 중 실제로 한 번이라도 쓰인 것만
  · 전체 384

아는 접두어만 분류로 올리고 나머지는 '그 밖'으로 묶는다. 레거시에는 규약에서 벗어난 이름이
섞여 있어서(오타 ECT_로그인사용자싸인 — bzDataInterface.vb:17850 의 실제 오타다,
접두어가 아예 없는 GetNurseShtCod) 접두어를 그대로 분류로 쓰면 1~3개짜리 분류가
목록을 채워 '무엇이 있는지'를 오히려 가린다.

■ 사용 건수 — '이게 표준인가'

이름만으로는 PAT_주민번호 와 PAT_주민번호_Dash 중 무엇이 표준인지 알 수 없다.
운영 실측(--db-tags)을 줄마다 배지로 띄운다(PAT_이름 669, PAT_차트번호 463 …).
한 번도 안 쓰인 태그는 '미사용'으로 표시한다 — 그것도 정보다.
정렬은 자주 쓰는 순 → 운영 건수 → 이름. 가나다순은 운영 절반을 차지하는 태그와
한 번도 안 쓰인 태그를 같은 무게로 보여 준다.

■ 초성 검색 — 손이 덜 가게

한글 태그 이름이 길어 전체를 칠 이유가 없다. "ㅊㅌㅂㅎ" 네 글자로 PAT_차트번호 에 닿는다.
입력이 초성만으로 이뤄졌을 때만 초성 대조를 시도한다 — 일반 검색어를 초성으로 오인하면 안 된다.
공백으로 나눈 여러 토큰은 모두 만족해야 하고(AND), 일반어와 초성을 섞어도 된다("OCM ㅈㄷㅅ").

■ 그 밖

· 검색칸에서 ↑↓·PageUp/Down 으로 바로 목록을 움직인다 — 검색하고 Tab 으로 옮겨 가는 왕복을 없앴다.
· 현재 값이 있으면 그 값이 속한 분류로 열고 그 줄을 선택해 둔다.
· 검색으로 지금 분류가 비면 결과가 있는 분류로 옮겨 간다(빈 화면을 보여 주지 않는다).
· 창을 720×620 으로 키우고 크기 조절을 열었다. 선택된 값을 하단에 항상 띄운다.
· AutomationProperties.Name 을 붙였다(사내 UI 가이드 접근성 항목).

사내 UI 가이드의 판단 기준을 따랐다 — '빈도 높은 동작에 모달을 쓰지 않는다'.
자주 쓰는 태그는 인스펙터 행의 칩으로 이미 빠져 있어 이 창을 열 필요가 없고,
이 창은 '둘러보기' 전용으로 남긴다(그 용도에는 집중 화면이 맞다).

단위 테스트 20건 추가(분류 매핑·규약 밖 이름 접기·초성 추출·초성 판정·부분 일치·
다중 토큰 AND·변형 묶기·건수 스냅샷).
회귀: 테스트 162/162, 편집 스모크 210건 실패 0,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 14:02:25 +09:00
MsystechandClaude Opus 5 de01012b7e 태그 선택 — 타입별로 자주 쓰는 것을 그 자리에 깐다
태그를 하나 걸려면 매번 모달을 열어 500여 개(액션 160여 개) 목록에서 찾아야 했다.
한 서식이 쓰는 서로 다른 태그는 중앙값 6개, 최대 27개다(--db-tags) — 서식 하나 만들 때마다
그 짓을 예닐곱 번 한다.

■ 실측이 설계를 정했다

--db-tags 진단을 신설해 운영 디자인 1,271건의 태그 사용 8,032건을 컨트롤 타입별로 집계했다.
후보가 타입마다 거의 완전히 갈린다:

  이미지 · 데이터 태그        343건 / 7종 — 상위 16종이 100%  (서명·직인·로고가 전부)
  라디오 · 액션 태그          736건 / 7종 — 100%             (RadCheckControl 이 580건)
  체크박스 · 액션 태그        357건 / 7종 — 100%
  텍스트박스 · 액션 태그      200건 / 6종 — 100%
  마스크입력 · 데이터 태그     57건 / 13종 — 100%
  버튼 · 액션 태그            393건 / 46종 — 상위 16종이 83%
  텍스트박스 · 데이터 태그   5,927건 / 144종 — 상위 16종이 65%  (여기만 꼬리가 길다)

즉 일곱 조합 중 다섯은 **짧은 목록이 운영 전체를 덮는다**. 이미지 하나 고르려고
500개를 뒤지고 있었다는 뜻이다.

■ 바꾼 것

· 인스펙터 행 아래에 그 타입에서 자주 쓰는 태그를 빈도순 칩으로 깐다(최대 8개). 누르면 바로 값이 된다.
  칩은 **값이 비어 있을 때만** 뜬다 — 이미 정해진 행 아래에도 깔면 좁은 패널이 금방 길어진다.
· 피커를 열면 자주 쓰는 것이 맨 위로 올라온다(그 안에서는 빈도순, 나머지는 원래 가나다순 유지).
  건수 표시에 '자주 쓰는 것 N건이 위에' 를 덧붙였다.
· 전체 카탈로그와 자유 입력은 그대로다. 제안은 제안이지 제한이 아니다 —
  타입별 자료가 없는 조합의 액션 태그는 아예 비워 둔다. 추측으로 권하면
  그 칸은 배포된 뒤에야 빈칸으로 드러난다.

순서를 가나다순이 아니라 빈도순으로 둔 것이 핵심이다. 가나다순은 운영의 절반을 차지하는
RadCheckControl 과 한 번도 안 쓰인 태그를 같은 무게로 보여 준다.

■ 곁다리로 확인한 것

카탈로그에 없는 값이 데이터 태그 1종(PAT_전체주소 ×11), 액션 태그 1종(OprEmrList ×5) 나왔다.
둘 다 레거시 bzDataInterface / EN_DataActionTyp 에 존재하지 않는다 —
우리 카탈로그가 맞고, 저 값들은 운영 서식에 이미 박힌 죽은 태그다(런타임이 해석하지 못한다).
우리가 만든 것이 아니라 손대지 않았다.

편집 스모크 17건 추가(타입별 좁힘 / 빈도 순서 / 자료 없는 타입은 빈 목록 /
제안이 전부 카탈로그에 있는지 12조합 검사 / 값이 정해지면 칩 감춤).
회귀: 테스트 142/142, 편집 스모크 210건 실패 0,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 13:52:54 +09:00
MsystechandClaude Opus 5 ddf2bdaee8 같은 서식 중복 열기 차단 — 출처를 가리지 않는다 (+ 3단계 판단용 실측 진단)
■ 중복 열기로 편집분이 통째로 사라지던 문제

중복 차단이 d.IsFromDb 를 요구했다. 그래서 XML 파일로 이미 열어 둔 서식을 목록에서
더블클릭해 DB 로 또 열 수 있었고, 두 탭에서 교대로 저장하면 낙관적 충돌 검출이
양쪽 어디에도 없어 마지막 저장이 앞선 편집을 통째로 덮었다.

레거시 MultiSheetFormAllow 는 탭 제목의 [서식코드]만 보고 출처와 무관하게 막는다
(frmSheetDesigner.vb:242-252). 같은 규칙으로 맞췄다:
  · 판정을 FindOpenSheet(서식코드) 하나로 모으고 IsFromDb 조건을 뺐다.
  · 열기 경로 3곳(DB / XML 파일 / JSON 가져오기)이 전부 이 지점을 지난다.
    종전에는 DB 경로에만 검사가 있었다.
  · 이력 탭은 계속 제외한다 — 현재본을 열려는 요청이 과거 스냅샷을 활성화하면 안 된다.
  · 빈 서식코드는 서로 다른 문서로 본다. 묶으면 '새 서식'을 두 번 만들 수 없다.
  · 막을 때 어느 탭으로 갔는지 상태줄에 적는다. 아무 일도 안 일어난 것처럼 보이면 사용자는 다시 누른다.

비교용으로 나란히 열어야 하는 경우를 위해 탈출구를 남겼다 —
DataConfig.AllowMultiSheetForm(기본 false, 레거시 AllowMultiSheetForm 기본 "N" 과 동일).

■ 함께 고친 것 — 이력본을 저장해도 탭이 '이력'으로 남던 문제

DB 저장 후 HistorySdgKey 를 null 로 되돌리지 않아 탭 제목이 '(이력 N)' 으로 남고,
종료 확인이 '저장할 수 없는 탭' 분기로 들어가 저장 선택지 없이
'변경을 버리고 종료할까요?' 만 물었다 — 저장 뒤의 편집분이 조용히 버려진다.
저장한 순간 그 탭은 스냅샷이 아니라 활성 디자인이므로 표식을 지운다.

이 수정이 만드는 경계가 하나 있다: 이력 탭을 되살려 저장하면 같은 서식의 다른 탭이
이제 옛 내용을 들고 있게 된다. 그 탭에서 저장하면 방금 저장분을 덮으므로,
어느 탭이 낡았는지 알리는 경고를 띄운다(자동으로 닫지는 않는다 — 미저장 편집분이 있을 수 있다).

■ 3단계 판단용 실측(--db-phase3)

사이트 확인을 기다리는 동안 DB 로 알 수 있는 것부터 쟀다:
  · ShtCneYon='Y' 40건, 그중 현역 19건 — TK_MODIFY(제자리 정정) 부재의 실제 영향 범위.
  · ShtDepCls 는 418건(24%)에 실제 분류가 들어 있다(산업재해·물리치료실·병동출력물…).
    비어 있지 않으므로 진료과 2단계 그룹 미이식은 목록 구성을 실제로 바꾼다.
  · ShtDspSeq 는 1,714건(98.7%)이 1 이상이다. 표시 순번은 사실상 전 서식에 채워져 있어,
    정렬 미이식은 거의 모든 서식의 목록 순서를 레거시와 다르게 만든다.
  · ShtTyp='S'(스캔형) 108건, 현역 105건. 사이트 조건 이식이 걸리는 모집단이 실재한다.
  · EMRShtSortTyp 는 확인하지 못했다 — 코드 마스터(DtlCod/DtlCodVal) 테이블이
    이 접속 스키마에 없다. 운영 접속에서 재측정이 필요하다.

편집 스모크 5건 추가(출처 무관 판정 / 대소문자 / 이력 제외 / 무관 서식 / 빈 코드).
회귀: 테스트 142/142, 편집 스모크 192건 실패 0,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 13:27:00 +09:00
MsystechandClaude Opus 5 a90dd1301e 데이터소스 배선 전용 편집기 — 조용히 실패하던 문법을 코드가 조립한다
이 시스템의 핵심 자동 채움 경로다. MDataTable 조회 결과의 한 칸을 입력 컨트롤에 꽂는 배선인데,
지금까지 SheetMe 는 ComboBox 에만 안내 없는 자유 텍스트 한 줄이 있었고 나머지 7종에는 항목조차 없었다.

문제는 형식을 어겼을 때의 증상이다. 런타임 파서(ucLoadSheetBase.DataTableBinding_)는 문자열을
쪼개서 읽고, 실패하면 **빈 catch 가 예외를 삼킨다**. 로그도 없다. 디자이너에서는 정상으로 보이고
임상 화면에서만 값이 안 채워지므로 원인 추적이 어렵다. 그래서 문법 골격은 코드가 조립하고
저장 전에 검사한다.

■ 실측이 통설을 뒤집었다

레거시 전용 편집기(fmMDataTableField)는 Select 형 한 가지만 만든다:
  {이름}.Select("{필터}")({행}).Item("{필드}")   ← 공백 0개
그래서 이것이 '정본'이라고 알려져 있었다.

--db-dtf 진단을 새로 만들어 운영 디자인 1,271건을 센 결과는 반대였다:
  Rows 형 1,624건(76%) · Select 형 268건(12%) · 미완성 "MDataTable1.Rows" 234건(11%)
Select 형만 읽었다면 기존 배선의 4분의 3을 편집기가 못 여는 상태로 출시할 뻔했다.
게다가 형태를 함부로 바꾸면 안 된다 — 보조 로더(bzDesignSheetLoader)는 .Select( 분기가 아예 없어
Rows 형만 이해한다. 그래서 **연 값의 형태를 그대로 유지**하고, 새로 만들 때만 Select 형으로 시작한다.

같은 집계에서 운영 결함도 드러났다: 같은 서식에 없는 데이터소스를 가리키는 값 20건
(S0003 은 MDataTable2 를 가리키는데 그 서식에는 MDataTable3 만 있다). 전부 조용히 실패한다.
런타임 조회가 대소문자를 구분하므로 검증에 이름 일치 검사를 넣었다.

■ 레거시보다 나은 점 3가지 (파리티 초과지만 위험 없음)

1. 기존 값을 되읽어 채운다. 레거시는 열 때마다 백지에서 시작해(InitializeSelectedData)
   필드 하나만 고치려 해도 전부 다시 골라야 했다.
2. 아무것도 안 고르고 확인하는 경로를 막는다. 레거시는 5조각을 이어붙이므로 리터럴 ")(" 가 저장된다.
3. 저장 전 검사 — 필터·필드의 큰따옴표(파서가 따옴표로 쪼갠다), 이름의 마침표(첫 조각만 떼어 간다),
   음수 행. 레거시는 검증이 0이다.

필드 후보를 목록으로 주지는 않았다. 레거시는 쿼리를 실제로 실행해 결과 컬럼을 쓰는데,
그 쿼리에는 테스트 환자 설정이 필요한 치환 토큰이 들어 있어 재현이 별개 작업이다.
대신 필드는 자유 입력으로 두고 그 사정을 화면에 적었다. 실제로 필드에는 SQL 별칭 표현식이
들어오기도 한다(운영 값 EMDOPRDTE||'-'||EMDOPNAME) — 식별자 화이트리스트로 막으면 안 된다.

배선을 붙인 타입은 레거시가 bzMDataTableField 편집기를 다는 8종 그대로다:
TextBox · ComboBox · CheckBox · RadioButton · DateTimePicker · MaskedTextBox · CalcBox · PictureBox.

해석 못 하는 값은 조용히 고쳐 쓰지 않는다 — 인스펙터에 "⚠ 형식을 알 수 없음: 원문" 으로 드러낸다
(운영의 미완성 값 234건이 여기 해당한다).

단위 테스트 18건 추가(표본은 전부 실측·레거시 소스 인용). 편집 스모크 4건 추가.
회귀: 테스트 142/142, 편집 스모크 187건 실패 0,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 12:49:38 +09:00
MsystechandClaude Opus 5 59b0e457fe 2단계 — 신규 서식에 런타임 설정을 걸 수 있게 (선 3종·공통 속성·키 자동완성)
레거시로는 되는데 SheetMe 로는 못 만들던 것들을 메웠다. 어떤 속성을 올릴지는 취향으로 정하지 않고
--db-props 로 운영 디자인 1,271건 / 컨트롤 164,091개를 세고, 각 컨트롤 .vb 의 필드 선언을 읽어 정했다.

■ 선(Line) — 새로 그은 세로선이 EMR 에서 사라지던 문제

MLine 은 방향을 크기가 아니라 Orientation 으로 정한다(MLine.vb:181-190).
키가 없으면 Horizontal 이라 (0,0)→(Width,0) 을 그리므로, 폭 1·높이 210 으로 세로처럼 만든 선은
디자이너에만 보이고 기록지에서는 1px 점이 된다. 운영 선 32,599개 중 Orientation 보유 12,811개이고
그 중 Vertical 이 12,721개 — 세로선은 예외가 아니라 주류다.

Orientation·BorderWidth·DashStyle 을 인스펙터에 올렸다. 그리고 값만 올리지 않고
레거시 컨트롤이 스스로 지키는 불변식을 함께 옮겼다(LineGeometry):
  · 두께 축은 항상 BorderWidth 다 — MLine.OnResize 가 매번 되돌린다. 그래서 선을 끌어
    두껍게 만들 수 없다. 이걸 허용하면 '세로처럼 보이지만 EMR 에선 점'인 선을 다시 만들 수 있다.
  · 방향 전환은 길이를 보존한 채 축을 바꾼다(레거시 Orientation 세터: Width = Height 후 OnResize).
  · 가로로 되돌리면 Orientation 키를 지운다 — <DefaultValue(Horizontal)> 라 레거시 직렬화기도 생략한다.

--db-lines 를 새로 만들어 확인한 결과 운영 선 32,599개에서 경계 모양과 Orientation 불일치는 0건이다.
레거시가 구조적으로 막아 왔다는 뜻이고, 그래서 캔버스 렌더는 건드리지 않았다(P062 바이트 동일).

■ 공통 런타임 속성 — 서명란·인쇄 제외·필수입력·자동높이·재조회

신규 컨트롤은 Props 가 사실상 비어 있어 '전체 속성(0개)'이었고, 고급 편집도 이미 있는 키만 나열한다.
결과적으로 서명이 필요한 동의서나 점수를 합산하는 평가지를 SheetMe 만으로 새로 만들 수 없었다.
타입별 기술자에 다음을 추가했다(기본값은 각 컨트롤 .vb 필드 선언에서 확인한 값이다):
  PrintOutPut(기본 True) · PreventEditing · IsRequiredValue(No/Yes) · Visible · AutoHeight ·
  ReLoadData 3종 · MPictureBox 의 IsSignature/SignatureIndex.

PrintOutPut 을 True 로 표시하는 것이 중요하다 — 키가 없는 컨트롤이 '인쇄 안 함'으로 보이면
사용자가 껐다 켜는 순간 명시 False 가 기록돼 실제로 인쇄에서 빠진다
(런타임 필터: If Me.Visible = False OrElse mbPrintOutPut = False Then Return False).

■ 감사 권고와 다르게 한 것 3가지 — 근거가 반대였다

1. Score 를 5개 타입에 추가하라는 권고는 따르지 않았다. 실제로 저장값을 읽는 것은
   CheckBox·RadioButton 뿐이다(게터가 mdScore 반환). TextBox·MaskedTextBox 의 Score 게터는
   저장값을 무시하고 Me.Text 를 숫자로 읽으며, CalcBox 는 세터가 오히려 Text 를 덮어쓴다.
   편집기를 붙였으면 사용자는 배점을 걸었다고 믿고 런타임은 무시하는 상태가 된다.
   ComboBox 는 Score 가 아니라 ItemScore("/" 구분, 항목 순서와 1:1)를 노출했다.
   실측이 먼저 신호를 줬다: ComboBox 의 Score 값 분포가 Text 와 정확히 같았다(-×625, ++×22).
2. Button 에는 PrintOutPut 을 붙이지 않았다 — MButton.vb 에 그 속성 자체가 없다.
3. 라벨의 표시 여부는 소문자 visible 이다. Label.vb:209-217 이 <Browsable(False)> Shadows 로
   Control.Visible 을 가리고 직렬화기가 그 그림자를 쓴다. 운영에서도 소문자 58,864건(99.7%)
   대 대문자 196건. 대문자로 쓰면 라벨은 그대로 보인다.

■ '속성 추가' 자동완성

큐레이션 밖의 값을 걸려면 키를 손으로 쳐야 했는데, 철자가 틀려도 경고가 없고
그 서식은 배포된 뒤에야 이상하게 동작한다. 타입별 실사용 키 목록(LegacyPropertyCatalog,
--db-props 집계 기반)을 제안 칩으로 깔았다. 이미 있는 키와 인스펙터가 이미 다루는 키는 빼고,
부분 일치로 좁히며 접두 일치를 앞에 둔다. 자유 입력은 그대로 열어 뒀다 — 제안이지 검증이 아니고
사이트가 추가한 키도 있다.

■ 1단계 잔여분: 선택 밖 컨트롤을 Ctrl+드래그하면 엉뚱한 것이 끌리던 문제

Ctrl/Shift 클릭은 선택을 바꾸지 않고 업에서 토글하므로, 그 상태로 드래그가 시작되면
잡은 컨트롤이 아니라 기존 선택분이 끌려갔다(선택이 비어 있으면 아무것도 안 끌렸다).
드래그가 실제로 시작되는 지점에서 잡은 것을 선택에 넣는다 — 기존 다중선택은 유지한다(업 토글과 같은 규칙).

편집 스모크 14건 추가(선 기하 6 · Ctrl+드래그 2 · 자동완성 5 · 라벨 소문자 함정 1).
회귀: 테스트 124/124, 편집 스모크 183건 실패 0,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 12:38:37 +09:00
MsystechandClaude Opus 5 df1a29afa2 속성 큐레이션 근거를 재는 진단 2종 — --db-props / --db-lines
2단계(신규 서식 제작 가능 선언)에서 어떤 속성을 인스펙터에 올릴지 정해야 하는데,
그 판단을 취향이나 레거시 소스 읽기만으로 하고 싶지 않았다. 실제 서식이 무엇을 쓰는지 셌다.

--db-props : 타입별 속성 전수 집계(디자인 1,271건 / 컨트롤 164,091개).
  키별 보유율과 상위 값 분포를 함께 찍고, 현재 기술자에 없어 편집할 수 없는 키는 + 로 표시한다.
  이 목록이 곧 '레거시로는 되는데 우리로는 못 만드는 것'의 목록이다.

--db-lines : 선의 경계 모양과 Orientation 이 어긋나는 건수.
  우리 캔버스는 사각형으로 선을 그리고 EMR 은 Orientation 으로 그린다
  (MLine: Horizontal 이면 (0,0)→(Width,0), Vertical 이면 (0,0)→(0,Height)).
  두 규칙이 실제로 충돌하는지 모르면 렌더를 건드려야 하는지 알 수 없다.

첫 집계에서 이미 드러난 것:
- 선 32,599개 중 경계 모양과 Orientation 불일치 0건. 기존 서식에서는 두 규칙이 완전히 일치한다.
  → 캔버스 렌더를 바꿀 이유가 없다. 문제는 SheetMe 가 만드는 새 선뿐이다(Orientation 을 못 쓴다).
  → 세로선은 예외가 아니라 주류다: Vertical 12,721 대 Horizontal 90.
- Score 는 TextBox·CheckBox·RadioButton·CalcBox 에서 숫자지만
  ComboBox 에서는 문자열이다(-×625, ++×22, normal×10). 숫자 편집기를 붙이면 조용히 망가진다.
- 대소문자 함정이 실측으로 확인됐다: Label 은 소문자 visible 58,864건(99.7%),
  대문자 Visible 은 196건뿐. CheckBox·RadioButton 은 ControlVisible 을 따로 쓴다.
- PrintOutPut 은 사실상 항상 True(Label 54,510건 중 False 1건, Line 은 False 0건).
- IsRequiredValue 값은 True/False 가 아니라 No/Yes 다.

읽기 전용 진단이며 문서를 건드리지 않는다.
회귀: 편집 스모크 실패 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 12:19:21 +09:00
MsystechandClaude Opus 5 8eeff42a72 전수조사 1단계 — 저장된 서식을 오염시키는 결함 7건 수정
레거시 대비 전수조사에서 나온 출시 차단 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>
2026-08-13 12:08:06 +09:00
MsystechandClaude Opus 5 fdb1476b0b 색 견본 커맨드의 null 허용 경고 정리
Command 가 받는 대리자는 Action<object> 인데 람다를 object? 로 선언해 CS8622 가 났다.
파라미터는 항상 견본이 오지만 바인딩이 어긋날 때를 대비한 형 검사는 그대로 둔다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 11:03:23 +09:00
MsystechandClaude Opus 5 e84337705f 색상 행에 인라인 팔레트 — 스와치를 누르면 그 자리에서 고르고, '자세히…' 로 피커
색을 바꾸려면 매번 대화상자를 열어야 했다. 실측해 보니 그럴 이유가 없다 —
--db-colors 진단을 신설해 운영 디자인 1,271건의 BackColor·ForeColor 60,319건을 집계한 결과
상위 두 값이 전체의 84%다: White 29,010건(48%), "224, 224, 224" 21,497건(36%).
뒤는 꼬리가 길고 얇다(Transparent 792 · Silver 557 · Window 440 · Gainsboro 415 …).
ForeColor 는 Black 4,920건(85%) · ControlText 345건이 사실상 전부.

스와치를 토글로 바꾸고 팝업에 실사용 상위 값을 배경 12종 / 글자·강조 12종으로 깔았다.
표준 색상표를 늘어놓지 않은 것은 의도다 — 목적은 자주 쓰는 값을 한 번에 고르게 하는 것이지
색을 발명하게 하는 것이 아니다. 임의 색은 팝업 아래 '자세히…' 로 기존 피커
(채도·명도 사각형 + 색상 슬라이더 + HEX·RGB + 프리셋·최근색)를 연다.

핵심은 견본이 레거시 원문 값을 그대로 넣는다는 점이다. 명명색을 RGB 로 풀면
원문과 달라져 왕복 diff 가 생기고, 시스템색이 갖는 의미(Window/Control/ControlText)도 잃는다.
'Window' 를 고르면 "Window" 가 저장된다. 툴팁에는 실사용 건수를 함께 띄운다.

'비우기'는 기본값을 저장하는 것이 아니라 키를 지운다 — 레거시 PropertyGrid 의 재설정과
같고, 원래 BackColor 가 없던 페이지(12.7%)를 원상태로 되돌릴 수 있어야 한다.

함께 고친 것: 페이지 속성 모드에서 '컨트롤을 선택하면 속성이 표시됩니다' 안내가 행과 같이
뜨던 것(HasSelection 이 컨트롤 선택만 보고 있었다). 탭 스트립은 페이지 모드에서 숨긴다.

편집 스모크 4건 추가(원문 값 적용 / 명명색 보존 / 적용 후 닫힘 / 비우기는 키 제거).
회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0,
종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 10:56:47 +09:00
MsystechandClaude Opus 5 ac36f93a03 페이지 배경색 기본 표시를 White → Control 로 (레거시 PropertyGrid 와 일치)
페이지 루트에 BackColor 가 없을 때 내가 임의로 'White' 를 지어내 보여 주고 있었다.
페이지 루트는 WinForms UserControl 이고 그 BackColor 기본값은 SystemColors.Control 이라
레거시 PropertyGrid 는 'Control' 을 표시한다 — 사용자가 짚은 그대로다.

근거를 위해 --db-pageprops 진단을 신설해 전수 집계했다(디자인 1,271건 / 페이지 1,816장).

  BackColor  White 1,487장(81.9%) · (없음) 230장(12.7%) · Window 91장(5.0%)
             ButtonHighlight/ControlLightLight/MenuBar/Control/ControlLight 8장
  Font       (없음) 95.6% — 없을 때 굴림 9pt 로 보이는 현행 기본값이 맞다
  기타 키     Margin 9장, BorderStyle 6장, DoNotPrintThisPage 2장 …

12.7% 는 예외가 아니라 흔한 경우다. 표시만 고치고 저장은 하지 않는다 — 지어낸 값이 XML 에
새로 생기면 왕복 diff 0 이 조용히 깨진다. 이 규약을 두 겹으로 못 박았다.
- 편집 스모크: 값 없는 페이지에서 Control 표시 / 미저장 / 용지는 흰색
- --db-pageprops: BackColor 없는 실제 서식 40건을 열어 XML 이 그대로인지 대조

렌더는 흰색을 유지한다. 사용자 확인 결과 레거시 실행 시에도 흰색으로 나오고,
#F0F0F0 종이는 라이트 테마 캔버스 배경(#D8D8D8)과 1.13:1 이라 용지 경계가 사라진다.
대신 값이 없을 때는 라벨 툴팁으로 '저장된 값 없음 / 실행·인쇄는 흰색 / 바꾸면 그때 저장'을
알린다 — 안 그러면 "Control 인데 왜 희게 보이지?" 로 다시 읽힌다.

부수: PropertyRowViewModel 에 Hint 를 추가하고 색상 행 라벨 툴팁에 배선.

회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0,
종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 10:44:52 +09:00
MsystechandClaude Opus 5 38093691ad 페이지 속성 편집 — 레이어의 페이지 행을 고르면 속성 패널이 페이지를 연다
레거시에서 페이지는 MDesignerHost 라는 하나의 Object 이고 자기 속성을 갖는다.
SheetMe 도 그 루트 노드를 FormPage.Root 로 통째로 보존해 왔고(그래서 운영 1,271건 왕복
diff 가 0이다), 값도 일부는 이미 쓰고 있었다 — BackColor 는 용지 배경으로 렌더하고
Font 는 자식이 상속한다. 없던 것은 편집 화면뿐이었다.

그 결과 용지 크기조차 바꿀 수 없는 상태였다. ApplyPaperSize 는 구현돼 있었지만 호출처가
편집 스모크뿐이고 메뉴·XAML 어디에도 배선이 없었다 — 새 서식은 720×856 고정으로만 시작했다.

레이어 아웃라인의 페이지 헤더 행을 누르면 선택을 비우고 속성 패널이 페이지 모드로 바뀐다.
컨트롤을 하나라도 고르면 자동으로 빠져나온다.

  용지        너비·높이(한 줄), 배경색
  기본 글꼴    글꼴·크기 — 여기를 바꾸면 Font 를 명시하지 않은 컨트롤이 전부 따라온다
  전체 속성    나머지 원본 속성(레거시 PropertyGrid 등가)

저장 규약을 지켰다. writer 는 저장할 때 Size 를 Bounds 에서, Name 을 Id 에서 다시 만들어
덮으므로 그 두 키는 raw 편집 대상에서 뺐다 — 열어 두면 고쳐도 저장하면 되돌아간다.
크기는 raw Size 가 아니라 Bounds 를 고치고, 편집 결과가 저장 XML 의 Size 와 같은지
스모크로 못 박았다.

용지 크기가 바뀌면 페이지 스택 오프셋과 월드 크기를 다시 계산한다(여러 장이면 한 장의
높이 변화가 아래 페이지를 전부 밀어낸다). 기본 글꼴이 바뀌면 그 페이지 컨트롤의 상속
컨텍스트를 다시 흘린다.

편집 스모크에 페이지 속성 검사 11건 추가.
회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0,
종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 10:23:49 +09:00
MsystechandClaude Opus 5 f5cce00019 잘못 생성된 빈 파일 3개 제거
앞 커밋에서 스크립트의 cd 가 적용되지 않아 저장소 루트에 0바이트 파일이 만들어졌다.
실제 수정 대상인 src/SheetMe.Designer/Views/ 아래 파일들은 정상이다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 10:07:56 +09:00
MsystechandClaude Opus 5 604f8f7d1b 순정 MessageBox 를 앱 테마 대화상자로 교체 (53곳 중 49곳)
다크 테마에서 흰 시스템 창이 튀어나오던 알림을 자체 창(MessageDialogView)으로 바꿨다.
앱 셸과 같은 커스텀 타이틀바(36px) + B.Surface 본문 + 성격 아이콘 + 우측 버튼 줄.
폭 440 고정, 내용에 따라 높이 자동, 긴 예외는 스크롤로 흡수한다.

진입점은 DialogService 정적 메서드 4개로 모았다 —
Notify / Confirm / ConfirmWithCancel / ShowError.

MessageBox 는 Win32 호출이라 아무 데서나 되지만 WPF 창은 아니다. 교체로 새로 생기는
제약 셋을 DialogService 한곳에서 막는다.
- 진단 모드에서는 창을 만들지 않고 기각 기본값을 즉시 돌려준다. 스모크는 무인 실행이라
  모달이 하나라도 뜨면 타임아웃 없이 영원히 멈춘다 — 실제로 편집 스모크가 지나는 경로에
  붙여넣기·개명 가드가 있다. 규약을 스모크 검사 3건으로 고정했다.
- UI 스레드가 아니면 Dispatcher 로 넘긴다.
- 소유 창은 '살아 있는 것'만 건다. 아직 안 보였거나 이미 닫힌 창을 Owner 로 주면 예외이고,
  진단 렌더러가 도는 동안 MainWindow 가 닫힌 창을 가리킬 수 있다.

App.xaml.cs 4곳은 다르게 처리했다.
- 기동 실패(:49)·예외 폭주(:82)·복구 안내(:89) 3곳은 순정 유지. 창이 없는 시점이라
  자체 창을 띄우면 종료코드가 유실되고, 이미 예외가 터진 자리에서 WPF 창을 새로 만들면
  같은 핸들러로 재진입한다. 이유를 각 자리에 주석으로 남겼다.
  덤으로 :89 는 e.Handled 를 알림보다 먼저 세우도록 순서를 바로잡았다 —
  알림이 던지면 '복구 가능한 예외'가 하드 크래시로 바뀐다.
- 알 수 없는 진단 옵션(:179)은 알림을 없앴다. 옵션 오타 하나로 무인 실행이 멈추던 자리다.

함께 고친 것 — Primary 버튼 스타일. 공유 버튼 템플릿의 호버 트리거가 TargetName 으로
채움을 회색으로 덮는데(TargetName 트리거는 TemplateBinding 을 이긴다) 글자는 흰색 그대로라,
라이트에서 마우스를 올리면 #F3F3F3 위 흰 글자 1.08:1 로 사라진다. 지금까지 Primary 사용처가
0건이라 드러난 적이 없었고 이 창이 첫 사용이다. 전용 템플릿 + 채움 호버·누름 토큰 2종 신설.

신설 토큰: B.Warning / B.Danger(라이트는 다크값을 못 쓴다 — 앰버 #E0A33A 는 흰 면 위 2.22:1 로
아이콘 기준 3:1 도 미달), B.AccentFillHover / B.AccentFillPressed. 전부 양 테마에 동시 추가.
Lucide 아이콘 4종(info·circle-alert·triangle-alert·circle-help) 추가.

바꾸지 않은 것: 예외 원문. 18곳을 ShowError 로 수렴시키면 화면에서 ex.Message 가
오류코드+로그로 대체되는 동작 변경이 된다 — 요청은 시각 변경이라 문구·정보량을 그대로 뒀다.
남는 시스템 대화상자: 인쇄(PrintDialog)와 파일 열기/저장 — OS 셸 대화상자라 대상이 아니다.

진단 렌더러에 알림 5종(오류·경고·확인·저장확인·정보)을 등록해 라이트/다크 10장이 자동으로 남는다.
회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0,
종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 10:07:25 +09:00
MsystechandClaude Opus 5 bd0d055e0d 페이지 행의 복제·삭제도 호버할 때만 표시
컨트롤 행의 눈·자물쇠는 호버 규칙으로 바꿔 놓고 페이지 행 버튼만 상시 노출이라
목록 우측이 들쭉날쭉했다. 같은 규칙으로 맞춘다.

버튼 각각이 아니라 묶음(StackPanel)에 거는 이유가 있다 — 삭제 버튼은 CanDelete 에
Visibility 를 물려 있는데 인스턴스의 지역 값이 스타일 트리거를 이기므로, 버튼마다 걸면
삭제만 상시 노출된다. 바깥을 접으면 둘 다 숨고, 펴지면 삭제는 원래대로 CanDelete 를 따른다.

되돌리기가 번거로운 '페이지 삭제'가 항상 노출돼 있지 않게 되는 부수 효과도 있다.

회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0,
종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 09:08:13 +09:00
MsystechandClaude Opus 5 b47c7af8df 레이어 행 이름 잘림 해소 + 라이트 선택색을 연한 파랑으로
이름이 잘려 행을 구분할 수 없었다
  눈·자물쇠가 48px 를 상시 차지해 이름 칸이 한글 11자로 줄었다. 운영 서식에는
  '입원일 복용약-전립선약' 처럼 앞이 같고 끝에서 갈리는 이름이 흔해 전부
  '입원일 복용약-…' 로 잘렸다 — 목록만 보고는 어느 행인지 알 수 없었다.
  두 토글을 평소엔 숨기고 마우스를 올린 행에서만 띄운다. 잠김·숨김이 켜진 행에서는
  계속 띄운다(거기서까지 숨기면 마우스를 올리기 전에는 상태를 알 방법이 없다).
  [200] 은 항상 띄우지만 여기서만 갈라선다 — 폭이 곧 가독성이라서.

라이트 모드 선택 밴드가 너무 무거웠다
  양 테마 모두 짙은 파랑(#0F6FBF)에 흰 글자를 쓰다 보니, 흰 배경에 짙은 슬라브가
  얹혀 선택 몇 줄이 패널 전체를 짓눌렀다. 라이트만 방향을 뒤집는다 —
  연한 채움(#A8D3F7, 흰 패널 대비 1.58:1) + 진한 글자(10.1:1).
  서브트리도 한 단계 옅게(#DCEBFB) 해서 '폴더가 주인, 멤버는 딸림'이 색 세기로 읽힌다.
  밴드 위 글자색을 B.OnAccent(흰색 고정)에서 신설 토큰 B.OnLayerSel 로 바꿔
  테마별로 갈라지게 했다(다크 흰색 / 라이트 #222222). 보조 텍스트도 라이트만
  #35536C 로(연한 채움 위 5.12:1 — 기존 #E6F1FB 는 라이트에서 사라진다).

회귀: 테스트 124/124, 편집 스모크 실패 0(레이어 검사 45건), DB 왕복 1,271건
diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 18:19:59 +09:00
MsystechandClaude Opus 5 c22d84fb61 레이어 패널 3건 수정 — 목록에서 Ctrl+G, 스크롤바 잘림, 검색칩 이중 테두리
1) 목록에서 단축키가 안 먹었다
   CanvasKeyboardBehavior 는 캔버스에 붙어 있어 포커스가 레이어 목록에 있으면 이벤트가
   거기까지 가지 않는다. 여러 행을 골라 놓고 Ctrl+G 를 눌러도 아무 일이 없던 이유다.
   목록에 KeyDown 을 달아 그룹·삭제·복사·되돌리기를 처리한다. 방향키·Home/End·문자 입력은
   넘기지 않는다(목록 자체의 이동과 형식 검색이 우선). 이름 편집 중에는 통과시킨다.

2) 세로 스크롤바가 오른쪽이 잘려 보였다 — 앱 전체 공통 결함
   Thumb 스타일의 MinWidth/MinHeight 24 가 폭 9 짜리 막대 안에서 썸을 24 폭으로 강제해
   오른쪽이 통째로 잘렸다(둥근 모서리가 사라지고 수직으로 싹둑 잘린 막대). 가로 막대는
   아래쪽이 같은 식으로 잘렸다. 최소 '길이'는 MinThumbTrack.MinThumbLength=44 가 이미
   보장하므로 최소 '두께'는 지운다.
   화살표 글리프도 7 단위 + 스트로크 1.4 = 8.4 라 폭 9 에서 캡이 경계에 닿아 오른팔이
   잘렸다 — 막대 폭을 11 로 올려 양옆 여유를 준다.

3) 검색칩 안에 상자가 하나 더 그려졌다
   전역 TextBox 템플릿은 BorderThickness=1 이 하드코딩돼 있어 인스턴스에서 0 을 줘도
   무시된다. 칩 전용 ChipTextBox 스타일(테두리 없는 템플릿)을 만들고, 포커스 표시는
   칩 쪽 IsKeyboardFocusWithin 으로 옮겼다.

회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0,
종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 18:03:19 +09:00
MsystechandClaude Opus 5 76584e5004 레이어 패널을 [200]SheetMe 아웃라인으로 교체 — 페이지 노드·선택 밴드·드래그 재정렬
[200] 의 패널은 코드로 Border 를 쌓는 비가상화 렌더고 SheetMe 는 가상화 ListBox 다.
골격은 SheetMe 것을 지키고 겉모습과 조작 표면만 옮겼다 — 통째로 베끼면 1,067 컨트롤
서식에서 검색어 한 글자마다 수천 비주얼을 다시 만들게 되고, [200] 에 대응물이 없는
컨테이너 자식 재귀 표시가 사라진다.

구조
- 페이지를 별도 섹션이 아니라 아웃라인의 최상위 노드로. 접기·복제·삭제·컨트롤 수·현재 강조
- 맨 아래 '+ 페이지 추가' 행
- 목록 순서를 뒤집었다 — 이제 맨 위가 z-order 최상위(캔버스에서 위에 겹쳐 보이는 것이 목록 위)
- 들여쓰기 12 / 32 / +16
- 레이어 패널을 LayerPanelView 로 분리(MainView.xaml 이 1,000줄을 넘겼고, 진단 렌더러가
  이 패널만 따로 찍어 라이트·다크·선택 상태를 검사할 수 있어야 한다)

선택 표시
- IsSelected 가 아니라 Highlight 3상태(None/Blue/Navy)가 배경을 그린다. 그룹 전원 선택 시
  폴더는 파랑, 멤버는 서브트리 배경이 되고 맞닿는 모서리를 각지게 해 하나의 블록으로 이어진다
- 밴드 위에서는 아이콘·보조 텍스트 색도 함께 뒤집는다. 안 그러면 눈·자물쇠가 1.6:1 로 묻혀
  선택한 행에서만 잠금 상태가 안 보이는 상태가 된다
- 신설 토큰 3종(B.LayerSel / B.LayerSubtree / B.OnSelMuted) — [200] 원본값은 각각
  흰 글자 3.00:1, 다크 밴드 1.03:1, 라이트 밴드 1.14:1 로 이 저장소가 이미 기각한 값이다

기능
- 드래그로 z-order 재정렬(삽입선 어도너). 임의 index 이동 연산 MoveWithinParent 신설
- 페이지 복제 — 컨트롤 이름을 전부 새로 발급하고 그룹도 복제본끼리 다시 묶는다
  ([200] 은 GroupId 를 그대로 둬서 복제 그룹이 원본과 gid 를 공유한다)
- 페이지 헤더 클릭 → 캔버스가 그 페이지로 이동(ScrollToPage)
- 데이터 배선 배지 — 값이 조회·데이터소스에서 오는 컨트롤 표시
- 검색칩에 테두리(라이트에서 B.Input=B.Panel 이라 경계 대비가 1.00:1 이었다)

이식하지 않은 것: 컨트롤 행 인라인 개명. [200] 이 고치는 값은 표시 전용 별칭이지만
SheetMe 의 컨트롤 이름은 외부 참조 키라 목록에서 무심코 고치면 배선이 끊긴다.
더블클릭은 요청대로 캔버스 이동을 유지하고, 그룹 행에서만 이름 편집으로 간다.

동반 수정: 눈·자물쇠 토글에 되돌리기 지점 추가(400ms 코얼레스), 그룹 이름 비우기 경로가
스냅숏 없이 지워지던 것, 숨김 흐리기를 행 전체가 아니라 이름 줄에만.

편집 스모크의 레이어 검사 19 → 42건. 회귀: 테스트 124/124, 편집 스모크 실패 0,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 17:48:48 +09:00
MsystechandClaude Opus 5 a30421f9b5 레이어 패널에 [200]SheetMe 그룹 기능 이식 — 폴더 행·계층·타입 필터·다중선택
[200] 의 그룹도 컨테이너가 아니라 GroupId 태그 + 이름 사전이었다. SheetMe 가 이미
같은 구조(ControlElement.GroupId, JsonIgnore)를 갖고 있어 저장 포맷은 그대로 두고
표시·조작 계층만 얹는다 — 왕복 XML 에는 아무 흔적도 남지 않는다.

레이어 목록을 평탄화 결과(LayerRows)로 바꿨다. 그룹 폴더 행과 컨트롤 행이 한
시퀀스로 섞이고, 계층은 들여쓰기 여백으로만 표현한다(TreeView 를 쓰면 가상화와
기존 행 템플릿을 다 버려야 한다 — 인스펙터 섹션이 쓰는 방식과 같게 맞췄다).

새로 되는 것
- 그룹 폴더 행: 접기/펼치기, 멤버 수, 이름 인라인 편집(더블클릭), 그룹 전체 눈·자물쇠
- 그룹 이름 — FormDocument.GroupNames(편집 전용). 비우면 기본 그룹 으로 복귀
- 컨테이너(패널/그룹박스) 자식이 레이어에 보인다. 지금까지는 아예 접근할 수 없었다
- 목록 다중선택(Shift/Ctrl) — 캔버스 선택과 양방향 동기
- 타입 필터: 그 페이지에 실제로 있는 타입만 후보로 띄운다
- 검색 0건·빈 페이지 안내 문구

함께 고친 결함
- 붙여넣기가 원본 그룹에 끼어들었다(Clone 이 GroupId 를 그대로 복사) → 복제본끼리 새 그룹
- 그룹 해제가 선택된 멤버만 풀어 1개짜리 반쪽 그룹이 남았다 → 그룹 전 멤버를 푼다
- 멤버가 1개 이하로 줄어든 그룹이 계속 남았다 → CleanupGroups 자가치유
- 셰브론이 늘 눌린 것처럼 보였다(IsChecked 채움) → MiniChevron 스타일 분리

--edit-smoke 에 레이어 검사 19건 추가(그룹 위생·평탄화·필터·XML 무영향).
회귀: 테스트 124/124, edit-smoke 실패 0, db-smoke 1,271건 diff 0/예외 0,
종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:43:37 +09:00
MsystechandClaude Opus 5 d09bd90c8e 레이어 패널: 컨트롤 이름 병기 + 더블클릭으로 캔버스 이동
레이어 목록이 Text 만 보여줘서 chkOrientation1~5 처럼 문구가 같은
컨트롤들이 전부 같은 줄로 보였다. 두 줄로 나눠 아래에 Id 를 병기한다
(Text 가 없어 LayerName 이 곧 Id 인 경우는 중복이라 숨긴다).

그리고 컨트롤이 수백 개인 서식에서는 목록에서 이름을 찾아도 종이
어디에 있는지 알 수 없었다 — 더블클릭하면 해당 페이지로 전환 + 선택 +
캔버스 스크롤. 이미 전부 보이면 스크롤하지 않는다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:08:19 +09:00
MsystechandClaude Opus 5 e18ad87100 피그마식 속성 조작 — Enter 확정·화살표 증감·정렬 격자
사용자 지적: "피그마같은 도구처럼 보기 편하게, 특히 속성 사용하기 불편해".
불편의 핵심은 밀도가 아니라 **조작**이었다. 모든 칸이 UpdateSourceTrigger=LostFocus 하나라
값을 고치고 나서 **다른 곳을 눌러야** 반영됐다 — 캔버스를 보며 숫자를 맞춰 가는 작업이
성립하지 않는다.

[Enter 확정] 즉시 커밋하고 결과를 전체 선택한다(확정 신호이자 곧바로 덮어 칠 수 있는 상태).
여러 줄 입력은 Enter 가 줄바꿈이어야 하므로 AcceptsReturn 으로 코드에서 가른다 — 스타일 예외로
빼면 새 여러 줄 칸이 생길 때마다 잊는다.

[Esc 되돌리기] **편집 전 값을 따로 보관하지 않는다.** LostFocus 트리거라 소스는 아직 손대지 않은
상태이고 소스가 곧 백업이다 — UpdateTarget() 으로 화면만 다시 읽는다.
(초안은 포커스 시점 문자열을 기억했는데, 그러면 칸에 포커스를 둔 채 캔버스에서 컨트롤을 끌었을 때
Esc 가 낡은 값을 되살리고 LostFocus 가 그걸 커밋해 **컨트롤이 원위치로 돌아간다**. 검증에서 잡았다.)

[↑/↓ 증감] 1씩, Shift 10씩. 증분은 칸마다 지정한다 — **Ctrl 미세조정은 기본 끔**이다.
X/Y/너비/높이는 읽기·쓰기 양쪽에서 반올림하므로 0.1 을 더해도 모델은 그대로인데 문자열만 바뀌어,
화면은 그대로면서 Undo 스냅샷과 '수정됨' 표시만 남는다. 소수를 실제로 받는 글꼴 크기에서만 켠다.
그리고 **글꼴 크기는 NumberRow 가 아니라 TextRow 였다** — 정작 가장 미세조정하고 싶은 칸이 스핀
대상에서 빠져 있었다. NumberRow 로 승격했다.

[Undo 폭주 차단] 커밋 1회 = 문서 전체 딥클론 1회다. ↑ 를 스무 번 누르면 스냅샷 스무 개가 쌓여
용량(100)을 갉아먹고, 저장 지점이 밀려나면 영구 '수정됨'이 된다. 행에 CoalesceUndo 플래그를 두고
경계·크기 행만 기존 SnapshotForNudge(400ms 코얼레스)로 보낸다. 값이 아니라 **커밋 방식만** 바꾸므로
Initialize/Commit 규약은 그대로다.

[정렬 격자] 콤보에 "MiddleCenter" 같은 레거시 원문 열거값이 그대로 떴다. 무슨 뜻인지 알려면 아홉 개를
다 열어 봐야 하고 고르고 나서도 글자로만 확인된다. 3열 격자로 바꿔 9값은 3×3 방향 화살표,
3값은 1×3 정렬 아이콘으로 접힌다(같은 템플릿이 값 개수에 따라 알아서).
**ToggleButton 이 아니라 Button 을 쓴다** — 토글이면 켜진 칸을 다시 눌러 해제되고, 그때 값이 빈
문자열이 되어 Set 이 속성을 **삭제**한다. ControlRegistry 주석이 경고하는 '선택만으로 속성이
사라지는' 사고(운영 249건)와 같은 계열이다.

[바꾸지 않은 것] UpdateSourceTrigger 는 그대로 둔다. PropertyChanged 로 바꾸면 한 글자마다 딥클론이
쌓이고, 이름 행은 글자마다 RenameControl 이 돌며, 경계 행은 커밋 결과가 정규화 값으로 되쓰여 캐럿이
튄다. 피그마식 사용감은 트리거가 아니라 명시적 확정 제스처로 얻는다.

[다음 후보 — 이번엔 넣지 않았다] 라벨을 끌어서 값을 바꾸는 스크럽은 UndoService 에
BeginBatch/EndBatch 를 넣어야 안전하다(지금 구조면 드래그 프레임마다 스냅샷 1개). 캡처 해제 처리도
이 저장소에 사고 선례가 있어 별도 작업으로 남긴다.

검증: 테스트 124/124, edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 픽셀 차이 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 14:35:16 +09:00
MsystechandClaude Opus 5 394807f8df 인스펙터 3탭 분할 — 빈 탭은 그리지 않는다
사용자 요청으로 [200] 식 「디자인 / 데이터 / 동작」 3탭을 넣었다.

[분류] 속성 49종을 레거시 실사용 근거로 배정했다. 이름만 보고 찍으면 사용자가 속성 하나를 찾아
세 탭을 다 뒤지게 되므로, 각 속성이 레거시에서 실제로 어떻게 쓰이는지를 근거로 삼았다.
· GetValue() 가 그 속성을 돌려주면 값이다 — CheckBox/RadioButton 의 Checked 는 IIf(Checked,'Y','')
  이므로 초기값(데이터).
· DateTimePicker.Format/CustomFormat 은 데이터처럼 보이지만 GetValue 가 Format 과 무관하게 항상
  yyyyMMddHHmmss 를 돌려준다 — 그리기 문자열만 바꾸므로 디자인. 확정적.
· Mask 는 WinForms 분류로는 Behavior 지만 실제로는 날짜·시간 입력 형식 제약이라 데이터.
· Score 는 '점수'라는 이름과 달리 체크됐을 때만 값을 내는 데이터 항목이고 CalcBox.Formula 가
  컨트롤명으로 합산한다.
· 상용구 2종(RwdRsvWrdYon/RwdOrderAutYon)은 입력 보조처럼 보이지만 조작 훅이다 — 전자는 포커스
  핸들러 부착 게이트, 후자는 상용구를 고른 결과로 OCS 처방을 전송하는 게이트. **동작**으로 정정했다
  (초안에서는 데이터로 뒀다).
결과: 디자인 21 / 데이터 20 / 동작 8.

[빈 탭은 그리지 않는다 — 이번 결정의 핵심]
분류를 마치고 세어 보니 **동작 속성을 가진 타입은 TextBox·Button 둘뿐이고 나머지 14타입(87.5%)에서
동작 탭이 통째로 빈다.** 데이터 탭도 6타입에서 빈다. 빈 탭을 남겨 두면 대부분의 컨트롤에서
'여긴 없습니다'만 보게 되고 사용자는 빈 탭을 계속 눌러보게 된다 — 탭이 없느니만 못하다.
그래서 개수 0인 탭은 알약을 아예 그리지 않고, 남는 탭이 하나뿐이면 알약 줄 자체를 숨긴다.
결과적으로 라벨·선·패널·그룹상자·날짜선택은 탭 없이 평평하게 보이고, TextBox 만 3개가 뜬다.
현재 탭이 새 선택에서 비면 디자인으로 되돌린다 — 디자인은 공통·글꼴이 있어 절대 비지 않는
안전한 착지점이다.

[유지] 선택한 탭은 컨트롤을 바꿔도 유지된다. 데이터 탭에서 여러 컨트롤의 태그를 잇달아 손보는
작업이 흔한데 매번 디자인으로 돌아가면 계속 다시 눌러야 한다([200] 은 매번 리셋된다 — 따르지 않았다).

[탈출구는 쪼개지 않는다] '전체 속성(고급)'의 레거시 원문 키는 분류 근거가 없고 무엇보다 탈출구다.
탭에 가두면 '고급에도 없네' 상황이 생기므로 모든 탭 하단에 그대로 둔다.

[분류표 위치] Core 가 아니라 Designer 계층에 뒀다(PaletteIconCatalog 선례). 탭 구성은 화면 사정이지
서식 데이터의 성질이 아니다. 모르는 키는 편집기 종류로 추론해 사이트 커스텀 속성에 대비한다.

검증: 테스트 124/124, edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 픽셀 차이 0.
dialog-shots 진단에 데이터·동작 탭 스냅샷을 추가해 세 탭 전부를 양 테마로 확인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 14:12:29 +09:00
MsystechandClaude Opus 5 abd7eb7d0e 속성 패널 고도화 — 쌍 배치·스타일 세그먼트·행 축소로 세로 25% 절감
사용자 지적: "속성패널 디자인 보기 불편한데? 고도화해줘".

[실측한 낭비] 단일 선택 시 '공통+글꼴'만으로 354px 를 쓴다. 표준 행 34px 중 실제 필요 높이는
18px(테두리 2 + 글자 16)뿐이라 행마다 12px 가 공기다. 가장 긴 TextBox 는 25행 801px 로 기본 창
뷰포트(약 790px)를 이미 넘겨 스크롤이 강제됐다.

[쌍 배치] X|Y, 너비|높이를 한 줄 2칸으로. 라벨은 칩 바깥 왼쪽 자연폭 — [200] 의 XYPair 규약
(Grid star/10/star)과 같다. 네 줄이 두 줄이 된다.
구조가 핵심이다: **자식 행은 평범한 행 그대로**이고 컨테이너만 새로 만들었다. 값·커밋·"여러 값"
판정이 전부 자식 안에서 끝나므로 커밋 규약(Initialize 는 Commit 을 내지 않고, 값이 바뀔 때만
Undo 스냅샷 1회)이 한 글자도 안 바뀌고, boundsRows 가 자식을 들고 있어 드래그 중 실시간 갱신
(RefreshBoundsRows)도 그대로다. X 만 갈리고 Y 는 같은 다중선택도 칸별로 옳게 표시된다
(그래서 셀마다 전용 Grid 를 둔다 — 오버레이의 포커스 트리거가 AncestorType=Grid 다).

[스타일 세그먼트] 굵게·기울임·밑줄·취소선을 한 줄 아이콘 토글로. 종전에는 굵게·밑줄만 있었고
각각 한 줄이었다. **기울임·취소선은 기술적 제약이 아니라 단순 누락이었다** — LegacyFont 의
파싱·직렬화·렌더·폰트 일괄 변경 대화상자까지 전부 구현돼 있고 인스펙터만 호출을 빠뜨렸다.
CurrentFontOf 가 Italic/Strikeout 을 복사·보존하므로 네 개를 동시에 노출해도 왕복이 안 깨진다.

[행 치수] 인스펙터 전용 지역 스타일로 입력 높이 30→26. 전역 TextBox 를 낮추면 검색칩·대화상자
입력까지 따라 줄어 버튼과 정렬이 깨진다. **ComboBox 는 MinHeight 가 아니라 Height 고정**이라
Height 를 함께 내려야 한다 — 안 그러면 선택 행만 4px 어긋난다(설계 검토에서 잡은 실제 결함).

[라벨] 폭 86→92 + 줄바꿈. 실측상 가장 긴 라벨이 11자("입력 파라미터 컨트롤")라 86px 에서
"액션 대상 컨…" 처럼 잘렸다. 자르면 툴팁을 띄워야 아는 정보가 되므로 접는 쪽을 택했다.

[그 밖에] 태그·마스크 편집기를 여는 "…" 버튼은 26px 칩 옆에서 점 하나로 보여 아이콘 버튼으로.
섹션 헤더 건수를 줄 수가 아니라 **속성 수**로(LeafCount) — 쌍/세그먼트는 한 줄이지만 속성은
여럿이라 그냥 세면 "공통 3"으로 실제보다 적게 나온다.
인스펙터 패널 최소폭 220→250 — 스타일 행이 라벨 92 + 토글 4칸 128 = 220px 를 요구한다.

효과: 공통 5행→3행, 글꼴 4행→3행(속성은 4→6개로 늘었다). TextBox 기준 25행→18행.
'전체 속성 표시' 토글이 기본 창에서 스크롤 없이 보인다(종전에는 한참 아래).

검증: 테스트 124/124, edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0,
종이 렌더 P062 픽셀 차이 0. 인스펙터를 300px 호스트 창에 담아 양 테마로 렌더해 배치 확인
(dialog-shots 진단에 09/10 추가 — 속성 패널 레이아웃 회귀를 눈으로 잡는 자동 경로).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 13:48:10 +09:00
MsystechandClaude Opus 5 14944bd316 암시 스타일 누락 4종 추가 — ToolTip·ToolBar 자식·포커스링·스크롤 코너
앞 커밋(Window 파생 클래스)과 같은 부류의 결함을 창별 정적 점검이 4건 더 찾아냈다.
전부 "스타일이 없으면 WPF 기본(Aero2) 밝은 크롬으로 떨어지고, 그 위 글자는 앱 전역 암시
TextBlock(B.Ink)을 정상적으로 받아 다크에서 사라진다"는 같은 메커니즘이다.

[ToolTip — 48곳] 가장 넓다. MainView 31곳·InspectorView 15곳·대화상자 6곳 등에서 쓰는데
TargetType="ToolTip" 스타일이 저장소 전역에 하나도 없었다. 다크에서 명암비 1.15~1.17:1.
B.Surface 채움 + B.Line2 테두리 + 라운드 6 으로 앱 룩에 맞췄다.

[ToolBar 자식 키] ToolBar 는 직계 자식에 SetResourceReference 로 자기 키 스타일을 꽂아
암시 Button 스타일을 이긴다 — ToolBar 자체에 스타일을 줘도 그 안 버튼만 Aero2 로 남는다.
ButtonStyleKey/ToggleButtonStyleKey/SeparatorStyleKey/TextBoxStyleKey 를 채웠다(미리보기 창).

[포커스링] Aero2 기본 검정 점선이 다크 버튼 채움(#262626) 위에서 1.39:1 이고, 커스텀 Button
템플릿에 IsKeyboardFocused 트리거도 없어 Tab 위치를 알 단서가 전혀 없었다.
SystemParameters.FocusVisualStyleKey 를 강조색 점선으로 재정의.

[ScrollViewer 코너] 가로·세로 막대가 동시에 뜰 때 노출되는 우하단 사각형은 프레임워크 템플릿
소속이라 우리 스타일이 닿지 않는다. 템플릿이 SystemColors.ControlBrushKey 를 DynamicResource 로
읽으므로 그 키를 토큰 사전에서 덮어썼다 — 테마 전환도 따라간다.

[진단 보강] --dialog-shots 에 암시 스타일 자가 점검을 추가했다. 툴팁·포커스링·툴바 자식·스크롤
코너는 팝업이거나 프레임워크 템플릿 소속이라 **창을 찍어도 스크린샷에 안 잡힌다** — 값으로
확인하지 않으면 회귀를 놓친다. 떼어 놓은 인스턴스는 트리에 붙기 전이라 Style 이 null 이므로
리소스 사전 존재 여부로 판정한다(처음엔 인스턴스 Style 을 봤다가 오판했다).

검증: 자가 점검 8종 전부 정의됨, 테스트 124/124, edit-smoke 실패 0,
왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 픽셀 차이 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 13:14:10 +09:00
MsystechandClaude Opus 5 308197c15e 별도 창 전수 점검 — 암시 Window 스타일이 파생 클래스에 적용된 적이 없었다
사용자 지적: "다른 창으로(새창으로 열리는) 실행하는 항목들도 모두 점검해줘".

[근본 원인 — 대화상자 전체가 다크에서 흰 창이었다]
WPF 는 암시 스타일을 요소의 **정확한 런타임 타입**으로만 찾는다. 저장소의 모든 창이
Window 를 상속한 클래스(SheetOpenDialogView : Window, QueryEditorWindow : Window …)라
DesignerTheme.xaml 의 <Style TargetType="Window"> 는 **단 한 번도 적용된 적이 없다**.
결과: 창 배경이 WPF 기본 흰색으로 남고, 그 위 글자는 암시 TextBlock 스타일(B.Ink #EDEDED)을
정상적으로 받는다 → **다크 테마에서 흰 바탕에 흰 글자**. 쿼리 편집기의 변수 목록처럼 라벨이
통째로 사라지는 화면이 나왔다.

라이트에서 눈에 덜 띈 이유는 우연이다 — 기본 흰색(#FFFFFF)이 의도값 B.AppBg(#F5F5F5)와
거의 같아서다. 그래서 지금까지 라이트만 보고는 발견되지 않았다.

수정: 스타일에 x:Key="ThemedWindow" 를 주고 창 10종에 직접 걸었다(코드로 짓는
ColorPickerWindow 는 생성자에서 TryFindResource). 순수 Window 인스턴스용 암시 스타일은
BasedOn 으로 남겼다. 앞으로 창을 추가할 때 잊지 않도록 스타일 위에 경고 주석을 붙였다.

[신규 진단 --dialog-shots <출력폴더>]
이 결함은 정적 분석으로는 못 잡았다(앞선 감사에서 "대화상자들은 암시 Window 스타일을 그대로
받는다"고 잘못 결론냈다). 실입력 주입은 화면 잠금·세션 격리 상태에서 OS 가 거부한다
(SetCursorPos 무시, SendKeys "Access is denied" — 이번에도 중간부터 막혔다).
그래서 창을 화면 밖(-10000)에 띄워 RenderTargetBitmap 으로 찍는 진단을 만들었다.
창 8종 × 2테마 = 16장을 한 번에 남기고, 각 창의 **해석된 Background 와 Style 적용 여부를
텍스트로 함께 보고**한다 — 픽셀만 보면 원인을 못 가린다(실제로 이 한 줄이 원인을 확정했다).
표본 데이터는 실제 사용 시와 같은 형태로 넣었다(빈 껍데기를 찍으면 의미가 없다).

함정 2개를 코드에 남겼다.
· 기본 ShutdownMode 가 OnLastWindowClose 라 찍고 닫는 순간 앱이 종료된다 → OnExplicitShutdown.
· Window.Content 만 렌더하면 창 배경이 빠져 투명(=PNG 검정)이 되고, 다크처럼 보이는 착시로
  라이트 결함이 가려진다 → 배경을 먼저 칠하고 그 위에 콘텐츠를 그린다.

[함께 고친 것 — 여러 줄 입력이 세로 가운데 정렬]
쿼리 편집기의 SQL 이 큰 상자 한가운데 떠 있었다. 템플릿 트리거가 ScrollViewer 의
VerticalContentAlignment 만 Stretch 로 바꿨는데, 텍스트를 배치하는 건 TextBox 자신의
VerticalContentAlignment 이라 아무 효과가 없었다. Style.Triggers 로 옮겨 Top 으로 두고
여러 줄일 때 Padding 도 넉넉히 준다. 인스펙터의 여러 줄 속성 행에도 함께 적용된다.

검증: 창 8종 × 2테마 전부 재렌더해 배경 확인(다크 #0E0E0E / 라이트 #F5F5F5,
미리보기만 의도대로 B.CanvasBg). 테스트 124/124, edit-smoke 실패 0,
**종이 렌더 P062 픽셀 대조 차이 0**.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 12:54:55 +09:00
MsystechandClaude Opus 5 ff87353d0d 다크/라이트 대비 전수 감사 및 수정 — 전환 미추종 3종 + 라이트 대비 미달 일괄
사용자 지적: "다크, 화이트 모드 오류나는(대비색상 제대로 표현안됨) 부분 모두 확인해줘".
표면별 병렬 감사 후 지적 74건을 하나씩 반증 검증해 39건 확정(21건 반증). 원인은 셋으로 압축된다.

━━ 1. 전환을 아예 따라가지 못하던 것(라이브 전환 시 옛 색 유지) ━━

[아이콘 브러시 굽기 — 가장 컸다] IconParam.Parse 가 TryFindResource 로 Brush 를 꺼내
LucideIcons.Icon 이 Path.Stroke/Fill 에 대입했다. ThemeManager 는 토큰 사전을 통째로 갈아끼우므로
이미 만들어진 아이콘은 옛 색을 영원히 유지한다. 실측: 라이트 전환 후 플로팅 바의 선택/손 도구가
다크의 B.Ink(#EDEDED)를 붙든 채 흰 배경에 놓여 사라졌다.
→ Parse 가 Brush 대신 **키**를 반환하고 Icon 이 SetResourceReference 로 건다(DynamicResource 등가).
   시작 시점에는 LoadSaved 가 MainView 생성보다 앞이라 정상이었고, 전환 시에만 나던 결함이다.

[그림자 3곳] 플로팅 바는 <StaticResource ResourceKey="B.FloatShadow"/> 요소 구문이라 굳었고,
두 플라이아웃은 아예 리터럴 검정(Opacity 0.4)이었다. 라이트 설계값은 0.14 인데 흰 배경에 진한
검정이 그대로 얹혔다. 셋 다 DynamicResource 토큰 참조로.

[콤보 드롭다운 그림자] 같은 이유로 리터럴 #000000/0.22 → 토큰.

━━ 2. 암시 스타일이 없어 WPF 기본 크롬으로 떨어지던 것(다크에서 안 보임) ━━

ListView·ListViewItem·GridViewColumnHeader·ToolBar·ToolBarTray 는 각자 DefaultStyleKey 를 자기
타입으로 재정의해 기존 ListBox 계열 암시 스타일을 물려받지 못한다. 스타일이 없으니 WPF 기본(Aero2)
흰 시스템 크롬으로 떨어지는데 그 안의 글자는 앱 전역 암시 TextBlock(B.Ink)을 받는다 → **다크에서
흰 바탕에 흰 글자**. 피해 화면: DB에서 서식 열기, 서식 수정이력, 미리보기 창 툴바.
→ 5종 암시 스타일 신설. 행 템플릿은 반드시 GridViewRowPresenter 를 쓴다 — ListBoxItem 을
   BasedOn 으로 물려주면 ContentPresenter 가 열 구성을 대체해 행이 바인딩 객체의 ToString()
   한 줄로 렌더된다(구현 중 실제로 겪었고 스크린샷으로 확인 후 고쳤다).

[캡션 글리프] CaptionBtn 템플릿이 TextElement.Foreground 상속으로 색을 넘기는데, Content 가
<TextBlock> 요소면 그 자신의 암시 스타일(B.Ink)이 상속을 이긴다. B.CaptionFg/FgHover/CloseHover
4개 토큰이 통째로 죽은 코드였다 → ContentPresenter 지역 암시 스타일로 되돌려 잡음.

━━ 3. 값 자체가 라이트에서 기준 미달(토큰 한 곳을 고치면 수십 곳이 낫는다) ━━

B.Muted #969696 → #6B6B6B (라이트만)
  양 테마 동일값이라 라이트 배경에서 2.50~2.96:1 — 본문 4.5:1 은 물론 아이콘 3:1 도 미달이었다
  (다크는 같은 값이 4.85~6.53:1). 회색 하나를 양 테마가 공유할 수 없다. 사용처 59곳이 전부
  Foreground 라 채움 룩에는 영향이 없다. 새 값은 Panel 5.33 / AppBg 4.89 / Canvas 4.51 / Chip 4.63.

B.CanvasBg #ECECEC → #D8D8D8   종이 대비 1.18 → 1.43:1 (다크는 14.35:1인데 라이트만 용지 경계가
                                사실상 없었다). 용지 자체는 손대지 않았다.
B.InputBorder #E3E3E3 → #C4C4C4  라이트는 Input=Panel=Surface=#FFFFFF 라 이 테두리가 입력란의
                                유일한 경계인데 1.28:1 이었다.
B.Line2 #D8D8D8 → #C6C6C6      체크박스 상자·컨트롤 경계
B.Sel #DBEEFF → #C2E2FF        선택 채움 1.19:1 → 어느 항목이 선택됐는지 안 보였다
B.Hover #F3F3F3 → #EAEAEA
B.Accent2 #E4F2FF → #CFE7FF    도구 '켜짐' 표시
B.Success #2E9E6B → #217A52    결과 문구 2.94:1 → 4.6:1
B.SliderRing #CFCFCF → #9A9A9A 흰 썸의 유일한 윤곽

신규 토큰 2종(브랜드색 #0099FF 는 그대로 두고 역할만 분리):
  B.AccentText — 강조 '글자'. 흰 배경 위 3.0:1 로는 본문이 안 된다 → 라이트 #0F6FBF(5.19), 다크는
                 어두운 배경이라 브랜드색 그대로(6.09). 쿼리 편집기·마스크 편집기 헤더에 적용.
  B.AccentFill — Primary 버튼 채움. 흰 글자를 얹으므로 양 테마 #0F6FBF(5.19). 브랜드색 위 흰 글자는
                 3.0:1 로 본문 미달이었다.

그 외: MiniToggle Opacity 0.55 → 0.75(옅은 색을 또 반으로 깎아 라이트 2.22:1 이었다 → 3.19/4.02),
스크롤 막대·화살표가 이미 있는 B.ScrollThumb* 토큰을 무시하고 리터럴을 쓰던 것 정리(라이트에서
3:1 미달이고 호버·드래그로 갈수록 더 흐려지는 어포던스 역전이었다), TextBox 포커스 표시를 호버와
같은 B.Line2 에서 B.Accent 로(포커스 위치가 유일한 단서인데 1.3:1 이었다), 메뉴 키보드 하이라이트
B.Hover(1.1:1) → B.Sel, 도구 토글 '켜짐'에 강조 테두리 추가(채움만으로는 1.14~1.19:1),
쿼리 편집기 GridSplitter 를 공용 ColSplitter 스타일로, 색상 피커 견본 테두리를 테마색이 아니라
견본 자신의 명도로 결정(라이트에서 흰 견본·다크에서 검정 견본이 빈칸으로 보였다).

━━ 판정을 바꾸지 않은 것 ━━
용지 렌더 도메인(ControlTemplates/PageView/PrintService/SelectionOverlay/인라인 편집기)의 고정색은
**정상**이다. 레거시 서식을 1:1로 그리는 곳이라 테마가 새어 들어가는 쪽이 버그다. 감사에서 이 경계를
반대로 지적한 건들은 반증 처리했다.
남는 한계: 컨트롤 경계의 WCAG 3:1 을 완전히 채우려면 라이트 테두리를 #949494 까지 올려야 하는데
[200] 의 가벼운 룩을 크게 벗어나 가시성 확보선(#C4C4C4)에서 멈췄다.

검증: 테스트 124/124, edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0,
**종이 렌더 P062 픽셀 대조 차이 0**(토큰을 여럿 바꿨는데도 용지에 새지 않음을 확인).
실 UI: 다크↔라이트 왕복 전환 후 아이콘 색 추종 확인, 라이트 전 화면·인스펙터·레이어,
DB에서 서식 열기 대화상자를 양 테마에서 열어 열 구성과 가독성 확인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 12:13:28 +09:00
MsystechandClaude Opus 5 ca16630f3a 서식 목록을 분류(ShtClsCod)로 묶기 — 평면 1,629건 → 접이식 24분류
사용자 지적: "서식 목록 구분없이 하나로 다 나오는거 불편한데 묶여서 나와야해".

[기준 선정] 후보 컬럼 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>
2026-08-12 11:14:17 +09:00
MsystechandClaude Opus 5 9d20cc2971 [200] 디자인 동일화 2/2 — 좌측 패널·타이틀바·인스펙터 정보 밀도
1/2 가 껍데기(스크롤바·창 기본값)를 맞췄다면 이번은 그 안에 든 정보다. [200] 과의 실제 격차는
장식이 아니라 "한 화면에서 얼마나 읽히는가"였고, 그중 둘은 데이터 손상 경로이기도 했다.

[레이어] 행 라벨을 Model.Id → 신규 LayerName(Text 우선, 없으면 Id)으로. 운영 서식의 Id 는
대부분 Label12 꼴 자동 이름이라 291개짜리 목록이 아무것도 알려주지 않았다. Id 는 외부 참조
키이므로(DataTableField 대상명, EventHandlerMappingTag 의 ShtCod_컨트롤명 규약) 버리지 않고
툴팁 + 검색 대상으로 유지한다. 앞에 타입 아이콘(Accent), 뒤 숨김/잠금을 MiniToggle(eye/eye-off,
lock/unlock)로 — 사각 체크박스로는 상태가 안 읽혔다.
검색칩 신설(LayerFilter, 이름·Id 부분일치). 컨트롤 최대 1,067개 서식에서 스크롤만으로는 대상을
찾을 수 없다. 칩 안 TextBox 에 MinHeight=0 필수 — 전역 TextBox MinHeight 30 이 32px 칩에서
글자를 자른다.

[인스펙터·여러 값] IsMixed 가 설정만 되고 화면 어디에도 바인딩돼 있지 않았다. 다중선택에서 값이
갈리면 편집기가 빈 칸이 되는데 그 빈 칸은 "값 없음"과 구별되지 않는다. 무심코 입력하면 서로
달랐던 값이 전부 한 값으로 덮인다. 편집기 위에 비파괴 오버레이("여러 값")를 얹고, 포커스가
들어오면 감춘다 — 값은 LostFocus 에만 올라가므로 타이핑 중 ValueText 는 계속 "" 이고 안내가
방금 친 글자 위에 남는다.
토글 행은 IsOn 을 bool? 로 바꿔 3상태로. false 로 내리면 "전부 꺼짐"으로 보이고, 사용자가 한 번
켰다 끄면 전부 False 가 되어 조용히 뭉개진다.

[인스펙터·접이식] 섹션 헤더를 클릭 가능한 접기/펴기로(셰브런 + 소속 행 수). 고급 속성까지 켜면
수십 행이 된다. Rows 는 평면 컬렉션을 유지하고 섹션이 자기 행을 들고 있다가 컨테이너
Visibility 만 토글한다 — 행 VM 을 재생성하지 않아 편집 중 값·포커스가 보존되고, 다중선택
재구성이 잦은 이 화면에서 트리 재구축 비용도 피한다.

[타이틀바] 로고 메뉴 루트(셰브런) 아래로 5개 메뉴를 흡수하고, 편집 아이콘 스트립(실행취소·다시
실행 | 복사·삭제·그룹·해제)과 우측 액션(테마·저장·미리보기)을 배치. 중앙 제목은 뺐다 — 문서 탭이
같은 정보를 이미 들고 있다.

[빈 상태] 문서 0개 → 회색 허공 대신 다음 행동 3개(새 서식 / DB에서 열기 / 파일 열기).
선택 0개 → 인스펙터에 "선택 없음" 한 줄만 떠 패널이 고장 난 것처럼 보이던 것을 안내로 교체.

[그 외] 서식 목록에 하단 상태 한 줄(로딩·건수·DB 미접속) — 종전에는 목록 위에 "불러오는 중"
오버레이가 덮여 목록이 가려졌고 건수는 창 하단 전역 상태줄에만 있었다. 활성 문서 탭 아이콘을
Accent 로. 플로팅 바의 ±버튼 제거([200] 동일 — Ctrl+휠이 주 조작이고 배율 버튼이 플라이아웃을
겸한다), 대신 "화면 맞춤" 추가. ZoomFit 은 문서 전체가 아니라 활성 페이지 한 장 기준이다
(7페이지 서식을 통째로 맞추면 14% 가 되어 아무것도 안 보인다). 뷰포트 크기는 캔버스 뷰가
SizeChanged 에서 VM 에 통지 — VM 이 시각 트리를 몰라도 되게 한다.
열 기본 폭 232→248 / 280→300.

검증: 테스트 124/124, edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0.
**종이 렌더 P062 픽셀 대조 차이 0**(4,802바이트 전부 일치).
실 UI 확인: P062 레이어 행·인스펙터 접기, P093 291컨트롤에서 레이어 검색("알러지" → 10건),
116개 선택 시 X/Y/너비/높이 "여러 값" 표시.

남은 배치: E(선택 오버레이) · F(캔버스 크롬 — 종이에 닿는 유일한 배치) · G(토스트·ThemedDialog).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 10:33:48 +09:00
MsystechandClaude Fable 5 7591fbed74 [200] 디자인 동일화 1/2 — 스크롤바·창 기본값·아이콘 색 인자
[200]SheetMe Studio Designer 와 6개 표면을 대조한 결과, 뼈대는 이미 같았다 —
WindowChrome 파라미터, 5열 골격, 색 토큰 다크·라이트 64개 값 전부, Button/TextBox/Menu/
TabItem/ComboBox/CheckBox 등 스타일이 바이트 단위로 일치한다. 실제 격차는 "껍데기 안에
무엇이 들어가는가" 셋이었다: 스크롤바, 타이틀바 구성, 좌측 패널·인스펙터 정보 밀도.
이번 커밋은 그중 전 화면에 깔려 체감이 가장 큰 스크롤바와 테마 기반을 맞춘다.

[스크롤바] [200] 통일 규격 이식 — 9px 폭(기존 11px), 상하 14px 화살표 버튼, 막대 CornerRadius 5.
막대 색은 토큰이 아니라 리터럴 중립 그레이(#8A8F96/#A6ACB3/#C4CAD0)다 — 다크·라이트 양쪽에서
같은 회색으로 보여야 배경에 따라 튀지 않는다([200] 과 동일한 의도적 선택).
MinThumbTrack.cs 를 [200] src/Ui/SheetMe.Ui 에서 복사 이식(vendoring, 헤더에 동기화 의무 명시).
기본 WPF Track 은 Thumb.MinHeight 를 무시해 항목이 많은 목록에서 막대가 실처럼 가늘어지는데,
기존 ScrollThumb 의 MinHeight 48 이 바로 그래서 먹지 않고 있었다. MinThumbTrack 이 arrange 를
직접 해 최소 길이 44px 를 보장한다.

[창 기본값] Window 암시 스타일에 FontSize 13 + Foreground B.Ink 추가, Background 를
B.Panel → B.AppBg 로. 이게 없어 대화상자 8종이 WPF 기본 12px + 시스템 검정으로 렌더돼
메인 셸(13px)과 결이 어긋나 있었다. 토큰 참조는 DynamicResource 유지 — [200] 의
StaticResource 를 그대로 베끼면 다크에서 값이 굳는다.

[아이콘 색 인자] 컨버터 ConverterParameter 를 "크기" 단독에서 "크기|브러시키" 로 확장.
구분자가 없으면 종전 동작(크기만, B.Muted)을 그대로 유지해 기존 호출부 7곳이 안전하다.
지금까지 B.Muted 가 하드코딩돼 있어 활성 문서 탭 아이콘 강조나 레이어 타입 아이콘 색 구분이
구조적으로 불가능했다. 후속 배치의 선행 조건이다.
팔레트 아이콘 매핑도 정리 — MaskedTextBox 가 TextBox 와 같은 아이콘이라 구분이 안 됐다.

[그 외] DocTabItem 비활성 탭에 하단 테두리를 넣고 활성만 끊어 크롬식 룩으로.
BarToolToggle(플로팅 바 선택/손 — 켜짐을 B.Accent2 로 은은하게) 과 MiniToggle 키 스타일 신설.
암시 ToggleButton 스타일은 인스펙터·레이어 토글도 쓰므로 손대지 않고 키로 분리했다.
B.FloatShadow 토큰 신설 — 플로팅 바 그림자가 고정값 하나라 라이트에서 흰 배경에 검은 그림자가
그대로 얹혀 있었다. 다크 20/3/0.5, 라이트 26/5/0.14 로 분리. 기존 34개 토큰 값은 손대지 않았다.

검증: 테스트 124/124, edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0.
**종이 렌더 P062 픽셀 대조 차이 0** — 암시 스타일 변경이 용지에 새지 않음을 확인.
x:Key 없는 암시 스타일 개수도 이식 전과 동일(25개)하게 유지했다.

남은 배치: C(인스펙터 접이식 섹션·세그 토글) · D(좌측 패널·타이틀바·빈 상태) ·
E(선택 오버레이) · F(캔버스 크롬 — 종이에 닿는 유일한 배치) · G(토스트·ThemedDialog).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:10:06 +09:00
MsystechandClaude Fable 5 68f0fbd5a3 마스크 편집기 — 프리셋 목록 + 실시간 미리보기
레거시 MaskedTextBox 는 System.Windows.Forms.MaskedTextBox 를 상속하고 Mask 를 재정의하지 않아
WinForms 기본 마스크 디자이너(프리셋 목록 + 시험 입력)를 그대로 물려받는다. SheetMe 인스펙터는
평범한 텍스트 상자뿐이라 마스크 문법을 외워야 했다.

[프리셋] LegacyMaskCatalog — .NET 문화권 기본값(짧은 날짜/전화번호 등) 대신 운영 활성 디자인
전수 census 에서 실제로 쓰이는 마스크를 사용 건수와 함께 제공한다. 기존 서식과 같은 표기를
그대로 고를 수 있어 표기 흔들림이 줄어든다. 날짜/시간/날짜+시간/번호 그룹 + 표준 3종.

[미리보기] 두 줄로 보여준다.
  빈 값 표시 — 마스크만으로 화면에 어떻게 보이는지(ToPromptDisplay)
  시험 입력 — 값을 쳐 보면 어떻게 채워지는지(Apply, 신규)
인스펙터 행에도 '빈 값 표시'를 상시 노출해 편집기를 열지 않아도 확인된다.

[Apply 규칙] 구현 중 모호한 경우가 드러나 규칙을 단순하게 확정했다.
  1) 리터럴은 항상 그대로 출력되고 입력을 소비하지 않는다.
  2) 지시자는 입력에서 자기에게 맞는 첫 문자를 가져오며, 맞지 않는 문자와 공백은 건너뛴다.
값만 치든(20260812) 구분자를 같이 치든(2026-08-12, 2026년 08월 12일) 결과가 같아진다.
공백을 건너뛰는 이유는 '9' 가 공백도 받아들이기 때문이다 — 그대로 두면 구분자 공백이 데이터
자리를 먹어 한 칸씩 밀린다. '9' 의 공백 허용은 빈 자리를 위한 것이고 그건 프롬프트 문자가 이미
표현한다.
한계를 코드에 명시했다: 리터럴이 숫자인 마스크(2\000년 — 운영 460건 중 1건)는 사용자가
리터럴까지 치면 어디까지가 데이터인지 원리적으로 가릴 수 없다. 편집 가능한 자리만 치면 된다.
미리보기용이므로 휴리스틱을 쌓기보다 규칙을 단순하게 두는 편이 낫다고 판단했다.

안내문에서 백슬래시를 글리프로 보여주지 않는다 — 한글 글꼴에서 원화 기호로 렌더되어 오해를 부른다.

검증: 테스트 124/124(마스크 26건 추가 — Apply 자리 채움/구분자 무시/유효성 거부/대소문자 변환자/
이스케이프/공백 처리, 프리셋 형식·중복·운영 다빈도 포함 검사). edit-smoke 실패 0,
왕복 1,271건 diff 0/예외 0. 실제 UI 로 대화상자 열어 프리셋 그룹·건수·두 줄 미리보기 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 09:38:05 +09:00
MsystechandClaude Fable 5 a749e18e7b MaskedTextBox 를 레거시와 동일하게 프롬프트 문자로 표시
4단계 육안 대조에서 발견한 차이. 마스크 패턴을 그대로 그려서 값이 이미 입력된 것처럼 보였다.

    레거시    ___년 __월 __일   (프롬프트 문자)
    SheetMe   0000년 90월 90일  (마스크 패턴 원문)

저장 데이터와는 무관한 표시 문제지만, 운영 실사용이 MaskedTextBox 475개 / Mask 보유 460개라
자주 눈에 띈다.

LegacyMask.ToPromptDisplay 신설(Core, 순수 함수) — 입력 자리 지시자는 프롬프트 문자로,
리터럴은 그대로, 이스케이프(\)는 다음 문자를 리터럴로, 대소문자 변환자(< > |)는 표시하지 않는다.
활성 디자인 전수 census 로 지원 범위를 정했다: 등장 지시자는 0(2,675) 9(687) #(12) 와
이스케이프 \(2) 뿐이고 문자류(L ? & C A a)와 변환자는 쓰이지 않는다. PromptChar 속성을 가진
컨트롤은 0개라 전부 기본값 '_' 이지만, 속성이 있으면 반영하도록 구현했다.

구분자 . , : / $ 는 WinForms 가 문화권 구분자로 치환하지만 원문 그대로 둔다 —
운영 마스크는 보이는 대로 쓰이고 레거시 화면도 그렇게 나온다.

MaskDisplay 는 표시 전용 파생 속성이라 PropBag 에 아무것도 쓰지 않는다(왕복 무손실 유지).
인스펙터 편집용 Mask 원문 속성은 그대로 둔다.

검증: 테스트 98/98(마스크 28건 추가 — 운영 실사용 마스크 10종, 이스케이프, 문자류 지시자,
변환자, PromptChar 지정, 끝 백슬래시, 표시 길이 보존). S481 재렌더로 레거시와 동일 표시 확인.
edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 09:20:30 +09:00
MsystechandClaude Fable 5 d1e488625e 육안 검증 준비 — 전 페이지 렌더 + 제자리 갱신 E_SctMst 어서션 정정
[전 페이지 렌더] --db-render 가 첫 페이지만 뽑던 것을 전 페이지로 확장했다(이름_p1.png …).
운영 서식 128건이 다중 페이지라 첫 장만 봐서는 검증이 되지 않는다.

[E_SctMst 어서션 정정] --db-save-smoke 가 제자리 갱신 경로에서도 sctCount == walkerCount 를
요구하고 있었는데, 이 불변식은 버저닝 경로에만 성립한다.
제자리 갱신은 레거시와 동일하게 삭제된 컨트롤의 행을 지우지 않으므로(SctKey 안정성) 과거에
지워진 컨트롤의 고아 행이 누적된다. P347 에서 실제로 걸렸다 — E_SctMst 3행(MDesignerHost1,
TextBox1, TextBox2) 대 현재 XML 2개(MDesignerHost1, TextBox2)로, TextBox1 은 과거에 지워진
컨트롤의 잔존 행이었다. 기존 데이터이지 저장 경로의 결함이 아니다.
제자리 갱신은 "워커 산출 행이 전부 존재"(sctCount >= walkerCount)로 바꾸고 잔존 행 수를
리포트에 표시한다. 버저닝은 전량 재생성이므로 정확히 일치 요구를 유지한다.

[검증 준비 결과] 위험도 상위 16건을 SheetMe 로 재저장하고 저장 전/후 렌더를 픽셀 비교했다 —
39페이지 전부 동일(J209 6장, S256 7장 포함). 대상 선정은 운영 활성 디자인을 전수 스캔해
위험 요소로 가중치를 매겼다: 자리표시 15건 · Spread 7건 · Binary 569건(45%) ·
배경이미지 315건 · 다중페이지 128건 · 제자리갱신 36건.

레거시 뷰어 대조는 사용자가 직접 수행한다(공용 ServerInfo ini 의 CurrentServer 를 운영에서
테스트로 바꿔야 하는데, 그 파일을 레거시 EXE 187개가 공유하므로 자동화하지 않았다).
바탕화면 'SheetMe-검증패키지' 에 체크리스트·렌더 PNG 39장·자동검증 리포트를 준비했다.

검증: 테스트 70/70, edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0,
db-save-smoke 제자리 갱신(P163 정상/P347 잔존 1건 정상)·버저닝(S999) 양쪽 통과.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 08:57:26 +09:00