diff --git a/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts b/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts index 37d36b96..34ab74a7 100644 --- a/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts +++ b/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts @@ -175,6 +175,8 @@ export interface FrameTemplateResponse { drawing: CadDrawing; /** 회사가 고친 도각을 쓰고 있으면 true, 프로그램 기본 도각이면 false. */ customized: boolean; + /** 자리표에 보여 줄 실제 값 — 편집 화면 전용이고 저장값은 토큰 그대로다. */ + fields?: Record; } export function fetchFrameTemplate(projectId: string): Promise { diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Frame_Export.py b/B07_DesignDetail/B07_DesignDetail_Engine_Frame_Export.py index 09d7b0e2..535fdaf2 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Frame_Export.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Frame_Export.py @@ -20,6 +20,7 @@ from pathlib import Path from typing import Any import ezdxf +from ezdxf.enums import TextEntityAlignment from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Import import bundled_tool @@ -77,7 +78,11 @@ def _add_entity( shape["label"], dxfattribs={**attribs, "height": height, "rotation": rotation}, ) - text.set_placement(base) + # 자리표는 칸 한가운데에 선다 — 내보낸 파일에서도 같은 자리에 오게 가운데 맞춤. + if options.get("boxWidth") and options.get("boxHeight"): + text.set_placement(base, align=TextEntityAlignment.MIDDLE_CENTER) + else: + text.set_placement(base) elif point := _xy(shape.get("point")): space.add_point(point, dxfattribs=attribs) elif (center := _xy(shape.get("center"))) and isinstance(shape.get("radius"), (int, float)): diff --git a/B07_DesignDetail/B07_DesignDetail_Router.py b/B07_DesignDetail/B07_DesignDetail_Router.py index e8896077..e81516c3 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router.py +++ b/B07_DesignDetail/B07_DesignDetail_Router.py @@ -114,7 +114,7 @@ def _asset_data_url(relative_path: str | None) -> str: return f"data:{mime};base64,{b64encode(blob).decode('ascii')}" -async def _title_block_fields(project_id: UUID) -> dict[str, str]: +async def title_block_fields(project_id: UUID) -> dict[str, str]: """도각 표제란에 채울 값. **DB가 아는 것만** 담고 나머지는 담지 않는다. 담지 않은 자리는 `_fill_placeholders`가 빈칸으로 지운다 — 도각 원본에 남의 값이 @@ -275,7 +275,7 @@ async def get_design_drawing( # 저장 경로는 `storage/{회사}/{사용자}/{프로젝트}` 이므로 두 단계 위가 회사 폴더다. use_company_templates(project_root.parent.parent) # 표제란 값도 같은 요청 문맥에 세운다 — 값이 없는 칸은 빈칸으로 나간다. - use_title_fields(await _title_block_fields(project_id)) + use_title_fields(await title_block_fields(project_id)) # 횡단도는 B06 지정 설계를 먼저 읽어 CAD 계획선(design_line)과 응답에 함께 쓴다. design: dict[str, Any] | None = None source_design: Any = None diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Frame.py b/B07_DesignDetail/B07_DesignDetail_Router_Frame.py index 41f3e25e..6ad39fab 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Frame.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Frame.py @@ -26,6 +26,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Template import ( use_company_templates, validate_template_entities, ) +from B07_DesignDetail.B07_DesignDetail_Router import title_block_fields from B07_DesignDetail.B07_DesignDetail_Schema import ( DrawingExportRequest, FrameTemplateImportResponse, @@ -56,10 +57,14 @@ async def get_frame_template(project_id: UUID) -> FrameTemplateResponse | JSONRe try: company_dir = await _company_dir(project_id) use_company_templates(company_dir) + # 편집 화면이 자리표에 실제 값을 보여 줄 수 있게 함께 넘긴다 — 도면마다 달라지는 + # 도면명·도면번호는 여기 없다(그 자리는 자리표 이름 그대로 보인다). + fields = await title_block_fields(project_id) return FrameTemplateResponse( project_id=str(project_id), drawing=frame_template_document(), customized=company_template_path(company_dir).is_file(), + fields=fields, ) except FileNotFoundError as exc: return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)}) diff --git a/B07_DesignDetail/B07_DesignDetail_Schema.py b/B07_DesignDetail/B07_DesignDetail_Schema.py index 6cbb3ce1..c129d9fc 100644 --- a/B07_DesignDetail/B07_DesignDetail_Schema.py +++ b/B07_DesignDetail/B07_DesignDetail_Schema.py @@ -102,6 +102,9 @@ class FrameTemplateResponse(BaseModel): drawing: dict[str, Any] # 회사가 고친 도각을 쓰고 있으면 True, 프로그램 기본 도각이면 False. customized: bool = False + # 자리표에 **보여 줄** 실제 값 (2026-09-06 사용자 지시) — 편집 화면 전용이고 + # 저장값은 `{{키}}` 토큰 그대로다. 값이 없는 자리는 담기지 않는다. + fields: dict[str, str] = {} class FrameTemplateImportResponse(BaseModel): diff --git a/B07_DesignDetail/B07_DesignDetail_UI_FrameEdit.ts b/B07_DesignDetail/B07_DesignDetail_UI_FrameEdit.ts index 85c80f78..437afe99 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_FrameEdit.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_FrameEdit.ts @@ -30,7 +30,12 @@ interface Options { projectId: string; /** CAD에 도면을 싣는다 (meta null이면 수량 패널을 숨긴다). * frameEdit 을 켜면 캐드 안 자리표 패널이 함께 뜬다. */ - sendLoad: (drawing: CadDrawing, meta: null, frameEdit?: boolean) => void; + sendLoad: ( + drawing: CadDrawing, + meta: null, + frameEdit?: boolean, + frameFields?: Record, + ) => void; /** CAD에서 현재 편집본을 받아온다. */ requestCadDrawing: () => Promise; /** 편집을 마친 뒤 보던 도면으로 돌아간다. */ @@ -41,6 +46,8 @@ interface Options { export function createFrameTemplateEditor(options: Options): FrameTemplateEditor { let editing = false; + // 자리표에 보여 줄 실제 값 — 도각을 열 때 서버에서 받아 캐드에 함께 넘긴다. + let frameFields: Record = {}; const banner = document.createElement("div"); banner.className = "b07-frame-edit"; @@ -110,7 +117,7 @@ export function createFrameTemplateEditor(options: Options): FrameTemplateEditor importButton.disabled = true; try { const response = await importFrameTemplate(options.projectId, file); - options.sendLoad(response.drawing, null, true); + options.sendLoad(response.drawing, null, true, frameFields); label.textContent = `${file.name} 을(를) 불러왔습니다 — 자리표를 놓고 [완료]를 누르십시오.`; showToast(`도형 ${response.entity_count}개를 불러왔습니다.`, "success"); } catch (error) { @@ -126,13 +133,14 @@ export function createFrameTemplateEditor(options: Options): FrameTemplateEditor async function enter(): Promise { try { const response = await fetchFrameTemplate(options.projectId); + frameFields = response.fields ?? {}; editing = true; button.disabled = true; banner.hidden = false; label.textContent = response.customized ? "도각 편집 중 — 회사 도각을 고치고 있습니다." : "도각 편집 중 — 기본 도각을 고치면 회사 도각으로 저장됩니다."; - options.sendLoad(response.drawing, null, true); + options.sendLoad(response.drawing, null, true, frameFields); } catch (error) { showToast(error instanceof Error ? error.message : "도각을 불러오지 못했습니다.", "error"); } diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts index 1f07d90c..6731f52e 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts @@ -126,7 +126,14 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { cadHost.append(frame, license); let cadReady = false; - let pendingLoad: { drawing: CadDrawing; meta: DesignMeta | null; frameEdit: boolean } | undefined; + let pendingLoad: + | { + drawing: CadDrawing; + meta: DesignMeta | null; + frameEdit: boolean; + frameFields: Record; + } + | undefined; let currentDrawing: DesignDrawingItem | undefined; let currentIndex = -1; let currentConfirmed = false; @@ -209,11 +216,16 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { // frameEdit: 도각 편집으로 싣는 도면인가 — 캐드 안 자리표 패널을 이때만 띄운다 // (2026-09-06 사용자 지시로 패널을 캐드 안으로 옮김). - const sendLoad = (drawing: CadDrawing, meta: DesignMeta | null, frameEdit = false) => { - pendingLoad = { drawing, meta, frameEdit }; + const sendLoad = ( + drawing: CadDrawing, + meta: DesignMeta | null, + frameEdit = false, + frameFields: Record = {}, + ) => { + pendingLoad = { drawing, meta, frameEdit, frameFields }; if (!cadReady) return; frame.contentWindow?.postMessage( - { type: CAD_LOAD_MESSAGE, drawing, meta, frameEdit }, + { type: CAD_LOAD_MESSAGE, drawing, meta, frameEdit, frameFields }, window.location.origin, ); pendingLoad = undefined; @@ -479,7 +491,13 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { ); } else if (message.type === CAD_READY_MESSAGE) { cadReady = true; - if (pendingLoad) sendLoad(pendingLoad.drawing, pendingLoad.meta, pendingLoad.frameEdit); + if (pendingLoad) + sendLoad( + pendingLoad.drawing, + pendingLoad.meta, + pendingLoad.frameEdit, + pendingLoad.frameFields, + ); } else if (message.type === CAD_LOADED_MESSAGE) { cadHost.dataset.loading = "false"; } else if (message.type === CAD_ERROR_MESSAGE) { diff --git a/B07_DesignDetail/openwebcad/src/App.css b/B07_DesignDetail/openwebcad/src/App.css index 66c56125..9d3fb9cb 100644 --- a/B07_DesignDetail/openwebcad/src/App.css +++ b/B07_DesignDetail/openwebcad/src/App.css @@ -997,3 +997,22 @@ body > canvas[data-id="canvas"] { .cad-frame-tokens__button:hover { background: var(--cad-accent-soft, var(--cad-chrome-raised)); } + +/* 자리표 칸 크기 입력 (2026-09-06) */ +.cad-frame-tokens__size { + display: flex; + align-items: center; + gap: 4px; + color: var(--cad-text-dim); + font-size: 11px; +} + +.cad-frame-tokens__size input { + width: 64px; + padding: 2px 4px; + border: 1px solid var(--cad-line); + border-radius: 4px; + background: var(--cad-chrome); + color: var(--cad-text); + font-size: 11px; +} diff --git a/B07_DesignDetail/openwebcad/src/components/FramePlaceholderPanel.tsx b/B07_DesignDetail/openwebcad/src/components/FramePlaceholderPanel.tsx index 9dc78bd3..c98ff97c 100644 --- a/B07_DesignDetail/openwebcad/src/components/FramePlaceholderPanel.tsx +++ b/B07_DesignDetail/openwebcad/src/components/FramePlaceholderPanel.tsx @@ -6,7 +6,9 @@ import { TextEntity } from '../entities/TextEntity'; import { getActiveLayerId, getEntities, + getFrameFields, getScreenCanvasDrawController, + getSelectedEntities, isFrameEditMode, setEntities, } from '../state'; @@ -43,6 +45,9 @@ const TEXT_TOKENS = [ const IMAGE_TOKENS = ['회사로고', '과업책임자서명', '분야별책임자서명', '설계자서명'] as const; const TEXT_SIZE_MM = 5; +// 자리표가 차지하는 칸 기본 크기(mm). 놓은 뒤 아래 「칸 크기」에서 고친다. +const TEXT_BOX_WIDTH_MM = 60; +const TEXT_BOX_HEIGHT_MM = 10; const IMAGE_WIDTH_MM = 32; const IMAGE_HEIGHT_MM = 16; @@ -58,7 +63,11 @@ function addTextPlaceholder(token: string): void { const entity = new TextEntity(getActiveLayerId(), `{{${token}}}`, center, { fontSize: TEXT_SIZE_MM, textAlign: 'center', + boxWidth: TEXT_BOX_WIDTH_MM, + boxHeight: TEXT_BOX_HEIGHT_MM, }); + // 편집 중에는 실제 값을 보여 준다 — 저장값은 토큰 그대로다. + entity.previewLabel = getFrameFields()[token] ?? null; setEntities([...getEntities(), entity], true); } @@ -82,18 +91,49 @@ async function addImagePlaceholder(token: string): Promise { layerId: getActiveLayerId(), shapeData: { points, imageData: `{{${token}}}` }, } as Parameters[0]); + const preview = getFrameFields()[token]; + if (preview) entity.setPreviewImage(preview); setEntities([...getEntities(), entity], true); } +/** 지금 고른 자리표 하나 — 칸 크기를 고칠 대상. 없으면 null. */ +function selectedPlaceholder(): TextEntity | ImageEntity | null { + const selected = getSelectedEntities(); + if (selected.length !== 1) return null; + const entity = selected[0]; + if (entity instanceof TextEntity && entity.getLabel().includes('{{')) return entity; + if (entity instanceof ImageEntity && entity.isPlaceholder()) return entity; + return null; +} + +function boxSizeOf(entity: TextEntity | ImageEntity): { width: number; height: number } { + const box = entity.getBoundingBox(); + return { width: Math.round(box.width * 10) / 10, height: Math.round(box.height * 10) / 10 }; +} + export const FramePlaceholderPanel: FC = () => { const [visible, setVisible] = useState(isFrameEditMode()); + const [picked, setPicked] = useState(null); + const [size, setSize] = useState({ width: 0, height: 0 }); - const refresh = useCallback(() => setVisible(isFrameEditMode()), []); + const refresh = useCallback(() => { + setVisible(isFrameEditMode()); + const entity = selectedPlaceholder(); + setPicked(entity); + if (entity) setSize(boxSizeOf(entity)); + }, []); useEffect(() => { window.addEventListener(HtmlEvent.UPDATE_STATE, refresh); return () => window.removeEventListener(HtmlEvent.UPDATE_STATE, refresh); }, [refresh]); + const applySize = (width: number, height: number): void => { + if (!picked) return; + setSize({ width, height }); + picked.setBoxSize(width, height); + setEntities([...getEntities()], true); + }; + if (!visible) return null; return ( @@ -115,7 +155,7 @@ export const FramePlaceholderPanel: FC = () => { ))}
- 그림 + 그림 (칸에 비율 그대로 들어감) {IMAGE_TOKENS.map((token) => ( ))}
+ {picked && ( +
+ 고른 자리표 칸 크기 (mm) + + +
+ )} ); }; diff --git a/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts b/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts index ca2741b8..aa219991 100644 --- a/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/ImageEntity.ts @@ -33,6 +33,22 @@ export class ImageEntity implements Entity { * 그렇게 손상됐다). 원본을 들고 있다가 그대로 돌려준다. */ private sourceData: string | null = null; + /** 저장값(그림 주소 또는 자리표 토큰). */ + public getSourceData(): string | null { + return this.sourceData; + } + + /** 자리표인가 — `{{회사로고}}` 처럼 토큰을 들고 있는 그림. */ + public isPlaceholder(): boolean { + return (this.sourceData ?? '').includes('{{'); + } + + /** 도각 편집에서만 쓰는 보여 주기용 그림. 저장값(sourceData)은 토큰 그대로 둔다. */ + public setPreviewImage(dataUrl: string): void { + const image = new Image(); + image.src = dataUrl; + this.imageElement = image; + } constructor( layerId: string, @@ -72,7 +88,7 @@ export class ImageEntity implements Entity { // 자리표(`{{회사로고}}` 등)는 그림이 없어 화면에 아무것도 안 보였다 — 도각 편집에서 // 무엇을 어디에 놓았는지 알 수 없어, 자리표일 때는 테두리와 이름을 늘 그린다 // (2026-09-06). 출력 때는 서버가 값으로 바꾸거나 엔티티째 빼므로 산출물에 안 실린다. - const placeholder = (this.sourceData ?? '').includes('{{'); + const placeholder = this.isPlaceholder() && !this.imageElement.src; // 그 밖의 그림은 **집었을 때만** 테두리를 그린다. 늘 그리면 도각의 로고 자리에 흰 // 사각형이 남고, 출력·내보내기가 같은 draw()를 타므로 산출물에도 실린다(2026-09-02). if (highlighted || selected || placeholder) { @@ -82,6 +98,7 @@ export class ImageEntity implements Entity { } if (placeholder) { + // 아직 보여 줄 그림이 없으면 이름표만 남긴다. drawController.drawText(this.sourceData ?? '', this.polygon.box.center, { ...DEFAULT_TEXT_OPTIONS, textAlign: 'center', @@ -91,20 +108,38 @@ export class ImageEntity implements Entity { return; // 그림이 없으니 그릴 것도 없다 } - const width = this.polygon.box.width; - const height = this.polygon.box.height; + // 칸 안에 **비율을 지켜** 넣는다 (2026-09-06 사용자 지시) — 칸을 늘렸다고 그림이 + // 늘어나면 로고·서명이 찌그러진다. 남는 자리는 비운다(가운데 맞춤). + const boxWidth = this.polygon.box.width; + const boxHeight = this.polygon.box.height; + const naturalWidth = this.imageElement.naturalWidth || boxWidth; + const naturalHeight = this.imageElement.naturalHeight || boxHeight; + const fit = Math.min(boxWidth / naturalWidth, boxHeight / naturalHeight); + const width = naturalWidth * fit; + const height = naturalHeight * fit; // Draw image drawController.drawImage( this.imageElement, - this.polygon.box.xmin, - this.polygon.box.ymin, + this.polygon.box.xmin + (boxWidth - width) / 2, + this.polygon.box.ymin + (boxHeight - height) / 2, width, height, this.angle ); } + /** 자리표 칸 크기(mm)를 바꾼다. 가운데는 그대로 두고 네 귀만 다시 잡는다. */ + public setBoxSize(width: number, height: number): void { + const center = this.polygon.box.center; + const halfWidth = Math.max(width, 1) / 2; + const halfHeight = Math.max(height, 1) / 2; + this.polygon = twoPointBoxToPolygon( + new Point(center.x - halfWidth, center.y - halfHeight), + new Point(center.x + halfWidth, center.y + halfHeight) + ); + } + public move(x: number, y: number) { this.polygon = this.polygon.translate(new Vector(x, y)); } diff --git a/B07_DesignDetail/openwebcad/src/entities/TextEntity.ts b/B07_DesignDetail/openwebcad/src/entities/TextEntity.ts index 2a033abe..a0e06f2c 100644 --- a/B07_DesignDetail/openwebcad/src/entities/TextEntity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/TextEntity.ts @@ -20,6 +20,13 @@ export interface TextOptions { /** 굵게·기울임 (문자 편집기 기본 서식). 밑줄은 캔버스에 없어 넣지 않았다 */ bold?: boolean; italic?: boolean; + /** + * 도각 자리표의 칸 크기(mm). 있으면 `basePoint` 가 **칸의 한가운데**이고 글자는 + * 가로·세로 가운데 맞춤으로 그려진다 (2026-09-06 사용자 지시). 없으면 예전처럼 + * 글자 하나로만 산다. + */ + boxWidth?: number; + boxHeight?: number; } export class TextEntity implements Entity { @@ -32,6 +39,12 @@ export class TextEntity implements Entity { public opacity?: number; /** GROUP으로 묶인 객체가 공유하는 식별자 */ public groupId?: string; + /** + * 도각 편집에서만 쓰는 **보여 주기용 값** (2026-09-06 사용자 지시). 자리표 + * `{{공사명}}` 대신 실제 공사명을 그려 사용자가 어디에 무엇이 들어가는지 알게 한다. + * 저장값(`label`)은 토큰 그대로다 — 값이 아니라 연결을 저장한다. + */ + public previewLabel: string | null = null; private readonly options: TextOptions; constructor( @@ -55,7 +68,7 @@ export class TextEntity implements Entity { const highlighted = parentHighlighted ?? isEntityHighlighted(this); const selected = parentSelected ?? isEntitySelected(this); drawController.setLineStyles(highlighted, selected, this.lineColor, this.lineWidth, this.lineDash); - drawController.drawText(this.label, this.basePoint, this.options); + drawController.drawText(this.previewLabel ?? this.label, this.basePoint, this.options); // 집었을 때만 회색 외곽선을 두른다 (2026-09-06 사용자 지시) — 글자는 선 모양이 // 바뀌어도 티가 안 나 무엇을 골랐는지 보이지 않았다. 출력·내보내기는 선택 상태가 // 없어 이 선이 실리지 않는다. @@ -114,6 +127,16 @@ export class TextEntity implements Entity { } public getBoundingBox(): Box { + const { boxWidth, boxHeight } = this.options; + if (boxWidth && boxHeight) { + // 자리표는 칸이 곧 경계다 — basePoint 가 칸 한가운데다. + return new Box( + this.basePoint.x - boxWidth / 2, + this.basePoint.y - boxHeight / 2, + this.basePoint.x + boxWidth / 2, + this.basePoint.y + boxHeight / 2 + ); + } // TODO find better way of determining the text bounding box return new Box( this.basePoint.x, @@ -123,6 +146,12 @@ export class TextEntity implements Entity { ); } + /** 자리표 칸 크기(mm)를 바꾼다. 글자 크기는 그대로 둔다. */ + public setBoxSize(width: number, height: number): void { + this.options.boxWidth = Math.max(width, 1); + this.options.boxHeight = Math.max(height, 1); + } + public getTextOptions(): TextOptions { return this.options; } @@ -196,6 +225,8 @@ export class TextEntity implements Entity { fontFamily: this.options.fontFamily, bold: this.options.bold, italic: this.options.italic, + boxWidth: this.options.boxWidth, + boxHeight: this.options.boxHeight, }, }, }; @@ -220,6 +251,8 @@ export class TextEntity implements Entity { fontFamily: jsonEntity.shapeData.options.fontFamily, bold: jsonEntity.shapeData.options.bold, italic: jsonEntity.shapeData.options.italic, + boxWidth: jsonEntity.shapeData.options.boxWidth, + boxHeight: jsonEntity.shapeData.options.boxHeight, } ); textEntity.id = jsonEntity.id; @@ -241,5 +274,8 @@ export interface TextJsonData { fontFamily: string; bold?: boolean; italic?: boolean; + /** 도각 자리표 칸 크기(mm) — basePoint 가 칸 한가운데다. */ + boxWidth?: number; + boxHeight?: number; }; } diff --git a/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts b/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts index d9395713..58511053 100644 --- a/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts +++ b/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts @@ -1,5 +1,6 @@ import { Point } from '@flatten-js/core'; import { type DesignMeta, HtmlEvent } from '../App.types.ts'; +import { ImageEntity } from '../entities/ImageEntity.ts'; import { TextEntity } from '../entities/TextEntity.ts'; import type { JsonDrawingFileSerialized } from '../helpers/import-export-handlers/export-entities-to-json.ts'; import { exportEntitiesAndLayersToJsonString } from '../helpers/import-export-handlers/export-entities-to-json.ts'; @@ -9,6 +10,7 @@ import { getCanvas, getDesignMeta, getEntities, + getFrameFields, getLayers, getScreenCanvasDrawController, isDrawingDirty, @@ -40,6 +42,8 @@ interface DrawingLoadMessage { meta?: DesignMeta | null; /** 도각 편집으로 실은 도면인가 — 캐드 안 자리표 패널을 이때만 띄운다. */ frameEdit?: boolean; + /** 자리표에 보여 줄 실제 값 (편집 화면 전용 — 저장값은 토큰 그대로). */ + frameFields?: Record; } interface DrawingSaveRequestMessage { @@ -125,6 +129,27 @@ function registerTextDoubleClickEdit() { }); } +/** + * 자리표에 실제 값을 입힌다 — 저장값은 그대로 두고 **보여 주기만** 바꾼다 + * (2026-09-06 사용자 지시). 편집 화면에서 어디에 무엇이 들어가는지 보이게 하는 것이 목적. + */ +function applyFramePreview(): void { + const fields = getFrameFields(); + if (Object.keys(fields).length === 0) return; + const token = /^\{\{(.+)\}\}$/; + for (const entity of getEntities()) { + if (entity instanceof TextEntity) { + const match = token.exec(entity.getLabel().trim()); + const value = match ? fields[match[1]] : undefined; + entity.previewLabel = value ?? null; + } else if (entity instanceof ImageEntity && entity.isPlaceholder()) { + const match = token.exec((entity.getSourceData() ?? '').trim()); + const value = match ? fields[match[1]] : undefined; + if (value) entity.setPreviewImage(value); + } + } +} + /** * B08 parent page와 CAD 앱 사이의 same-origin JSON 경계다. * DXF/DWG 파일이나 파서 객체는 이 경계를 통과하지 않는다. @@ -160,7 +185,8 @@ export function registerAisloDrawingBridge() { resetUndoBaseline(); // 설계 컨텍스트(제목·측점정보·확정상태·수량표)를 수량 패널에 반영 setDesignMeta(event.data.meta ?? null); - setFrameEditMode(event.data.frameEdit === true); + setFrameEditMode(event.data.frameEdit === true, event.data.frameFields ?? {}); + if (event.data.frameEdit) applyFramePreview(); // 앞 도면에서 켜 둔 그리기 도구를 내린다. 안 내리면 **확정한 도면 위에도** // 그 도구가 계속 그린다 — 읽기 전용은 새 명령만 막기 때문이다(2026-09-01 실측: // 확정본에서 클릭 두 번에 선 2개가 늘었다). 새 도면에서 앞 도면의 작도 도중 diff --git a/B07_DesignDetail/openwebcad/src/state.ts b/B07_DesignDetail/openwebcad/src/state.ts index 69510310..1fcf3217 100644 --- a/B07_DesignDetail/openwebcad/src/state.ts +++ b/B07_DesignDetail/openwebcad/src/state.ts @@ -188,6 +188,8 @@ let snapTrackingEnabled = true; let designMeta: DesignMeta | null = null; /** 도각 편집 모드인가 — 부모(B07 화면)가 도각을 실을 때 켠다. 자리표 패널이 이때만 뜬다. */ let frameEditMode = false; +/** 자리표에 보여 줄 실제 값 — `{{공사명}}` → 공사명, `{{회사로고}}` → 그림 주소. */ +let frameFields: Record = {}; /** * 실은 뒤로 실제 편집이 있었는가. 도면을 바꾸기 전에 부모가 물어보는 근거다 — @@ -259,6 +261,7 @@ export const getGridEnabled = () => gridEnabled; export const getSnapTrackingEnabled = () => snapTrackingEnabled; export const getDesignMeta = (): DesignMeta | null => designMeta; export const isFrameEditMode = (): boolean => frameEditMode; +export const getFrameFields = (): Record => frameFields; export const isDrawingDirty = () => drawingDirty; /** * 확정한 도면은 읽기 전용이다 — 그리기·수정·값 편집이 모두 막힌다(2026-09-01 사용자 @@ -490,8 +493,9 @@ export const setDesignMeta = (newMeta: DesignMeta | null) => { triggerReactUpdate(StateVariable.designMeta); }; /** 도각 편집 모드 켜고 끄기 — 자리표 패널의 표시 여부를 가른다 (2026-09-06 사용자 지시). */ -export const setFrameEditMode = (enabled: boolean) => { +export const setFrameEditMode = (enabled: boolean, fields: Record = {}) => { frameEditMode = enabled; + frameFields = enabled ? fields : {}; notifyWindow(HtmlEvent.UPDATE_STATE); }; // 수량표는 앞 단계(B05·B06) 산출물이라 B07에서 고치지 않는다(2026-09-01 사용자 확정).