# B07 사용자 편의성 전면 정리 — 교차검증 보류 원문 - 상태: 구현·사용자 실측 완료 기록, Codex 교차검증 실패로 완료 아카이빙 보류 - 상시 계획 후속: `docs/raw/PLAN.md`의 「B07 사용자 편의성 전면 정리 — 교차검증 보류」 - 위키: `docs/wiki/pages/B08_DesignDetail/B08_CAD_usability_2026-09-01.md` - 실패 근거: `docs/raw/verification/2026-09-01_verify_remaining_checked_items.md` ### B07 사용자 편의성 전면 정리 (2026-09-01 신규, 보조폴더) **배경**: 사용자 지시로 B07 상세설계를 사용자 편의성 관점에서 전수 점검했다. 공용 브라우저(5174, 프로젝트 `wdw` `c1bb453f`)로 실제 조작해 15건을 짚었고, 사용자가 항목별로 반영 여부를 확정했다(2026-09-01). 아래는 **반영 확정분 12건**이다. **반영하지 않는 것**(사용자 확정): ⑤ 진행단계 패널 겹침 — 현행 유지. ⑭ 목록 조회가 오래된 횡단 파일을 지우는 것 — 현행 유지. ⑮ 명령행이 하단 86px 차지 — 현행 유지. #### ① 편집 손실 방지 — 도면을 바꿔도 작업이 사라지지 않게 **실측 재현**(2026-09-01, `19_dataloss.py`): 종단면도에 선 하나를 긋고(캔버스 서명 `398928318` → `2270709582`) 표지로 옮겼다가 돌아오니 서명이 **원본과 완전히 동일** (`398928318`). 그은 선이 사라졌고 확인창도 뜨지 않았다(`DIALOGS_ON_SWITCH=[]`). `UI_Page.ts:454 loadDrawing`에 미저장 검사가 없고, 브리지가 받는 즉시 `setEntities(…,false)`로 덮은 뒤 `resetUndoBaseline()`으로 되돌리기 기록까지 지운다. > 교차검증 상태(2026-09-01): 구현 기록은 보존하되 CAD 전체 시험 86건 중 6건 실패로 완료 아카이빙을 보류한다. 아래 항목은 재검증 대기 상태다. - [X] CAD에 **미저장 여부**를 뒀다 (2026-09-01 완료). `state.ts`의 `drawingDirty`를 `setEntities(…,true)`·`undo`·`redo`에서 세우고, `resetUndoBaseline()`(도면 적재)과 저장 응답에서 내린다. 브리지가 `drawing-changed`에 `dirty`를 실어 부모가 받는다. - 단위 시험 `src/state.test.ts` 2건: 편집→섬/적재→내려감, 되돌리기도 미저장. - [X] 도면을 바꾸기 전에 **묻는다** (2026-09-01 완료). `mayDiscardEdits()`를 `selectDrawing`·`navigateDrawing` 앞에 뒀다. 겹쳐 누르면 늦게 온 응답이 화면을 덮던 것도 `loadInFlight` 가드로 막았다. - 자체검증(`42_verify_guard.py`, 종단면도): 선 하나 그어 `n=487→488 dirty=true`. 표지를 누르니 **경고가 떴고**(문구 확인), [취소]에 `n=488` 그대로. 다시 눌러 [확인]에 표지로 이동(`n=14`, `dirty=false`). 편집이 없는 상태로 토적도를 누르니 **묻지 않았다**. 고치기 전에는 경고 없이 `n`이 원본으로 돌아갔다. #### ② 배경 도면층은 마우스가 가도 반응하지 않게 (등고선·도각) **사용자 지시**: 배수유역도의 수치지형 등고선을 도각처럼 배경으로 다룬다. 단, **마우스가 근처로 가도 하이라이트하지 않는다 — 도각도 포함**. 등고선(`b08-basin-contour`)·계류·도각은 이미 `locked=True`라 선택·스냅에서 빠진다 (`Engine_Cad_Basin.py:411-417`). 남은 구멍은 하이라이트다 — `input-controller.ts:272`는 `getPickableEntities()`(잠금 제외)를 쓰지만 `main.tsx:97`은 `queryEntitiesNearPoint()` 결과를 **거르지 않고** 그대로 넣는다. - [X] 근접 하이라이트에서 잠금 도면층 객체를 뺐다 (2026-09-01 완료). 거르는 자리는 `state.ts:setHighlightedEntityIds()` 한 곳 — `main.tsx`·`input-controller.ts`가 모두 이 함수를 지나므로 호출부가 늘어도 새지 않는다. - 자체검증(`33_verify_rest.py`): 도각만 있는 화면(표준도)에서 마우스를 가로로 **40지점 훑어 하이라이트 0회**. - 단위 시험 `src/state.test.ts` 2건: 잠금+자유 섞이면 자유만, 잠금뿐이면 빈 목록. #### ③ 준비 중 도면도 도각은 보이게 **사용자 지시**: 아직 만들지 않은 도면(계획평면도 4종·표준 횡단면도·표준도·용지도)도 누르면 **빈 도각이라도** 나오게 한다. 지금은 13칸 중 7칸이 눌리지 않는 회색 버튼이다 (`17_overlap.py`: `disabled:7 / total:13`). - [X] 백엔드에 **빈 도각 도면**을 열었다 (2026-09-01 완료). `Router_Support.BLANK_DRAWINGS` 7종을 `kind="blank"`로 목록에 싣고, 단건 조회는 `build_blank_drawing()`이 도각(잠금)과 자유 도면층 `작도` 한 장을 낸다. 도면명은 도각 표제란에 채운다. - [X] 프런트 `DRAWING_GROUPS`의 `kind` 없는 항목을 `blankId`로 물렸다. `disabled`를 없애고 "준비 중"은 옅은 표시(`data-pending`)와 툴팁으로만 남겼다. - [X] 준비 중 도면은 **확정 대상이 아니다** — `applyConfirmButtonState()`가 `blank`이면 확정 버튼을 비활성으로 둔다. `all_confirmed`(횡단만)는 손대지 않았다. - 자체검증(`30_verify_reload.py`·`33_verify_rest.py`): 목록 13칸 중 **disabled 0** (고치기 전 7). 표준도를 누르니 도면층 `작도(자유)·도각(잠금)`이 실렸고 확정 버튼은 비활성이었다. - 시험 `tmp/tests/test_b07_usability_2026_09_01.py` 2건: 7종 라벨·id 중복 없음, 빈 도면에 도각(잠금)과 자유층이 함께 있음. #### ④ 도면층 목록 버튼이 잘리지 않게 **실측**(`17_overlap.py`): 도면층 행 5개 모두 내용 폭이 보이는 폭을 넘는다 (`scrollW 237~286 / clientW 229`). `aside`도 `scrollW 267 / clientW 247`, `overflow-x: visible` — 잘린 채 넘쳐 삭제 아이콘이 반만 보인다. - [X] 이름은 줄이고 아이콘은 줄지 않게 잡았다 (2026-09-01 완료). 이름에 `min-width:0`+`text-overflow:ellipsis`, 아이콘 묶음에 `flex-shrink:0`, 행 안 아이콘 버튼 좌우 여백을 8px→2px로 줄였다. - [X] **패널을 248px→300px로 넓혔다**. 이름만 줄이니 이름 몫이 21px밖에 남지 않아 글자가 뭉개졌다(아이콘 6개가 229px 중 176px을 먹는다). 넓히려면 `Toolbar.tsx`의 인라인 지정도 걷어야 했다 — 거기 `248px`이 박혀 있어 CSS의 `--cad-panel-width`를 덮고 있었다. 이제 접힘만 `0px`로 두고 편 폭은 CSS가 정한다. - 자체검증(`32_verify_core.py`·`34_verify_readonly.py`): **잘린 요소 0개** (고치기 전 5개), 이름 폭 `21px→81px`, 패널 폭 `248→300`. #### ⑥ 되돌리기로는 확정이 풀리지 않게 `state.ts:528-542`의 `undo`/`redo`가 **무조건** `DRAWING_CHANGED`를 쏜다. 한 번 고쳤다가 `Ctrl+Z`로 원상복구해도 확정이 풀리고 서버로 해제 요청까지 나간다. - [X] 되돌리기·다시하기는 **확정을 풀지 않는다** (2026-09-01 완료). ⑧에서 부모가 `drawing-changed`로 확정을 푸는 경로 자체를 없애 해결했다 — 그 통지는 이제 **미저장 표시만** 세운다. `undo`/`redo`는 통지를 그대로 두되(미저장은 맞다) 확정 해제와는 무관해졌다. - 확정 도면은 애초에 되돌리기 명령이 막힌다(⑧ 읽기 전용, `UNDO`는 보기 명령이 아님). - 자체검증(`35_verify_final.py`): 그림 `487→488` 뒤 `Ctrl+Z`로 `→487`, 확정 해제 요청 없음(`36_diagnose.py`에서 네트워크 호출 0건). #### ⑦ 색상 선택기가 보이지 않는 문제 + 확정 해제 연발 **실측**(`21_colorwidget.py`): 색상 입력은 화면 안에 정상 배치돼 있다 (`x:880 w:44 h:19 inView:true`). 안 보이는 이유는 **값이 `#ffffff`**라 흰 종이 배경 위 흰 스와치이기 때문이다. `state.ts:130` `activeLineColor='#ffffff'`, `:148` `textColor:'#ffffff'` — 검은 화면 CAD의 기본값이 흰 종이에 그대로 남아 있다. **그 상태로 그으면 선도 안 보인다.** - [X] 기본 그리기 색을 검정(`#000000`)으로 바꿨다 — 선·글자 둘 다 (2026-09-01 완료). - [X] 색상 스와치 테두리를 진하게(`--cad-text-muted`) 줘 흰색을 골라도 자리가 보인다. - [X] 색 고르는 동안 연발하지 않는다. `paintNowCommitLater()`가 드래그 중에는 화면만 칠하고(`setEntities(…,false)`), 멎고 350ms 뒤 **한 번만** 되돌리기·통지에 싣는다. - 자체검증(`30_verify_reload.py`): 색상 입력 `value=#000000`, 테두리 `rgb(139,134,160)`(전에는 배경과 같은 흰색이라 안 보였다). #### ⑧ 확정한 도면은 잠그고, 풀려면 [수정]을 누르게 **사용자 지시**: 확정하면 수정이 안 되게 한다. 수정하려면 **확정 버튼 자리에 [수정] 버튼**을 둬 명시적으로 푼다. 지금은 확정된 도면도 그대로 고쳐지고, 그 편집이 확정을 조용히 해제한다 — ⑥·⑦·⑩의 오작동이 모두 이 통로로 들어온다. - [X] CAD에 **읽기 전용 모드**를 만들었다 (2026-09-01 완료). `isDrawingReadOnly()`가 참이면 `runCommand()`가 **보는 명령만** 통과시킨다 — 리본·명령행·단축키가 모두 이 함수를 지나므로 한 곳에서 막는다. 허용 목록(`isViewOnlyCommand`)은 **허용을 명시하는 쪽**으로 짰다: 보기·조회 전체 + 도면층 보이기/잠금 + 내보내기·복사. 새 편집 명령이 늘어도 자동으로 막힌다. 더블클릭 값 편집도 함께 막았다. - [X] **도면을 실을 때 앞 도면의 그리기 도구를 내린다**(`runCommandInput('SELECT')`). 이게 없으면 읽기 전용이 새 명령만 막아서, **켜 둔 채 확정한 도구는 확정본 위에도 계속 그렸다** — 실측으로 확정본에서 클릭 두 번에 선 2개가 늘었다. - [X] 확정 버튼 자리를 상태에 따라 바꿨다 — 미확정 [현재 도면 확정] / 확정 [수정]. [수정]은 `invalidate` 뒤 원본 도면을 다시 실어 편집을 연다. - [X] 확정 직후 **저장본 재적재를 기다린다**(`await loadDrawing`). 기다리지 않으면 오버레이가 먼저 걷혀 버튼은 [수정]인데 CAD는 아직 편집이 열린 순간이 생겼다. - [X] 도면 변경 통지로 확정이 풀리던 경로를 **없앴다** — `CAD_CHANGED_MESSAGE`는 이제 `cadDirty`만 세운다. 확정 해제는 [수정] 한 곳뿐이다. - 자체검증(`40_verify_final2.py`, 종단면도 487개): 그림 `487→488` → 확정하니 `btn=수정 readOnly=true dirty=false n=488` → **확정본에서 그리기 시도에 `488→488` (막힘)** → [수정]을 누르니 `btn=현재 도면 확정 readOnly=false` → 다시 그리니 `487→488`. `37_diagnose2.py`로 명령 입력·클릭·Escape 전 단계에서 읽기 전용이 유지됨을 확인(호출 0건). - 단위 시험 `src/state.test.ts` 1건: `confirmed` 참/거짓/`null`에 따른 읽기 전용. #### ⑨ 수량표는 B07에서 고치지 않는다 **사용자 확정**: 수량표는 앞 단계(B05·B06) 결과물이다. 값을 바꾸려면 앞 페이지에서 고치고 다시 와야 한다. 지금은 도면 위 숫자를 더블클릭해 고칠 수 있는데 **그림 글자만 바뀌고 저장되는 수량표 값은 그대로**여서 화면과 저장값이 어긋난다 (`aislo-drawing-bridge.ts:91-94`가 `setDesignQuantityTable`을 부르지 않는다). - [X] 더블클릭 편집에서 **수량 산출표 도면층**(`b08-cross-table`)을 뺐다 (2026-09-01 완료). 누르면 "수량표는 횡단설계(B06)에서 고칩니다"로 안내한다. 확정 도면이면 그보다 먼저 "[수정]을 누르세요"로 막힌다(⑧). - [X] 쓰이지 않던 `setDesignQuantityTable`·`notifyDrawingChangedByTable`을 지웠다 — 둘 다 호출부가 하나도 없으면서 "고칠 수 있는 값"으로 오해를 남겼다. #### ⑩ 자동 백업을 도면별로 나눈다 백업 칸이 `OPEN_WEB_CAD__RECOVERY` 하나뿐이고 어느 도면 것인지 기록이 없다 (`autosave.ts:34-47`). 도면을 넘겨보면 **마지막에 본 것만** 남고, 그 백업을 되살리면 지금 열린 다른 도면 위에 붙는다. - [X] 백업 키에 **도면 id**를 붙였다(`…__RECOVERY__{drawingId}`) (2026-09-01 완료). `DesignMeta`에 `drawingId`를 추가하고 브리지가 적재마다 `setRecoveryScope()`로 옮긴다. - [X] 되살리기는 그 도면 칸만 읽는다 — 다른 도면 것이 붙지 않는다. 확정 도면에서는 되살리기 자체를 막았다(⑧과 같은 이유). - [X] 서버 도면을 싣는 동안에는 백업하지 않는다(`suspended`) — 적재가 깨우는 `UPDATE_STATE` 때문에 5초 뒤 서버 도면이 사용자 백업을 덮고 있었다. - 자체검증(`43_restore.py`): 도면 넷을 열어 본 뒤 `localStorage` 키가 `…__RECOVERY__longitudinal` · `__cover` · `__mass_haul` · `__blank_standard`로 **도면마다 따로** 생겼다. 고치기 전에는 칸이 하나뿐이었다. #### ⑪ 횡단 장 번호가 구간을 따라가게 장 id를 순서대로 붙인다(`Engine_Cad_Sheet.py:149` `cross_s{번호}`). B06 설계가 바뀌어 한 장에 담기는 측점 수가 달라지면 **"1장"이라는 이름은 그대로인데 담는 구간이 달라지고** 확정 표시도 그대로 남는다. - [X] 장 id를 **시작 측점 기준**으로 바꿨다(`cross_s00020m` 꼴) (2026-09-01 완료). 구간이 달라지면 id도 달라지므로 옛 확정이 새 구간에 붙지 않는다. - [X] 목록에 없는 id의 옛 확정 기록은 매니페스트에서 **무시**된다 — 확정 여부를 `manifest_drawings.get(sheet["id"])`로만 보므로 id가 달라지면 자연히 안 붙는다. 기록은 지우지 않는다. - [X] 라벨의 "N장"은 그대로 뒀다 — 사용자가 부르는 이름은 바뀌지 않는다. `CROSS_SHEET_ID`는 옛 순번형(`cross_s01`)도 계속 읽는다(이미 확정한 매니페스트 호환). - 시험 `tmp/tests/test_b07_usability_2026_09_01.py` 2건: 장 나누기가 달라져도 같은 시작 측점이면 같은 id(`cross_s00000m`), 순번만 바뀐다. 패턴이 신형·옛형은 받고 측점 도면(`cross_00020m`)은 받지 않는다. - 화면 확인: 사이드 목록이 `1장 (0~240m)` · `2장 (260~350m)` 그대로 보인다. #### ⑫ 회사 도각을 잘못 저장해도 페이지가 죽지 않게 저장할 때 `entities`가 목록인지만 본다(`Router.py:415-417`). 좌표가 빠진 도형 하나면 이후 **모든 도면 요청이 500**이 되고 되돌릴 버튼이 없다. - [X] 저장 전에 도각 엔티티를 **검사**한다 (2026-09-01 완료). `validate_template_entities()`가 도면 조립이 실제로 쓰는 좌표(`startPoint`·`endPoint`· `basePoint`·`point`·`center`의 `x`·`y`와 `radius`)가 숫자인지 본다. 자식(`children`)까지 내려간다. 어긋나면 파일을 쓰지 않고 400으로 거절한다. - [X] **기본 도각으로 되돌리기**를 열었다. `DELETE /projects/{id}/frame-template`와 도각 편집 띠의 [기본 도각으로] 버튼(확인 후 실행). - 시험 `tmp/tests/test_b07_usability_2026_09_01.py` 7건: 정상값 3종 통과, 깨진 값 5종(좌표 없음·문자열·반지름 문자열·자식 좌표 `None`·도형 아님) 거절, 되돌리기가 파일을 지우고 두 번째 호출은 `False`. #### ⑬ 횡단 파일이 없어도 나머지 목록은 나오게 `_cross_files`가 `cross_sections/` 없으면 예외를 던지고(`Router_Support.py:69`) `_drawing_list` 전체가 무너진다 — 표지·종단면도·토적도·유역도까지 함께 사라진다. - [X] 횡단 장 계획이 실패해도 **나머지 도면은 목록에 남긴다** (2026-09-01 완료). `_drawing_list`가 `_cross_sheet_plan` 실패를 잡아 그 그룹만 비우고 경고를 남긴다. - 사유를 응답에 싣는 것은 하지 않았다 — 횡단 그룹이 비면 사이드 패널이 이미 「횡단면도」 자리를 비워 보여 준다. 응답 스키마를 늘릴 만한 이득이 없다고 판단했다. - 시험 `tmp/tests/test_b07_usability_2026_09_01.py` 1건: 횡단 계획을 실패시키면 `cross`만 빠지고 `longitudinal`·`cover`·`mass_haul`·`watershed`·`blank`은 남는다. **자체검증 종합** (2026-09-01, 보조 워크트리 `D:\aislo-wt\sub`, 5174/8001): | 갈래 | 결과 | |---|---| | 프런트 타입검사 `npm run typecheck` | 통과 | | CAD 타입검사 `npm run check-types` | 통과 | | CAD 단위시험 `vitest src/state.test.ts` | 8건 통과 (신규 6건) | | CAD 전체 `vitest` | 81건 중 75건 통과. **실패 6건은 변경 전부터 있던 것** — 노드 환경에 `window`가 없어 나는 것으로, 2026-09-01 앞 판 기록과 같은 6건이다 | | 파이썬 `pytest tmp/tests/` | 12건 통과 (신규 12건) | | 포맷 | `ruff format` · `prettier`(변경 파일만) | | 화면 | 공용 브라우저 실조작 — `30`·`32`·`33`·`35`~`42` 명령 파일 | **검증 중 드러나 함께 고친 것** (계획에 없던 것): - 확정 직후 재적재를 기다리지 않아 **버튼은 [수정]인데 CAD는 편집이 열린** 순간이 있었다 → `await loadDrawing`. - 읽기 전용이 새 명령만 막아 **켜 둔 도구는 확정본에도 계속 그렸다** → 도면 적재 때 `SELECT`로 도구를 내린다. - 도면 전환을 겹쳐 누르면 늦게 온 응답이 화면을 덮었다 → `loadInFlight` 가드. - 도면층 삭제·객체 이동이 되돌리기에 안 실려 `Ctrl+Z`로 못 돌아왔다 → `LayerManager`의 두 곳에 `setEntities(…, true)`. - 불러오기 실패가 화면에 아무 표시도 없었다(②의 짝) → `data-loading`·`data-error`를 받는 CSS와 토스트를 달았다. 유역도처럼 2~3초 걸리는 도면에서 "도면을 불러오는 중…"이 뜬다. **화면 상태 복구**: 검증 중 바꾼 프로젝트(`wdw`)·테마(다크)·자동백업 칸을 사용자가 쓰던 값으로 되돌렸다 — 프로젝트 `e418fcfc`, 테마 `light`, 백업 칸 정리(`44_restore2.py`). ---