- 서버 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
151 lines
6.1 KiB
Python
151 lines
6.1 KiB
Python
"""M02 도면 양식 서버 길 — 외부 도각 파일 불러오기 · 자리표 키 목록 (PLAN 10-3).
|
|
|
|
양식을 읽고 쓰는 길은 `M02_MasterTemplete_Router.py`(시스템 층)·`_Router_Layers.py`(층)가 맡는다.
|
|
여기는 도면 양식 편집 화면만 쓰는 두 길 — B07 의 불러오기 엔진 · 표제란 값을 그대로 다시 쓴다.
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
from uuid import UUID
|
|
|
|
import ezdxf
|
|
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 (
|
|
entities_bbox,
|
|
frame_document,
|
|
frame_entities,
|
|
usable_bbox,
|
|
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 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)
|
|
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 (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(
|
|
status_code=500,
|
|
content={"status": "error", "message": "도각 파일을 읽지 못했습니다."},
|
|
)
|
|
|
|
|
|
# 얹은 도각 층 — 화면이 돌려줄 때 이 층을 통째로 뺌 · 불러온 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 를 주면 그 프로젝트의 표제란 값도 함께(미리보기용)."""
|
|
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,
|
|
}
|
|
)
|