refactor(B07): 자리표 팔레트를 CAD 내부 패널로 이동

- 부모 사이드바 팔레트(B07_DesignDetail_UI_FramePlaceholders.ts) 제거, CAD 앱에 FramePlaceholderPanel 신설
- 도면 로드 메시지에 frameEdit 플래그 추가 — 도각 편집으로 실은 도면에서만 패널 표시
- 자리표를 화면 중앙에 직접 생성 (부모 왕복 없이 CAD 상태에 추가, 실행취소 스택 반영)
- 그림 자리표는 값이 없어도 테두리와 이름을 그려 배치 위치가 보이게 함

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 12:53:07 +09:00
co-authored by Claude Opus 5
parent 784d6df801
commit 6f201242be
10 changed files with 233 additions and 261 deletions
@@ -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<void> {
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<typeof ImageEntity.fromJson>[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 (
<section className="cad-frame-tokens controls">
<header className="cad-frame-tokens__header">
자리표 놓기 — 누르면 화면 가운데에 서고, 끌어서 자리를 잡습니다
</header>
<div className="cad-frame-tokens__group">
<span className="cad-frame-tokens__title">글자</span>
{TEXT_TOKENS.map((token) => (
<button
key={token}
type="button"
className="cad-frame-tokens__button"
onClick={() => addTextPlaceholder(token)}
>
{token}
</button>
))}
</div>
<div className="cad-frame-tokens__group">
<span className="cad-frame-tokens__title">그림</span>
{IMAGE_TOKENS.map((token) => (
<button
key={token}
type="button"
className="cad-frame-tokens__button"
onClick={() => void addImagePlaceholder(token)}
>
{token}
</button>
))}
</div>
</section>
);
};