- 저장 모듈 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>
126 lines
5.7 KiB
Python
126 lines
5.7 KiB
Python
"""D01 예정노선 등록 API · 대시보드 「받은 예정노선」 (요구 345 · 452 · 요구서 3-1 · 2-2).
|
|
|
|
화면은 이름 · 시작 연도 · 사업 지역만 보냄(요구서 389 3장 1 · 3) — 끝 연도 = 시작 · 임도 종류 없음
|
|
= 설계속도 작업임도 20 · 측점 없음(DB 칸 그대로).
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
import aiomysql
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from pydantic import BaseModel, Field
|
|
|
|
from B05_Profile.B05_Profile_Engine_Grade import resolve_design_speed
|
|
from B05_Profile.B05_Profile_PageSettings import grade_class_of
|
|
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 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)$")
|
|
design_speed_kph: int | None = None
|
|
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="노선 시작 누가거리는 종료 누가거리보다 작아야 합니다."
|
|
)
|
|
speed = resolve_design_speed(grade_class_of(payload.road_type, {}), payload.design_speed_kph)
|
|
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,
|
|
design_speed_kph=speed,
|
|
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}
|