"""D 예정노선 1단계 기준 지정 — 지점 목록 API (요구서 389 3장 1 · 440 1 · 445 A2 · 452 · PLAN 560-2 · 579-1 · 585-1 · 590-1). - `GET /api/projects/{id}/plan/points` — {epsg, points: [점]} · 점 = {lon, lat, x, y} - `PUT …/plan/points` — {points: [{lon, lat}]} → TM 셈 → 설계 내용 파일 `points` (`D03_PlanAnalysis_Store` · 좌표계 = 파일 머리 · 없으면 점 평균 경도의 TM 띠) - 시점 · 종점 구분 없음 — 옛 꼴(DB `plan_base_json` · 연도 행 접기)은 저장 모듈의 옮김 몫. - 고침 = 산림청만(지정 회사 GET 은 공용 판정 `require_project_access` 몫). """ from __future__ import annotations from typing import Any from uuid import UUID from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from common_util.common_util_auth import is_forest_service, require_company from common_util.common_util_crs import lonlat_to_tm, tm_epsg_for_lon, tm_to_lonlat from common_util.common_util_save_guard import project_lock from D03_PlanAnalysis import D03_PlanAnalysis_Store as store router = APIRouter(prefix="/api/projects", tags=["D02 Plan Base"]) MAX_POINTS = 40 # 지점 상한(옛 시점 20 + 종점 20) class LonLat(BaseModel): lon: float = Field(..., ge=120, le=135) lat: float = Field(..., ge=30, le=45) class BaseRequest(BaseModel): points: list[LonLat] = Field(default_factory=list, max_length=MAX_POINTS) def _point(x: Any, y: Any, epsg: Any) -> dict[str, float] | None: if x is None or y is None or epsg is None: return None lon, lat = tm_to_lonlat(float(x), float(y), int(epsg)) return {"lon": lon, "lat": lat, "x": float(x), "y": float(y)} def base_view(plan: dict[str, Any]) -> dict[str, Any]: """{epsg, points} — 설계 내용의 지점(TM) + 경위도 · 지점이 없으면 epsg None.""" epsg = plan.get("epsg") points = [p for p in (_point(i.get("x"), i.get("y"), epsg) for i in plan["points"]) if p] return {"epsg": epsg if points else None, "points": points} async def load_base(project_id: UUID) -> dict[str, Any]: """지점 목록 — D03 이 읽는 길.""" _, plan = await store.load_plan(project_id) return base_view(plan) def stored_base(payload: BaseRequest, epsg: int | None = None) -> dict[str, Any]: """PUT 몸 → 파일 꼴(TM m) — 좌표계 = `epsg`(저장된 값) · 없으면 점 평균 경도의 TM 띠.""" given = payload.points if not given: epsg = None elif epsg is None: epsg = tm_epsg_for_lon(sum(p.lon for p in given) / len(given)) points = [dict(zip(("x", "y"), lonlat_to_tm(p.lon, p.lat, epsg))) for p in given] return {"epsg": epsg, "points": points} @router.get("/{project_id}/plan/points", response_model=None) async def get_plan_points(project_id: UUID) -> dict[str, Any]: return {"status": "success", **await load_base(project_id)} @router.put("/{project_id}/plan/points", response_model=None) async def put_plan_points( project_id: UUID, payload: BaseRequest, session: dict[str, Any] = Depends(require_company), ) -> dict[str, Any]: if not is_forest_service(session): raise HTTPException(status_code=403, detail="예정노선 지점은 산림청만 고칠 수 있습니다.") async with project_lock(str(project_id)): root, plan = await store.load_plan(project_id) # 없음 · 예정노선 아님 거절 base = stored_base(payload, plan["epsg"]) epsg = plan["epsg"] if plan["lines"] else base["epsg"] # 선이 있으면 좌표계 그대로 plan = {**plan, "epsg": epsg, "points": base["points"]} store.save_plan(root, plan) return {"status": "success", **base_view(plan)}