"""B07 계획평면도 종류 · 대피소 자리 라우터 (PLAN 60장). 종류 = 정보 조각 고르기 목록. 추가 · 업데이트 · 지우기는 파일 하나씩(`plan_kinds/{id}.json`)이다. 대피소 · 사토장 자리는 구조물 정본 밖에서 사용자가 더 정한 자리(`plan_marks.json`)다. """ import asyncio import logging from typing import Any from uuid import UUID from fastapi import APIRouter from fastapi.responses import JSONResponse from pydantic import BaseModel, Field from B07_DesignDetail.B07_DesignDetail_Engine_Cad import infer_station_interval from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import PIECES from B07_DesignDetail.B07_DesignDetail_Router import _confirmed_source from B07_DesignDetail.B07_DesignDetail_Router_Frame import _project_root from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import _read_json from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import ( add_kind, delete_kind, list_kinds, read_marks, update_kind, write_marks, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/projects", tags=["B07 Design Detail"]) class PlanKindRequest(BaseModel): name: str pieces: list[str] = Field(default_factory=list) class PlanMark(BaseModel): chainage_m: float = Field(ge=0) refuge: bool = False spoil: bool = False class PlanMarksRequest(BaseModel): marks: list[PlanMark] = Field(default_factory=list) def _state(root: Any) -> dict[str, Any]: return { "status": "success", "pieces": [{"id": piece, "name": name} for piece, name, _locked in PIECES], "kinds": list_kinds(root), "marks": read_marks(root), } def _error(status: int, exc: Exception) -> JSONResponse: return JSONResponse(status_code=status, content={"status": "error", "message": str(exc)}) async def _station_interval(project_id: UUID) -> float | None: """종단 측점 간격(m) — 측점 NO + 거리 입력을 누가거리로 바꾸는 기준. 종단이 없으면 None.""" try: _route_id, _root, longitudinal_path, _bypassed = await _confirmed_source(project_id) longitudinal = await asyncio.to_thread(_read_json, longitudinal_path) except (FileNotFoundError, PermissionError, OSError, ValueError): return None return infer_station_interval(longitudinal.get("stations") or []) @router.get("/{project_id}/plan-kinds") async def get_plan_kinds(project_id: UUID) -> Any: """고를 수 있는 조각 · 지금 종류 · 대피소 자리 · 측점 간격.""" root = await _project_root(project_id) state = await asyncio.to_thread(_state, root) return {**state, "station_interval_m": await _station_interval(project_id)} @router.post("/{project_id}/plan-kinds") async def post_plan_kind(project_id: UUID, request: PlanKindRequest) -> Any: """종류 추가 — 조각을 골라 이름을 붙인다.""" try: root = await _project_root(project_id) await asyncio.to_thread(add_kind, root, request.name, request.pieces) return await asyncio.to_thread(_state, root) except ValueError as exc: return _error(400, exc) @router.put("/{project_id}/plan-kinds/{kind_id}") async def put_plan_kind(project_id: UUID, kind_id: str, request: PlanKindRequest) -> Any: """종류 업데이트 — 이름 · 조각을 고친다(기본 넷도).""" try: root = await _project_root(project_id) await asyncio.to_thread(update_kind, root, kind_id, request.name, request.pieces) return await asyncio.to_thread(_state, root) except ValueError as exc: return _error(400, exc) except FileNotFoundError as exc: return _error(404, exc) @router.delete("/{project_id}/plan-kinds/{kind_id}") async def delete_plan_kind(project_id: UUID, kind_id: str) -> Any: """종류 지우기 — 기본 넷도 지울 수 있다.""" try: root = await _project_root(project_id) await asyncio.to_thread(delete_kind, root, kind_id) return await asyncio.to_thread(_state, root) except ValueError as exc: return _error(400, exc) except FileNotFoundError as exc: return _error(404, exc) @router.put("/{project_id}/plan-marks") async def put_plan_marks(project_id: UUID, request: PlanMarksRequest) -> Any: """사용자가 정한 대피소 · 사토장 자리를 통째로 갈아 쓴다.""" try: root = await _project_root(project_id) marks = [mark.model_dump() for mark in request.marks] await asyncio.to_thread(write_marks, root, marks) return await asyncio.to_thread(_state, root) except ValueError as exc: return _error(400, exc)