From 1c5a803e1b56913ea82785e23e3fa0ad7b31f0fe Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 27 Sep 2026 19:57:24 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix(M02):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EB=8F=84=EB=A9=B4=20=EB=AF=B8=EB=A6=AC=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=ED=8A=B9=EC=84=B1=C2=B7=EB=8F=84=EB=A9=B4=EC=B8=B5=20=EC=88=A8?= =?UTF-8?q?=EA=B9=80=20=C2=B7=20CAD=20=EB=B9=88=20=EB=8F=84=EB=A9=B4=20?= =?UTF-8?q?=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 구조물 도면 미리보기(도면 정보 카드)에서 CAD 특성·도면층 패널(.cad-inspector)도 같이 숨김 - B07 CAD `layers` 칸이 없는 도면(빈 구조물 도면 · 「새 도면」 빈 도면 공통)을 열면 "Cannot read properties of undefined (reading 'length')" 로 죽던 것을 고침 (`layers` 없으면 새 도면층 하나로 채움) Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01K7JS47SacjPxZ718QsZKyr --- .../import-entities-from-json.ts | 168 +++++++++--------- .../M02_MasterTemplete_Structure.ts | 3 +- 2 files changed, 86 insertions(+), 85 deletions(-) diff --git a/B07_DesignDetail/openwebcad/src/helpers/import-export-handlers/import-entities-from-json.ts b/B07_DesignDetail/openwebcad/src/helpers/import-export-handlers/import-entities-from-json.ts index 6cfbf049..f02d5cd2 100644 --- a/B07_DesignDetail/openwebcad/src/helpers/import-export-handlers/import-entities-from-json.ts +++ b/B07_DesignDetail/openwebcad/src/helpers/import-export-handlers/import-entities-from-json.ts @@ -1,22 +1,22 @@ -import { compact } from 'es-toolkit'; -import { ArcEntity, type ArcJsonData } from '../../entities/ArcEntity'; -import { CircleEntity, type CircleJsonData } from '../../entities/CircleEntity'; -import { type Entity, EntityName, type JsonEntity } from '../../entities/Entity'; -import { HatchEntity, type HatchJsonData } from '../../entities/HatchEntity'; -import { ImageEntity, type ImageJsonData } from '../../entities/ImageEntity.ts'; -import { LineEntity, type LineJsonData } from '../../entities/LineEntity'; -import { MeasurementEntity, type MeasurementJsonData } from '../../entities/MeasurementEntity.ts'; -import { PointEntity, type PointJsonData } from '../../entities/PointEntity'; -import { PolyLineEntity, type PolyLineJsonData } from '../../entities/PolyLineEntity.ts'; -import { RectangleEntity, type RectangleJsonData } from '../../entities/RectangleEntity'; -import { TableEntity, type TableJsonData } from '../../entities/TableEntity.ts'; -import { TextEntity, type TextJsonData } from '../../entities/TextEntity.ts'; -import { setActiveLayerId, setEntities, setLayers } from '../../state'; -import { getNewLayer } from '../get-new-layer.ts'; +import { compact } from "es-toolkit"; +import { ArcEntity, type ArcJsonData } from "../../entities/ArcEntity"; +import { CircleEntity, type CircleJsonData } from "../../entities/CircleEntity"; +import { type Entity, EntityName, type JsonEntity } from "../../entities/Entity"; +import { HatchEntity, type HatchJsonData } from "../../entities/HatchEntity"; +import { ImageEntity, type ImageJsonData } from "../../entities/ImageEntity.ts"; +import { LineEntity, type LineJsonData } from "../../entities/LineEntity"; +import { MeasurementEntity, type MeasurementJsonData } from "../../entities/MeasurementEntity.ts"; +import { PointEntity, type PointJsonData } from "../../entities/PointEntity"; +import { PolyLineEntity, type PolyLineJsonData } from "../../entities/PolyLineEntity.ts"; +import { RectangleEntity, type RectangleJsonData } from "../../entities/RectangleEntity"; +import { TableEntity, type TableJsonData } from "../../entities/TableEntity.ts"; +import { TextEntity, type TextJsonData } from "../../entities/TextEntity.ts"; +import { setActiveLayerId, setEntities, setLayers } from "../../state"; +import { getNewLayer } from "../get-new-layer.ts"; import type { - JsonDrawingFileDeserialized, - JsonDrawingFileSerialized, -} from './export-entities-to-json'; + JsonDrawingFileDeserialized, + JsonDrawingFileSerialized, +} from "./export-entities-to-json"; /** * Open a file selection dialog to select *.json files @@ -25,83 +25,83 @@ import type { * Set the entities in the state */ export function importEntitiesFromJsonFile(file: File | null | undefined) { - return new Promise((resolve) => { - if (!file) return; + return new Promise((resolve) => { + if (!file) return; - const reader = new FileReader(); - reader.addEventListener('load', async () => { - const json = reader.result as string; - const file = await getEntitiesAndLayersFromJsonString(json); - setEntities(file.entities); - setLayers(file.layers); - setActiveLayerId(file.layers[0].id); - resolve(); - }); - reader.readAsText(file, 'utf-8'); - }); + const reader = new FileReader(); + reader.addEventListener("load", async () => { + const json = reader.result as string; + const file = await getEntitiesAndLayersFromJsonString(json); + setEntities(file.entities); + setLayers(file.layers); + setActiveLayerId(file.layers[0].id); + resolve(); + }); + reader.readAsText(file, "utf-8"); + }); } export async function getEntitiesAndLayersFromJsonObject( - data: JsonDrawingFileSerialized + data: JsonDrawingFileSerialized, ): Promise { - const entityPromises: Promise[] = compact( - data.entities.map((entity) => { - switch (entity.type) { - case EntityName.Arc: - return ArcEntity.fromJson(entity as JsonEntity); - case EntityName.Circle: - return CircleEntity.fromJson(entity as JsonEntity); - case EntityName.Line: - return LineEntity.fromJson(entity as JsonEntity); - case EntityName.Point: - return PointEntity.fromJson(entity as JsonEntity); - case EntityName.Rectangle: - return RectangleEntity.fromJson(entity as JsonEntity); - case EntityName.Text: - return TextEntity.fromJson(entity as JsonEntity); - case EntityName.Measurement: - return MeasurementEntity.fromJson(entity as JsonEntity); - case EntityName.Image: - return ImageEntity.fromJson(entity as JsonEntity); - case EntityName.PolyLine: - return PolyLineEntity.fromJson(entity as JsonEntity); - case EntityName.Hatch: - return HatchEntity.fromJson(entity as JsonEntity); - case EntityName.Table: - return TableEntity.fromJson(entity as JsonEntity); + const entityPromises: Promise[] = compact( + data.entities.map((entity) => { + switch (entity.type) { + case EntityName.Arc: + return ArcEntity.fromJson(entity as JsonEntity); + case EntityName.Circle: + return CircleEntity.fromJson(entity as JsonEntity); + case EntityName.Line: + return LineEntity.fromJson(entity as JsonEntity); + case EntityName.Point: + return PointEntity.fromJson(entity as JsonEntity); + case EntityName.Rectangle: + return RectangleEntity.fromJson(entity as JsonEntity); + case EntityName.Text: + return TextEntity.fromJson(entity as JsonEntity); + case EntityName.Measurement: + return MeasurementEntity.fromJson(entity as JsonEntity); + case EntityName.Image: + return ImageEntity.fromJson(entity as JsonEntity); + case EntityName.PolyLine: + return PolyLineEntity.fromJson(entity as JsonEntity); + case EntityName.Hatch: + return HatchEntity.fromJson(entity as JsonEntity); + case EntityName.Table: + return TableEntity.fromJson(entity as JsonEntity); - default: - throw new Error(`Invalid entity type: ${entity.type}`); - } - }) - ); + default: + throw new Error(`Invalid entity type: ${entity.type}`); + } + }), + ); - const entities = compact(await Promise.all(entityPromises)); - // 순번이 아니라 id로 되돌린다 — compact가 null을 걷어 index가 어긋날 수 있다. - const groupIdById = new Map(data.entities.map((entity) => [entity.id, entity.groupId])); - for (const entity of entities) { - const groupId = groupIdById.get(entity.id); - if (groupId) entity.groupId = groupId; - } - let layers = data.layers; - if (data.layers.length === 0) { - layers = [getNewLayer()]; - } - return { - entities, - layers, - }; + const entities = compact(await Promise.all(entityPromises)); + // 순번이 아니라 id로 되돌린다 — compact가 null을 걷어 index가 어긋날 수 있다. + const groupIdById = new Map(data.entities.map((entity) => [entity.id, entity.groupId])); + for (const entity of entities) { + const groupId = groupIdById.get(entity.id); + if (groupId) entity.groupId = groupId; + } + let layers = data.layers; + if (!layers || layers.length === 0) { + layers = [getNewLayer()]; + } + return { + entities, + layers, + }; } export async function getEntitiesAndLayersFromJsonString( - json: string + json: string, ): Promise { - const data = JSON.parse(json) as JsonDrawingFileSerialized; + const data = JSON.parse(json) as JsonDrawingFileSerialized; - if (!data.entities) { - throw new Error('Invalid JSON file'); - } + if (!data.entities) { + throw new Error("Invalid JSON file"); + } - // TODO use map limit to avoid overloading the event loop - return await getEntitiesAndLayersFromJsonObject(data); + // TODO use map limit to avoid overloading the event loop + return await getEntitiesAndLayersFromJsonObject(data); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index 0dee58a3..8d90fec8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -138,7 +138,8 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane if (!inner?.head || inner.getElementById("m02-struct-hide")) return; const style = inner.createElement("style"); style.id = "m02-struct-hide"; - style.textContent = ".cad-titlebar, .cad-ribbon { display: none !important; }"; + style.textContent = + ".cad-titlebar, .cad-ribbon, .cad-inspector { display: none !important; }"; inner.head.append(style); }; frame.addEventListener("load", apply); From 4a6e0ffb3a7d752f7aa263f839a87e99dc91e492 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 27 Sep 2026 19:58:39 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(B07):=20CAD=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=BF=8C=EB=A6=AC=20=EC=84=9C=EC=8B=9D=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=90=98=EB=8F=8C=EB=A6=BC(=EB=B0=A9=EA=B8=88=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B=EC=97=90=20=EB=94=B8=EB=A0=A4=20=EC=98=A8=20=EC=84=9C?= =?UTF-8?q?=EC=8B=9D=20=EC=82=AC=EA=B3=A0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 바로 앞 커밋에 뿌리 prettier 서식(큰따옴표·스페이스)이 실수로 같이 실려 옴 — 이 파일은 openwebcad 자기 서식(biome · tab · 홑따옴표)을 그대로 씀 · 논리는 그대로. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01K7JS47SacjPxZ718QsZKyr --- .../import-entities-from-json.ts | 168 +++++++++--------- 1 file changed, 84 insertions(+), 84 deletions(-) diff --git a/B07_DesignDetail/openwebcad/src/helpers/import-export-handlers/import-entities-from-json.ts b/B07_DesignDetail/openwebcad/src/helpers/import-export-handlers/import-entities-from-json.ts index f02d5cd2..6783c378 100644 --- a/B07_DesignDetail/openwebcad/src/helpers/import-export-handlers/import-entities-from-json.ts +++ b/B07_DesignDetail/openwebcad/src/helpers/import-export-handlers/import-entities-from-json.ts @@ -1,22 +1,22 @@ -import { compact } from "es-toolkit"; -import { ArcEntity, type ArcJsonData } from "../../entities/ArcEntity"; -import { CircleEntity, type CircleJsonData } from "../../entities/CircleEntity"; -import { type Entity, EntityName, type JsonEntity } from "../../entities/Entity"; -import { HatchEntity, type HatchJsonData } from "../../entities/HatchEntity"; -import { ImageEntity, type ImageJsonData } from "../../entities/ImageEntity.ts"; -import { LineEntity, type LineJsonData } from "../../entities/LineEntity"; -import { MeasurementEntity, type MeasurementJsonData } from "../../entities/MeasurementEntity.ts"; -import { PointEntity, type PointJsonData } from "../../entities/PointEntity"; -import { PolyLineEntity, type PolyLineJsonData } from "../../entities/PolyLineEntity.ts"; -import { RectangleEntity, type RectangleJsonData } from "../../entities/RectangleEntity"; -import { TableEntity, type TableJsonData } from "../../entities/TableEntity.ts"; -import { TextEntity, type TextJsonData } from "../../entities/TextEntity.ts"; -import { setActiveLayerId, setEntities, setLayers } from "../../state"; -import { getNewLayer } from "../get-new-layer.ts"; +import { compact } from 'es-toolkit'; +import { ArcEntity, type ArcJsonData } from '../../entities/ArcEntity'; +import { CircleEntity, type CircleJsonData } from '../../entities/CircleEntity'; +import { type Entity, EntityName, type JsonEntity } from '../../entities/Entity'; +import { HatchEntity, type HatchJsonData } from '../../entities/HatchEntity'; +import { ImageEntity, type ImageJsonData } from '../../entities/ImageEntity.ts'; +import { LineEntity, type LineJsonData } from '../../entities/LineEntity'; +import { MeasurementEntity, type MeasurementJsonData } from '../../entities/MeasurementEntity.ts'; +import { PointEntity, type PointJsonData } from '../../entities/PointEntity'; +import { PolyLineEntity, type PolyLineJsonData } from '../../entities/PolyLineEntity.ts'; +import { RectangleEntity, type RectangleJsonData } from '../../entities/RectangleEntity'; +import { TableEntity, type TableJsonData } from '../../entities/TableEntity.ts'; +import { TextEntity, type TextJsonData } from '../../entities/TextEntity.ts'; +import { setActiveLayerId, setEntities, setLayers } from '../../state'; +import { getNewLayer } from '../get-new-layer.ts'; import type { - JsonDrawingFileDeserialized, - JsonDrawingFileSerialized, -} from "./export-entities-to-json"; + JsonDrawingFileDeserialized, + JsonDrawingFileSerialized, +} from './export-entities-to-json'; /** * Open a file selection dialog to select *.json files @@ -25,83 +25,83 @@ import type { * Set the entities in the state */ export function importEntitiesFromJsonFile(file: File | null | undefined) { - return new Promise((resolve) => { - if (!file) return; + return new Promise((resolve) => { + if (!file) return; - const reader = new FileReader(); - reader.addEventListener("load", async () => { - const json = reader.result as string; - const file = await getEntitiesAndLayersFromJsonString(json); - setEntities(file.entities); - setLayers(file.layers); - setActiveLayerId(file.layers[0].id); - resolve(); - }); - reader.readAsText(file, "utf-8"); - }); + const reader = new FileReader(); + reader.addEventListener('load', async () => { + const json = reader.result as string; + const file = await getEntitiesAndLayersFromJsonString(json); + setEntities(file.entities); + setLayers(file.layers); + setActiveLayerId(file.layers[0].id); + resolve(); + }); + reader.readAsText(file, 'utf-8'); + }); } export async function getEntitiesAndLayersFromJsonObject( - data: JsonDrawingFileSerialized, + data: JsonDrawingFileSerialized ): Promise { - const entityPromises: Promise[] = compact( - data.entities.map((entity) => { - switch (entity.type) { - case EntityName.Arc: - return ArcEntity.fromJson(entity as JsonEntity); - case EntityName.Circle: - return CircleEntity.fromJson(entity as JsonEntity); - case EntityName.Line: - return LineEntity.fromJson(entity as JsonEntity); - case EntityName.Point: - return PointEntity.fromJson(entity as JsonEntity); - case EntityName.Rectangle: - return RectangleEntity.fromJson(entity as JsonEntity); - case EntityName.Text: - return TextEntity.fromJson(entity as JsonEntity); - case EntityName.Measurement: - return MeasurementEntity.fromJson(entity as JsonEntity); - case EntityName.Image: - return ImageEntity.fromJson(entity as JsonEntity); - case EntityName.PolyLine: - return PolyLineEntity.fromJson(entity as JsonEntity); - case EntityName.Hatch: - return HatchEntity.fromJson(entity as JsonEntity); - case EntityName.Table: - return TableEntity.fromJson(entity as JsonEntity); + const entityPromises: Promise[] = compact( + data.entities.map((entity) => { + switch (entity.type) { + case EntityName.Arc: + return ArcEntity.fromJson(entity as JsonEntity); + case EntityName.Circle: + return CircleEntity.fromJson(entity as JsonEntity); + case EntityName.Line: + return LineEntity.fromJson(entity as JsonEntity); + case EntityName.Point: + return PointEntity.fromJson(entity as JsonEntity); + case EntityName.Rectangle: + return RectangleEntity.fromJson(entity as JsonEntity); + case EntityName.Text: + return TextEntity.fromJson(entity as JsonEntity); + case EntityName.Measurement: + return MeasurementEntity.fromJson(entity as JsonEntity); + case EntityName.Image: + return ImageEntity.fromJson(entity as JsonEntity); + case EntityName.PolyLine: + return PolyLineEntity.fromJson(entity as JsonEntity); + case EntityName.Hatch: + return HatchEntity.fromJson(entity as JsonEntity); + case EntityName.Table: + return TableEntity.fromJson(entity as JsonEntity); - default: - throw new Error(`Invalid entity type: ${entity.type}`); - } - }), - ); + default: + throw new Error(`Invalid entity type: ${entity.type}`); + } + }) + ); - const entities = compact(await Promise.all(entityPromises)); - // 순번이 아니라 id로 되돌린다 — compact가 null을 걷어 index가 어긋날 수 있다. - const groupIdById = new Map(data.entities.map((entity) => [entity.id, entity.groupId])); - for (const entity of entities) { - const groupId = groupIdById.get(entity.id); - if (groupId) entity.groupId = groupId; - } - let layers = data.layers; - if (!layers || layers.length === 0) { - layers = [getNewLayer()]; - } - return { - entities, - layers, - }; + const entities = compact(await Promise.all(entityPromises)); + // 순번이 아니라 id로 되돌린다 — compact가 null을 걷어 index가 어긋날 수 있다. + const groupIdById = new Map(data.entities.map((entity) => [entity.id, entity.groupId])); + for (const entity of entities) { + const groupId = groupIdById.get(entity.id); + if (groupId) entity.groupId = groupId; + } + let layers = data.layers; + if (!layers || layers.length === 0) { + layers = [getNewLayer()]; + } + return { + entities, + layers, + }; } export async function getEntitiesAndLayersFromJsonString( - json: string, + json: string ): Promise { - const data = JSON.parse(json) as JsonDrawingFileSerialized; + const data = JSON.parse(json) as JsonDrawingFileSerialized; - if (!data.entities) { - throw new Error("Invalid JSON file"); - } + if (!data.entities) { + throw new Error('Invalid JSON file'); + } - // TODO use map limit to avoid overloading the event loop - return await getEntitiesAndLayersFromJsonObject(data); + // TODO use map limit to avoid overloading the event loop + return await getEntitiesAndLayersFromJsonObject(data); } From 2dc0a71ba788269d795e9e348847c897640d6c15 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 27 Sep 2026 20:00:53 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(cad):=20=EB=A6=AC=EB=B3=B8=20=EC=A0=91?= =?UTF-8?q?=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=C2=B7=20M02=20=EB=8F=84?= =?UTF-8?q?=EB=A9=B4=20=ED=8E=B8=EC=A7=91=EC=9D=84=20=EC=A0=91=ED=9E=8C=20?= =?UTF-8?q?=EC=B1=84=EB=A1=9C=20=EC=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ui-state.ts ribbonCollapsed · Ribbon.tsx 접기 단추 · 탭 누르면 펼침 - App.css --cad-ribbon-height 를 Toolbar.tsx 가 접힘 때 26px 로 - aislo-drawing-bridge.ts load 곁값 ribbonCollapsed 받아 반영 - cad_host.ts CadLoadExtra 칸 추가 · M02_MasterTemplete_Drawing.ts 가 true 로 보냄 (도면 양식 · 구조물 도면 둘 다 이 파일 한 곳을 씀) - B07 은 곁값을 안 보내 펼침 그대로(코드 확인 · cad.load 호출 둘 다 5번째 인자 없음) - 구조물 편집 머리 제목(② 열 머리 글)은 이미 다른 창이 끝냈음(commit 65f43ca0) — 손대지 않음 검증: CAD 빌드 · 루트 npm run typecheck · M02 시험 18개(구조물·도면 영역) 통과 · ORCA 로 도면 양식(00_template_A1) 처음 접힘(26px) → 단추로 펼침(122px) 확인. 구조물 도면(rv 등) 은 서버가 만드는 빈 문서에 `layers` 키가 없어 CAD import 단계에서 조용히 실패(레이어 접근 에러) — 리본 접기와 무관한 기존 결함 · 손대지 않음(브레인에게 보고). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01EUypcnp5d1gU2aeKh9F2H7 --- B07_DesignDetail/openwebcad/src/App.css | 16 +++++++ .../openwebcad/src/components/Toolbar.tsx | 7 ++- .../openwebcad/src/components/ui-state.ts | 8 ++++ .../src/integration/aislo-drawing-bridge.ts | 4 ++ .../openwebcad/src/ribbon/Ribbon.tsx | 45 +++++++++++++------ .../M02_MasterTemplete_Drawing.ts | 8 +++- ui_template/cad_host/cad_host.ts | 2 + 7 files changed, 75 insertions(+), 15 deletions(-) diff --git a/B07_DesignDetail/openwebcad/src/App.css b/B07_DesignDetail/openwebcad/src/App.css index fadc2fdc..1091d467 100644 --- a/B07_DesignDetail/openwebcad/src/App.css +++ b/B07_DesignDetail/openwebcad/src/App.css @@ -149,6 +149,7 @@ body > canvas[data-id="canvas"] { flex-direction: column; background: var(--cad-chrome-raised); border-bottom: 1px solid var(--cad-line); + transition: height 120ms ease; } .cad-ribbon-tabs { display: flex; @@ -157,6 +158,21 @@ body > canvas[data-id="canvas"] { padding: 0 8px; border-bottom: 1px solid var(--cad-line); } +.cad-ribbon-tabs__collapse { + margin-left: auto; + width: 24px; + height: 20px; + align-self: center; + padding: 0; + border: 1px solid transparent; + border-radius: 4px; + background: transparent; + color: var(--cad-text-dim); + font-size: 11px; +} +.cad-ribbon-tabs__collapse:hover { + background: var(--cad-hover); +} .cad-ribbon-tabs button { padding: 0 14px; border: 1px solid transparent; diff --git a/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx b/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx index c45c847c..f197d58e 100644 --- a/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx +++ b/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx @@ -12,6 +12,7 @@ import { QuickAccessBar } from './QuickAccessBar'; import { Ribbon } from '../ribbon/Ribbon'; import { StatusBar } from './StatusBar'; import { useCadRefresh } from './use-cad-refresh'; +import { isRibbonCollapsed } from './ui-state'; import { getActiveToolActor } from '../state'; import type { Tool } from '../tools'; import { ViewControls } from './ViewControls'; @@ -22,6 +23,7 @@ export const Toolbar: FC = () => { const [commandLineVisible, setCommandLineVisible] = useState(false); const activeTool = (getActiveToolActor()?.getSnapshot()?.context?.type ?? null) as Tool | null; + const ribbonCollapsed = isRibbonCollapsed(); useEffect(() => { // 편 상태의 폭은 App.css의 `--cad-panel-width` 기본값을 쓴다. 여기 숫자를 박아 @@ -32,8 +34,11 @@ export const Toolbar: FC = () => { '--cad-command-height', commandLineVisible ? '86px' : '0px' ); + // 리본 접힘 — 탭 줄(26px)만 남기고 패널 줄 자리를 걷는다(`Ribbon.tsx` 가 안 그림). + if (ribbonCollapsed) document.documentElement.style.setProperty('--cad-ribbon-height', '26px'); + else document.documentElement.style.removeProperty('--cad-ribbon-height'); window.dispatchEvent(new Event('resize')); - }, [panelCollapsed, commandLineVisible]); + }, [panelCollapsed, commandLineVisible, ribbonCollapsed]); return ( <> diff --git a/B07_DesignDetail/openwebcad/src/components/ui-state.ts b/B07_DesignDetail/openwebcad/src/components/ui-state.ts index b6a19552..868f1375 100644 --- a/B07_DesignDetail/openwebcad/src/components/ui-state.ts +++ b/B07_DesignDetail/openwebcad/src/components/ui-state.ts @@ -12,6 +12,8 @@ let inspectorCollapsed = false; let quickPropertiesVisible = false; let activeRibbonTab = 'home'; let blockLibraryVisible = false; +/** 호스트(M02)가 처음부터 접어 실을 때 true — 탭을 누르면 펼침(`setActiveRibbonTab`). */ +let ribbonCollapsed = false; const notify = () => notifyWindow(HtmlEvent.UPDATE_STATE); @@ -20,12 +22,18 @@ export const isInspectorCollapsed = () => inspectorCollapsed; export const isQuickPropertiesVisible = () => quickPropertiesVisible; export const getActiveRibbonTab = () => activeRibbonTab; export const isBlockLibraryVisible = () => blockLibraryVisible; +export const isRibbonCollapsed = () => ribbonCollapsed; export function setActiveRibbonTab(tabId: string): void { activeRibbonTab = tabId; notify(); } +export function setRibbonCollapsed(collapsed: boolean): void { + ribbonCollapsed = collapsed; + notify(); +} + export function openInspector(tab: InspectorTab): void { inspectorTab = tab; inspectorCollapsed = false; diff --git a/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts b/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts index a29f203c..b42e3130 100644 --- a/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts +++ b/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts @@ -25,6 +25,7 @@ import { setHostTitle, setLayers, } from '../state.ts'; +import { setRibbonCollapsed } from '../components/ui-state.ts'; import { toast } from 'react-toastify'; import { runCommandInput } from '../commands/run-command.ts'; import { setRecoveryScope } from '../helpers/autosave.ts'; @@ -57,6 +58,8 @@ interface DrawingLoadMessage { hostTitle?: string; /** 저장(💾 · Ctrl+S)을 부모 [저장]에 맡기는가. */ hostSave?: boolean; + /** 리본을 접힌 채로 여는가 — 없으면(B07) 펼침 그대로. */ + ribbonCollapsed?: boolean; } interface DrawingSaveRequestMessage { @@ -207,6 +210,7 @@ export function registerAisloDrawingBridge() { setHostReadOnly(event.data.readOnly === true); setHostTitle(event.data.hostTitle ?? 'B07 상세 설계'); setHostSave(event.data.hostSave === true); + setRibbonCollapsed(event.data.ribbonCollapsed === true); if (event.data.frameEdit) applyFramePreview(); // 앞 도면에서 켜 둔 그리기 도구를 내린다. 안 내리면 **확정한 도면 위에도** // 그 도구가 계속 그린다 — 읽기 전용은 새 명령만 막기 때문이다(2026-09-01 실측: diff --git a/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx b/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx index 57fe1f00..b46d3c77 100644 --- a/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx +++ b/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx @@ -3,7 +3,12 @@ import { type FC, type ReactNode, useEffect, useRef, useState } from 'react'; import type { CadCommand } from '../commands/command.types'; import { getCommandById } from '../commands/registry'; import { runCommand } from '../commands/run-command'; -import { getActiveRibbonTab, setActiveRibbonTab } from '../components/ui-state'; +import { + getActiveRibbonTab, + isRibbonCollapsed, + setActiveRibbonTab, + setRibbonCollapsed, +} from '../components/ui-state'; import type { Tool } from '../tools'; import { RIBBON_TABS, type RibbonPanel, type RibbonWidget } from './ribbon.config'; @@ -147,6 +152,7 @@ const Panel: FC<{ export const Ribbon: FC = ({ activeTool, widgets, onCommandRun }) => { const activeTabId = getActiveRibbonTab(); const activeTab = RIBBON_TABS.find((tab) => tab.id === activeTabId) ?? RIBBON_TABS[0]; + const collapsed = isRibbonCollapsed(); const handleRun = (command: CadCommand) => { runCommand(command); @@ -161,23 +167,36 @@ export const Ribbon: FC = ({ activeTool, widgets, onCommandRun }) = type="button" key={tab.id} data-active={tab.id === activeTab.id} - onClick={() => setActiveRibbonTab(tab.id)} + onClick={() => { + setActiveRibbonTab(tab.id); + if (collapsed) setRibbonCollapsed(false); + }} > {tab.label} ))} + -
- {activeTab.panels.map((panel) => ( - - ))} -
+ {!collapsed && ( +
+ {activeTab.panels.map((panel) => ( + + ))} +
+ )} ); }; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts b/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts index 63ae1738..6c0bec41 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts @@ -189,7 +189,13 @@ export function mountDrawingTemplate( null, !readOnly, {}, - { recoveryScope, readOnly, hostTitle: "도면 양식 편집", hostSave: true }, + { + recoveryScope, + readOnly, + hostTitle: "도면 양식 편집", + hostSave: true, + ribbonCollapsed: true, + }, ); }; diff --git a/ui_template/cad_host/cad_host.ts b/ui_template/cad_host/cad_host.ts index 397138cd..d9c8a3c0 100644 --- a/ui_template/cad_host/cad_host.ts +++ b/ui_template/cad_host/cad_host.ts @@ -63,6 +63,8 @@ export interface CadLoadExtra { hostTitle?: string; /** 저장(💾 · Ctrl+S)을 부모 [저장]에 맡긴다 — 켜면 `onHostSave` 로 옴. */ hostSave?: boolean; + /** 리본을 접힌 채로 연다 — 안 보내면 펼침 그대로. */ + ribbonCollapsed?: boolean; } export interface CadHost {