From 1778dbb505cf3f16845b168b56d38ae9a4a15d3a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 25 Sep 2026 09:37:04 +0900 Subject: [PATCH] =?UTF-8?q?feat(M02):=20=EB=8F=84=EB=A9=B4=20=EC=96=91?= =?UTF-8?q?=EC=8B=9D=20=EC=84=9C=EB=B2=84=20=EA=B8=B8=20=EB=91=98=20?= =?UTF-8?q?=E2=80=94=20POST=20/api/m02/drawing-import=20=C2=B7=20GET=20/ap?= =?UTF-8?q?i/m02/drawing-fields=20(PLAN=2010-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - M02_MasterTemplete_Router_Drawing.py — B07 Engine_Frame_Import · title_block_fields 다시 씀 - 자리표 키 목록 18 개(프로젝트 14 · 도면 4) · project_id 를 주면 표제란 값 함께 - main.py include 는 sub1 몫 - 시험 test_m02_router_drawing.py Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PxvYb5ufV1kWdBvZbpDfu6 --- .../M02_MasterTemplete_Router_Drawing.py | 95 +++++++++++++++++++ resources/tester/test_m02_router_drawing.py | 58 +++++++++++ 2 files changed, 153 insertions(+) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py create mode 100644 resources/tester/test_m02_router_drawing.py diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py new file mode 100644 index 00000000..d1c20db7 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py @@ -0,0 +1,95 @@ +"""M02 도면 양식 서버 길 — 외부 도각 파일 불러오기 · 자리표 키 목록 (PLAN 10-3). + +양식을 읽고 쓰는 길은 `M02_MasterTemplete_Router.py`(시스템 층)·`_Router_Layers.py`(층)가 맡는다. +여기는 도면 양식 편집 화면만 쓰는 두 길 — B07 의 불러오기 엔진 · 표제란 값을 그대로 다시 쓴다. +""" + +import asyncio +import logging +from uuid import UUID + +from fastapi import APIRouter, File, UploadFile +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 ( + frame_document, + validate_template_entities, +) +from B07_DesignDetail.B07_DesignDetail_Router import title_block_fields + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/m02", tags=["M02 MasterTemplete"]) + +# 도각 파일 상한 — A1 도각 한 장은 보통 1MB 아래다. 큰 도면 전체를 올리는 실수를 막는다. +_IMPORT_MAX_BYTES = 20 * 1024 * 1024 + +# 자리표 `{{키}}` 로 쓸 수 있는 키 — 도면을 그릴 때 채워지는 것만 둔다. +# 프로젝트 값은 B07 `title_block_fields` 가 · 도면 값은 도면을 그리는 엔진이 채운다. +DRAWING_FIELDS: tuple[tuple[str, str, str], ...] = ( + ("공사명", "프로젝트", "프로젝트 이름"), + ("위치", "프로젝트", "사업 위치"), + ("시행청", "프로젝트", "발주 기관"), + ("연도기번", "프로젝트", "프로젝트 번호"), + ("사업량", "프로젝트", "사업량"), + ("설계일자", "프로젝트", "설계 일자"), + ("용역회사", "프로젝트", "회사 이름"), + ("설계자", "프로젝트", "설계자 이름"), + ("과업책임자", "프로젝트", "과업책임자 이름"), + ("분야별책임자", "프로젝트", "분야별책임자 이름"), + ("회사로고", "프로젝트", "회사 로고 그림"), + ("설계자서명", "프로젝트", "설계자 서명 그림"), + ("과업책임자서명", "프로젝트", "과업책임자 서명 그림"), + ("분야별책임자서명", "프로젝트", "분야별책임자 서명 그림"), + ("도면명", "도면", "도면 이름"), + ("도면번호", "도면", "도면 목록 순번"), + ("축척_A1", "도면", "A1 축척 분모"), + ("축척_A3", "도면", "A3 축척 분모"), +) + + +@router.post("/drawing-import") +async def import_drawing_file(file: UploadFile = File(...)) -> JSONResponse: + """외부 도각 파일(DXF·DWG)을 읽어 **편집 화면에 실을 도면**으로 돌려준다 — 저장하지 않는다.""" + try: + data = await file.read() + if len(data) > _IMPORT_MAX_BYTES: + raise ValueError("도각 파일이 너무 큽니다(20MB 넘음).") + entities = await asyncio.to_thread(import_frame_file, file.filename or "", data) + validate_template_entities(entities) + return JSONResponse( + { + "status": "success", + "drawing": frame_document(entities), + "entity_count": len(entities), + } + ) + except ValueError as exc: + return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)}) + except Exception: + logger.exception("M02 도각 불러오기 실패: %s", file.filename) + return JSONResponse( + status_code=500, + content={"status": "error", "message": "도각 파일을 읽지 못했습니다."}, + ) + + +@router.get("/drawing-fields") +async def get_drawing_fields(project_id: UUID | None = None) -> JSONResponse: + """자리표 키 목록. project_id 를 주면 그 프로젝트의 표제란 값도 함께(미리보기용).""" + try: + values = await title_block_fields(project_id) if project_id else {} + except Exception: + logger.exception("M02 자리표 값 조회 실패: project_id=%s", project_id) + values = {} + return JSONResponse( + { + "status": "success", + "fields": [ + {"key": key, "source": source, "label": label} + for key, source, label in DRAWING_FIELDS + ], + "values": values, + } + ) diff --git a/resources/tester/test_m02_router_drawing.py b/resources/tester/test_m02_router_drawing.py new file mode 100644 index 00000000..da03fd1d --- /dev/null +++ b/resources/tester/test_m02_router_drawing.py @@ -0,0 +1,58 @@ +"""M02 도면 양식 서버 길 — 자리표 키 목록 · 도각 파일 불러오기 (PLAN 10-3).""" + +import io +import json +import re +from pathlib import Path + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from M02_MasterTemplete.M02_MasterTemplete_Router_Drawing import DRAWING_FIELDS, router + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + + +def test_fields_cover_system_placeholders(): + # 시스템 도면 양식에 박힌 자리표는 전부 키 목록에 있다 — 없으면 화면이 모르는 칸이 생긴다. + keys = {key for key, _, _ in DRAWING_FIELDS} + used = set() + for path in Path("resources/master_template/drawing").glob("00_*.json"): + text = json.dumps(json.loads(path.read_text(encoding="utf-8")), ensure_ascii=False) + used |= {match.strip() for match in re.findall(r"\{\{\s*([^}]+?)\s*\}\}", text)} + assert used and used <= keys, used - keys + + +def test_fields_without_project(): + response = client.get("/api/m02/drawing-fields") + assert response.status_code == 200 + body = response.json() + assert [item["key"] for item in body["fields"]] == [key for key, _, _ in DRAWING_FIELDS] + assert body["values"] == {} + + +def test_import_rejects_other_files(): + response = client.post( + "/api/m02/drawing-import", files={"file": ("a.txt", b"hello", "text/plain")} + ) + assert response.status_code == 400 + assert "DXF" in response.json()["message"] + + +def test_import_dxf(): + import ezdxf + + document = ezdxf.new() + document.modelspace().add_line((0, 0), (100, 50)) + stream = io.StringIO() + document.write(stream) + response = client.post( + "/api/m02/drawing-import", + files={"file": ("frame.dxf", stream.getvalue().encode("utf-8"), "application/dxf")}, + ) + assert response.status_code == 200, response.text + body = response.json() + assert body["entity_count"] == 1 + assert body["drawing"]["entities"][0]["type"] == "Line"