diff --git a/B07_DesignDetail/B07_DesignDetail_UI_FrameEdit.ts b/B07_DesignDetail/B07_DesignDetail_UI_FrameEdit.ts index 7cd15aff..85c80f78 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_FrameEdit.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_FrameEdit.ts @@ -16,23 +16,21 @@ import { resetFrameTemplate, saveFrameTemplate, } from "./B07_DesignDetail_Api_Fetch"; -import { createPlaceholderPalette } from "./B07_DesignDetail_UI_FramePlaceholders"; export interface FrameTemplateEditor { /** 도면 목록 아래에 놓는 「도각 편집」 버튼. */ button: HTMLButtonElement; /** 편집 중임을 알리는 띠 — 도면 목록 하단 액션 칸의 1행 (평소엔 숨김). */ banner: HTMLElement; - /** 자리표 목록 — 편집 중에만 보인다. */ - tokens: HTMLElement; /** 편집 중인가 — 도면 변경 알림(확정 해제)을 이 동안 막는 데 쓴다. */ isEditing: () => boolean; } interface Options { projectId: string; - /** CAD에 도면을 싣는다 (meta null이면 수량 패널을 숨긴다). */ - sendLoad: (drawing: CadDrawing, meta: null) => void; + /** CAD에 도면을 싣는다 (meta null이면 수량 패널을 숨긴다). + * frameEdit 을 켜면 캐드 안 자리표 패널이 함께 뜬다. */ + sendLoad: (drawing: CadDrawing, meta: null, frameEdit?: boolean) => void; /** CAD에서 현재 편집본을 받아온다. */ requestCadDrawing: () => Promise; /** 편집을 마친 뒤 보던 도면으로 돌아간다. */ @@ -92,11 +90,6 @@ export function createFrameTemplateEditor(options: Options): FrameTemplateEditor bannerButtons.append(finishButton, importButton, resetButton, cancelButton); banner.append(bannerButtons, fileInput); - const palette = createPlaceholderPalette({ - requestCadDrawing: () => options.requestCadDrawing(), - sendLoad: (drawing, meta) => options.sendLoad(drawing, meta), - }); - const button = createButton({ label: "도각 편집", variant: "ghost", @@ -106,7 +99,6 @@ export function createFrameTemplateEditor(options: Options): FrameTemplateEditor const leave = (): void => { editing = false; banner.hidden = true; - palette.setVisible(false); button.disabled = false; options.restoreDrawing(); }; @@ -118,7 +110,7 @@ export function createFrameTemplateEditor(options: Options): FrameTemplateEditor importButton.disabled = true; try { const response = await importFrameTemplate(options.projectId, file); - options.sendLoad(response.drawing, null); + options.sendLoad(response.drawing, null, true); label.textContent = `${file.name} 을(를) 불러왔습니다 — 자리표를 놓고 [완료]를 누르십시오.`; showToast(`도형 ${response.entity_count}개를 불러왔습니다.`, "success"); } catch (error) { @@ -137,11 +129,10 @@ export function createFrameTemplateEditor(options: Options): FrameTemplateEditor editing = true; button.disabled = true; banner.hidden = false; - palette.setVisible(true); label.textContent = response.customized ? "도각 편집 중 — 회사 도각을 고치고 있습니다." : "도각 편집 중 — 기본 도각을 고치면 회사 도각으로 저장됩니다."; - options.sendLoad(response.drawing, null); + options.sendLoad(response.drawing, null, true); } catch (error) { showToast(error instanceof Error ? error.message : "도각을 불러오지 못했습니다.", "error"); } @@ -187,5 +178,5 @@ export function createFrameTemplateEditor(options: Options): FrameTemplateEditor } } - return { button, banner, tokens: palette.root, isEditing: () => editing }; + return { button, banner, isEditing: () => editing }; } diff --git a/B07_DesignDetail/B07_DesignDetail_UI_FramePlaceholders.ts b/B07_DesignDetail/B07_DesignDetail_UI_FramePlaceholders.ts deleted file mode 100644 index cd5b41f3..00000000 --- a/B07_DesignDetail/B07_DesignDetail_UI_FramePlaceholders.ts +++ /dev/null @@ -1,196 +0,0 @@ -/** - * B07 도각 자리표 배치 — 프로그램 값이 들어갈 자리를 사용자가 직접 놓는다 (2026-09-06 사용자 확정). - * - * 값을 알아맞히는 규칙은 만들지 않는다. 여기서 놓은 `{{키}}` 토큰을 출력 때 기존 치환 - * 엔진(`_fill_placeholders`)이 그대로 채운다. 자리표를 안 놓은 값은 빈칸으로 남는다. - * - * 놓는 방식 — 단추를 누르면 도면 한가운데에 자리표가 서고, 그 뒤 캐드의 이동·크기 도구로 - * 자리를 잡는다. 캐드 안쪽 코드는 건드리지 않는다(도면을 통째로 다시 싣는 방식). - */ - -import { createButton, showToast } from "@ui/ui_template_elements"; -import type { CadDrawing } from "./B07_DesignDetail_Api_Fetch"; - -/** 글자 자리표 — 출력 때 표제란 값으로 바뀐다. */ -const TEXT_TOKENS: readonly string[] = [ - "도면명", - "도면번호", - "공사명", - "위치", - "시행청", - "용역회사", - "연도기번", - "사업량", - "과업책임자", - "분야별책임자", - "설계자", - "설계일자", - "축척_A1", - "축척_A3", -]; - -/** 그림 자리표 — 회사 로고와 사람 서명. 값이 없으면 그림째 빠진다. */ -const IMAGE_TOKENS: readonly string[] = [ - "회사로고", - "과업책임자서명", - "분야별책임자서명", - "설계자서명", -]; - -const TEXT_SIZE_MM = 5; -const IMAGE_WIDTH_MM = 32; -const IMAGE_HEIGHT_MM = 16; -/** 겹쳐 놓지 않도록 하나 놓을 때마다 이만큼 내려 찍는다. */ -const STACK_STEP_MM = 8; - -interface Options { - /** CAD에서 현재 편집본을 받아온다. */ - requestCadDrawing: () => Promise; - /** CAD에 도면을 다시 싣는다. */ - sendLoad: (drawing: CadDrawing, meta: null) => void; -} - -interface Point { - x: number; - y: number; -} - -/** 엔티티 목록의 한가운데 — 자리표를 처음 놓는 자리. 좌표가 없으면 원점. */ -function centerOf(entities: Record[]): Point { - const xs: number[] = []; - const ys: number[] = []; - const visit = (entity: Record): void => { - const shape = (entity.shapeData ?? {}) as Record; - for (const key of ["startPoint", "endPoint", "basePoint", "point"]) { - const value = shape[key] as Point | undefined; - if (value && typeof value.x === "number" && typeof value.y === "number") { - xs.push(value.x); - ys.push(value.y); - } - } - for (const vertex of (shape.points as Point[] | undefined) ?? []) { - if (vertex && typeof vertex.x === "number") { - xs.push(vertex.x); - ys.push(vertex.y); - } - } - for (const child of (entity.children as Record[] | undefined) ?? []) { - visit(child); - } - }; - for (const entity of entities) visit(entity); - if (xs.length === 0) return { x: 0, y: 0 }; - const mid = (values: number[]): number => (Math.min(...values) + Math.max(...values)) / 2; - return { x: mid(xs), y: mid(ys) }; -} - -function layerIdOf(drawing: CadDrawing): string { - return drawing.layers[0]?.id ?? "0"; -} - -function textEntity(token: string, at: Point, layerId: string): Record { - return { - id: crypto.randomUUID(), - type: "Text", - lineColor: "#f5f7fa", - lineWidth: 1, - layerId, - shapeData: { - label: `{{${token}}}`, - basePoint: { x: at.x, y: at.y }, - options: { - textDirection: { x: 1, y: 0 }, - textAlign: "center", - textColor: "#f5f7fa", - fontSize: TEXT_SIZE_MM, - fontFamily: "sans-serif", - }, - }, - }; -} - -function imageEntity(token: string, at: Point, layerId: string): Record { - const halfWidth = IMAGE_WIDTH_MM / 2; - const halfHeight = IMAGE_HEIGHT_MM / 2; - return { - id: crypto.randomUUID(), - type: "Image", - lineColor: "#f5f7fa", - lineWidth: 1, - layerId, - shapeData: { - points: [ - { x: at.x - halfWidth, y: at.y - halfHeight }, - { x: at.x + halfWidth, y: at.y - halfHeight }, - { x: at.x + halfWidth, y: at.y + halfHeight }, - { x: at.x - halfWidth, y: at.y + halfHeight }, - ], - imageData: `{{${token}}}`, - }, - }; -} - -export interface PlaceholderPalette { - /** 도각 편집 띠 아래에 붙는 자리표 목록 (평소엔 숨김). */ - root: HTMLElement; - setVisible: (visible: boolean) => void; -} - -export function createPlaceholderPalette(options: Options): PlaceholderPalette { - const root = document.createElement("div"); - root.className = "b07-frame-tokens"; - root.hidden = true; - - const hint = document.createElement("span"); - hint.className = "b07-frame-tokens__hint"; - hint.textContent = "자리표 놓기 — 누르면 도면 가운데에 서고, 끌어서 자리를 잡음"; - root.append(hint); - - let placed = 0; - - const place = async (token: string, kind: "text" | "image"): Promise => { - try { - const drawing = await options.requestCadDrawing(); - const center = centerOf(drawing.entities); - const at = { x: center.x, y: center.y - placed * STACK_STEP_MM }; - const layerId = layerIdOf(drawing); - const entity = - kind === "text" ? textEntity(token, at, layerId) : imageEntity(token, at, layerId); - options.sendLoad({ ...drawing, entities: [...drawing.entities, entity] }, null); - placed += 1; - showToast(`「${token}」 자리표를 놓았습니다. 끌어서 자리를 잡으십시오.`, "success"); - } catch (error) { - showToast(error instanceof Error ? error.message : "자리표를 놓지 못했습니다.", "error"); - } - }; - - const group = (label: string, tokens: readonly string[], kind: "text" | "image"): void => { - const box = document.createElement("div"); - box.className = "b07-frame-tokens__group"; - const title = document.createElement("span"); - title.className = "b07-frame-tokens__title"; - title.textContent = label; - box.append(title); - for (const token of tokens) { - box.append( - createButton({ - label: token, - variant: "ghost", - onClick: () => void place(token, kind), - }), - ); - } - root.append(box); - }; - - group("글자", TEXT_TOKENS, "text"); - group("그림", IMAGE_TOKENS, "image"); - - return { - root, - setVisible: (visible: boolean) => { - root.hidden = !visible; - if (!visible) placed = 0; - }, - }; -} diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts index 34517c7e..7db46664 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts @@ -124,7 +124,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { cadHost.append(frame, license); let cadReady = false; - let pendingLoad: { drawing: CadDrawing; meta: DesignMeta | null } | undefined; + let pendingLoad: { drawing: CadDrawing; meta: DesignMeta | null; frameEdit: boolean } | undefined; let currentDrawing: DesignDrawingItem | undefined; let currentIndex = -1; let currentConfirmed = false; @@ -205,11 +205,13 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { hasNext: index < drawings.length - 1, }); - const sendLoad = (drawing: CadDrawing, meta: DesignMeta | null) => { - pendingLoad = { drawing, meta }; + // frameEdit: 도각 편집으로 싣는 도면인가 — 캐드 안 자리표 패널을 이때만 띄운다 + // (2026-09-06 사용자 지시로 패널을 캐드 안으로 옮김). + const sendLoad = (drawing: CadDrawing, meta: DesignMeta | null, frameEdit = false) => { + pendingLoad = { drawing, meta, frameEdit }; if (!cadReady) return; frame.contentWindow?.postMessage( - { type: CAD_LOAD_MESSAGE, drawing, meta }, + { type: CAD_LOAD_MESSAGE, drawing, meta, frameEdit }, window.location.origin, ); pendingLoad = undefined; @@ -446,7 +448,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { ); } else if (message.type === CAD_READY_MESSAGE) { cadReady = true; - if (pendingLoad) sendLoad(pendingLoad.drawing, pendingLoad.meta); + if (pendingLoad) sendLoad(pendingLoad.drawing, pendingLoad.meta, pendingLoad.frameEdit); } else if (message.type === CAD_LOADED_MESSAGE) { cadHost.dataset.loading = "false"; } else if (message.type === CAD_ERROR_MESSAGE) { @@ -481,7 +483,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { const confirmButtonRow = document.createElement("div"); confirmButtonRow.className = "b07-drawing-actions__row"; confirmButtonRow.append(frameEditor.button, confirmButton); - confirmActions.append(frameEditor.banner, frameEditor.tokens, confirmButtonRow); + confirmActions.append(frameEditor.banner, confirmButtonRow); drawingPanel.append(infoPanelHost, confirmActions); diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Style.css b/B07_DesignDetail/B07_DesignDetail_UI_Style.css index 1736bdd1..1dcfce89 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Style.css +++ b/B07_DesignDetail/B07_DesignDetail_UI_Style.css @@ -321,43 +321,3 @@ padding: 4px 8px; font-size: 0.78rem; } - -/* 도각 자리표 목록 — 편집 중에만 보인다 (2026-09-06). 사이드바가 좁아 단추를 감싼다. */ -.b07-frame-tokens { - display: flex; - flex-direction: column; - gap: var(--spacing-4, 4px); - margin-top: var(--spacing-8); - padding: var(--spacing-8); - border: 1px solid var(--color-border); - border-radius: var(--radius-cards); - background-color: var(--color-surface); -} - -.b07-frame-tokens[hidden] { - display: none; -} - -.b07-frame-tokens__hint { - font-size: 0.72rem; - line-height: 1.4; - color: var(--color-text-secondary); -} - -.b07-frame-tokens__group { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 4px; -} - -.b07-frame-tokens__title { - width: 100%; - font-size: 0.72rem; - color: var(--color-text-secondary); -} - -.b07-frame-tokens__group > button { - padding: 2px 6px; - font-size: 0.72rem; -} diff --git a/B07_DesignDetail/openwebcad/src/App.css b/B07_DesignDetail/openwebcad/src/App.css index f69191bf..66c56125 100644 --- a/B07_DesignDetail/openwebcad/src/App.css +++ b/B07_DesignDetail/openwebcad/src/App.css @@ -942,3 +942,58 @@ body > canvas[data-id="canvas"] { color: var(--cad-text-dim); font-size: 11px; } + +/* 도각 자리표 패널 — 도각 편집으로 도면을 실었을 때만 뜬다 (2026-09-06 사용자 지시로 + 부모 사이드바에서 캐드 안으로 옮김). 도면 오른쪽 위, 리본 아래에 붙는다. */ +.cad-frame-tokens { + position: fixed; + /* 오른쪽 위는 진행단계 패널이 쓴다 — 아래쪽(상태막대·명령행 위)에 붙인다. */ + right: 12px; + bottom: calc(var(--cad-status-height) + var(--cad-command-height) + 8px); + z-index: 3; + max-height: 46vh; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 6px; + width: 236px; + padding: 8px; + border: 1px solid var(--cad-line); + border-radius: 6px; + background: var(--cad-chrome-raised); + box-shadow: var(--shadow-lg); + color: var(--cad-text); +} + +.cad-frame-tokens__header { + color: var(--cad-text-dim); + font-size: 11px; + line-height: 1.4; +} + +.cad-frame-tokens__group { + display: flex; + flex-wrap: wrap; + gap: 4px; + align-items: center; +} + +.cad-frame-tokens__title { + width: 100%; + color: var(--cad-text-dim); + font-size: 11px; +} + +.cad-frame-tokens__button { + padding: 3px 7px; + border: 1px solid var(--cad-line); + border-radius: 4px; + background: var(--cad-chrome); + color: var(--cad-text); + font-size: 11px; + cursor: pointer; +} + +.cad-frame-tokens__button:hover { + background: var(--cad-accent-soft, var(--cad-chrome-raised)); +} diff --git a/B07_DesignDetail/openwebcad/src/App.tsx b/B07_DesignDetail/openwebcad/src/App.tsx index d5f6c178..93ce68b1 100644 --- a/B07_DesignDetail/openwebcad/src/App.tsx +++ b/B07_DesignDetail/openwebcad/src/App.tsx @@ -1,5 +1,6 @@ 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'; @@ -8,6 +9,7 @@ function App() {
+
); diff --git a/B07_DesignDetail/openwebcad/src/components/FramePlaceholderPanel.tsx b/B07_DesignDetail/openwebcad/src/components/FramePlaceholderPanel.tsx new file mode 100644 index 00000000..9dc78bd3 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/components/FramePlaceholderPanel.tsx @@ -0,0 +1,132 @@ +import { Point } from '@flatten-js/core'; +import { type FC, useCallback, useEffect, useState } from 'react'; +import { HtmlEvent } from '../App.types'; +import { ImageEntity } from '../entities/ImageEntity'; +import { TextEntity } from '../entities/TextEntity'; +import { + getActiveLayerId, + getEntities, + getScreenCanvasDrawController, + isFrameEditMode, + setEntities, +} from '../state'; + +/** + * 도각 자리표 패널 — 프로그램 값이 들어갈 자리를 사용자가 직접 놓는다 (2026-09-06 사용자 확정). + * + * 값을 알아맞히는 규칙은 만들지 않는다. 여기서 놓은 `{{키}}` 토큰을 도면 출력 때 서버의 + * 치환 엔진이 채운다. 자리표를 안 놓은 값은 빈칸으로 남는다. + * + * 도각 편집으로 도면을 실었을 때만 뜬다. 놓은 자리표는 화면 한가운데에 서고, 그 뒤 + * 캐드의 이동·크기 도구로 자리를 잡는다. + */ + +/** 글자 자리표 — 출력 때 표제란 값으로 바뀐다. */ +const TEXT_TOKENS = [ + '도면명', + '도면번호', + '공사명', + '위치', + '시행청', + '용역회사', + '연도기번', + '사업량', + '과업책임자', + '분야별책임자', + '설계자', + '설계일자', + '축척_A1', + '축척_A3', +] as const; + +/** 그림 자리표 — 회사 로고와 사람 서명. 값이 없으면 도면에서 그림째 빠진다. */ +const IMAGE_TOKENS = ['회사로고', '과업책임자서명', '분야별책임자서명', '설계자서명'] as const; + +const TEXT_SIZE_MM = 5; +const IMAGE_WIDTH_MM = 32; +const IMAGE_HEIGHT_MM = 16; + +/** 지금 보고 있는 화면의 한가운데 (도면 좌표). 자리표가 처음 서는 자리다. */ +function viewCenter(): Point { + const drawController = getScreenCanvasDrawController(); + const size = drawController.getCanvasSize(); + return drawController.targetToWorld(new Point(size.x / 2, size.y / 2)); +} + +function addTextPlaceholder(token: string): void { + const center = viewCenter(); + const entity = new TextEntity(getActiveLayerId(), `{{${token}}}`, center, { + fontSize: TEXT_SIZE_MM, + textAlign: 'center', + }); + setEntities([...getEntities(), entity], true); +} + +async function addImagePlaceholder(token: string): Promise { + const center = viewCenter(); + const halfWidth = IMAGE_WIDTH_MM / 2; + const halfHeight = IMAGE_HEIGHT_MM / 2; + const points = [ + { x: center.x - halfWidth, y: center.y - halfHeight }, + { x: center.x + halfWidth, y: center.y - halfHeight }, + { x: center.x + halfWidth, y: center.y + halfHeight }, + { x: center.x - halfWidth, y: center.y + halfHeight }, + ]; + // 자리표는 그림 주소가 아니라 토큰이라 fromJson 으로 만든다 — 그래야 원본 문자열이 + // 그대로 보존돼 저장 한 번에 주소로 굳지 않는다. + const entity = await ImageEntity.fromJson({ + id: crypto.randomUUID(), + type: 'Image', + lineColor: '#f5f7fa', + lineWidth: 1, + layerId: getActiveLayerId(), + shapeData: { points, imageData: `{{${token}}}` }, + } as Parameters[0]); + setEntities([...getEntities(), entity], true); +} + +export const FramePlaceholderPanel: FC = () => { + const [visible, setVisible] = useState(isFrameEditMode()); + + const refresh = useCallback(() => setVisible(isFrameEditMode()), []); + useEffect(() => { + window.addEventListener(HtmlEvent.UPDATE_STATE, refresh); + return () => window.removeEventListener(HtmlEvent.UPDATE_STATE, refresh); + }, [refresh]); + + if (!visible) return null; + + return ( +
+
+ 자리표 놓기 — 누르면 화면 가운데에 서고, 끌어서 자리를 잡습니다 +
+
+ 글자 + {TEXT_TOKENS.map((token) => ( + + ))} +
+
+ 그림 + {IMAGE_TOKENS.map((token) => ( + + ))} +
+
+ ); +}; diff --git a/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts b/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts index cca181dd..ca2741b8 100644 --- a/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts @@ -1,7 +1,7 @@ import type * as Flatten from '@flatten-js/core'; import { type Box, Point, Polygon, Relations, type Segment, Vector } from '@flatten-js/core'; import { type Shape, type SnapPoint, SnapPointType } from '../App.types'; -import type { DrawController } from '../drawControllers/DrawController.ts'; +import { DEFAULT_TEXT_OPTIONS, type DrawController } from '../drawControllers/DrawController.ts'; import { twoPointBoxToPolygon } from '../helpers/box-to-polygon'; import { getExportColor } from '../helpers/get-export-color'; import { mirrorAngleOverAxis } from '../helpers/mirror-angle-over-axis.ts'; @@ -69,14 +69,28 @@ export class ImageEntity implements Entity { this.lineWidth, this.lineDash ); - // 테두리는 **집었을 때만** 그린다. 늘 그리면 도각의 로고·서명 자리에 흰 사각형이 - // 남고, 출력·내보내기가 같은 draw()를 타므로 산출물에도 실린다(2026-09-02). - if (highlighted || selected) { + // 자리표(`{{회사로고}}` 등)는 그림이 없어 화면에 아무것도 안 보였다 — 도각 편집에서 + // 무엇을 어디에 놓았는지 알 수 없어, 자리표일 때는 테두리와 이름을 늘 그린다 + // (2026-09-06). 출력 때는 서버가 값으로 바꾸거나 엔티티째 빼므로 산출물에 안 실린다. + const placeholder = (this.sourceData ?? '').includes('{{'); + // 그 밖의 그림은 **집었을 때만** 테두리를 그린다. 늘 그리면 도각의 로고 자리에 흰 + // 사각형이 남고, 출력·내보내기가 같은 draw()를 타므로 산출물에도 실린다(2026-09-02). + if (highlighted || selected || placeholder) { for (const edge of polygonToSegments(this.polygon)) { drawController.drawLine(edge.start, edge.end); } } + if (placeholder) { + drawController.drawText(this.sourceData ?? '', this.polygon.box.center, { + ...DEFAULT_TEXT_OPTIONS, + textAlign: 'center', + fontSize: Math.max(this.polygon.box.height / 4, 2), + textColor: this.lineColor, + }); + return; // 그림이 없으니 그릴 것도 없다 + } + const width = this.polygon.box.width; const height = this.polygon.box.height; diff --git a/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts b/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts index e5d135b1..d7b3b99f 100644 --- a/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts +++ b/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts @@ -17,6 +17,7 @@ import { setActiveLayerId, setDesignMeta, setEntities, + setFrameEditMode, setLayers, } from '../state.ts'; import { toast } from 'react-toastify'; @@ -36,6 +37,8 @@ interface DrawingLoadMessage { type: typeof AISLO_DRAWING_LOAD_MESSAGE; drawing: JsonDrawingFileSerialized; meta?: DesignMeta | null; + /** 도각 편집으로 실은 도면인가 — 캐드 안 자리표 패널을 이때만 띄운다. */ + frameEdit?: boolean; } interface DrawingSaveRequestMessage { @@ -148,6 +151,7 @@ export function registerAisloDrawingBridge() { resetUndoBaseline(); // 설계 컨텍스트(제목·측점정보·확정상태·수량표)를 수량 패널에 반영 setDesignMeta(event.data.meta ?? null); + setFrameEditMode(event.data.frameEdit === true); // 앞 도면에서 켜 둔 그리기 도구를 내린다. 안 내리면 **확정한 도면 위에도** // 그 도구가 계속 그린다 — 읽기 전용은 새 명령만 막기 때문이다(2026-09-01 실측: // 확정본에서 클릭 두 번에 선 2개가 늘었다). 새 도면에서 앞 도면의 작도 도중 diff --git a/B07_DesignDetail/openwebcad/src/state.ts b/B07_DesignDetail/openwebcad/src/state.ts index d8b7d70d..69510310 100644 --- a/B07_DesignDetail/openwebcad/src/state.ts +++ b/B07_DesignDetail/openwebcad/src/state.ts @@ -186,6 +186,8 @@ let snapTrackingEnabled = true; * 제목·측점정보·확정상태·수량표를 렌더한다. null이면 패널을 숨긴다. */ let designMeta: DesignMeta | null = null; +/** 도각 편집 모드인가 — 부모(B07 화면)가 도각을 실을 때 켠다. 자리표 패널이 이때만 뜬다. */ +let frameEditMode = false; /** * 실은 뒤로 실제 편집이 있었는가. 도면을 바꾸기 전에 부모가 물어보는 근거다 — @@ -256,6 +258,7 @@ export const getSnapEnabled = () => snapEnabled; export const getGridEnabled = () => gridEnabled; export const getSnapTrackingEnabled = () => snapTrackingEnabled; export const getDesignMeta = (): DesignMeta | null => designMeta; +export const isFrameEditMode = (): boolean => frameEditMode; export const isDrawingDirty = () => drawingDirty; /** * 확정한 도면은 읽기 전용이다 — 그리기·수정·값 편집이 모두 막힌다(2026-09-01 사용자 @@ -486,6 +489,11 @@ export const setDesignMeta = (newMeta: DesignMeta | null) => { designMeta = newMeta; triggerReactUpdate(StateVariable.designMeta); }; +/** 도각 편집 모드 켜고 끄기 — 자리표 패널의 표시 여부를 가른다 (2026-09-06 사용자 지시). */ +export const setFrameEditMode = (enabled: boolean) => { + frameEditMode = enabled; + notifyWindow(HtmlEvent.UPDATE_STATE); +}; // 수량표는 앞 단계(B05·B06) 산출물이라 B07에서 고치지 않는다(2026-09-01 사용자 확정). // 값을 바꾸려면 횡단설계에서 고치고 돌아온다 — 여기 있던 setDesignQuantityTable은 // 어디서도 부르지 않으면서 "고칠 수 있는 값"으로 오해를 남겨 지웠다.