feat(528): 신규 프로젝트 그룹 공통 백엔드 — 종류 칸 · 산림청 판정 · D01 · E01 생성 API · 공유 GET 만 · 가입 go.kr = 산림청
- DB 020(추가만): projects 종류 · 사업연도 끝 · 설계속도 · 원본 칸 · plan_route_years · plan_route_assignees · 회사 「산림청」 행 - is_forest_email · is_forest_service · /api/auth/session 에 is_forest_service - require_project_access 한 곳 — 지정 회사 = GET · HEAD 만 · 그 밖 403 - POST /api/d01/project(산림청만 · PLAN · 설계속도 기본 = resolve_design_speed) · GET /api/dashboard/plan-routes/assigned - GET /api/e01/source-candidates · POST /api/e01/project(B06 완료만 · STABILITY · 원본 칸 · 자료 복사는 528-4) - 종류별 단계 시드(PLAN 3 · STABILITY 4) · storage 허용 D0x · E0x · B01 목록에 project_kind - 가입 인증 — 메일 *.go.kr = 회사 산림청 · ACTIVE · 화면 회사 칸 「산림청」 잠금 표시 - 지킴 시험 test_528_project_groups.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""D01 예정노선 등록 API · 대시보드 「받은 예정노선」 (요구 345 · 요구서 3-1 · 2-2)."""
|
||||
|
||||
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 = Field(..., 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]:
|
||||
"""우리 회사에 지정된 예정노선 연도 — 보기 · 연결 상태."""
|
||||
async with (
|
||||
get_db_pool().acquire() as connection,
|
||||
connection.cursor(aiomysql.DictCursor) as cursor,
|
||||
):
|
||||
await cursor.execute(
|
||||
"""SELECT a.project_id, p.name AS project_name, a.year, a.linked_project_id
|
||||
FROM plan_route_assignees a
|
||||
JOIN projects p ON p.id = a.project_id AND p.deleted_at IS NULL
|
||||
WHERE a.company_id = %s
|
||||
ORDER BY p.name, a.year""",
|
||||
(session.get("company_id"),),
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
# ponytail: update_available 자리만 — 지문 비교는 531-3.
|
||||
routes = [{**r, "update_available": False} for r in rows]
|
||||
return {"status": "success", "routes": routes}
|
||||
Reference in New Issue
Block a user