Files
Aislo/B07_DesignDetail/B07_DesignDetail_Router_PlanKinds.py
T
eomsangdonandClaude Opus 5.5 c2ed0fad54 feat(512-1 B07 유역도 등고선 간격): 유역도 등고선 5 m 기본 · 평면도 관리 모달에서 10 m 고름(1 m 없음 · B05 · B06 전용) — 요구 326
- 원인 확인: 유역도 23 MB = 1 m 아님 · 도엽 N3L 5 m 주곡선 그대로(등고선 층이 응답 대부분)
- 유역도 종류 파일 contour_interval_m(5 · 10) · 배경선 = 간격 배수 높이만 · 10 m = 23.17 → 11.91 MB
- 지문: 종류 목록 지문에서 간격 뺌 → 간격 바꾸면 유역도만 다시 받음 · 기본 종류 조각 차례 맞춤(파일로 내려도 지문 그대로)
- 시험: test_512_1_b07_basin_contour 새로 · test_b07_plan_kinds · helper_130 새 칸 · test_cad_host_tabs 508-1f 뒤 ☰ 자리 단언 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:56:06 +09:00

177 lines
6.4 KiB
Python

"""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,
reorder_kinds,
update_kind,
write_marks,
)
from config.config_system import (
BASIN_CONTOUR_INTERVAL_CHOICES,
BASIN_CONTOUR_INTERVAL_M,
DRAWING_SCALE_BASIN,
DRAWING_SCALE_PLAN,
DRAWING_SCALE_PLAN_CHOICES,
)
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)
# 양식 — plan(평면도 · 조각 고름) · watershed(유역도). 추가에서만 쓰고 업데이트는 그대로 둔다.
layout: str = "plan"
# 축척 분모(`scales` 중 하나) — 없으면 추가는 1/1,200 · 업데이트는 지금 값 · 유역도 1/6,000.
scale: int | None = None
# 유역도 등고선 간격(m · `contour_intervals` 중 하나) — 없으면 추가는 5 m · 업데이트는 지금 값.
contour_interval_m: int | None = None
class PlanKindsOrderRequest(BaseModel):
ids: 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],
"scales": list(DRAWING_SCALE_PLAN_CHOICES),
"default_scale": DRAWING_SCALE_PLAN,
"watershed_scale": DRAWING_SCALE_BASIN,
"contour_intervals": list(BASIN_CONTOUR_INTERVAL_CHOICES),
"default_contour_interval": BASIN_CONTOUR_INTERVAL_M,
"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,
request.layout,
request.scale,
request.contour_interval_m,
)
return await asyncio.to_thread(_state, root)
except ValueError as exc:
return _error(400, exc)
@router.put("/{project_id}/plan-kinds-order")
async def put_plan_kinds_order(project_id: UUID, request: PlanKindsOrderRequest) -> Any:
"""종류 차례 — 모달에서 줄을 끌어 놓은 차례 그대로(140-2 · 놓는 즉시)."""
try:
root = await _project_root(project_id)
await asyncio.to_thread(reorder_kinds, root, request.ids)
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,
request.scale,
request.contour_interval_m,
)
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)