Commit Graph
177 Commits
Author SHA1 Message Date
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