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
@@ -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<CadDrawing>;
/** 편집을 마친 뒤 보던 도면으로 돌아간다. */
@@ -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 };
}
@@ -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<CadDrawing>;
/** CAD에 도면을 다시 싣는다. */
sendLoad: (drawing: CadDrawing, meta: null) => void;
}
interface Point {
x: number;
y: number;
}
/** 엔티티 목록의 한가운데 — 자리표를 처음 놓는 자리. 좌표가 없으면 원점. */
function centerOf(entities: Record<string, unknown>[]): Point {
const xs: number[] = [];
const ys: number[] = [];
const visit = (entity: Record<string, unknown>): void => {
const shape = (entity.shapeData ?? {}) as Record<string, unknown>;
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<string, unknown>[] | 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<string, unknown> {
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<string, unknown> {
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<void> => {
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;
},
};
}
+8 -6
View File
@@ -124,7 +124,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
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<void> {
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<void> {
);
} 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<void> {
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);
@@ -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;
}
+55
View File
@@ -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));
}
+2
View File
@@ -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() {
<div className="cad-app">
<Toolbar />
<QuantityPanel />
<FramePlaceholderPanel />
<ToastContainer position="bottom-right" theme="light" />
</div>
);
@@ -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>
);
};
@@ -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;
@@ -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개가 늘었다). 새 도면에서 앞 도면의 작도 도중
+8
View File
@@ -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은
// 어디서도 부르지 않으면서 "고칠 수 있는 값"으로 오해를 남겨 지웠다.