"""D01 예정노선 등록 API · 대시보드 「받은 예정노선」 (요구 345 · 452 · 요구서 3-1 · 2-2). 화면은 이름 · 시작 연도 · 사업 지역 · 기준점(경위도) 보냄(요구서 389 3장 1 · 3 · 452 · PLAN 591-1) — 끝 연도 = 시작 · 임도 종류 · 설계속도 없음(B 로 넘길 때 B05 기본) · 측점 없음(DB 칸 그대로). 기준점 = D03 처음 자료 받는 기준 · 주소 = 서버가 VWorld 로 다시 찾아 저장(못 찾음 = 빈 값). """ from typing import Any import aiomysql from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel, Field from B01_Dashboard.B01_Dashboard_Map import reverse_place from common_util.common_util_auth import is_forest_service, require_company from common_util.common_util_project_kind import create_kind_project from config.config_db import get_db_pool router = APIRouter(prefix="/api/d01", tags=["D01_PlanRegister"]) dashboard_router = APIRouter(prefix="/api/dashboard", tags=["D01_PlanRegister"]) class Site(BaseModel): lon: float = Field(..., ge=120, le=135) lat: float = Field(..., ge=30, le=45) class CreatePlanRequest(BaseModel): project_name: str = Field(..., min_length=1, max_length=255) project_year: int = Field(..., ge=2000, le=2100) project_year_end: int | None = Field(default=None, ge=2000, le=2100) region: str = Field(..., min_length=1, max_length=100) road_type: str | None = Field(default=None, pattern="^(main|fire|work)$") site: Site start_chainage_m: float | None = Field(default=None, ge=0) end_chainage_m: float | None = Field(default=None, ge=0) @router.post("/project") async def post_plan_project( request: Request, payload: CreatePlanRequest, session: dict[str, Any] = Depends(require_company), ) -> dict[str, str]: """예정노선 프로젝트 생성 — 산림청(시스템관리자 · *.go.kr)만.""" if not is_forest_service(session): raise HTTPException(status_code=403, detail="예정노선은 산림청만 만들 수 있습니다.") if session.get("company_id") is None: raise HTTPException(status_code=403, detail="회사 연결이 필요합니다.") year_end = payload.project_year_end or payload.project_year if year_end < payload.project_year: raise HTTPException(status_code=400, detail="사업연도 끝이 시작보다 앞입니다.") start_m, end_m = payload.start_chainage_m, payload.end_chainage_m if start_m is not None and end_m is not None and start_m >= end_m: raise HTTPException( status_code=400, detail="노선 시작 누가거리는 종료 누가거리보다 작아야 합니다." ) site = payload.site address = (await reverse_place(site.lon, site.lat))["address"] project_id = await create_kind_project( request=request, user_id=int(session["user_id"]), company_id=int(session["company_id"]), kind="PLAN", name=payload.project_name.strip(), region=payload.region.strip(), road_type=payload.road_type, project_year=payload.project_year, project_year_end=year_end, site=(site.lon, site.lat, address), route_start_m=start_m, route_end_m=end_m, ) return {"project_id": project_id} @dashboard_router.get("/plan-routes/assigned") async def assigned_plan_routes( session: dict[str, Any] = Depends(require_company), ) -> dict[str, Any]: """우리 회사에 지정된 예정노선 조각 — 확정된 것(또는 이미 연결한 것) · 연결 상태 · 업데이트 · 조각 이름 · 길이 · 연도(설계 내용 파일 `plan_state` · 업체 줄은 조각 번호만).""" from D03_PlanAnalysis import D03_PlanAnalysis_Store as store from D04_PlanConfirm.D04_PlanConfirm_Link import update_available from D04_PlanConfirm.D04_PlanConfirm_Router import piece_label async with ( get_db_pool().acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor, ): await cursor.execute( """SELECT a.id AS assignee_id, a.project_id, p.name AS project_name, a.piece_id, l.id AS linked_project_id, l.name AS linked_project_name, l.source_fingerprint FROM plan_route_assignees a JOIN projects p ON p.id = a.project_id AND p.deleted_at IS NULL LEFT JOIN projects l ON l.id = a.linked_project_id AND l.deleted_at IS NULL WHERE a.company_id = %s ORDER BY p.name, a.piece_id""", (session.get("company_id"),), ) rows = await cursor.fetchall() pieces: dict[tuple[str, str], dict[str, Any]] = {} for project_id in {str(r["project_id"]) for r in rows}: try: units = (await store.plan_state(project_id))["units"] except HTTPException: # 노선 파일이 틀린 프로젝트 = 건너뜀(목록 전체를 막지 않음) continue for unit in units: pieces[(project_id, unit["id"])] = unit routes = [] for row in rows: fingerprint = row.pop("source_fingerprint") piece = pieces.get((str(row["project_id"]), row["piece_id"])) confirmed_at = piece and piece["confirmed_at"] if confirmed_at is None and row["linked_project_id"] is None: continue route = piece and {"planned": piece["planned"]} routes.append( { **row, "year": piece and piece["year"], "confirmed_at": confirmed_at, "piece_label": piece and piece_label(piece["line_name"], piece["from_m"], piece["to_m"]), "length_m": piece and piece["length_m"], "update_available": update_available(route, fingerprint, bool(confirmed_at)), } ) routes.sort(key=lambda r: (r["project_name"], r["year"] or 0, r["piece_id"])) return {"status": "success", "routes": routes}