From dce63ccc21a845368c95da51d86c46b5c9e4d9a0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 19 Oct 2026 22:07:35 +0900 Subject: [PATCH] =?UTF-8?q?feat(486=20B07=20CAD=20=ED=99=94=EB=A9=B4):=20?= =?UTF-8?q?=EC=A0=9C=EB=AA=A9=EC=A4=84=20=C2=B7=20=EB=9E=A8=ED=94=84=20?= =?UTF-8?q?=C2=B7=20=ED=95=98=EB=8B=A8=20=EB=8F=84=EB=A9=B4=20=EC=B9=B8=20?= =?UTF-8?q?=EC=A7=80=EC=9B=80=20=C2=B7=20=E2=86=B6=20=E2=86=B7=20=3D=20?= =?UTF-8?q?=EB=A6=AC=EB=B3=B8=20=ED=83=AD=20=EC=A4=84=20=EC=98=A4=EB=A5=B8?= =?UTF-8?q?=EC=AA=BD=20=C2=B7=20=EC=86=90=EA=B0=80=EB=9D=BD=20=EB=8B=A8?= =?UTF-8?q?=EC=B6=94=20=3D=20=EC=9E=90=ED=8C=90=20=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EA=B8=B0=EB=A7=8C=20=C2=B7=20=ED=83=AD=20=EC=A4=84=20?= =?UTF-8?q?=EB=B9=88=20=EC=9E=90=EB=A6=AC=20=EB=B3=80=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 제목줄(Aislo CAD · 부제 · 「현재 도면」 램프) 지움 · --cad-title-height 0 · 💾 · JSON 내보내기 = DOM 두고 숨김(QUICK_ACCESS_HIDDEN) - 빠른 실행 = 리본 탭 줄 빈 자리 뒤 · 접기 단추 앞(줄 높이 26 그대로 · M02 hostSave 면 안 그림) - 빈 자리([출력] 오른쪽 ~ ↶ 왼쪽) → .cad-host 에 --cad-tab-slot-left/right(iframe 끝 기준 px) — 부모 탭 줄 겹침 자리 - ☰ 비키기 패딩 = 리본 탭 줄로 · 손가락 단추 = (hover: none) and (pointer: coarse) 만 - 하단 도면 이름 · ‹ › · 확정 칸(QuantityPanel) · 이동 요청 지움 - 시험 test_cad_touch(미디어 조건) · test_cad_host_tabs(머리줄 · 빈 자리 변수) 고쳐 씀 Co-Authored-By: Claude Opus 5.5 --- B07_DesignDetail/openwebcad/src/App.css | 223 +----------------- B07_DesignDetail/openwebcad/src/App.tsx | 2 - .../src/components/QuantityPanel.tsx | 57 ----- .../src/components/QuickAccessBar.tsx | 30 +-- .../openwebcad/src/components/Toolbar.tsx | 4 +- .../src/inputController/input-touch.css | 4 +- .../src/integration/aislo-drawing-bridge.ts | 6 - .../src/integration/host-burger.css | 4 +- .../openwebcad/src/integration/host-burger.ts | 2 +- .../openwebcad/src/ribbon/Ribbon.tsx | 28 +++ .../openwebcad/src/ribbon/ribbon.config.ts | 4 +- resources/tester/test_cad_host_tabs.py | 21 +- resources/tester/test_cad_touch.py | 13 +- 13 files changed, 87 insertions(+), 311 deletions(-) delete mode 100644 B07_DesignDetail/openwebcad/src/components/QuantityPanel.tsx diff --git a/B07_DesignDetail/openwebcad/src/App.css b/B07_DesignDetail/openwebcad/src/App.css index ba1b7de29..e8490b530 100644 --- a/B07_DesignDetail/openwebcad/src/App.css +++ b/B07_DesignDetail/openwebcad/src/App.css @@ -3,7 +3,8 @@ @import "../../../ui_template/ui_template_theme.css"; :root { - --cad-title-height: 42px; + /* 제목줄 없음(요구 280-6) — 칸들이 이 변수로 자리 잡아 0 으로 둠 */ + --cad-title-height: 0px; /* 116px 에서는 리본 줄의 내용(85px)이 보이는 높이(81px)를 넘어 세로 스크롤바가 생겼다(2026-09-04 실측) — 모자란 4px + 여유 2px 를 더한다. */ --cad-ribbon-height: 122px; @@ -55,11 +56,7 @@ button { z-index: 2; pointer-events: none; } -.cad-host-save { - --cad-title-height: 0px; -} .cad-bare { - --cad-title-height: 0px; --cad-status-height: 0px; } .controls { @@ -85,64 +82,6 @@ body > canvas[data-id="canvas"] { cursor: none; } -.cad-titlebar { - position: fixed; - inset: 0 0 auto 0; - height: var(--cad-title-height); - display: flex; - align-items: center; - gap: 24px; - padding: 0 12px; - background: var(--cad-chrome); - border-bottom: 1px solid var(--cad-line); - box-shadow: var(--shadow-sm); -} -.cad-brand { - display: flex; - align-items: baseline; - gap: 9px; - min-width: 220px; -} -.cad-brand strong { - color: var(--color-text); - font-size: 14px; -} -.cad-brand span, -.cad-file-state { - color: var(--cad-text-dim); - font-size: 11px; -} -.cad-file-state { - display: flex; - align-items: center; - gap: 7px; -} -.cad-file-state__dot { - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--color-success); - box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-success) 25%, transparent); -} -.cad-title-actions { - display: flex; - gap: 4px; - margin-left: auto; -} -.cad-title-actions button { - height: 28px; - min-width: 32px; - padding: 0 9px; - border: 1px solid transparent; - border-radius: 3px; - background: transparent; - font-size: 12px; -} -.cad-title-actions button:hover { - border-color: var(--cad-line); - background: var(--cad-hover); -} - .cad-ribbon { position: fixed; top: var(--cad-title-height); @@ -168,8 +107,11 @@ body > canvas[data-id="canvas"] { padding: 0 8px; border-bottom: 1px solid var(--cad-line); } +.cad-ribbon-tabs__slot { + flex: 1; + min-width: 0; +} .cad-ribbon-tabs__collapse { - margin-left: auto; width: 24px; height: 20px; align-self: center; @@ -275,14 +217,15 @@ body > canvas[data-id="canvas"] { .cad-quick-access { display: flex; gap: 2px; - margin-left: 14px; + align-self: center; } -.cad-quick-access button { - width: 26px; - height: 24px; +.cad-ribbon-tabs .cad-quick-access button { + width: 24px; + height: 20px; + padding: 0; border: 1px solid transparent; - border-radius: 3px; - background: transparent; + border-radius: 4px; + line-height: 1; font-size: 13px; } .cad-quick-access button:hover { @@ -718,153 +661,13 @@ body.cad-file-dragging::after { font-size: 10px; } -/* ----------------------------------------------------------------------------- - * 수량 산출표 패널 (CAD 화면 하단 중심, 접이식) — 첨부 양식 - * -------------------------------------------------------------------------- */ -.cad-qtable { - position: fixed; - z-index: 3; - /* 명령행 위로 띄운다 — 겹치면 명령 입력칸을 클릭으로 누를 수 없다(2026-08-31). - 명령행을 접으면 --cad-command-height가 0이라 예전처럼 상태막대 위에 붙는다. */ - bottom: calc(var(--cad-status-height) + var(--cad-command-height) + 8px); - left: calc(var(--cad-panel-width) + (100vw - var(--cad-panel-width)) / 2); - transform: translateX(-50%); - /* 표(가장 넓은 자식) 기준 폭 고정 → 접었다 펴도 폭이 변하지 않는다 */ - width: max-content; - max-width: min(1040px, calc(100vw - var(--cad-panel-width) - 24px)); - border: 1px solid var(--cad-line); - border-radius: 6px; - background: var(--cad-chrome-raised); - box-shadow: var(--shadow-lg); - color: var(--cad-text); -} - -.cad-qtable__header { - display: flex; - align-items: center; - gap: 8px; - padding: 5px 8px; -} -.cad-qtable__title { - display: flex; - flex: 1; - align-items: baseline; - justify-content: center; - gap: 8px; - min-width: 0; -} -.cad-qtable__title strong { - color: var(--color-text); - font-size: 14px; -} -.cad-qtable__title span { - overflow: hidden; - color: var(--cad-text-dim); - font-size: 11px; - text-overflow: ellipsis; - white-space: nowrap; -} -.cad-qtable__status { - padding: 1px 7px; - border-radius: 999px; - font-size: 10px; - font-style: normal; -} -.cad-qtable__status[data-confirmed="true"] { - background: color-mix(in srgb, var(--color-success) 22%, transparent); - color: var(--color-success); -} -.cad-qtable__status[data-confirmed="false"] { - background: color-mix(in srgb, var(--color-warning) 22%, transparent); - color: var(--color-warning); -} -.cad-qtable__nav, -.cad-qtable__collapse { - width: 26px; - height: 24px; - border: 1px solid var(--cad-line); - border-radius: 4px; - background: var(--cad-chrome-sunken); - color: var(--cad-text); - font-size: 13px; -} -.cad-qtable__nav:hover:not(:disabled), -.cad-qtable__collapse:hover { - background: var(--cad-hover); -} -.cad-qtable__nav:disabled { - opacity: 0.35; - cursor: not-allowed; -} - -.cad-qtable__body { - padding: 0 8px 8px; - overflow-x: auto; -} -/* 접힘: 표는 DOM에 남겨 폭은 유지하되 세로로만 감춘다 */ -.cad-qtable[data-collapsed="true"] .cad-qtable__body { - max-height: 0; - padding-top: 0; - padding-bottom: 0; - overflow: hidden; - visibility: hidden; -} -.cad-qtable table { - margin: 0 auto; - border-collapse: collapse; -} -.cad-qtable th, -.cad-qtable td { - padding: 0; - border: 1px solid var(--cad-line); - font-size: 11px; - text-align: center; - vertical-align: middle; -} -.cad-qtable__label { - padding: 2px 9px; - color: var(--cad-text-dim); - font-weight: 600; - white-space: nowrap; - background: var(--cad-chrome); -} -.cad-qtable__label--vertical { - width: 18px; - writing-mode: vertical-rl; - text-orientation: upright; - letter-spacing: 1px; -} -.cad-qtable__value input { - width: 72px; - padding: 3px 4px; - border: 0; - background: transparent; - color: var(--cad-text); - font: inherit; - text-align: center; -} -.cad-qtable__value input:focus { - outline: 2px solid var(--color-focus-ring); - outline-offset: -2px; - background: var(--cad-hover); -} -.cad-qtable__value--derived input { - color: var(--cad-text-muted); - background: var(--cad-chrome-sunken); - cursor: not-allowed; -} - @media (max-width: 800px) { :root { --cad-panel-width: 200px; } - .cad-file-state, .cad-statusbar__hint { display: none; } - .cad-brand { - min-width: auto; - } } /* 특성 팔레트 — 값을 바로 고치는 입력들 */ diff --git a/B07_DesignDetail/openwebcad/src/App.tsx b/B07_DesignDetail/openwebcad/src/App.tsx index 93ce68b1d..370698bba 100644 --- a/B07_DesignDetail/openwebcad/src/App.tsx +++ b/B07_DesignDetail/openwebcad/src/App.tsx @@ -1,14 +1,12 @@ import './App.css'; import { ToastContainer } from 'react-toastify'; import { FramePlaceholderPanel } from './components/FramePlaceholderPanel.tsx'; -import { QuantityPanel } from './components/QuantityPanel.tsx'; import { Toolbar } from './components/Toolbar.tsx'; function App() { return (
-
diff --git a/B07_DesignDetail/openwebcad/src/components/QuantityPanel.tsx b/B07_DesignDetail/openwebcad/src/components/QuantityPanel.tsx deleted file mode 100644 index 19f5b2631..000000000 --- a/B07_DesignDetail/openwebcad/src/components/QuantityPanel.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { type FC, useCallback, useEffect, useState } from 'react'; -import { HtmlEvent } from '../App.types'; -import { requestDrawingNavigation } from '../integration/aislo-drawing-bridge'; -import { getDesignMeta } from '../state'; - -/** - * 도면 내비게이션 바 (CAD 화면 하단 중심). - * 수량 산출표는 HTML 테이블 대신 도면 자체의 CAD 테이블 레이어(b08-cross-table)로 - * 렌더되므로, 여기서는 제목·측점정보·확정상태·이전/다음 이동만 담당한다. - */ -export const QuantityPanel: FC = () => { - const [meta, setMeta] = useState(getDesignMeta()); - - const refresh = useCallback(() => setMeta(getDesignMeta()), []); - useEffect(() => { - window.addEventListener(HtmlEvent.UPDATE_STATE, refresh); - return () => window.removeEventListener(HtmlEvent.UPDATE_STATE, refresh); - }, [refresh]); - - if (!meta) return null; - - return ( -
-
- -
- {meta.title} - {meta.info && {meta.info}} - - {meta.confirmed ? '확정' : '미확정'} - -
- -
-
- ); -}; diff --git a/B07_DesignDetail/openwebcad/src/components/QuickAccessBar.tsx b/B07_DesignDetail/openwebcad/src/components/QuickAccessBar.tsx index b51e26dc3..c99d8e61c 100644 --- a/B07_DesignDetail/openwebcad/src/components/QuickAccessBar.tsx +++ b/B07_DesignDetail/openwebcad/src/components/QuickAccessBar.tsx @@ -1,26 +1,14 @@ -/** 제목표시줄 + 빠른 실행 도구막대 (AutoCAD 상단 막대) */ -import { type FC, useEffect, useState } from 'react'; -import { HtmlEvent } from '../App.types'; +/** 빠른 실행 단추(↶ ↷) — 리본 탭 줄 오른쪽(접기 단추 앞)에 그린다 · 숨김 목록은 DOM 만 둔다 */ +import type { FC } from 'react'; import { getCommandById } from '../commands/registry'; import { runCommand } from '../commands/run-command'; -import { QUICK_ACCESS_COMMANDS } from '../ribbon/ribbon.config'; -import { getHostTitle, isHostSave } from '../state'; +import { QUICK_ACCESS_COMMANDS, QUICK_ACCESS_HIDDEN } from '../ribbon/ribbon.config'; +import { isHostSave } from '../state'; export const QuickAccessBar: FC = () => { - const [hostTitle, setTitle] = useState(getHostTitle()); - useEffect(() => { - const refresh = () => setTitle(getHostTitle()); - window.addEventListener(HtmlEvent.UPDATE_STATE, refresh); - return () => window.removeEventListener(HtmlEvent.UPDATE_STATE, refresh); - }, []); - // 부모 페이지가 저장·이름을 맡는 M02 에선 제목 줄을 숨겨 그림 칸을 넓힌다. + // 부모 [저장]이 맡는 M02 는 우하단 ViewControls 에 ↶ ↷ 가 있어 안 그린다 if (isHostSave()) return null; return ( -
-
- Aislo CAD - {hostTitle} -
{QUICK_ACCESS_COMMANDS.map((id) => { const command = getCommandById(id); @@ -29,6 +17,7 @@ export const QuickAccessBar: FC = () => {
-
- - 현재 도면 -
-
-); + ); }; diff --git a/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx b/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx index ba0b5e184..204258e81 100644 --- a/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx +++ b/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx @@ -1,5 +1,5 @@ /** - * CAD 화면 골격 조립 — 제목표시줄 · 리본 · 좌측 팔레트 · 탐색막대 · 명령행 · 상태막대. + * CAD 화면 골격 조립 — 리본(빠른 실행 포함) · 좌측 팔레트 · 탐색막대 · 명령행 · 상태막대. * 각 조각은 자기 파일에 있고, 여기서는 배치와 표시 상태만 다룬다. */ import { type FC, useEffect, useState } from 'react'; @@ -8,7 +8,6 @@ import { InspectorPanel } from './InspectorPanel'; import { QuickProperties } from './QuickProperties'; import { BlockLibraryPanel } from './BlockLibraryPanel'; import { LayersWidget, PropertiesWidget, TextStyleWidget } from './RibbonWidgets'; -import { QuickAccessBar } from './QuickAccessBar'; import { Ribbon } from '../ribbon/Ribbon'; import { StatusBar } from './StatusBar'; import { useCadRefresh } from './use-cad-refresh'; @@ -50,7 +49,6 @@ export const Toolbar: FC = () => { return ( <> - = {}) { window.parent.postMessage({ type, ...payload }, window.location.origin); } -/** 수량 패널에서 이전/다음 도면으로 이동 요청 (부모가 처리). */ -export function requestDrawingNavigation(direction: 'prev' | 'next') { - notifyParent(AISLO_DRAWING_NAVIGATE_MESSAGE, { direction }); -} - /** 저장 요청을 부모 페이지 [저장]에 넘긴다 — 캐드는 양식 파일을 모른다. */ export function requestHostSave() { notifyParent(AISLO_DRAWING_HOST_SAVE_MESSAGE); diff --git a/B07_DesignDetail/openwebcad/src/integration/host-burger.css b/B07_DesignDetail/openwebcad/src/integration/host-burger.css index 0099c06d9..3fd4af82b 100644 --- a/B07_DesignDetail/openwebcad/src/integration/host-burger.css +++ b/B07_DesignDetail/openwebcad/src/integration/host-burger.css @@ -1,4 +1,4 @@ /* 앱 ☰ 자리 비키기(기기대응 7-2) — 값은 host-burger.ts 가 적음 · 틀 없이 띄우면 0 */ -html .cad-titlebar { - padding-left: max(12px, var(--host-burger-pad, 0px)); +html .cad-ribbon-tabs { + padding-left: max(8px, 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 index 543058937..f096efe80 100644 --- a/B07_DesignDetail/openwebcad/src/integration/host-burger.ts +++ b/B07_DesignDetail/openwebcad/src/integration/host-burger.ts @@ -1,6 +1,6 @@ /** * 앱 ☰ 자리 비키기(기기대응 7-2) — 앱(B07)이 왼쪽 위에 띄운 ☰ 가 이 틀(iframe) 맨 위 줄에 걸치면 - * 걸친 폭 + 틈 8 을 `--host-burger-pad` 로 적음(App.css `.cad-titlebar` 가 그만큼 왼쪽을 비움). + * 걸친 폭 + 틈 8 을 `--host-burger-pad` 로 적음(리본 탭 줄 `.cad-ribbon-tabs` 가 그만큼 왼쪽을 비움). * 좌측 패널을 여닫으면 틀 폭이 바뀌어 resize 로 다시 잼 · 홀로 띄운 CAD(틀 없음)는 0. */ import './host-burger.css'; diff --git a/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx b/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx index 4afe48c35..87ea40472 100644 --- a/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx +++ b/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx @@ -1,6 +1,7 @@ /** 리본 렌더러 — ribbon.config.ts의 데이터와 명령 레지스트리만 보고 그린다. */ import { type FC, type ReactNode, useEffect, useRef, useState } from 'react'; import type { CadCommand } from '../commands/command.types'; +import { QuickAccessBar } from '../components/QuickAccessBar'; import { getCommandById } from '../commands/registry'; import { runCommand } from '../commands/run-command'; import { @@ -157,8 +158,33 @@ export const Ribbon: FC = ({ activeTool, widgets, onCommandRun }) = // (오피스 「리본 자동 숨기기」). 접기 단추로 펼치면 고정 펼침. const [peek, setPeek] = useState(false); const navRef = useRef(null); + const slotRef = useRef(null); const peeking = collapsed && peek; + // 탭 줄 빈 자리([출력] 오른쪽 ~ ↶ 왼쪽)를 부모 `.cad-host` 에 적음 — 부모 페이지가 자기 탭 줄을 거기 겹침. + // 값 = iframe 왼쪽 끝 → 빈 자리 왼쪽 · 빈 자리 오른쪽 → iframe 오른쪽 끝(px · CSS right 꼴) · 짝 선례 `integration/host-burger.ts`. + useEffect(() => { + const host = window.frameElement?.parentElement; + const slot = slotRef.current; + if (!host?.classList.contains('cad-host') || !slot) return; + const update = (): void => { + const s = slot.getBoundingClientRect(); + host.style.setProperty('--cad-tab-slot-left', `${Math.round(s.left)}px`); + host.style.setProperty( + '--cad-tab-slot-right', + `${Math.round(window.innerWidth - s.right)}px` + ); + }; + update(); + const observer = new ResizeObserver(update); + observer.observe(slot); + window.addEventListener('resize', update); + return () => { + observer.disconnect(); + window.removeEventListener('resize', update); + }; + }, []); + useEffect(() => { if (!peeking) return; const close = (event: PointerEvent) => { @@ -189,6 +215,8 @@ export const Ribbon: FC = ({ activeTool, widgets, onCommandRun }) = {tab.label} ))} + +