feat(M02): 도면 양식 서버 길 둘 — POST /api/m02/drawing-import · GET /api/m02/drawing-fields (PLAN 10-3)
- 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PxvYb5ufV1kWdBvZbpDfu6
This commit is contained in:
@@ -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,
|
||||
}
|
||||
)
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user