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>
직전 커밋(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>
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>
패널로 감싸 놓고 정작 자식을 클릭하면 자식만 잡혀서 그 하나만 끌려갔다. 그룹처럼 안 움직인다.
이제 컨트롤을 클릭하면 최상위 컨테이너까지 거슬러 올라가 그것을 고른다.
패널이 잡히므로 끌면 자식들이 부모를 따라 함께 움직이고, 자식의 상대좌표는 그대로 남는다.
마퀴로 자식 하나가 걸려도 같은 규칙이라 선택 단위가 한 가지로 유지된다.
안으로 들어가려면 더블클릭한다. 한 번 들어가면 그 자식이 선택된 상태이므로
다시 더블클릭하면 예전처럼 글자 편집이다. 최상위 컨트롤은 들어갈 곳이 없으니
첫 더블클릭이 바로 글자 편집으로 간다(기존 동작 그대로).
이 규칙은 그룹용 패널뿐 아니라 사용자가 직접 만든 패널에도 똑같이 걸린다.
둘을 구분할 방법이 없기도 하고, 구분하면 같은 모양이 다르게 동작해 더 헷갈린다.
디자인 도구의 관례(프레임을 클릭하면 프레임, 들어가려면 더블클릭)와도 맞는다.
--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>
지금까지 그룹은 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>
검색칩이 항상 한 줄을 먹고 있었다. 컨트롤이 몇 개뿐인 서식이 대부분이라 그 줄은 대개 빈 상자다.
돋보기·필터·필터해제를 '레이어' 글씨 오른쪽으로 올리고, 검색바는 돋보기를 눌러야 나오게 했다.
돋보기를 끌 때 검색어를 지운다. 상자가 접힌 채 필터만 살아 있으면 목록이 통째로 비어 보이는데
원인을 찾을 단서가 화면에 하나도 없다 — 열어 봐야 비로소 "아, 검색어가 남아 있었구나"가 된다.
켤 때는 한 틱 뒤에 포커스를 준다. 상자가 실제로 보이게 된 다음이어야 캐럿이 들어간다
(그룹 이름 편집 상자가 같은 이유로 같은 방식을 쓴다).
필터 버튼 둘은 위치만 옮겼다 — 활성 표시(파란 깔때기)와 해제 버튼을 흐리게 두는 규칙은 그대로다.
게이트: 테스트 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>
레이어 탭 하나에 페이지 마디와 컨트롤이 한 시퀀스로 섞여 있었다. 페이지를 추가·복제·삭제하는
조작이 컨트롤을 눈으로 찾는 일과 같은 목록에서 경쟁했다. 위는 페이지, 아래는 레이어로 나눈다.
페이지 행을 누르면 그 페이지를 활성화한다 — 캔버스가 거기로 스크롤하고 인스펙터가 용지 속성을 연다.
경로는 이미 있던 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>
라벨을 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>
우측 속성 패널을 [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>
"전체 선택 후 오브젝트끼리 정렬 / 페이지에서 정렬" — 둘 다 필요한데 하나만 됐다.
직전 커밋의 규칙은 자동이었다: 여럿이면 무조건 선택 영역, 하나면 무조건 부모/페이지.
여럿을 골라 놓고 종이에 맞추고 싶을 때 방법이 없었다.
기준을 명시적으로 고르게 했다. 인스펙터 정렬 행 아래에 알약 둘 — 왼쪽은 선택에 따라
문구가 바뀐다(여럿이면 '선택 영역', 컨테이너 안의 하나면 '부모'), 오른쪽은 '페이지'.
최상위 컨트롤 하나만 골랐을 때는 이 줄을 아예 접는다 — 그 경우 '선택 영역'도 곧 페이지라
알약 둘이 똑같은 말을 하게 된다(실제로 그렇게 렌더돼서 접기로 했다).
컨테이너 안의 것을 페이지 기준으로 맞추면 부모 밖으로 나간다. 막지는 않는다 —
사용자가 고른 것이고 되돌릴 수 있다 — 대신 '페이지' 알약 툴팁이 그 사실을 말한다.
간격 균등 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>
"폼 기준으로 정렬"이 없었다. 정확히는 정렬 자체가 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>
정렬 스냅 자체는 있었다. 다만 축마다 선 하나만, 그것도 화면 끝에서 끝까지 긋는 무한 점선이라
무엇에 맞춘 것인지가 안 보였다. 판단을 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>
메인 창은 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>
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>
목업 사양을 글로 옮긴 뒤, 라이트·다크 렌더 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>
■ 치수 원복
앞서 목업과 나란히 놓고 전체를 키웠는데(a4acfe4), 요청은 크기가 아니라 스타일이었다.
창 1440×960 → 1180×760 을 비롯해 글자·여백·행 높이를 모두 되돌린다.
직전 커밋의 스타일 수정(줄번호 홈통 회색 면 · 구분선 직선화 · 결과 표 가로선)은 유지한다.
■ 툴바 '포맷' 강조
목업에서 '포맷'만 파란 글자 + 밑줄로 그려져 있다. 셋 다 동작이라 '선택된 탭'은 아니지만
문서를 통째로 바꾸는 주 동작이라 그 자리를 늘 표시해 둔다(ToolBtnPrimary).
회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업·정렬·결과표·단축키 점검 16/16.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
크기가 아니라 스타일이 달랐던 부분을 고쳤다.
· 줄번호 홈통에 회색 면(B.Chip)을 깔았다. 본문과 같은 흰 면이라 홈통이 있는지 안 보였다.
· 편집 상자의 모서리 라운드를 없앴다. 둥글리면 클리핑 때문에 홈통의 회색 면과
본문 사이 구분선까지 함께 휘어 보인다 — 지목된 그 선이다. 구분선도 B.Line2 로 또렷하게.
· 결과 표: 바깥 테두리를 표가 아니라 감싸는 상자가 그리게 하고, 격자를 가로선만 남겼다.
세로선까지 그으면 컬럼이 52개인 조회에서 화면이 격자로 가득 찬다.
머리글은 회색 면 + 아래·오른쪽 경계만, 행 높이 38, 셀 여백 12.
회귀: 편집 스모크 실패 0, 팝업·정렬·결과표·단축키 점검 16/16, 종이 렌더 P062 바이트 동일.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
■ 목업에 맞춘 것
· 툴바(포맷·주석·자동완성)를 탭 모양으로 — 밑줄이 들어오는 글자 버튼.
· '전체 지우기'를 빨강에서 강조색 링크로. 되돌릴 수 있는 동작이라 위험색을 쓸 이유가 없다
(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>
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>
화면에서 지목된 세 덩어리를 뺐다.
· 제목 앞 아이콘 배지 — 창 제목이 이미 같은 말을 한다.
· 제목 아래 안내문("작성 시점에 <<변수>> 가 …") — 우측 목록과 자동완성이 그 자리를 대신하고,
두 줄이 편집 영역을 그만큼 밀어 올리고 있었다.
· 변수 목록 아래 안내 상자(ⓘ 118개 · + 를 누르거나 …) — 개수는 칩에 이미 있고
삽입 방법은 + 버튼 자체가 보여 준다.
SQL 이 아닌 용도(상용구 편집)에서는 안내문이 제목 역할을 하고 있었으므로,
그 경우 제목 자리에 대상 이름을 넣고 코드 칩은 감춘다.
회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업·정렬·결과표 점검 12/12.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
결과를 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>
■ 한글을 치면 색칠이 글자와 어긋났다 (가장 중요)
색칠 레이어가 글자 위치를 <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>
목업대로 다시 짰다. 색·모서리·간격은 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>
두 가지 신고를 고쳤다: 위치가 안 맞고, 항목을 고르기 전까지 사라지지 않는다.
■ 사라지지 않던 문제
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>
■ 팝업이 화면 구석으로 날아가던 문제
두 가지가 겹쳐 있었다.
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>
색칠만으로는 편집기라 하기 어렵다. 실제로 쿼리를 칠 때 손이 가는 곳을 메웠다.
■ 자동완성 (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>
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>
배포본에 서드파티 편집기 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>
칩으로 자주 쓰는 것을 앞에 깔았지만, 피커 자체는 여전히 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>
태그를 하나 걸려면 매번 모달을 열어 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>
■ 중복 열기로 편집분이 통째로 사라지던 문제
중복 차단이 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>
이 시스템의 핵심 자동 채움 경로다. 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>
레거시로는 되는데 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>
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>
레거시 대비 전수조사에서 나온 출시 차단 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>
Command 가 받는 대리자는 Action<object> 인데 람다를 object? 로 선언해 CS8622 가 났다.
파라미터는 항상 견본이 오지만 바인딩이 어긋날 때를 대비한 형 검사는 그대로 둔다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
색을 바꾸려면 매번 대화상자를 열어야 했다. 실측해 보니 그럴 이유가 없다 —
--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>
페이지 루트에 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>
레거시에서 페이지는 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>
다크 테마에서 흰 시스템 창이 튀어나오던 알림을 자체 창(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>
컨트롤 행의 눈·자물쇠는 호버 규칙으로 바꿔 놓고 페이지 행 버튼만 상시 노출이라
목록 우측이 들쭉날쭉했다. 같은 규칙으로 맞춘다.
버튼 각각이 아니라 묶음(StackPanel)에 거는 이유가 있다 — 삭제 버튼은 CanDelete 에
Visibility 를 물려 있는데 인스턴스의 지역 값이 스타일 트리거를 이기므로, 버튼마다 걸면
삭제만 상시 노출된다. 바깥을 접으면 둘 다 숨고, 펴지면 삭제는 원래대로 CanDelete 를 따른다.
되돌리기가 번거로운 '페이지 삭제'가 항상 노출돼 있지 않게 되는 부수 효과도 있다.
회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0,
종이 렌더 P062 바이트 동일.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
이름이 잘려 행을 구분할 수 없었다
눈·자물쇠가 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>
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>
[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>
[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>
레이어 목록이 Text 만 보여줘서 chkOrientation1~5 처럼 문구가 같은
컨트롤들이 전부 같은 줄로 보였다. 두 줄로 나눠 아래에 Id 를 병기한다
(Text 가 없어 LayerName 이 곧 Id 인 경우는 중복이라 숨긴다).
그리고 컨트롤이 수백 개인 서식에서는 목록에서 이름을 찾아도 종이
어디에 있는지 알 수 없었다 — 더블클릭하면 해당 페이지로 전환 + 선택 +
캔버스 스크롤. 이미 전부 보이면 스크롤하지 않는다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
사용자 지적: "피그마같은 도구처럼 보기 편하게, 특히 속성 사용하기 불편해".
불편의 핵심은 밀도가 아니라 **조작**이었다. 모든 칸이 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>
사용자 요청으로 [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>
사용자 지적: "속성패널 디자인 보기 불편한데? 고도화해줘".
[실측한 낭비] 단일 선택 시 '공통+글꼴'만으로 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>