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:
2026-09-25 16:56:05 +09:00
co-authored by Claude Sonnet 5
parent 2aa4c80577
commit 1cd52cfdfb
10 changed files with 104 additions and 6 deletions
@@ -24,7 +24,7 @@ export interface DrawingTemplateDoc extends CadHostDrawing {
}
export interface DrawingTemplateOptions {
/** 쓰지 않음 — 저장은 페이지 머리 [저장] 한 곳(`getDoc()` 으로 편집본을 가져감 · 판 · 409 흐름). */
/** CAD 안 💾 · Ctrl+S 가 부른다 — 페이지 머리 [저장] 과 같은 일(판 · 409 흐름)을 하게 페이지가 넘긴다. */
onSave?: (doc: DrawingTemplateDoc) => void | Promise<void>;
/** 보기 전용 — CAD 그리기·수정 · 작도 영역 · 불러오기가 막힌다. */
readOnly?: boolean;
@@ -167,7 +167,21 @@ export function mountDrawingTemplate(
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 => {
cad.beginLoading();
cad.load(
@@ -175,7 +189,7 @@ export function mountDrawingTemplate(
null,
!readOnly,
{},
{ recoveryScope, readOnly, hostTitle: "도면 양식 편집" },
{ recoveryScope, readOnly, hostTitle: "도면 양식 편집", hostSave: true },
);
};
@@ -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(