fix(M02): 도면 칸 CAD 💾 · Ctrl+S 가 페이지 [저장] 을 부름 · 엉터리 도각 파일은 400 · 자리표 패널 접기 단추 (PLAN 10-3)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PxvYb5ufV1kWdBvZbpDfu6
This commit is contained in:
@@ -966,11 +966,24 @@ body > canvas[data-id="canvas"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cad-frame-tokens__header {
|
.cad-frame-tokens__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
color: var(--cad-text-dim);
|
color: var(--cad-text-dim);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 1.4;
|
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 {
|
.cad-frame-tokens__group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { toast } from 'react-toastify';
|
|||||||
import { clearRecovery, restoreRecovery } from '../helpers/autosave';
|
import { clearRecovery, restoreRecovery } from '../helpers/autosave';
|
||||||
import { exportEntitiesToJsonFile } from '../helpers/import-export-handlers/export-entities-to-json';
|
import { exportEntitiesToJsonFile } from '../helpers/import-export-handlers/export-entities-to-json';
|
||||||
import { exportEntitiesToLocalStorage } from '../helpers/import-export-handlers/export-entities-to-local-storage';
|
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 { exportEntitiesToPngFile } from '../helpers/import-export-handlers/export-entities-to-png';
|
||||||
import { exportEntitiesToSvgFile } from '../helpers/import-export-handlers/export-entities-to-svg';
|
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';
|
import type { CadCommand } from './command.types';
|
||||||
|
|
||||||
export const FILE_COMMANDS: CadCommand[] = [
|
export const FILE_COMMANDS: CadCommand[] = [
|
||||||
@@ -17,6 +17,10 @@ export const FILE_COMMANDS: CadCommand[] = [
|
|||||||
glyph: '💾',
|
glyph: '💾',
|
||||||
hint: '현재 도면을 브라우저에 저장한다',
|
hint: '현재 도면을 브라우저에 저장한다',
|
||||||
run: () => {
|
run: () => {
|
||||||
|
if (isHostSave()) {
|
||||||
|
requestHostSave();
|
||||||
|
return '양식 저장 요청';
|
||||||
|
}
|
||||||
void exportEntitiesToLocalStorage().then(() => {
|
void exportEntitiesToLocalStorage().then(() => {
|
||||||
clearRecovery();
|
clearRecovery();
|
||||||
toast.success('도면을 저장했습니다.');
|
toast.success('도면을 저장했습니다.');
|
||||||
|
|||||||
@@ -113,6 +113,8 @@ function boxSizeOf(entity: TextEntity | ImageEntity): { width: number; height: n
|
|||||||
|
|
||||||
export const FramePlaceholderPanel: FC = () => {
|
export const FramePlaceholderPanel: FC = () => {
|
||||||
const [visible, setVisible] = useState(isFrameEditMode());
|
const [visible, setVisible] = useState(isFrameEditMode());
|
||||||
|
// 접으면 그림 오른쪽 아래를 덮지 않는다 — 머리 줄만 남는다.
|
||||||
|
const [folded, setFolded] = useState(false);
|
||||||
const [picked, setPicked] = useState<TextEntity | ImageEntity | null>(null);
|
const [picked, setPicked] = useState<TextEntity | ImageEntity | null>(null);
|
||||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
@@ -139,8 +141,21 @@ export const FramePlaceholderPanel: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<section className="cad-frame-tokens controls">
|
<section className="cad-frame-tokens controls">
|
||||||
<header className="cad-frame-tokens__header">
|
<header className="cad-frame-tokens__header">
|
||||||
자리표 놓기 — 누르면 화면 가운데에 서고, 끌어서 자리를 잡습니다
|
<span>
|
||||||
|
{folded ? '자리표 놓기' : '자리표 놓기 — 누르면 화면 가운데에 서고, 끌어서 자리를 잡습니다'}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="cad-frame-tokens__fold"
|
||||||
|
aria-expanded={!folded}
|
||||||
|
title={folded ? '자리표 패널 펴기' : '자리표 패널 접기'}
|
||||||
|
onClick={() => setFolded(!folded)}
|
||||||
|
>
|
||||||
|
{folded ? '▴' : '▾'}
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
{!folded && (
|
||||||
|
<>
|
||||||
<div className="cad-frame-tokens__group">
|
<div className="cad-frame-tokens__group">
|
||||||
<span className="cad-frame-tokens__title">글자</span>
|
<span className="cad-frame-tokens__title">글자</span>
|
||||||
{TEXT_TOKENS.map((token) => (
|
{TEXT_TOKENS.map((token) => (
|
||||||
@@ -190,6 +205,8 @@ export const FramePlaceholderPanel: FC = () => {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
resolveCommandInput,
|
resolveCommandInput,
|
||||||
} from '../commands/registry.ts';
|
} from '../commands/registry.ts';
|
||||||
import { runCommand } from '../commands/run-command.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 type { ScreenCanvasDrawController } from '../drawControllers/screenCanvas.drawController.ts';
|
||||||
import { calculateAngleGuidesAndSnapPoints } from '../helpers/calculate-angle-guides-and-snap-points.ts';
|
import { calculateAngleGuidesAndSnapPoints } from '../helpers/calculate-angle-guides-and-snap-points.ts';
|
||||||
import { findClosestEntity } from '../helpers/find-closest-entity.ts';
|
import { findClosestEntity } from '../helpers/find-closest-entity.ts';
|
||||||
@@ -40,6 +41,7 @@ import {
|
|||||||
getSnapEnabled,
|
getSnapEnabled,
|
||||||
getSnapPoint,
|
getSnapPoint,
|
||||||
getSnapPointOnAngleGuide,
|
getSnapPointOnAngleGuide,
|
||||||
|
isHostSave,
|
||||||
redo,
|
redo,
|
||||||
setActiveToolActor,
|
setActiveToolActor,
|
||||||
setAngleStep,
|
setAngleStep,
|
||||||
@@ -426,6 +428,9 @@ export class InputController {
|
|||||||
} else if (evt.ctrlKey && evt.key === 'y') {
|
} else if (evt.ctrlKey && evt.key === 'y') {
|
||||||
// User wants to redo the last action
|
// User wants to redo the last action
|
||||||
this.handleRedo(evt);
|
this.handleRedo(evt);
|
||||||
|
} else if (evt.ctrlKey && evt.key === 's') {
|
||||||
|
// 부모 페이지가 저장을 맡은 화면(M02 양식)만 — B07 은 예전처럼 아무 일 없음
|
||||||
|
if (isHostSave()) requestHostSave();
|
||||||
} else if (evt.ctrlKey && evt.key === 'a') {
|
} else if (evt.ctrlKey && evt.key === 'a') {
|
||||||
// User wants to select everything
|
// User wants to select everything
|
||||||
setSelectedEntityIds(getPickableEntities().map((entity) => entity.id));
|
setSelectedEntityIds(getPickableEntities().map((entity) => entity.id));
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
setEntities,
|
setEntities,
|
||||||
setFrameEditMode,
|
setFrameEditMode,
|
||||||
setHostReadOnly,
|
setHostReadOnly,
|
||||||
|
setHostSave,
|
||||||
setHostTitle,
|
setHostTitle,
|
||||||
setLayers,
|
setLayers,
|
||||||
} from '../state.ts';
|
} 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_SAVE_RESPONSE_MESSAGE = 'aislo:b08:save-response';
|
||||||
export const AISLO_DRAWING_NAVIGATE_MESSAGE = 'aislo:b08:navigate';
|
export const AISLO_DRAWING_NAVIGATE_MESSAGE = 'aislo:b08:navigate';
|
||||||
export const AISLO_DRAWING_EXPORT_MESSAGE = 'aislo:b08:export-file';
|
export const AISLO_DRAWING_EXPORT_MESSAGE = 'aislo:b08:export-file';
|
||||||
|
export const AISLO_DRAWING_HOST_SAVE_MESSAGE = 'aislo:b08:host-save';
|
||||||
|
|
||||||
interface DrawingLoadMessage {
|
interface DrawingLoadMessage {
|
||||||
type: typeof AISLO_DRAWING_LOAD_MESSAGE;
|
type: typeof AISLO_DRAWING_LOAD_MESSAGE;
|
||||||
@@ -53,6 +55,8 @@ interface DrawingLoadMessage {
|
|||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
/** 제목표시줄 부제 — 없으면 「B07 상세 설계」. */
|
/** 제목표시줄 부제 — 없으면 「B07 상세 설계」. */
|
||||||
hostTitle?: string;
|
hostTitle?: string;
|
||||||
|
/** 저장(💾 · Ctrl+S)을 부모 [저장]에 맡기는가. */
|
||||||
|
hostSave?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DrawingSaveRequestMessage {
|
interface DrawingSaveRequestMessage {
|
||||||
@@ -75,6 +79,11 @@ export function requestDrawingNavigation(direction: 'prev' | 'next') {
|
|||||||
notifyParent(AISLO_DRAWING_NAVIGATE_MESSAGE, { direction });
|
notifyParent(AISLO_DRAWING_NAVIGATE_MESSAGE, { direction });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 저장 요청을 부모 페이지 [저장]에 넘긴다 — 캐드는 양식 파일을 모른다. */
|
||||||
|
export function requestHostSave() {
|
||||||
|
notifyParent(AISLO_DRAWING_HOST_SAVE_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 지금 도면을 DXF·DWG 파일로 내려받도록 부모에게 요청한다 (2026-09-06 사용자 지시).
|
* 지금 도면을 DXF·DWG 파일로 내려받도록 부모에게 요청한다 (2026-09-06 사용자 지시).
|
||||||
* 캐드는 프로젝트를 모르므로 파일 만들기는 부모가 서버에 맡긴다.
|
* 캐드는 프로젝트를 모르므로 파일 만들기는 부모가 서버에 맡긴다.
|
||||||
@@ -197,6 +206,7 @@ export function registerAisloDrawingBridge() {
|
|||||||
setFrameEditMode(event.data.frameEdit === true, event.data.frameFields ?? {});
|
setFrameEditMode(event.data.frameEdit === true, event.data.frameFields ?? {});
|
||||||
setHostReadOnly(event.data.readOnly === true);
|
setHostReadOnly(event.data.readOnly === true);
|
||||||
setHostTitle(event.data.hostTitle ?? 'B07 상세 설계');
|
setHostTitle(event.data.hostTitle ?? 'B07 상세 설계');
|
||||||
|
setHostSave(event.data.hostSave === true);
|
||||||
if (event.data.frameEdit) applyFramePreview();
|
if (event.data.frameEdit) applyFramePreview();
|
||||||
// 앞 도면에서 켜 둔 그리기 도구를 내린다. 안 내리면 **확정한 도면 위에도**
|
// 앞 도면에서 켜 둔 그리기 도구를 내린다. 안 내리면 **확정한 도면 위에도**
|
||||||
// 그 도구가 계속 그린다 — 읽기 전용은 새 명령만 막기 때문이다(2026-09-01 실측:
|
// 그 도구가 계속 그린다 — 읽기 전용은 새 명령만 막기 때문이다(2026-09-01 실측:
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ let frameFields: Record<string, string> = {};
|
|||||||
/** 부모가 보기 전용으로 실었는가 — M02 에서 고칠 권한이 없는 양식을 볼 때. */
|
/** 부모가 보기 전용으로 실었는가 — M02 에서 고칠 권한이 없는 양식을 볼 때. */
|
||||||
let hostReadOnly = false;
|
let hostReadOnly = false;
|
||||||
let hostTitle = 'B07 상세 설계';
|
let hostTitle = 'B07 상세 설계';
|
||||||
|
let hostSave = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 실은 뒤로 실제 편집이 있었는가. 도면을 바꾸기 전에 부모가 물어보는 근거다 —
|
* 실은 뒤로 실제 편집이 있었는가. 도면을 바꾸기 전에 부모가 물어보는 근거다 —
|
||||||
@@ -498,6 +499,11 @@ export const setDesignMeta = (newMeta: DesignMeta | null) => {
|
|||||||
/** 도각 편집 모드 켜고 끄기 — 자리표 패널의 표시 여부를 가른다 (2026-09-06 사용자 지시). */
|
/** 도각 편집 모드 켜고 끄기 — 자리표 패널의 표시 여부를 가른다 (2026-09-06 사용자 지시). */
|
||||||
/** 부모가 실은 도면을 보기 전용으로 둔다 — 확정본과 같은 막힘(도면을 실을 때마다 새로 정함). */
|
/** 부모가 실은 도면을 보기 전용으로 둔다 — 확정본과 같은 막힘(도면을 실을 때마다 새로 정함). */
|
||||||
/** 제목표시줄 부제 — 부모가 도면을 실을 때 정함(없으면 B07 기본 글). */
|
/** 제목표시줄 부제 — 부모가 도면을 실을 때 정함(없으면 B07 기본 글). */
|
||||||
|
/** 저장(💾 · Ctrl+S)을 부모 페이지 [저장]에 맡기는가 — M02 양식만 켠다. */
|
||||||
|
export const isHostSave = (): boolean => hostSave;
|
||||||
|
export const setHostSave = (enabled: boolean) => {
|
||||||
|
hostSave = enabled;
|
||||||
|
};
|
||||||
export const getHostTitle = (): string => hostTitle;
|
export const getHostTitle = (): string => hostTitle;
|
||||||
export const setHostTitle = (title: string) => {
|
export const setHostTitle = (title: string) => {
|
||||||
hostTitle = title;
|
hostTitle = title;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export interface DrawingTemplateDoc extends CadHostDrawing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DrawingTemplateOptions {
|
export interface DrawingTemplateOptions {
|
||||||
/** 쓰지 않음 — 저장은 페이지 머리 [저장] 한 곳(`getDoc()` 으로 편집본을 가져감 · 판 · 409 흐름). */
|
/** CAD 안 💾 · Ctrl+S 가 부른다 — 페이지 머리 [저장] 과 같은 일(판 · 409 흐름)을 하게 페이지가 넘긴다. */
|
||||||
onSave?: (doc: DrawingTemplateDoc) => void | Promise<void>;
|
onSave?: (doc: DrawingTemplateDoc) => void | Promise<void>;
|
||||||
/** 보기 전용 — CAD 그리기·수정 · 작도 영역 · 불러오기가 막힌다. */
|
/** 보기 전용 — CAD 그리기·수정 · 작도 영역 · 불러오기가 막힌다. */
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@@ -167,7 +167,21 @@ export function mountDrawingTemplate(
|
|||||||
showToast(error instanceof Error ? error.message : "자리표 목록을 받지 못했습니다.", "error"),
|
showToast(error instanceof Error ? error.message : "자리표 목록을 받지 못했습니다.", "error"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const cad = createCadHost<DrawingTemplateDoc>({ 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<DrawingTemplateDoc>({ title: "도면 양식", onHostSave: hostSave });
|
||||||
const load = (drawing: DrawingTemplateDoc): void => {
|
const load = (drawing: DrawingTemplateDoc): void => {
|
||||||
cad.beginLoading();
|
cad.beginLoading();
|
||||||
cad.load(
|
cad.load(
|
||||||
@@ -175,7 +189,7 @@ export function mountDrawingTemplate(
|
|||||||
null,
|
null,
|
||||||
!readOnly,
|
!readOnly,
|
||||||
{},
|
{},
|
||||||
{ recoveryScope, readOnly, hostTitle: "도면 양식 편집" },
|
{ recoveryScope, readOnly, hostTitle: "도면 양식 편집", hostSave: true },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
import ezdxf
|
||||||
from fastapi import APIRouter, File, UploadFile
|
from fastapi import APIRouter, File, UploadFile
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
@@ -54,6 +55,8 @@ async def import_drawing_file(file: UploadFile = File(...)) -> JSONResponse:
|
|||||||
"""외부 도각 파일(DXF·DWG)을 읽어 **편집 화면에 실을 도면**으로 돌려준다 — 저장하지 않는다."""
|
"""외부 도각 파일(DXF·DWG)을 읽어 **편집 화면에 실을 도면**으로 돌려준다 — 저장하지 않는다."""
|
||||||
try:
|
try:
|
||||||
data = await file.read()
|
data = await file.read()
|
||||||
|
if not data:
|
||||||
|
raise ValueError("빈 파일입니다.")
|
||||||
if len(data) > _IMPORT_MAX_BYTES:
|
if len(data) > _IMPORT_MAX_BYTES:
|
||||||
raise ValueError("도각 파일이 너무 큽니다(20MB 넘음).")
|
raise ValueError("도각 파일이 너무 큽니다(20MB 넘음).")
|
||||||
entities = await asyncio.to_thread(import_frame_file, file.filename or "", data)
|
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:
|
except ValueError as exc:
|
||||||
return JSONResponse(status_code=400, content={"status": "error", "message": str(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:
|
except Exception:
|
||||||
logger.exception("M02 도각 불러오기 실패: %s", file.filename)
|
logger.exception("M02 도각 불러오기 실패: %s", file.filename)
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|||||||
@@ -56,3 +56,12 @@ def test_import_dxf():
|
|||||||
body = response.json()
|
body = response.json()
|
||||||
assert body["entity_count"] == 1
|
assert body["entity_count"] == 1
|
||||||
assert body["drawing"]["entities"][0]["type"] == "Line"
|
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"]
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const CAD_SAVE_REQUEST_MESSAGE = "aislo:b08:save-request";
|
|||||||
const CAD_SAVE_RESPONSE_MESSAGE = "aislo:b08:save-response";
|
const CAD_SAVE_RESPONSE_MESSAGE = "aislo:b08:save-response";
|
||||||
const CAD_NAVIGATE_MESSAGE = "aislo:b08:navigate";
|
const CAD_NAVIGATE_MESSAGE = "aislo:b08:navigate";
|
||||||
const CAD_EXPORT_MESSAGE = "aislo:b08:export-file";
|
const CAD_EXPORT_MESSAGE = "aislo:b08:export-file";
|
||||||
|
const CAD_HOST_SAVE_MESSAGE = "aislo:b08:host-save";
|
||||||
/** CAD 앱 알림 — 프로젝트 공용 토스트로 띄운다(2026-08-30 사용자 지시).
|
/** CAD 앱 알림 — 프로젝트 공용 토스트로 띄운다(2026-08-30 사용자 지시).
|
||||||
* CAD 안 react-toastify는 모양·자리가 달라 한 화면에 두 종류가 섞여 보였다. */
|
* CAD 안 react-toastify는 모양·자리가 달라 한 화면에 두 종류가 섞여 보였다. */
|
||||||
const CAD_TOAST_MESSAGE = "aislo:b08:toast";
|
const CAD_TOAST_MESSAGE = "aislo:b08:toast";
|
||||||
@@ -48,6 +49,8 @@ export interface CadHostOptions {
|
|||||||
onNavigate?: (direction: "prev" | "next") => void;
|
onNavigate?: (direction: "prev" | "next") => void;
|
||||||
/** CAD 리본의 DXF·DWG 내보내기 단추. */
|
/** CAD 리본의 DXF·DWG 내보내기 단추. */
|
||||||
onExport?: (fileFormat: "dxf" | "dwg") => void;
|
onExport?: (fileFormat: "dxf" | "dwg") => void;
|
||||||
|
/** CAD 안 💾 · Ctrl+S — `hostSave` 로 실은 화면(M02 양식)만 온다. */
|
||||||
|
onHostSave?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 싣기 곁값 — 수량 패널(meta) 없이 싣는 M02 양식이 쓴다. */
|
/** 싣기 곁값 — 수량 패널(meta) 없이 싣는 M02 양식이 쓴다. */
|
||||||
@@ -58,6 +61,8 @@ export interface CadLoadExtra {
|
|||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
/** CAD 제목표시줄 부제 — 없으면 「B07 상세 설계」. */
|
/** CAD 제목표시줄 부제 — 없으면 「B07 상세 설계」. */
|
||||||
hostTitle?: string;
|
hostTitle?: string;
|
||||||
|
/** 저장(💾 · Ctrl+S)을 부모 [저장]에 맡긴다 — 켜면 `onHostSave` 로 옴. */
|
||||||
|
hostSave?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CadHost<D extends CadHostDrawing, M, Q> {
|
export interface CadHost<D extends CadHostDrawing, M, Q> {
|
||||||
@@ -229,6 +234,8 @@ export function createCadHost<D extends CadHostDrawing, M = unknown, Q = unknown
|
|||||||
options.onChanged?.(message.dirty !== false);
|
options.onChanged?.(message.dirty !== false);
|
||||||
} else if (message.type === CAD_NAVIGATE_MESSAGE && message.direction) {
|
} else if (message.type === CAD_NAVIGATE_MESSAGE && message.direction) {
|
||||||
options.onNavigate?.(message.direction);
|
options.onNavigate?.(message.direction);
|
||||||
|
} else if (message.type === CAD_HOST_SAVE_MESSAGE) {
|
||||||
|
options.onHostSave?.();
|
||||||
} else if (message.type === CAD_EXPORT_MESSAGE && message.fileFormat) {
|
} else if (message.type === CAD_EXPORT_MESSAGE && message.fileFormat) {
|
||||||
options.onExport?.(message.fileFormat);
|
options.onExport?.(message.fileFormat);
|
||||||
} else if (message.type === CAD_SAVE_RESPONSE_MESSAGE && message.drawing && resolveSave) {
|
} else if (message.type === CAD_SAVE_RESPONSE_MESSAGE && message.drawing && resolveSave) {
|
||||||
|
|||||||
Reference in New Issue
Block a user