- 저장 모듈 D03_PlanAnalysis_Store — 읽기 · 원자 쓰기 · 판 · 옛 DB 칸 읽을 때 옮김(지점 셋 꼴 · 연도 행 접기 · 옛 연도 노선) · plan_state(조각 · 연도 지문 · 확정 = 지문 같을 때만) - D03 [저장] · [초기화] · D02 지점 = 파일만 + 연도 상태 줄(지문 · 확정) · 프로젝트 잠금 · 파일 쓰기 실패 = DB 되돌림 · 빈 연도 줄 지움 - D04 요약 · 업체 지정 · 확정 메일 · 내보내기 · 대시보드 [연결] · 받은 예정노선 · 산림청 홈 · 개요 연결 = 파일에서 · 업체 줄 연도 칸 안 읽음(⑤) - 산림청 홈 「분석 전」 = 노선 안 그은 예정노선 프로젝트 수 · plan-source 지운 원본 거름 - 옛 연도 단위 지정 · 옛 연도 행 노선 길 걷음(DB 실측 0) · 연결 때 설계속도 씨앗 걷음(591-1 이음) - SQL 023 = 업체 줄 year NULL 허용(옛 칸은 590-3 에서 지움) - 시험: 새 지킴 590_1(옮김 · 원자 쓰기 · 연도 읽기 · 잠금) · 낡은 시험 고침 — 529_3 · 530_2 · 531_3 · 531_d04 · 547_2 · 563_2(DB 조각 · 연도 행 흉내 → 파일 흉내 · 옛 연도 지정 시험 · 설계속도 씨앗 시험 지움 = 그 길 걷음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
89 lines
3.7 KiB
Python
89 lines
3.7 KiB
Python
"""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)}
|