From fbe0f982d3f307c04e639757c0f95b58e66af3f7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 13 Oct 2026 17:17:12 +0900 Subject: [PATCH] =?UTF-8?q?feat(device):=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=92=A4=20=EC=AA=BD=EC=9D=80=20=EB=8D=B0=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=ED=83=91=EB=8F=84=20=EC=9C=84=20=EC=A4=84=20=EB=8C=80=EC=8B=A0?= =?UTF-8?q?=20=E2=98=B0=20=C2=B7=20=E2=98=B0=20=EC=99=BC=EC=AA=BD=20?= =?UTF-8?q?=EC=9C=84=2036px(=EC=86=90=EA=B0=80=EB=9D=BD=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=2044)=20=C2=B7=20=EC=84=9C=EB=9E=8D=20=EC=99=BC?= =?UTF-8?q?=EC=AA=BD=EC=97=90=EC=84=9C=20=C2=B7=20=EC=A7=84=ED=96=89=20?= =?UTF-8?q?=ED=8C=90=20=EB=8C=80=EC=8B=A0=20=EC=84=9C=EB=9E=8D=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=20=EB=8B=A8=EA=B3=84=20=C2=B7=20=EC=B2=AB=20=EC=A4=84?= =?UTF-8?q?=20=EB=93=A4=EC=97=AC=EC=93=B0=EA=B8=B0=20=3D=20--app-burger-in?= =?UTF-8?q?set=20=ED=95=98=EB=82=98=20=C2=B7=20=EA=B3=B5=EC=9A=A9(?= =?UTF-8?q?=EC=A2=8C=EC=B8=A1=20=ED=8C=A8=EB=84=90=20=EC=A0=9C=EB=AA=A9=20?= =?UTF-8?q?=C2=B7=20=EC=9D=BC=EB=B0=98=20=EC=AA=BD=20=EC=A0=9C=EB=AA=A9=20?= =?UTF-8?q?=C2=B7=20--wf-burger-pad)=20+=20=EC=AA=BD=EB=A7=88=EB=8B=A4=20?= =?UTF-8?q?=ED=95=9C=20=EC=A4=84=20=C2=B7=20CAD=20=EC=A0=9C=EB=AA=A9=20?= =?UTF-8?q?=EC=A4=84=EC=9D=80=20=ED=8B=80=20=EC=9E=90=EB=A6=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9E=BC=20=C2=B7=20Esc=20=EB=8A=94=20=E2=98=B0=20=ED=8F=AC?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=EC=97=90=EC=84=9C=EB=8F=84=20=EB=8B=AB?= =?UTF-8?q?=ED=9E=98=20(=EA=B8=B0=EA=B8=B0=EB=8C=80=EC=9D=91=207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD --- A00_Common/app_shell.ts | 40 +++++--- A00_Common/device_context.css | 8 +- A00_Common/device_context.ts | 2 +- B05_Profile/B05_Profile_UI_Style.css | 5 + B06_Section/B06_Section_UI_Style_Cross.css | 5 + .../B07_DesignDetail_UI_Style.css | 5 + .../src/integration/host-burger.css | 4 + .../openwebcad/src/integration/host-burger.ts | 19 ++++ B07_DesignDetail/openwebcad/src/main.tsx | 2 + B08_Quantity/B08_Quantity_UI_Page.css | 5 + B09_Estimation/B09_Estimation_UI_Page.css | 5 + M01_MasterData/M01_MasterData_UI_Style.css | 5 + .../M02_MasterTemplete_UI_Style.css | 5 + resources/tester/test_device_audit.py | 97 +++++++++++++++++-- resources/tester/test_device_drawer.py | 68 +++++++++---- ui_template/ui_template_general_layout.css | 6 ++ ui_template/ui_template_overlay.css | 15 ++- ui_template/ui_template_workflow_layout.css | 17 ++++ 18 files changed, 271 insertions(+), 42 deletions(-) create mode 100644 B07_DesignDetail/openwebcad/src/integration/host-burger.css create mode 100644 B07_DesignDetail/openwebcad/src/integration/host-burger.ts diff --git a/A00_Common/app_shell.ts b/A00_Common/app_shell.ts index f187e7319..13aec256f 100644 --- a/A00_Common/app_shell.ts +++ b/A00_Common/app_shell.ts @@ -143,8 +143,8 @@ function buildNavButtons(): HTMLElement[] { } /* ----------------------------------------------------------------------------- - * ☰ 서랍(PLAN_기기대응 2-2) — 폭 900 이하(위 메뉴가 숨는 폭) · 터치 모드에서 보임. - * 위 메뉴 · 언어 · 테마 · 로그인 단추와 같은 항목 + 터치 모드 스위치. 데스크탑 마우스엔 안 보임. + * ☰ 서랍(PLAN_기기대응 2-2 · 7-1) — 공개 쪽은 폭 900 이하 · 터치 모드에서 · 로그인 뒤 쪽(B · M)은 늘 보임. + * 위 메뉴 · 언어 · 테마 · 로그인 단추와 같은 항목 + 터치 모드 스위치. * -------------------------------------------------------------------------- */ function buildDrawer(header: HTMLElement): HTMLElement[] { const burger = document.createElement("button"); @@ -170,7 +170,7 @@ function buildDrawer(header: HTMLElement): HTMLElement[] { }; burger.addEventListener("click", () => setOpen(!header.classList.contains("is-drawer-open"))); scrim.addEventListener("click", () => setOpen(false)); - drawer.addEventListener("keydown", (e) => e.key === "Escape" && setOpen(false)); + header.addEventListener("keydown", (e) => e.key === "Escape" && setOpen(false)); // ☰ 에 포커스가 있어도 닫힘 // 로고 · 진행 단계 · 메뉴 · 로그인 항목을 누르면 닫힘(언어 · 테마 · 스위치는 그대로 둠) drawer.addEventListener("click", (e) => { const hit = (e.target as Element).closest("button"); @@ -410,7 +410,7 @@ const SHELL_CSS = ` .app-nav { display: none; } .app-actions { margin-left: auto; } } -/* ☰ 서랍 — 폭 900 이하 · 터치 모드에서만 보임(데스크탑 마우스엔 없음) */ +/* ☰ 서랍 — 공개 쪽은 폭 900 이하 · 터치 모드에서만 · 로그인 뒤 쪽(B · M)은 늘 */ .app-burger, .app-drawer, .app-drawer__scrim { @@ -428,7 +428,7 @@ const SHELL_CSS = ` font-size: 20px; cursor: pointer; } -html[data-burger="on"] .app-burger { display: inline-flex; } +html:is([data-burger="on"], .is-bm-route) .app-burger { display: inline-flex; } .app-header.is-drawer-open { z-index: var(--z-overlay); } .app-header.is-drawer-open .app-drawer__scrim { display: block; @@ -487,23 +487,41 @@ html[data-burger="on"] .app-burger { display: inline-flex; } html[data-size="compact"] .app-header { padding: 0 var(--spacing-16); gap: var(--spacing-8); } html[data-size="compact"] .app-actions { display: none; } html[data-size="compact"] .app-burger { margin-left: auto; } -/* 로그인 뒤 쪽(B · M) + ☰ — 위 줄을 걷고 ☰ 만 화면 위 오른쪽 모서리에 띄움(6-1) · 본문이 화면 높이를 다 씀 */ -html.is-bm-route[data-burger="on"] .app-header { +/* 로그인 뒤 쪽(B · M) — 폭 · 입력과 상관없이 위 줄을 걷고 ☰ 만 화면 왼쪽 위 모서리에 띄움(7-1) + 본문이 화면 높이를 다 씀 · 서랍은 왼쪽에서 열림 · 위 줄 항목은 모두 서랍에 있음 */ +html.is-bm-route .app-header { height: 0; padding: 0; border: 0; z-index: calc(var(--z-dropdown) + 1); } -html.is-bm-route[data-burger="on"] .app-header > :is(.app-brand, .app-nav, .app-actions) { +html.is-bm-route .app-header > :is(.app-brand, .app-nav, .app-actions) { display: none; } -html.is-bm-route[data-burger="on"] .app-burger { +/* ☰ 폭 36(7-4) · 높이는 손가락 44 · 마우스 36 */ +html.is-bm-route .app-burger { + min-width: var(--app-burger-w); + min-height: var(--app-burger-w); position: fixed; top: var(--spacing-8); - right: var(--spacing-8); + left: var(--spacing-8); + z-index: 1; + margin: 0; + padding: 0; box-shadow: var(--shadow-lg); } -html.is-bm-route[data-burger="on"] .app-drawer__brand .app-brand { align-self: flex-start; } +html.is-bm-route[data-touch="on"] .app-burger { min-height: 44px; } +html.is-bm-route .app-header.is-drawer-open .app-drawer { + left: 0; + right: auto; + border-left: 0; + border-right: 1px solid var(--color-border); +} +html.is-bm-route .app-drawer__brand { + min-height: 44px; + padding-left: calc(var(--app-burger-inset) - var(--spacing-16)); +} +html.is-bm-route .app-drawer__brand .app-brand { align-self: flex-start; } `; export function injectShellStyles(): void { diff --git a/A00_Common/device_context.css b/A00_Common/device_context.css index 4d84c8a30..85751d560 100644 --- a/A00_Common/device_context.css +++ b/A00_Common/device_context.css @@ -84,11 +84,15 @@ html:not([data-touch="on"]) .ui-touch-only { display: none !important; } -/* 위 줄 높이(6-1) — 화면 높이 계산이 모두 이 값을 씀 · ☰ 가 메뉴를 맡는 로그인 뒤 쪽(B · M)은 위 줄을 걷어 0 */ +/* 위 줄 높이(6-1) — 화면 높이 계산이 모두 이 값을 씀 · 로그인 뒤 쪽(B · M)은 위 줄을 걷어 0(7-1) + ☰ 폭(7-4) · 들여쓰기(7-2) = 왼쪽 위에 뜬 ☰(자리 8 + 폭) + 틈 8 · 그 자리에 걸치는 첫 줄이 이만큼 왼쪽을 비움 */ :root { --app-header-h: 64px; + --app-burger-w: 36px; + --app-burger-inset: 0px; } -html.is-bm-route[data-burger="on"] { +html.is-bm-route { --app-header-h: 0px; + --app-burger-inset: calc(var(--spacing-8) * 2 + var(--app-burger-w)); } diff --git a/A00_Common/device_context.ts b/A00_Common/device_context.ts index b868a3e19..4ea4e6d42 100644 --- a/A00_Common/device_context.ts +++ b/A00_Common/device_context.ts @@ -7,7 +7,7 @@ * - data-input = 마지막 누름의 pointerType(mouse · touch · pen) * - data-touch-mode = 사용자 스위치(auto · on · off · 이 기기 브라우저에만 저장) * - data-touch = on · off — 누를 자리 크기를 키울지(auto 면 주 입력 coarse 일 때) - * - data-burger = on · off — ☰ 서랍이 메뉴를 맡는지(폭 900 이하 · 터치) — 로그인 뒤 쪽은 위 줄을 걷음(6-1) + * - data-burger = on · off — 공개 쪽에서 ☰ 서랍이 메뉴를 맡는지(폭 900 이하 · 터치) · 로그인 뒤 쪽(B · M)은 늘 ☰(7-1) * 바뀔 때만 window 에 `app:context` 이벤트(detail = 지금 상태). * 경계 표는 여기 한 벌 — CSS 는 위 표시(data-size)로 가르고, 숫자가 필요하면 --bp-* 변수. * 첫 화면 깜빡임 — 화면은 모두 JS 가 그리므로 main.ts 가 맨 먼저 import 하면 그리기 전에 적힘. diff --git a/B05_Profile/B05_Profile_UI_Style.css b/B05_Profile/B05_Profile_UI_Style.css index 60fbf4df3..763fe83d6 100644 --- a/B05_Profile/B05_Profile_UI_Style.css +++ b/B05_Profile/B05_Profile_UI_Style.css @@ -192,6 +192,11 @@ gap: var(--spacing-8); } +/* ☰ 들여쓰기(7-2) */ +html.is-bm-route .b05-route__view-controls { + left: max(var(--spacing-16), var(--wf-burger-pad, 0px)); +} + /* 방위 나침반을 놓을 자리 — ISO 버튼 바로 아래(2026-09-03 사용자 지시). 버튼 줄 높이 34px + 위 여백 16px 아래에 붙인다. 위젯 모양·색은 공용 (`ui_template/ui_template_compass.css`)이고 여기서는 위치만 준다. */ diff --git a/B06_Section/B06_Section_UI_Style_Cross.css b/B06_Section/B06_Section_UI_Style_Cross.css index 6bd6c56e3..e0288d254 100644 --- a/B06_Section/B06_Section_UI_Style_Cross.css +++ b/B06_Section/B06_Section_UI_Style_Cross.css @@ -138,6 +138,11 @@ background: var(--color-bg); } +/* ☰ 들여쓰기(7-2) */ +html.is-bm-route .b06-section__notice-bar { + padding-left: max(0px, calc(var(--wf-burger-pad, 0px) - var(--spacing-24))); +} + .b06-section__notice-blocks { flex: 1 1 auto; min-width: 0; diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Style.css b/B07_DesignDetail/B07_DesignDetail_UI_Style.css index e9d5398a5..0ee251ae8 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Style.css +++ b/B07_DesignDetail/B07_DesignDetail_UI_Style.css @@ -450,6 +450,11 @@ min-height: 0; } +/* ☰ 들여쓰기(7-2) — 탭 줄이 본문 맨 위 · CAD 제목 줄은 CAD 앱이 스스로(`host-burger.ts`) */ +html.is-bm-route .b07-cad-stack > .cad-tabs { + padding-left: max(6px, var(--wf-burger-pad, 0px)); +} + .b07-cad-stack > .cad-host { flex: 1 1 0; height: auto; diff --git a/B07_DesignDetail/openwebcad/src/integration/host-burger.css b/B07_DesignDetail/openwebcad/src/integration/host-burger.css new file mode 100644 index 000000000..0099c06d9 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/integration/host-burger.css @@ -0,0 +1,4 @@ +/* 앱 ☰ 자리 비키기(기기대응 7-2) — 값은 host-burger.ts 가 적음 · 틀 없이 띄우면 0 */ +html .cad-titlebar { + padding-left: max(12px, var(--host-burger-pad, 0px)); +} diff --git a/B07_DesignDetail/openwebcad/src/integration/host-burger.ts b/B07_DesignDetail/openwebcad/src/integration/host-burger.ts new file mode 100644 index 000000000..543058937 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/integration/host-burger.ts @@ -0,0 +1,19 @@ +/** + * 앱 ☰ 자리 비키기(기기대응 7-2) — 앱(B07)이 왼쪽 위에 띄운 ☰ 가 이 틀(iframe) 맨 위 줄에 걸치면 + * 걸친 폭 + 틈 8 을 `--host-burger-pad` 로 적음(App.css `.cad-titlebar` 가 그만큼 왼쪽을 비움). + * 좌측 패널을 여닫으면 틀 폭이 바뀌어 resize 로 다시 잼 · 홀로 띄운 CAD(틀 없음)는 0. + */ +import './host-burger.css'; + +export function registerHostBurgerPad(): void { + const frame = window.frameElement; + if (!frame) return; + const update = (): void => { + const b = frame.ownerDocument.querySelector('.app-burger')?.getBoundingClientRect(); + const f = frame.getBoundingClientRect(); + const pad = b?.width && b.bottom > f.top ? Math.max(0, Math.ceil(b.right + 8 - f.left)) : 0; + document.documentElement.style.setProperty('--host-burger-pad', `${pad}px`); + }; + update(); + window.addEventListener('resize', update); +} diff --git a/B07_DesignDetail/openwebcad/src/main.tsx b/B07_DesignDetail/openwebcad/src/main.tsx index 043965ab2..abf4f4d7b 100644 --- a/B07_DesignDetail/openwebcad/src/main.tsx +++ b/B07_DesignDetail/openwebcad/src/main.tsx @@ -20,6 +20,7 @@ import { InputController } from './inputController/input-controller.ts'; import { registerAisloDrawingBridge } from './integration/aislo-drawing-bridge.ts'; import { registerAisloTableExport } from './integration/aislo-table-export.ts'; import { registerDxfClipPaste } from './integration/dxf-clip-paste.ts'; +import { registerHostBurgerPad } from './integration/host-burger.ts'; import { getActiveToolActor, getCanvas, @@ -158,6 +159,7 @@ function initApplication() { registerAisloDrawingBridge(); registerAisloTableExport(); // 고른 범위 → 면적산출 표(M02 83장) registerDxfClipPaste(); + registerHostBurgerPad(); // 앱 ☰ 자리 비키기(기기대응 7-2) registerCadDebugHook(); registerAutoSave(); const screenCanvasDrawController = new ScreenCanvasDrawController(context); diff --git a/B08_Quantity/B08_Quantity_UI_Page.css b/B08_Quantity/B08_Quantity_UI_Page.css index c443aea19..164bda6da 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.css +++ b/B08_Quantity/B08_Quantity_UI_Page.css @@ -19,6 +19,11 @@ border-bottom: 1px solid var(--color-border); } +/* ☰ 들여쓰기(7-2) */ +html.is-bm-route .b08-quantity > .b08-quantity__tabs:first-child { + padding-left: max(0px, calc(var(--wf-burger-pad, 0px) - var(--spacing-24))); +} + /* 구조물 집계표 보조 탭 줄 자리(170-4) — 단추는 높이만 채움 */ .b08-quantity__tabs--blank > .ui-btn { visibility: hidden; diff --git a/B09_Estimation/B09_Estimation_UI_Page.css b/B09_Estimation/B09_Estimation_UI_Page.css index 43773307e..fe87eb951 100644 --- a/B09_Estimation/B09_Estimation_UI_Page.css +++ b/B09_Estimation/B09_Estimation_UI_Page.css @@ -25,6 +25,11 @@ border-bottom: 1px solid var(--color-border); } +/* ☰ 들여쓰기(7-2) */ +html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child { + padding-left: max(0px, calc(var(--wf-burger-pad, 0px) - var(--spacing-24))); +} + /* 아래 탭 없는 위 탭 — 보이지 않는 단추 한 줄로 높이만 채움(탭 바꿀 때 몸통이 안 튐) */ .b09-estimation__tabs--blank > .ui-btn { visibility: hidden; diff --git a/M01_MasterData/M01_MasterData_UI_Style.css b/M01_MasterData/M01_MasterData_UI_Style.css index 5af4e0033..f3ef10723 100644 --- a/M01_MasterData/M01_MasterData_UI_Style.css +++ b/M01_MasterData/M01_MasterData_UI_Style.css @@ -47,6 +47,11 @@ gap: var(--spacing-8); } +/* ☰ 들여쓰기(7-2) */ +html.is-bm-route .m01-master__headbox { + padding-left: max(0px, calc(var(--wf-burger-pad, 0px) - var(--spacing-24))); +} + .m01-master__summary { color: var(--color-accent); font-size: var(--text-body-sm); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css index 3fe97039b..88bd8585d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css @@ -78,6 +78,11 @@ gap: var(--spacing-12); } +/* ☰ 들여쓰기(7-2) */ +html.is-bm-route .m02-main__head { + padding-left: max(0px, calc(var(--wf-burger-pad, 0px) - var(--spacing-24))); +} + .m02-main__title { flex: 0 1 auto; min-width: 3rem; diff --git a/resources/tester/test_device_audit.py b/resources/tester/test_device_audit.py index ba919812f..dae54241d 100644 --- a/resources/tester/test_device_audit.py +++ b/resources/tester/test_device_audit.py @@ -122,6 +122,40 @@ AUDIT_JS = r""" } """ +# ☰ 상자와 겹치는 보이는 글 · 단추(7-3) — 왼쪽 위에 뜬 ☰ 아래 깔린 것만(맨 위에 보이는 것 · 같은 출처 iframe 안 포함) +BURGER_JS = r""" +() => { + const b = document.querySelector('.app-burger'); + if (!b || !b.getClientRects().length) return []; + const B = b.getBoundingClientRect(), hits = []; + const scan = (doc, ox, oy) => { + const L = B.left - ox, T = B.top - oy, R = B.right - ox, D = B.bottom - oy; + const cands = new Set(doc.querySelectorAll('button, a[href], input:not([type=hidden]), select, textarea, [role=button], [role=tab]')); + const w = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT); + for (let n = w.nextNode(); n; n = w.nextNode()) if (n.nodeValue.trim()) cands.add(n); + for (const c of cands) { + const el = c.nodeType === 3 ? c.parentElement : c; + if (!el || el.closest('.app-header, .ui-toast')) continue; + if (el.checkVisibility && !el.checkVisibility({ opacityProperty: true, visibilityProperty: true })) continue; + let r = el.getBoundingClientRect(); + if (c.nodeType === 3) { const g = doc.createRange(); g.selectNodeContents(c); r = g.getBoundingClientRect(); } + const l = Math.max(r.left, L), t = Math.max(r.top, T), rr = Math.min(r.right, R), bb = Math.min(r.bottom, D); + if (r.width < 1 || r.height < 1 || rr - l < 1 || bb - t < 1) continue; + const top = doc.elementsFromPoint((l + rr) / 2, (t + bb) / 2).find((e) => !e.closest('.app-header')); + if (top && top !== el && !el.contains(top) && !top.contains(el)) continue; + hits.push(`${el.tagName.toLowerCase()}:${(c.nodeType === 3 ? c.nodeValue : el.textContent).trim().slice(0, 16)}`); + } + for (const f of doc.querySelectorAll('iframe')) { + const fr = f.getBoundingClientRect(); + if (fr.right < L || fr.left > R || fr.bottom < T || fr.top > D) continue; + try { if (f.contentDocument?.body) scan(f.contentDocument, ox + fr.left + f.clientLeft, oy + fr.top + f.clientTop); } catch (e) {} + } + }; + scan(document, 0, 0); + return hits; +} +""" + # 데스크탑 배치 서명 — 화면 안에 보이는 요소 사각형(정수) · 순서대로. # 잡음 빼기 = 움직이는 중(CSS 애니메이션 · 로딩 원)인 요소와 그 안 · 화면 밖(스크롤 아래 1px 흔들림) LAYOUT_JS = r""" @@ -203,6 +237,7 @@ def run_audit(tag: str, pages=PAGES, sizes=SIZES) -> dict: _open(page, route) row = page.evaluate(AUDIT_JS) row["errors"] = list(errors) + row["burger_hits"] = page.evaluate(BURGER_JS) if size == DESKTOP: page.screenshot(path=str(shots / f"{route}.png")) (shots / f"{route}.layout.json").write_text( @@ -211,7 +246,8 @@ def run_audit(tag: str, pages=PAGES, sizes=SIZES) -> dict: result["pages"].setdefault(route, {})[size] = row print( f"{route:22} {size:24} hs={row['hscroll_px']:4} off={row['offscreen']:3} " - f"cov={row['covered']:3} small={row['small']:3} menu={row['menu_unreachable']}", + f"cov={row['covered']:3} small={row['small']:3} menu={row['menu_unreachable']} " + f"top={row['outlet_top']} burger={len(row['burger_hits'])}", flush=True, ) ctx.close() @@ -237,6 +273,46 @@ def layout_diff(route: str, a="before", b="after") -> int: ) +def _rect(row): + tag, xywh = row.split("|") + return tag, tuple(int(v) for v in xywh.split(",")) + + +def bm_moved(route: str, a="before", b="after") -> list: + """로그인 뒤 쪽(B · M) 데스크탑 — 위 줄(64)을 걷어 세로는 올라가도 가로 자리(태그 · left · width)는 같아야 함(7-3). + 원래 상자 가운데 지금 같은 가로 자리가 없는 것. 빼는 것 = 위 줄 · ☰ 띠(위 64 + 60 안 · 들여쓴 첫 줄) · + 없어진 떠 있는 진행 판(ASIDE) 안 · 화면 전체 상자.""" + ra = _boxes(json.loads((ROOT / "tmp" / f"desktop_{a}" / f"{route}.layout.json").read_text())) + rb = _boxes(json.loads((ROOT / "tmp" / f"desktop_{b}" / f"{route}.layout.json").read_text())) + import collections + + left = collections.Counter((t, r[0], r[2]) for t, r in map(_rect, rb)) + missing = [] + for row in ra: + t, r = _rect(row) + if left[(t, r[0], r[2])] > 0: + left[(t, r[0], r[2])] -= 1 + else: + missing.append((t, r)) + gone = [r for t, r in missing if t == "ASIDE"] + + def inside(r, g): + return ( + g[0] <= r[0] + and g[1] <= r[1] + and r[0] + r[2] <= g[0] + g[2] + and r[1] + r[3] <= g[1] + g[3] + ) + + return [ + f"{t}|{r}" + for t, r in missing + if r[1] >= 64 + 60 + and r[2] < SIZES[DESKTOP]["viewport"]["width"] + and not any(inside(r, g) for g in gone) + ] + + def _server_up() -> bool: import urllib.request @@ -254,17 +330,20 @@ def test_device_audit_all_pages_render_and_desktop_unchanged(): for size, row in sizes.items(): assert row["rendered"], f"{route} {size} 빈 화면" assert row["pointer"] == ("fine" if size == DESKTOP else "coarse"), (route, size) + # 로그인 뒤 쪽 = ☰ 왼쪽 위 · 그 상자와 겹치는 글 · 단추 0 · 본문 위 끝 0(7-3) + if route[0] in "bm": + assert not row["burger_hits"], (route, size, row["burger_hits"]) + assert row["outlet_top"] == (40 if route == "b03-file-input" else 0), (route, size) if (ROOT / "tmp" / "desktop_before2").exists(): - # 차이 0 = 같은 원래 코드 두 번 재기(before · before2) 잡음 이하 - noise = {r: layout_diff(r, "before", "before2") for r in PAGES} - over = [r for r in PAGES if layout_diff(r) > noise[r]] + # 공개 쪽 = 배치 그대로 · 로그인 뒤 쪽 = 가로 자리 그대로 — 같은 원래 코드 두 번 재기(before · before2) 잡음 이하 + def diff(r, b): + return len(bm_moved(r, "before", b)) if r[0] in "bm" else layout_diff(r, "before", b) + + noise = {r: diff(r, "before2") for r in PAGES} + over = [r for r in PAGES if diff(r, "after") > noise[r]] if over: # 불러오는 때에 따라 한 번 흔들림(B06 종단) — 그 쪽만 한 번 더 잼 run_audit("after_retry", pages=over, sizes={DESKTOP: SIZES[DESKTOP]}) - over = { - r: layout_diff(r, "before", "after_retry") - for r in over - if layout_diff(r, "before", "after_retry") > noise[r] - } + over = {r: diff(r, "after_retry") for r in over if diff(r, "after_retry") > noise[r]} assert not over, over diff --git a/resources/tester/test_device_drawer.py b/resources/tester/test_device_drawer.py index 3bfc51c44..e7f454e3a 100644 --- a/resources/tester/test_device_drawer.py +++ b/resources/tester/test_device_drawer.py @@ -1,10 +1,11 @@ -"""☰ 서랍 · 위 줄 걷기(PLAN_기기대응 6 · `app_shell.ts` · `ui_template_workflow_layout.ts`) — 실제 브라우저. +"""☰ 서랍 · 위 줄 걷기(PLAN_기기대응 6 · 7 · `app_shell.ts` · `ui_template_workflow_layout.ts`) — 실제 브라우저. -① 휴대폰 · 로그인 뒤 B 쪽 = 위 줄 걷힘(본문 위 끝 0) · ☰ 는 위 오른쪽 모서리 · 떠 있는 진행 판 없음 -② ☰ 서랍 = 로고 · 홈 ─ 진행 단계 ─ 메뉴 ─ 언어 · 테마 · 로그인 · 터치 모드 차례 · 진행 단계는 진행 판의 **그 요소**(한 벌) - 지금 단계 강조 그대로 · 닫으면 제자리(진행 판)로 돌아감 · 단계 누름 = 이동 + 서랍 닫힘 -③ 휴대폰 · 공개 쪽(A01) = 위 줄 그대로(본문 위 끝 64) -④ 데스크탑(1440 · 마우스) = 위 줄 · 진행 판 그대로 · ☰ 없음 +① 휴대폰 · 로그인 뒤 B 쪽 = 위 줄 걷힘(본문 위 끝 0) · ☰ 는 위 왼쪽 모서리(폭 36 · 손가락 높이 44) · 떠 있는 진행 판 없음 +② ☰ 서랍 = 왼쪽에서 열림 · ☰ 는 서랍 위에 그대로 · 로고 · 홈 ─ 진행 단계 ─ 메뉴 ─ 언어 · 테마 · 로그인 · 터치 모드 차례 + 진행 단계는 진행 판의 **그 요소**(한 벌) · 지금 단계 강조 그대로 · 닫으면 제자리(진행 판)로 돌아감 · 단계 누름 = 이동 + 서랍 닫힘 +③ 휴대폰 · 공개 쪽(A01) = 위 줄 그대로(본문 위 끝 64) · ☰ 는 위 줄 오른쪽 +④ 데스크탑(1440 · 마우스) · 로그인 뒤 B 쪽 = 위 줄 걷힘 · ☰ 왼쪽 위 36×36 · 진행 판 없음 · 위 줄 항목이 모두 서랍에 · Esc 로 닫힘 +⑤ 데스크탑 · 공개 쪽(A01) = 위 줄 그대로 · ☰ 없음 """ import json @@ -18,6 +19,17 @@ import test_device_audit as audit # noqa: E402 OUTLET_TOP = "Math.round(document.getElementById('app-outlet').getBoundingClientRect().top)" GROUPS = "[...document.querySelectorAll('.app-drawer > *')].map(e => e.className)" +BURGER = ( + "(() => { const r = document.querySelector('.app-burger').getBoundingClientRect();" + " return [Math.round(r.left), Math.round(r.top), Math.round(r.width), Math.round(r.height)," + " Math.round(r.right)]; })()" +) +PROGRESS_HIDDEN = ( + "!document.querySelector('.ui-workflow-overlay__panel--progress').getClientRects().length" +) +LABELS = ( + "(sel) => [...document.querySelectorAll(sel)].map(b => b.textContent.trim()).filter(Boolean)" +) def _ctx(browser, cred, size): @@ -43,20 +55,24 @@ def test_drawer_holds_steps_and_header_folds(): # ① assert page.evaluate(OUTLET_TOP) == 0 - burger = page.evaluate( - "(() => { const r = document.querySelector('.app-burger').getBoundingClientRect();" - " return [Math.round(r.right), Math.round(r.top), Math.round(r.width)]; })()" - ) - assert burger[0] >= 390 - 12 and burger[1] <= 12 and burger[2] >= 44, burger - assert page.evaluate( - "!document.querySelector('.ui-workflow-overlay__panel--progress').getClientRects().length" - ) + burger = page.evaluate(BURGER) + assert burger[:4] == [8, 8, 36, 44], burger + assert page.evaluate(PROGRESS_HIDDEN) # ② page.evaluate( "window.__steps = document.querySelector('.ui-workflow-overlay__panel--progress .ui-workflow-layout__steps')" ) page.evaluate("document.querySelector('.app-burger').click()") + assert ( + page.evaluate( + "Math.round(document.querySelector('.app-drawer').getBoundingClientRect().left)" + ) + == 0 + ) + assert page.evaluate( # ☰ 는 서랍 위에서 그대로 눌림(다시 누르면 닫힘) + "document.elementFromPoint(26, 30) === document.querySelector('.app-burger')" + ) groups = page.evaluate(GROUPS) order = [ next((k for k in ("brand", "steps", "nav", "touch") if k in c), "settings") @@ -87,13 +103,31 @@ def test_drawer_holds_steps_and_header_folds(): # ③ audit._open(page, "a01-home") assert page.evaluate(OUTLET_TOP) == 64 + assert page.evaluate(BURGER)[4] >= 390 - 16 # ④ desk = _ctx(browser, cred, audit.DESKTOP).new_page() audit._open(desk, "b07-design-detail") + assert desk.evaluate(OUTLET_TOP) == 0 + assert desk.evaluate(BURGER)[:4] == [8, 8, 36, 36] + assert desk.evaluate(PROGRESS_HIDDEN) + head_nav = desk.evaluate(LABELS, ".app-nav button") + head_actions = desk.evaluate(LABELS, ".app-actions button") + assert head_nav and head_actions + desk.click(".app-burger") + assert desk.evaluate(LABELS, ".app-drawer__nav button") == head_nav + drawer_all = desk.evaluate(LABELS, ".app-drawer button") + assert all(label in drawer_all for label in head_actions), (head_actions, drawer_all) + assert desk.evaluate( + "document.querySelector('.app-drawer__steps .ui-workflow-layout__step.is-active') !== null" + ) + desk.keyboard.press("Escape") + assert not desk.evaluate( + "document.querySelector('.app-header').classList.contains('is-drawer-open')" + ) + + # ⑤ + audit._open(desk, "a01-home") assert desk.evaluate(OUTLET_TOP) == 64 assert not desk.evaluate("document.querySelector('.app-burger').getClientRects().length") - assert desk.evaluate( - "document.querySelector('.ui-workflow-overlay__panel--progress').getClientRects().length > 0" - ) browser.close() diff --git a/ui_template/ui_template_general_layout.css b/ui_template/ui_template_general_layout.css index 12df27588..f5533249e 100644 --- a/ui_template/ui_template_general_layout.css +++ b/ui_template/ui_template_general_layout.css @@ -111,3 +111,9 @@ padding: var(--spacing-24) var(--spacing-16) var(--spacing-40); } } + +/* ☰ 들여쓰기(7-2) — 제목 줄이 왼쪽 위 ☰ 자리에 걸칠 때만 비킴(넓은 화면은 가운데 정렬로 이미 비켜 0). + (100vw - 100%) / 2 = 제목 줄 왼쪽 끝 · 세로 스크롤 막대 몫 8 을 더 비움 */ +html.is-bm-route .ui-general-layout__header { + padding-left: max(0px, calc(var(--app-burger-inset) + var(--spacing-8) - (100vw - 100%) / 2)); +} diff --git a/ui_template/ui_template_overlay.css b/ui_template/ui_template_overlay.css index f218b5432..7565877b4 100644 --- a/ui_template/ui_template_overlay.css +++ b/ui_template/ui_template_overlay.css @@ -541,8 +541,8 @@ html:is([data-size="compact"], [data-size="medium"]) pointer-events: auto; } -/* ☰ 가 메뉴를 맡는 때(폭 900 이하 · 터치)는 떠 있는 진행 판 대신 ☰ 서랍 안 진행 단계 묶음(6-2) */ -html[data-burger="on"] .ui-workflow-overlay__panel--progress { +/* 로그인 뒤 쪽(B · M)은 떠 있는 진행 판 대신 ☰ 서랍 안 진행 단계 묶음(6-2 · 7-1) */ +html.is-bm-route .ui-workflow-overlay__panel--progress { display: none; } @@ -552,3 +552,14 @@ html:is([data-size="compact"], [data-size="medium"]) > :not(.ui-workflow-overlay__handle) { visibility: hidden; } + +/* ☰ 들여쓰기(7-2) — 왼쪽 위에 뜬 ☰ 자리만큼 좌측 패널 제목 줄을 비킴 · 손가락 ☰(높이 44)는 제목 줄도 그만큼 높임 */ +html.is-bm-route .ui-workflow-overlay__panel--title > .ui-workflow-overlay__header { + padding-left: max(var(--spacing-16), var(--app-burger-inset)); +} + +html.is-bm-route[data-touch="on"] + .ui-workflow-overlay__panel--title + > .ui-workflow-overlay__header { + min-height: calc(var(--spacing-8) * 2 + 44px); +} diff --git a/ui_template/ui_template_workflow_layout.css b/ui_template/ui_template_workflow_layout.css index a8a7f8113..6f2cbf91d 100644 --- a/ui_template/ui_template_workflow_layout.css +++ b/ui_template/ui_template_workflow_layout.css @@ -136,3 +136,20 @@ overflow: auto; background: var(--color-canvas); } + +/* ☰ 들여쓰기(7-2) — 본문 첫 줄이 ☰ 자리를 비키려면 본문 왼쪽 끝에서 더 비울 폭. + 좌측 패널이 열려 본문을 민 때는 0 · 접힘(손잡이 24) · 좁은 폭에서 본문 위에 겹친 때는 남은 만큼. + 쪽마다 첫 줄(도구 줄 · 탭 · 제목)이 `padding-left: max(제 여백, var(--wf-burger-pad))` 한 줄로 씀 */ +html.is-bm-route .ui-workflow-layout { + --wf-burger-pad: max(0px, calc(var(--app-burger-inset) - var(--spacing-24))); +} + +html.is-bm-route .ui-workflow-layout.is-options-open { + --wf-burger-pad: 0px; +} + +@media (max-width: 860px) { + html.is-bm-route .ui-workflow-layout.is-options-open { + --wf-burger-pad: var(--app-burger-inset); + } +}