Files
Aislo/D01_PlanRegister/D01_PlanRegister_Router.py
T
eomsangdonandClaude Opus 5.5 3d7f6d0b70 feat(D01): 예정노선 등록 — 설계속도 지움 · 카카오 지도 기준점 하나 + 주소(VWorld · 출처 표기) · 사업 지역 채움 (PLAN 591-1 · 요구 452 · 453)
- 등록 화면 = 공용 카카오 지도(마우스 설정 따름 · 일반/위성) 누름 · 끌기로 기준점 하나 · 주소 글 + 출처
- 사업 지역 = 주소의 「시군구 읍면」 채움 · 손으로 고친 값은 안 덮음
- 서버 = 기준점 필수(경위도 범위) · 주소는 서버가 VWorld 로 다시 찾아 저장 · 설계속도 칸 안 씀(B05 기본 20)
- 새 길 GET /api/map/address — 지번 먼저(산 번지) · region · 출처 VWorld · B08 한 줄 주소 그대로
- DB 024 = projects 기준점 칸 셋 추가 · PLAN 설계속도 비움 · 옛 PLAN 기준점 = 지점 가운데(024_plan_site_fill.py · 적용 완료)
- 시험 528 = 설계속도 단언을 기준점 단언으로 · 새 지킴 시험 591_1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:41:31 +09:00

134 lines
6.2 KiB
Python

"""D01 예정노선 등록 API · 대시보드 「받은 예정노선」 (요구 345 · 요구서 3-1 · 2-2).
화면은 이름 · 시작 연도 · 사업 지역 · 기준점(경위도) 보냄(요구서 389 3장 1 · 3 · 452 · PLAN 591-1) —
끝 연도 = 시작 · 임도 종류 · 설계속도 없음(B 로 넘길 때 B05 기본) · 측점 없음(DB 칸 그대로).
기준점 = D03 처음 자료 받는 기준 · 주소 = 서버가 VWorld 로 다시 찾아 저장(못 찾음 = 빈 값).
"""
import json
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]:
"""우리 회사에 지정된 예정노선 조각(옛 = 연도) — 확정된 것(또는 이미 연결한 것) · 연결 상태 ·
업데이트 · 조각 이름 · 길이(조각 행 · 옛 = 연도 행)."""
from D04_PlanConfirm import D04_PlanConfirm_Repository as plan_repo
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.year,
a.piece_id, c.length_m AS piece_length_m,
l.id AS linked_project_id, l.name AS linked_project_name,
l.source_fingerprint, COALESCE(c.route_json, y.route_json) AS route_json,
y.confirmed_at
FROM plan_route_assignees a
JOIN projects p ON p.id = a.project_id AND p.deleted_at IS NULL
LEFT JOIN plan_route_years y ON y.project_id = a.project_id AND y.year = a.year
LEFT JOIN plan_route_pieces c
ON c.project_id = a.project_id AND c.piece_id = a.piece_id
LEFT JOIN projects l ON l.id = a.linked_project_id AND l.deleted_at IS NULL
WHERE a.company_id = %s
AND (y.confirmed_at IS NOT NULL OR l.id IS NOT NULL)
ORDER BY p.name, a.year, a.piece_id""",
(session.get("company_id"),),
)
rows = await cursor.fetchall()
pieces: dict[tuple[str, str], dict[str, Any]] = {}
for project_id in {r["project_id"] for r in rows if r.get("piece_id")}:
for piece in await plan_repo.list_pieces(str(project_id)):
pieces[(str(project_id), piece["piece_id"])] = piece
routes = []
for row in rows:
route_json, fingerprint = row.pop("route_json"), row.pop("source_fingerprint")
piece_length = row.pop("piece_length_m", None)
confirmed = row["confirmed_at"] is not None
body = json.loads(route_json) if isinstance(route_json, str | bytes) else route_json
piece = pieces.get((str(row["project_id"]), row.get("piece_id")))
routes.append(
{
**row,
"piece_id": row.get("piece_id"),
"piece_label": piece
and piece_label(piece["line_name"], piece["from_m"], piece["to_m"]),
"length_m": piece_length
if piece_length is not None
else (body or {}).get("length_m"),
"update_available": update_available(route_json, fingerprint, confirmed),
}
)
return {"status": "success", "routes": routes}