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, getFrameFields, getScreenCanvasDrawController, getSelectedEntities, isFrameEditMode, setEntities, } from '../state'; /** * 도각 자리표 패널 — 프로그램 값이 들어갈 자리를 사용자가 직접 놓는다 (2026-09-06 사용자 확정). * * 값을 알아맞히는 규칙은 만들지 않는다. 여기서 놓은 `{{키}}` 토큰을 도면 출력 때 서버의 * 치환 엔진이 채운다. 자리표를 안 놓은 값은 빈칸으로 남는다. * * 도각 편집으로 도면을 실었을 때만 뜬다. 놓은 자리표는 화면 한가운데에 서고, 그 뒤 * 캐드의 이동·크기 도구로 자리를 잡는다. */ /** 글자 자리표 — 출력 때 표제란 값으로 바뀐다. */ const TEXT_TOKENS = [ '도면명', '도면번호', '공사명', '위치', '시행청', '용역회사', '연도기번', '사업량', '과업책임자', '분야별책임자', '설계자', '설계일자', '축척_A1', '축척_A3', ] as const; /** 그림 자리표 — 회사 로고와 사람 서명. 값이 없으면 도면에서 그림째 빠진다. */ 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; /** 지금 보고 있는 화면의 한가운데 (도면 좌표). 자리표가 처음 서는 자리다. */ 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', boxWidth: TEXT_BOX_WIDTH_MM, boxHeight: TEXT_BOX_HEIGHT_MM, }); // 편집 중에는 실제 값을 보여 준다 — 저장값은 토큰 그대로다. entity.previewLabel = getFrameFields()[token] ?? null; 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]); 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 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 (
자리표 놓기 — 누르면 화면 가운데에 서고, 끌어서 자리를 잡습니다
글자 {TEXT_TOKENS.map((token) => ( ))}
그림 (칸에 비율 그대로 들어감) {IMAGE_TOKENS.map((token) => ( ))}
{picked && (
고른 자리표 칸 크기 (mm)
)}
); };