feat(M02): CAD 표 놓기 · 칸 갈기 호스트 함수 — cad_host placeTable · updateTable · 도면 손잡이 (41-2)

- cad_host: placeTable(entity, link) · updateTable(link, cells) → 바꾼 표 수 · 도면이 열리기 전이면 열린 뒤 보냄(싣기가 덮지 않게) · 5 초 넘으면 실패
- M02_MasterTemplete_Drawing: DrawingTemplateHandle 에 같은 함수 둘

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
This commit is contained in:
2026-10-04 22:44:36 +09:00
co-authored by Claude Opus 5.5
parent c426fa2ad6
commit 0bc5606638
2 changed files with 56 additions and 0 deletions
@@ -77,6 +77,9 @@ function migrateOldRecovery(options: DrawingTemplateOptions, scope: string): voi
export interface DrawingTemplateHandle {
getDoc: () => Promise<DrawingTemplateDoc>;
/** 표 놓기 · 칸 갈기(41장) — `cad_host` 그대로 */
placeTable: (entity: Record<string, unknown>, link: string) => void;
updateTable: (link: string, cells: unknown[][]) => Promise<number>;
destroy: () => void;
}
@@ -277,6 +280,8 @@ export function mountDrawingTemplate(
return {
getDoc,
placeTable: cad.placeTable,
updateTable: cad.updateTable,
destroy: () => {
areaModal?.close();
cad.destroy();
+51
View File
@@ -28,6 +28,10 @@ const CAD_HOST_SAVE_MESSAGE = "aislo:b08:host-save";
* CAD 안 react-toastify는 모양·자리가 달라 한 화면에 두 종류가 섞여 보였다. */
const CAD_TOAST_MESSAGE = "aislo:b08:toast";
const CAD_TOAST_ACTION_MESSAGE = "aislo:b08:toast-action";
/** 표(M02 41장) — 짝 = CAD `integration/aislo-table-bridge.ts` */
const CAD_PLACE_TABLE_MESSAGE = "aislo:b08:place-table";
const CAD_UPDATE_TABLE_MESSAGE = "aislo:b08:update-table";
const CAD_TABLE_UPDATED_MESSAGE = "aislo:b08:table-updated";
/** CAD 가 주고받는 도면 JSON — 도형·층 밖의 칸은 페이지마다 다르다. */
export interface CadHostDrawing {
@@ -93,6 +97,11 @@ export interface CadHost<D extends CadHostDrawing, M, Q> {
) => void;
/** CAD 의 지금 편집본을 받는다. */
requestSave: () => Promise<CadSaveResult<D, Q>>;
/** 표 놓기(41장) — `entity` = 표 JSON(TableEntity 꼴) · CAD 가 `style.연결 = link` 로 커서에 붙여 놓게 함.
* 도면이 아직 안 열렸으면 열린 뒤 보냄 · 놓기는 사용자가 누를 때 · 저장은 [저장]. */
placeTable: (entity: Record<string, unknown>, link: string) => void;
/** 표 칸 갈기(41장) — `style.연결 === link` 인 표의 칸만(자리 · 크기 그대로) · 돌려받는 수 = 바꾼 표 수(0 = 없음). */
updateTable: (link: string, cells: unknown[][]) => Promise<number>;
/** 메시지 듣기를 멈춘다 — 페이지를 떠날 때. */
destroy: () => void;
}
@@ -177,6 +186,35 @@ export function createCadHost<D extends CadHostDrawing, M = unknown, Q = unknown
frame.contentWindow?.postMessage(message, window.location.origin);
};
/** 도면이 열린 뒤에만 보낼 것(표) — 열리기 전에 보내면 싣기가 덮어씀 */
let drawingLoaded = false;
let afterLoad: Record<string, unknown>[] = [];
const postAfterLoad = (message: Record<string, unknown>): void => {
if (drawingLoaded) post(message);
else afterLoad.push(message);
};
const tableWaits = new Map<string, ((count: number) => void)[]>();
const placeTable: CadHost<D, M, Q>["placeTable"] = (entity, link) =>
postAfterLoad({ type: CAD_PLACE_TABLE_MESSAGE, entity, link });
const updateTable: CadHost<D, M, Q>["updateTable"] = (link, cells) =>
new Promise((resolve, reject) => {
const waits = tableWaits.get(link) ?? [];
waits.push(resolve);
tableWaits.set(link, waits);
postAfterLoad({ type: CAD_UPDATE_TABLE_MESSAGE, link, cells });
window.setTimeout(() => {
const left = tableWaits.get(link) ?? [];
if (!left.includes(resolve)) return;
tableWaits.set(
link,
left.filter((one) => one !== resolve),
);
reject(new Error("CAD 표 갈기 응답 시간이 초과되었습니다."));
}, 5000);
});
const load: CadHost<D, M, Q>["load"] = (
drawing,
meta,
@@ -185,6 +223,7 @@ export function createCadHost<D extends CadHostDrawing, M = unknown, Q = unknown
extra = {},
) => {
pendingLoad = { drawing, meta, frameEdit, frameFields, extra };
drawingLoaded = false; // 새로 싣는 중 — 표는 열린 뒤에
if (!cadReady) return;
post({ type: CAD_LOAD_MESSAGE, drawing, meta, frameEdit, frameFields, ...extra });
pendingLoad = undefined;
@@ -215,6 +254,8 @@ export function createCadHost<D extends CadHostDrawing, M = unknown, Q = unknown
text?: string;
actionId?: string;
durationMs?: number;
link?: string;
count?: number;
};
if (message.type === CAD_TOAST_MESSAGE) {
const kind = (["info", "success", "warning", "error"] as const).find(
@@ -240,6 +281,14 @@ export function createCadHost<D extends CadHostDrawing, M = unknown, Q = unknown
} else if (message.type === CAD_LOADED_MESSAGE) {
stopLoadWatchdog();
element.dataset.loading = "false";
drawingLoaded = true;
const queued = afterLoad;
afterLoad = [];
queued.forEach(post);
} else if (message.type === CAD_TABLE_UPDATED_MESSAGE && message.link) {
const [first, ...rest] = tableWaits.get(message.link) ?? [];
tableWaits.set(message.link, rest);
first?.(message.count ?? 0);
} else if (message.type === CAD_ERROR_MESSAGE) {
stopLoadWatchdog();
showFailure(message.detail ?? "CAD 도면을 표시하지 못했습니다.");
@@ -272,6 +321,8 @@ export function createCadHost<D extends CadHostDrawing, M = unknown, Q = unknown
},
load,
requestSave,
placeTable,
updateTable,
destroy: () => {
stopLoadWatchdog();
window.removeEventListener("message", onMessage);