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 (
-
- );
-};
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}
))}
+
+
' in ribbon
+ assert ribbon.index("cad-ribbon-tabs__slot") < ribbon.index("")
+ assert "'--cad-tab-slot-left', `${Math.round(s.left)}px`" in ribbon
+ assert "'--cad-tab-slot-right', `${Math.round(window.innerWidth - s.right)}px`" in ribbon
+ assert "hidden={QUICK_ACCESS_HIDDEN.includes(id)}" in quick and "cad-titlebar" not in quick
+ assert "QUICK_ACCESS_HIDDEN: string[] = ['QSAVE', 'EXPORT']" in (
+ src / "ribbon/ribbon.config.ts"
+ ).read_text("utf-8")
+ assert "--cad-title-height: 0px;" in css and ".cad-titlebar" not in css
+ assert ".cad-qtable" not in css and not (src / "components/QuantityPanel.tsx").exists()
+ assert "QuantityPanel" not in (src / "App.tsx").read_text("utf-8")
+ assert "html .cad-ribbon-tabs {" in (src / "integration/host-burger.css").read_text("utf-8")
def test_여러_장_문서_장_도번_백업_칸(got):
diff --git a/resources/tester/test_cad_touch.py b/resources/tester/test_cad_touch.py
index 8249943bf..b682dea34 100644
--- a/resources/tester/test_cad_touch.py
+++ b/resources/tester/test_cad_touch.py
@@ -1,14 +1,16 @@
"""B07 CAD 손가락(PLAN_기기대응 4-1 · `openwebcad/src/inputController/input-touch.ts`) — 빌드된 CAD 화면.
-① 손가락 기기 = 명령 단추 6개(44px) · 마우스 기기 = 안 보임 ② 확대 막기(user-scalable=no) 없음
+① 자판 없는 기기(hover:none · pointer:coarse) = 명령 단추 6개(44px) · 마우스 · 터치 노트북 = 안 보임(요구 280-6)
+② 확대 막기(user-scalable=no) 없음
③ 캔버스 touch-action none · 두 손가락 벌림 · 한 손가락 끌기가 페이지를 확대 · 밀지 않음(페이지 오류 0)
"""
+import os
import urllib.request
import pytest
-URL = "http://localhost:5174/b07-cad/"
+URL = os.environ.get("CAD_TOUCH_URL", "http://localhost:5174/b07-cad/")
def _up() -> bool:
@@ -42,7 +44,12 @@ def test_cad_touch_bar_and_gestures():
mouse.evaluate("getComputedStyle(document.querySelector('.cad-touch-bar')).display")
== "none"
)
-
+ # 터치 노트북(any-pointer: coarse · 주 입력 마우스) — 단추는 hover:none 일 때만(에뮬은 터치 노트북을 못 꾸밈)
+ media = mouse.evaluate(
+ "[...document.styleSheets].flatMap(s => [...s.cssRules]).filter(r => r.media"
+ " && [...r.cssRules].some(c => c.selectorText === '.cad-touch-bar')).map(r => r.media.mediaText)"
+ )
+ assert media == ["(hover: none) and (pointer: coarse)"], media
ctx = browser.new_context(
viewport={"width": 1180, "height": 820}, has_touch=True, is_mobile=True
)