From 1cd52cfdfb5a2faaa5c10910ded9e815c0f80666 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 25 Sep 2026 16:56:05 +0900 Subject: [PATCH] =?UTF-8?q?fix(M02):=20=EB=8F=84=EB=A9=B4=20=EC=B9=B8=20CA?= =?UTF-8?q?D=20=F0=9F=92=BE=20=C2=B7=20Ctrl+S=20=EA=B0=80=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20[=EC=A0=80=EC=9E=A5]=20=EC=9D=84=20?= =?UTF-8?q?=EB=B6=80=EB=A6=84=20=C2=B7=20=EC=97=89=ED=84=B0=EB=A6=AC=20?= =?UTF-8?q?=EB=8F=84=EA=B0=81=20=ED=8C=8C=EC=9D=BC=EC=9D=80=20400=20=C2=B7?= =?UTF-8?q?=20=EC=9E=90=EB=A6=AC=ED=91=9C=20=ED=8C=A8=EB=84=90=20=EC=A0=91?= =?UTF-8?q?=EA=B8=B0=20=EB=8B=A8=EC=B6=94=20(PLAN=2010-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PxvYb5ufV1kWdBvZbpDfu6 --- B07_DesignDetail/openwebcad/src/App.css | 13 ++++++++++++ .../openwebcad/src/commands/commands.file.ts | 8 ++++++-- .../src/components/FramePlaceholderPanel.tsx | 19 +++++++++++++++++- .../src/inputController/input-controller.ts | 5 +++++ .../src/integration/aislo-drawing-bridge.ts | 10 ++++++++++ B07_DesignDetail/openwebcad/src/state.ts | 6 ++++++ .../M02_MasterTemplete_Drawing.ts | 20 ++++++++++++++++--- .../M02_MasterTemplete_Router_Drawing.py | 13 ++++++++++++ resources/tester/test_m02_router_drawing.py | 9 +++++++++ ui_template/cad_host/cad_host.ts | 7 +++++++ 10 files changed, 104 insertions(+), 6 deletions(-) diff --git a/B07_DesignDetail/openwebcad/src/App.css b/B07_DesignDetail/openwebcad/src/App.css index 9d3fb9cb..f9979ba7 100644 --- a/B07_DesignDetail/openwebcad/src/App.css +++ b/B07_DesignDetail/openwebcad/src/App.css @@ -966,11 +966,24 @@ body > canvas[data-id="canvas"] { } .cad-frame-tokens__header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 6px; color: var(--cad-text-dim); font-size: 11px; line-height: 1.4; } +.cad-frame-tokens__fold { + flex: none; + border: 1px solid var(--cad-line); + border-radius: 4px; + background: transparent; + color: var(--cad-text); + cursor: pointer; +} + .cad-frame-tokens__group { display: flex; flex-wrap: wrap; diff --git a/B07_DesignDetail/openwebcad/src/commands/commands.file.ts b/B07_DesignDetail/openwebcad/src/commands/commands.file.ts index c401ee26..bfc01319 100644 --- a/B07_DesignDetail/openwebcad/src/commands/commands.file.ts +++ b/B07_DesignDetail/openwebcad/src/commands/commands.file.ts @@ -3,10 +3,10 @@ import { toast } from 'react-toastify'; import { clearRecovery, restoreRecovery } from '../helpers/autosave'; import { exportEntitiesToJsonFile } from '../helpers/import-export-handlers/export-entities-to-json'; import { exportEntitiesToLocalStorage } from '../helpers/import-export-handlers/export-entities-to-local-storage'; -import { requestDrawingExport } from '../integration/aislo-drawing-bridge'; +import { requestDrawingExport, requestHostSave } from '../integration/aislo-drawing-bridge'; import { exportEntitiesToPngFile } from '../helpers/import-export-handlers/export-entities-to-png'; import { exportEntitiesToSvgFile } from '../helpers/import-export-handlers/export-entities-to-svg'; -import { redo, undo } from '../state'; +import { isHostSave, redo, undo } from '../state'; import type { CadCommand } from './command.types'; export const FILE_COMMANDS: CadCommand[] = [ @@ -17,6 +17,10 @@ export const FILE_COMMANDS: CadCommand[] = [ glyph: '๐Ÿ’พ', hint: 'ํ˜„์žฌ ๋„๋ฉด์„ ๋ธŒ๋ผ์šฐ์ €์— ์ €์žฅํ•œ๋‹ค', run: () => { + if (isHostSave()) { + requestHostSave(); + return '์–‘์‹ ์ €์žฅ ์š”์ฒญ'; + } void exportEntitiesToLocalStorage().then(() => { clearRecovery(); toast.success('๋„๋ฉด์„ ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.'); diff --git a/B07_DesignDetail/openwebcad/src/components/FramePlaceholderPanel.tsx b/B07_DesignDetail/openwebcad/src/components/FramePlaceholderPanel.tsx index c98ff97c..706adf57 100644 --- a/B07_DesignDetail/openwebcad/src/components/FramePlaceholderPanel.tsx +++ b/B07_DesignDetail/openwebcad/src/components/FramePlaceholderPanel.tsx @@ -113,6 +113,8 @@ function boxSizeOf(entity: TextEntity | ImageEntity): { width: number; height: n export const FramePlaceholderPanel: FC = () => { const [visible, setVisible] = useState(isFrameEditMode()); + // ์ ‘์œผ๋ฉด ๊ทธ๋ฆผ ์˜ค๋ฅธ์ชฝ ์•„๋ž˜๋ฅผ ๋ฎ์ง€ ์•Š๋Š”๋‹ค โ€” ๋จธ๋ฆฌ ์ค„๋งŒ ๋‚จ๋Š”๋‹ค. + const [folded, setFolded] = useState(false); const [picked, setPicked] = useState(null); const [size, setSize] = useState({ width: 0, height: 0 }); @@ -139,8 +141,21 @@ export const FramePlaceholderPanel: FC = () => { return (
- ์ž๋ฆฌํ‘œ ๋†“๊ธฐ โ€” ๋ˆ„๋ฅด๋ฉด ํ™”๋ฉด ๊ฐ€์šด๋ฐ์— ์„œ๊ณ , ๋Œ์–ด์„œ ์ž๋ฆฌ๋ฅผ ์žก์Šต๋‹ˆ๋‹ค + + {folded ? '์ž๋ฆฌํ‘œ ๋†“๊ธฐ' : '์ž๋ฆฌํ‘œ ๋†“๊ธฐ โ€” ๋ˆ„๋ฅด๋ฉด ํ™”๋ฉด ๊ฐ€์šด๋ฐ์— ์„œ๊ณ , ๋Œ์–ด์„œ ์ž๋ฆฌ๋ฅผ ์žก์Šต๋‹ˆ๋‹ค'} + +
+ {!folded && ( + <>
๊ธ€์ž {TEXT_TOKENS.map((token) => ( @@ -190,6 +205,8 @@ export const FramePlaceholderPanel: FC = () => {
)} + + )}
); }; diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts b/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts index ac38c81d..a3166cc1 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts @@ -22,6 +22,7 @@ import { resolveCommandInput, } from '../commands/registry.ts'; import { runCommand } from '../commands/run-command.ts'; +import { requestHostSave } from '../integration/aislo-drawing-bridge.ts'; import type { ScreenCanvasDrawController } from '../drawControllers/screenCanvas.drawController.ts'; import { calculateAngleGuidesAndSnapPoints } from '../helpers/calculate-angle-guides-and-snap-points.ts'; import { findClosestEntity } from '../helpers/find-closest-entity.ts'; @@ -40,6 +41,7 @@ import { getSnapEnabled, getSnapPoint, getSnapPointOnAngleGuide, + isHostSave, redo, setActiveToolActor, setAngleStep, @@ -426,6 +428,9 @@ export class InputController { } else if (evt.ctrlKey && evt.key === 'y') { // User wants to redo the last action this.handleRedo(evt); + } else if (evt.ctrlKey && evt.key === 's') { + // ๋ถ€๋ชจ ํŽ˜์ด์ง€๊ฐ€ ์ €์žฅ์„ ๋งก์€ ํ™”๋ฉด(M02 ์–‘์‹)๋งŒ โ€” B07 ์€ ์˜ˆ์ „์ฒ˜๋Ÿผ ์•„๋ฌด ์ผ ์—†์Œ + if (isHostSave()) requestHostSave(); } else if (evt.ctrlKey && evt.key === 'a') { // User wants to select everything setSelectedEntityIds(getPickableEntities().map((entity) => entity.id)); diff --git a/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts b/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts index 755abd0f..a29f203c 100644 --- a/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts +++ b/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts @@ -21,6 +21,7 @@ import { setEntities, setFrameEditMode, setHostReadOnly, + setHostSave, setHostTitle, setLayers, } from '../state.ts'; @@ -38,6 +39,7 @@ export const AISLO_DRAWING_SAVE_REQUEST_MESSAGE = 'aislo:b08:save-request'; export const AISLO_DRAWING_SAVE_RESPONSE_MESSAGE = 'aislo:b08:save-response'; export const AISLO_DRAWING_NAVIGATE_MESSAGE = 'aislo:b08:navigate'; export const AISLO_DRAWING_EXPORT_MESSAGE = 'aislo:b08:export-file'; +export const AISLO_DRAWING_HOST_SAVE_MESSAGE = 'aislo:b08:host-save'; interface DrawingLoadMessage { type: typeof AISLO_DRAWING_LOAD_MESSAGE; @@ -53,6 +55,8 @@ interface DrawingLoadMessage { readOnly?: boolean; /** ์ œ๋ชฉํ‘œ์‹œ์ค„ ๋ถ€์ œ โ€” ์—†์œผ๋ฉด ใ€ŒB07 ์ƒ์„ธ ์„ค๊ณ„ใ€. */ hostTitle?: string; + /** ์ €์žฅ(๐Ÿ’พ ยท Ctrl+S)์„ ๋ถ€๋ชจ [์ €์žฅ]์— ๋งก๊ธฐ๋Š”๊ฐ€. */ + hostSave?: boolean; } interface DrawingSaveRequestMessage { @@ -75,6 +79,11 @@ export function requestDrawingNavigation(direction: 'prev' | 'next') { notifyParent(AISLO_DRAWING_NAVIGATE_MESSAGE, { direction }); } +/** ์ €์žฅ ์š”์ฒญ์„ ๋ถ€๋ชจ ํŽ˜์ด์ง€ [์ €์žฅ]์— ๋„˜๊ธด๋‹ค โ€” ์บ๋“œ๋Š” ์–‘์‹ ํŒŒ์ผ์„ ๋ชจ๋ฅธ๋‹ค. */ +export function requestHostSave() { + notifyParent(AISLO_DRAWING_HOST_SAVE_MESSAGE); +} + /** * ์ง€๊ธˆ ๋„๋ฉด์„ DXFยทDWG ํŒŒ์ผ๋กœ ๋‚ด๋ ค๋ฐ›๋„๋ก ๋ถ€๋ชจ์—๊ฒŒ ์š”์ฒญํ•œ๋‹ค (2026-09-06 ์‚ฌ์šฉ์ž ์ง€์‹œ). * ์บ๋“œ๋Š” ํ”„๋กœ์ ํŠธ๋ฅผ ๋ชจ๋ฅด๋ฏ€๋กœ ํŒŒ์ผ ๋งŒ๋“ค๊ธฐ๋Š” ๋ถ€๋ชจ๊ฐ€ ์„œ๋ฒ„์— ๋งก๊ธด๋‹ค. @@ -197,6 +206,7 @@ export function registerAisloDrawingBridge() { setFrameEditMode(event.data.frameEdit === true, event.data.frameFields ?? {}); setHostReadOnly(event.data.readOnly === true); setHostTitle(event.data.hostTitle ?? 'B07 ์ƒ์„ธ ์„ค๊ณ„'); + setHostSave(event.data.hostSave === true); if (event.data.frameEdit) applyFramePreview(); // ์•ž ๋„๋ฉด์—์„œ ์ผœ ๋‘” ๊ทธ๋ฆฌ๊ธฐ ๋„๊ตฌ๋ฅผ ๋‚ด๋ฆฐ๋‹ค. ์•ˆ ๋‚ด๋ฆฌ๋ฉด **ํ™•์ •ํ•œ ๋„๋ฉด ์œ„์—๋„** // ๊ทธ ๋„๊ตฌ๊ฐ€ ๊ณ„์† ๊ทธ๋ฆฐ๋‹ค โ€” ์ฝ๊ธฐ ์ „์šฉ์€ ์ƒˆ ๋ช…๋ น๋งŒ ๋ง‰๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค(2026-09-01 ์‹ค์ธก: diff --git a/B07_DesignDetail/openwebcad/src/state.ts b/B07_DesignDetail/openwebcad/src/state.ts index 785555dd..244e96a9 100644 --- a/B07_DesignDetail/openwebcad/src/state.ts +++ b/B07_DesignDetail/openwebcad/src/state.ts @@ -193,6 +193,7 @@ let frameFields: Record = {}; /** ๋ถ€๋ชจ๊ฐ€ ๋ณด๊ธฐ ์ „์šฉ์œผ๋กœ ์‹ค์—ˆ๋Š”๊ฐ€ โ€” M02 ์—์„œ ๊ณ ์น  ๊ถŒํ•œ์ด ์—†๋Š” ์–‘์‹์„ ๋ณผ ๋•Œ. */ let hostReadOnly = false; let hostTitle = 'B07 ์ƒ์„ธ ์„ค๊ณ„'; +let hostSave = false; /** * ์‹ค์€ ๋’ค๋กœ ์‹ค์ œ ํŽธ์ง‘์ด ์žˆ์—ˆ๋Š”๊ฐ€. ๋„๋ฉด์„ ๋ฐ”๊พธ๊ธฐ ์ „์— ๋ถ€๋ชจ๊ฐ€ ๋ฌผ์–ด๋ณด๋Š” ๊ทผ๊ฑฐ๋‹ค โ€” @@ -498,6 +499,11 @@ export const setDesignMeta = (newMeta: DesignMeta | null) => { /** ๋„๊ฐ ํŽธ์ง‘ ๋ชจ๋“œ ์ผœ๊ณ  ๋„๊ธฐ โ€” ์ž๋ฆฌํ‘œ ํŒจ๋„์˜ ํ‘œ์‹œ ์—ฌ๋ถ€๋ฅผ ๊ฐ€๋ฅธ๋‹ค (2026-09-06 ์‚ฌ์šฉ์ž ์ง€์‹œ). */ /** ๋ถ€๋ชจ๊ฐ€ ์‹ค์€ ๋„๋ฉด์„ ๋ณด๊ธฐ ์ „์šฉ์œผ๋กœ ๋‘”๋‹ค โ€” ํ™•์ •๋ณธ๊ณผ ๊ฐ™์€ ๋ง‰ํž˜(๋„๋ฉด์„ ์‹ค์„ ๋•Œ๋งˆ๋‹ค ์ƒˆ๋กœ ์ •ํ•จ). */ /** ์ œ๋ชฉํ‘œ์‹œ์ค„ ๋ถ€์ œ โ€” ๋ถ€๋ชจ๊ฐ€ ๋„๋ฉด์„ ์‹ค์„ ๋•Œ ์ •ํ•จ(์—†์œผ๋ฉด B07 ๊ธฐ๋ณธ ๊ธ€). */ +/** ์ €์žฅ(๐Ÿ’พ ยท Ctrl+S)์„ ๋ถ€๋ชจ ํŽ˜์ด์ง€ [์ €์žฅ]์— ๋งก๊ธฐ๋Š”๊ฐ€ โ€” M02 ์–‘์‹๋งŒ ์ผ ๋‹ค. */ +export const isHostSave = (): boolean => hostSave; +export const setHostSave = (enabled: boolean) => { + hostSave = enabled; +}; export const getHostTitle = (): string => hostTitle; export const setHostTitle = (title: string) => { hostTitle = title; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts b/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts index b3bbbecb..c4ab9cc1 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts @@ -24,7 +24,7 @@ export interface DrawingTemplateDoc extends CadHostDrawing { } export interface DrawingTemplateOptions { - /** ์“ฐ์ง€ ์•Š์Œ โ€” ์ €์žฅ์€ ํŽ˜์ด์ง€ ๋จธ๋ฆฌ [์ €์žฅ] ํ•œ ๊ณณ(`getDoc()` ์œผ๋กœ ํŽธ์ง‘๋ณธ์„ ๊ฐ€์ ธ๊ฐ ยท ํŒ ยท 409 ํ๋ฆ„). */ + /** CAD ์•ˆ ๐Ÿ’พ ยท Ctrl+S ๊ฐ€ ๋ถ€๋ฅธ๋‹ค โ€” ํŽ˜์ด์ง€ ๋จธ๋ฆฌ [์ €์žฅ] ๊ณผ ๊ฐ™์€ ์ผ(ํŒ ยท 409 ํ๋ฆ„)์„ ํ•˜๊ฒŒ ํŽ˜์ด์ง€๊ฐ€ ๋„˜๊ธด๋‹ค. */ onSave?: (doc: DrawingTemplateDoc) => void | Promise; /** ๋ณด๊ธฐ ์ „์šฉ โ€” CAD ๊ทธ๋ฆฌ๊ธฐยท์ˆ˜์ • ยท ์ž‘๋„ ์˜์—ญ ยท ๋ถˆ๋Ÿฌ์˜ค๊ธฐ๊ฐ€ ๋ง‰ํžŒ๋‹ค. */ readOnly?: boolean; @@ -167,7 +167,21 @@ export function mountDrawingTemplate( showToast(error instanceof Error ? error.message : "์ž๋ฆฌํ‘œ ๋ชฉ๋ก์„ ๋ฐ›์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.", "error"), ); - const cad = createCadHost({ title: "๋„๋ฉด ์–‘์‹" }); + // CAD ์•ˆ ๐Ÿ’พ ยท Ctrl+S โ€” ์–‘์‹ ํŒŒ์ผ์€ ํŽ˜์ด์ง€ [์ €์žฅ] ๋งŒ ์“ด๋‹ค. ์ €์žฅ ์ค‘ ๋˜ ๋ˆŒ๋Ÿฌ๋„ ํ•œ ๋ฒˆ๋งŒ ๋ถ€๋ฅธ๋‹ค. + let saving = false; + const hostSave = (): void => { + if (readOnly) return void showToast("์ด ์–‘์‹์€ ๋ณผ ์ˆ˜๋งŒ ์žˆ์Šต๋‹ˆ๋‹ค.", "info"); + if (saving || !options.onSave) return; + saving = true; + getDoc() + .then((next) => options.onSave?.(next)) + .catch((error) => + showToast(error instanceof Error ? error.message : "์–‘์‹์„ ์ €์žฅํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.", "error"), + ) + .finally(() => (saving = false)); + }; + + const cad = createCadHost({ title: "๋„๋ฉด ์–‘์‹", onHostSave: hostSave }); const load = (drawing: DrawingTemplateDoc): void => { cad.beginLoading(); cad.load( @@ -175,7 +189,7 @@ export function mountDrawingTemplate( null, !readOnly, {}, - { recoveryScope, readOnly, hostTitle: "๋„๋ฉด ์–‘์‹ ํŽธ์ง‘" }, + { recoveryScope, readOnly, hostTitle: "๋„๋ฉด ์–‘์‹ ํŽธ์ง‘", hostSave: true }, ); }; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py index d1c20db7..047d2817 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py @@ -8,6 +8,7 @@ import asyncio import logging from uuid import UUID +import ezdxf from fastapi import APIRouter, File, UploadFile from fastapi.responses import JSONResponse @@ -54,6 +55,8 @@ async def import_drawing_file(file: UploadFile = File(...)) -> JSONResponse: """์™ธ๋ถ€ ๋„๊ฐ ํŒŒ์ผ(DXFยทDWG)์„ ์ฝ์–ด **ํŽธ์ง‘ ํ™”๋ฉด์— ์‹ค์„ ๋„๋ฉด**์œผ๋กœ ๋Œ๋ ค์ค€๋‹ค โ€” ์ €์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค.""" try: data = await file.read() + if not data: + raise ValueError("๋นˆ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค.") if len(data) > _IMPORT_MAX_BYTES: raise ValueError("๋„๊ฐ ํŒŒ์ผ์ด ๋„ˆ๋ฌด ํฝ๋‹ˆ๋‹ค(20MB ๋„˜์Œ).") entities = await asyncio.to_thread(import_frame_file, file.filename or "", data) @@ -67,6 +70,16 @@ async def import_drawing_file(file: UploadFile = File(...)) -> JSONResponse: ) except ValueError as exc: return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)}) + except (OSError, UnicodeError, ezdxf.DXFError): + # ์—‰ํ„ฐ๋ฆฌ ํŒŒ์ผ โ€” ์ฝ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ValueError ์•„๋‹Œ ๊ฒƒ์œผ๋กœ ๋˜์ง(DXF ์•„๋‹˜ ยท ๊ธ€์ž ๊นจ์ง). + # ๋˜์ง„ ๊ธ€์— ์ž„์‹œ ํด๋” ๊ฒฝ๋กœ๊ฐ€ ์„ž์—ฌ ์žˆ์–ด ๊ทธ๋Œ€๋กœ ๋‚ด์ง€ ์•Š๋Š”๋‹ค. + return JSONResponse( + status_code=400, + content={ + "status": "error", + "message": "๋„๊ฐ ํŒŒ์ผ์„ ์ฝ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค โ€” DXF ๊ฐ€ ์•„๋‹ˆ๊ฑฐ๋‚˜ ๊นจ์ง„ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค.", + }, + ) except Exception: logger.exception("M02 ๋„๊ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์‹คํŒจ: %s", file.filename) return JSONResponse( diff --git a/resources/tester/test_m02_router_drawing.py b/resources/tester/test_m02_router_drawing.py index da03fd1d..8d6f35b3 100644 --- a/resources/tester/test_m02_router_drawing.py +++ b/resources/tester/test_m02_router_drawing.py @@ -56,3 +56,12 @@ def test_import_dxf(): body = response.json() assert body["entity_count"] == 1 assert body["drawing"]["entities"][0]["type"] == "Line" + + +def test_import_garbage_is_400_not_500(): + for data in (b"garbage\x00\xff", b"", b"0\nSECTION\n2\nENTITIES\n0\nLINE\n10\nabc\n"): + response = client.post( + "/api/m02/drawing-import", files={"file": ("bad.dxf", data, "application/dxf")} + ) + assert response.status_code == 400, (data, response.text) + assert response.json()["message"] diff --git a/ui_template/cad_host/cad_host.ts b/ui_template/cad_host/cad_host.ts index 34e4a674..397138cd 100644 --- a/ui_template/cad_host/cad_host.ts +++ b/ui_template/cad_host/cad_host.ts @@ -22,6 +22,7 @@ const CAD_SAVE_REQUEST_MESSAGE = "aislo:b08:save-request"; const CAD_SAVE_RESPONSE_MESSAGE = "aislo:b08:save-response"; const CAD_NAVIGATE_MESSAGE = "aislo:b08:navigate"; const CAD_EXPORT_MESSAGE = "aislo:b08:export-file"; +const CAD_HOST_SAVE_MESSAGE = "aislo:b08:host-save"; /** CAD ์•ฑ ์•Œ๋ฆผ โ€” ํ”„๋กœ์ ํŠธ ๊ณต์šฉ ํ† ์ŠคํŠธ๋กœ ๋„์šด๋‹ค(2026-08-30 ์‚ฌ์šฉ์ž ์ง€์‹œ). * CAD ์•ˆ react-toastify๋Š” ๋ชจ์–‘ยท์ž๋ฆฌ๊ฐ€ ๋‹ฌ๋ผ ํ•œ ํ™”๋ฉด์— ๋‘ ์ข…๋ฅ˜๊ฐ€ ์„ž์—ฌ ๋ณด์˜€๋‹ค. */ const CAD_TOAST_MESSAGE = "aislo:b08:toast"; @@ -48,6 +49,8 @@ export interface CadHostOptions { onNavigate?: (direction: "prev" | "next") => void; /** CAD ๋ฆฌ๋ณธ์˜ DXFยทDWG ๋‚ด๋ณด๋‚ด๊ธฐ ๋‹จ์ถ”. */ onExport?: (fileFormat: "dxf" | "dwg") => void; + /** CAD ์•ˆ ๐Ÿ’พ ยท Ctrl+S โ€” `hostSave` ๋กœ ์‹ค์€ ํ™”๋ฉด(M02 ์–‘์‹)๋งŒ ์˜จ๋‹ค. */ + onHostSave?: () => void; } /** ์‹ฃ๊ธฐ ๊ณ๊ฐ’ โ€” ์ˆ˜๋Ÿ‰ ํŒจ๋„(meta) ์—†์ด ์‹ฃ๋Š” M02 ์–‘์‹์ด ์“ด๋‹ค. */ @@ -58,6 +61,8 @@ export interface CadLoadExtra { readOnly?: boolean; /** CAD ์ œ๋ชฉํ‘œ์‹œ์ค„ ๋ถ€์ œ โ€” ์—†์œผ๋ฉด ใ€ŒB07 ์ƒ์„ธ ์„ค๊ณ„ใ€. */ hostTitle?: string; + /** ์ €์žฅ(๐Ÿ’พ ยท Ctrl+S)์„ ๋ถ€๋ชจ [์ €์žฅ]์— ๋งก๊ธด๋‹ค โ€” ์ผœ๋ฉด `onHostSave` ๋กœ ์˜ด. */ + hostSave?: boolean; } export interface CadHost { @@ -229,6 +234,8 @@ export function createCadHost