From ab39bcf986ff24c4f57ecb20c0da027a8d702497 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 17:52:13 +0900 Subject: [PATCH] =?UTF-8?q?feat(M02):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EB=8F=84=EB=A9=B4=20=EC=83=81=EC=84=B8=20=C2=B7=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EB=AF=B8=EB=A6=AC=EB=B3=B4=EA=B8=B0=EC=97=90=20?= =?UTF-8?q?=EB=8F=84=EA=B0=81=20=EC=96=B9=EC=9D=8C=20=E2=80=94=20=EC=99=B8?= =?UTF-8?q?=EA=B3=BD=20=EC=A2=8C=ED=95=98=EB=8B=A8=20(0,0)=20=C2=B7=20?= =?UTF-8?q?=EB=B3=B4=EC=97=AC=EC=A3=BC=EA=B8=B0=20=EC=B8=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 서버 GET /api/m02/structure-frame — B07 시스템 양식 00_template_A1 을 frame_entities(fit=False)로 1:1 배치 · 외곽 좌하단 (0,0) 으로 옮김 · 잠금 층 m02-structure-frame「도각(보기)」(마스터 양식 json 그대로) - 화면 mountDrawingTemplate frame 갈래 — 실을 때 도각 엔티티 · 층 덧붙임 · getDoc 에서 그 층 통째로 뺌(구조물 엔티티 · 저장 본문에 안 섞임) - 층 없는 빈 구조물 문서는 기본 층을 채움 — 잠금 도각 층만 남아 새 그림이 도각 층에 떨어지는 것 막음 - 불러온 DXF 층(b08-frame)과 층 id 갈라 둠 - 시험 test_m02_router_drawing 도각 원점 · 층 · 원본 양식 불변 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4 --- .../M02_MasterTemplete_Drawing.ts | 58 +++++++++++++++++-- .../M02_MasterTemplete_DrawingCard.ts | 3 +- .../M02_MasterTemplete_Router_Drawing.py | 42 ++++++++++++++ .../M02_MasterTemplete_UI_Main.ts | 1 + resources/tester/test_m02_router_drawing.py | 15 +++++ 5 files changed, 112 insertions(+), 7 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts b/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts index 5623e0ffe..fe5fabb1a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts @@ -45,8 +45,10 @@ export interface DrawingTemplateOptions { preview?: boolean; /** 좌표 0,0 기준점 십자를 그린다 — 구조물 도면(편집 · 미리보기)만. */ originMarker?: boolean; - /** 구조물 도면 — 도각이 없으므로 작도 영역 단추 · 자리표(도각 자리) 표시를 없애고, 문서의 작도 영역 값은 무시. */ + /** 구조물 도면 — 작도 영역 단추 · 자리표(도각 자리) 표시를 없애고, 문서의 작도 영역 값은 무시. */ noArea?: boolean; + /** B07 도각을 외곽 좌하단 (0,0) 에 얹음 — 잠금 층 · 보여주기만(돌려줄 때 뺌 · 저장 안 됨). */ + frame?: boolean; /** 파일 불러오기(📂) 단추를 안 둠 — 집계표 카드 미리보기(불러오기는 도면 상세에서만). */ noImport?: boolean; } @@ -89,6 +91,36 @@ export interface DrawingTemplateHandle { const DEFAULT_AREA: [number, number, number, number] = [42, 47, 812, 567]; const AREA_LABELS = ["왼쪽 x", "아래 y", "오른쪽 x", "위 y"]; +/** 얹은 도각 층 — 서버 `STRUCTURE_FRAME_LAYER` 와 같음 · 돌려줄 때 이 층을 통째로 뺌 */ +const FRAME_LAYER_ID = "m02-structure-frame"; +interface StructureFrame { + entities: Record[]; + layer: Record | null; +} +const BLANK_LAYER = { id: "기본", name: "기본", isVisible: true, isLocked: false }; +let framePromise: Promise | null = null; + +/** 구조물 도각 — 한 번만 받아 돌려씀 · 못 받으면 도각 없이 */ +function fetchFrame(): Promise { + framePromise ??= fetch(`${API_BASE_URL}/m02/structure-frame`, { credentials: "include" }) + .then((response) => (response.ok ? (response.json() as Promise) : null)) + .catch(() => null) + .then((frame) => { + if (!frame?.layer) framePromise = null; // 다음에 다시 받음 + return frame?.layer ? frame : null; + }); + return framePromise; +} + +/** 도각 층을 뺀 도면 — 구조물 엔티티 · 층만 남음 */ +function withoutFrame(drawing: DrawingTemplateDoc): DrawingTemplateDoc { + return { + ...drawing, + entities: drawing.entities.filter((e) => e.layerId !== FRAME_LAYER_ID), + layers: drawing.layers?.filter((layer) => layer.id !== FRAME_LAYER_ID), + }; +} + async function importDrawingFile( file: File, ): Promise<{ drawing: DrawingTemplateDoc; entity_count: number }> { @@ -202,10 +234,23 @@ export function mountDrawingTemplate( onChanged: (dirty) => options.onChanged?.(dirty), onHostSave: hostSave, }); - const load = (drawing: DrawingTemplateDoc): void => { + let loadSeq = 0; + const load = async (drawing: DrawingTemplateDoc): Promise => { + const mine = ++loadSeq; cad.beginLoading(); + const frame = options.frame ? await fetchFrame() : null; + if (mine !== loadSeq) return; // 그새 다른 도면을 실음 + const own = withoutFrame(drawing); + const shown = frame?.layer + ? { + ...own, + entities: [...own.entities, ...frame.entities], + // 층 없는 빈 문서 — 잠금 도각 층만 남으면 새 그림이 도각 층에 떨어짐 · 기본 층(서버 `_BLANK_LAYER`)을 채움 + layers: [...(own.layers?.length ? own.layers : [BLANK_LAYER]), frame.layer], + } + : drawing; cad.load( - drawing, + shown, null, !readOnly && !noArea, {}, @@ -238,7 +283,7 @@ export function mountDrawingTemplate( importDrawingFile(file) .then((response) => { // 불러온 도각은 아직 저장하지 않는다 — 자리표를 놓고 [저장]을 눌러야 양식이 된다. - load(response.drawing); + void load(response.drawing); options.onChanged?.(true); // 불러온 것은 아직 저장 전 — 떠날 때 묻게 함 showToast(`도형 ${response.entity_count}개를 불러왔습니다.`, "success"); }) @@ -260,7 +305,8 @@ export function mountDrawingTemplate( } const getDoc = async (): Promise => { - const { drawing } = await cad.requestSave(); + const { drawing: saved } = await cad.requestSave(); + const drawing = options.frame ? withoutFrame(saved) : saved; if (noArea) { base = { ...base, ...drawing }; return base; @@ -279,7 +325,7 @@ export function mountDrawingTemplate( root.append(cad.element); } else root.append(...(noArea && !canImport ? [] : [toolbar]), cad.element); container.append(root); - load(doc); + void load(doc); return { getDoc, diff --git a/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts b/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts index ef623c928..43d010d1e 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts @@ -80,7 +80,8 @@ function mountStructure( panelsCollapsed: true, preview: !edit, // 보기만 = 왼쪽 끌기 화면 옮기기 · 고치기 = 누름 고르기 · 단축키 originMarker: true, // 좌표 0,0 기준점 십자 - noArea: true, // 구조물도는 도각이 없음 + noArea: true, // 작도 영역 칸 없음 + frame: true, // 도각 = 보여주기만 · 외곽 좌하단 (0,0)(56장) noImport: true, // 파일 불러오기는 도면 상세에서만(47-2) ...hooks, }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py index 047d28170..871f27967 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py @@ -14,7 +14,10 @@ from fastapi.responses import JSONResponse from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Import import import_frame_file from B07_DesignDetail.B07_DesignDetail_Engine_Template import ( + entities_bbox, frame_document, + frame_entities, + usable_bbox, validate_template_entities, ) from B07_DesignDetail.B07_DesignDetail_Router import title_block_fields @@ -88,6 +91,45 @@ async def import_drawing_file(file: UploadFile = File(...)) -> JSONResponse: ) +# 얹은 도각 층 — 화면이 돌려줄 때 이 층을 통째로 뺌 · 불러온 DXF 층(`b08-frame`)과 갈라 둠 +STRUCTURE_FRAME_LAYER = "m02-structure-frame" + + +def _on_frame_layer(entity: dict) -> dict: + children = entity.get("children") + return { + **entity, + "layerId": STRUCTURE_FRAME_LAYER, + **({"children": [_on_frame_layer(c) for c in children]} if children else {}), + } + + +def structure_frame() -> dict: + """구조물 도면에 얹는 도각 — B07 시스템 양식 1:1 · 외곽 좌하단 (0,0) · 보여주기 층.""" + placed = frame_entities("m02-structure", usable_bbox(), fit=False) + box = entities_bbox(placed) + if not box: + return {"entities": [], "layer": None} + ox, oy = box[0], box[1] + x0, y0, x1, y1 = usable_bbox() + entities = frame_entities("m02-structure", (x0 - ox, y0 - oy, x1 - ox, y1 - oy), fit=False) + return { + "entities": [_on_frame_layer(entity) for entity in entities], + "layer": { + "id": STRUCTURE_FRAME_LAYER, + "name": "도각(보기)", + "isVisible": True, + "isLocked": True, + }, + } + + +@router.get("/structure-frame") +async def get_structure_frame() -> JSONResponse: + """구조물 도면 상세 · 카드 미리보기가 얹는 도각(56장).""" + return JSONResponse({"status": "success", **structure_frame()}) + + @router.get("/drawing-fields") async def get_drawing_fields(project_id: UUID | None = None) -> JSONResponse: """자리표 키 목록. project_id 를 주면 그 프로젝트의 표제란 값도 함께(미리보기용).""" diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 7fe44d24a..8bdf22c42 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -433,6 +433,7 @@ export function createMain( headerSlot: slot, originMarker: at.kind === "structure", noArea: at.kind === "structure", + frame: at.kind === "structure", // 도각 = 보여주기만(56장) }); if (at.kind === "structure") toDrawing.onDrawing(editor, at, doc as StructureDoc); return; diff --git a/resources/tester/test_m02_router_drawing.py b/resources/tester/test_m02_router_drawing.py index 8d6f35b3d..1fabe5cac 100644 --- a/resources/tester/test_m02_router_drawing.py +++ b/resources/tester/test_m02_router_drawing.py @@ -65,3 +65,18 @@ def test_import_garbage_is_400_not_500(): ) assert response.status_code == 400, (data, response.text) assert response.json()["message"] + + +def test_structure_frame_origin_at_zero(): + # 56장 — 구조물 도면에 얹는 도각: 외곽 좌하단 (0,0) · 잠금 도각 층 · 원본 양식은 그대로 + from B07_DesignDetail.B07_DesignDetail_Engine_Template import entities_bbox, template_entities + + before = entities_bbox(template_entities()) + body = client.get("/api/m02/structure-frame").json() + x0, y0, x1, y1 = entities_bbox(body["entities"]) + assert abs(x0) < 1e-9 and abs(y0) < 1e-9 + assert abs((x1 - x0) - (before[2] - before[0])) < 1e-6 + assert body["layer"]["isLocked"] is True + assert {e["layerId"] for e in body["entities"]} == {"m02-structure-frame"} + assert body["layer"]["id"] == "m02-structure-frame" # 불러온 DXF 층(b08-frame)과 갈림 + assert entities_bbox(template_entities()) == before