Files
Aislo/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Router.py
2026-07-10 19:01:33 +09:00

199 lines
8.7 KiB
Python

"""B06 종횡단 생성 FastAPI 라우터."""
import asyncio
import logging
from pathlib import Path
from uuid import UUID
from fastapi import APIRouter
from fastapi.responses import JSONResponse
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
from B05_wf2_Route.B05_wf2_Route_Repository import get_latest_route
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Engine import run_section_generation
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Engine_Section import SectionGenerationOptions
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Repository import (
confirm_sections_for_route,
create_longitudinal_section,
delete_sections_for_route,
get_longitudinal_section,
insert_cross_sections,
)
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Schema import (
SectionConfirmResponse,
SectionGenerateRequest,
SectionGenerateResponse,
SectionSummaryResponse,
)
from common_util.common_util_storage import resolve_stored_project_path
from common_util.common_util_workflow_state import complete_stage, fail_stage, start_stage
from config.config_db import get_db_pool
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/projects", tags=["B06 Profile Cross"])
def _build_options(request: SectionGenerateRequest) -> SectionGenerationOptions:
"""요청의 옵션(미지정은 config 기본값)으로 SectionGenerationOptions를 만든다."""
defaults = SectionGenerationOptions()
return SectionGenerationOptions(
station_interval_m=request.station_interval_m or defaults.station_interval_m,
cross_half_width_m=request.cross_half_width_m or defaults.cross_half_width_m,
cross_sample_interval_m=request.cross_sample_interval_m or defaults.cross_sample_interval_m,
long_sample_interval_m=request.long_sample_interval_m or defaults.long_sample_interval_m,
include_endpoint=defaults.include_endpoint,
)
@router.post("/{project_id}/sections/generate", response_model=SectionGenerateResponse)
async def generate_sections(
project_id: UUID, request: SectionGenerateRequest
) -> SectionGenerateResponse | JSONResponse:
"""확정 경로에서 종횡단을 생성·저장하고 DB에 기록한다."""
pool = get_db_pool()
try:
params = {
"route_id": request.route_id,
"filter_key": request.filter_key,
"method": request.method,
"smooth": request.smooth,
"station_interval_m": request.station_interval_m,
"cross_half_width_m": request.cross_half_width_m,
"cross_sample_interval_m": request.cross_sample_interval_m,
"long_sample_interval_m": request.long_sample_interval_m,
"crs": request.crs,
}
async with pool.acquire() as connection:
async with connection.cursor() as cursor:
await start_stage(cursor, str(project_id), 3, params)
await connection.commit()
stored_path = await get_project_storage_relative_path(connection, project_id)
project_root = Path(resolve_stored_project_path(stored_path))
latest = await get_latest_route(connection, project_id)
if not latest or latest["id"] != request.route_id:
return JSONResponse(
status_code=404,
content={"status": "error", "message": "대상 경로를 찾을 수 없습니다."},
)
route_data_path = latest["route_data_path"]
options = _build_options(request)
design = await asyncio.to_thread(
run_section_generation,
project_root,
route_data_path,
request.filter_key,
request.method,
request.smooth,
options=options,
crs=request.crs,
)
await connection.begin()
try:
await delete_sections_for_route(connection, request.route_id)
longitudinal_id = await create_longitudinal_section(
connection,
project_id=project_id,
route_id=request.route_id,
data=design["longitudinal"]["data"],
longitudinal_file_path=design["longitudinal"]["file_path"],
)
await insert_cross_sections(
connection,
project_id=project_id,
route_id=request.route_id,
sections=design["cross_sections"],
)
async with connection.cursor() as cursor:
await complete_stage(cursor, str(project_id), 3)
await connection.commit()
except Exception:
await connection.rollback()
raise
return SectionGenerateResponse(
project_id=str(project_id),
route_id=request.route_id,
longitudinal_id=longitudinal_id,
cross_section_count=len(design["cross_sections"]),
length_m=design["longitudinal"]["data"]["length_m"],
longitudinal_file_path=design["longitudinal"]["file_path"],
)
except LookupError as exc:
async with pool.acquire() as connection, connection.cursor() as cursor:
await fail_stage(cursor, str(project_id), 3, str(exc))
await connection.commit()
return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)})
except FileNotFoundError as exc:
async with pool.acquire() as connection, connection.cursor() as cursor:
await fail_stage(cursor, str(project_id), 3, str(exc))
await connection.commit()
return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)})
except (OSError, ValueError) as exc:
async with pool.acquire() as connection, connection.cursor() as cursor:
await fail_stage(cursor, str(project_id), 3, str(exc))
await connection.commit()
return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)})
except Exception as exc:
logger.exception("B06 종횡단 생성 실패: project_id=%s", project_id)
async with pool.acquire() as connection, connection.cursor() as cursor:
await fail_stage(cursor, str(project_id), 3, str(exc))
await connection.commit()
return JSONResponse(
status_code=500,
content={"status": "error", "message": "종횡단 생성 처리 중 오류가 발생했습니다."},
)
@router.get("/{project_id}/sections/{route_id}", response_model=SectionSummaryResponse)
async def get_sections(project_id: UUID, route_id: int) -> SectionSummaryResponse | JSONResponse:
"""경로의 종단면 요약을 조회한다."""
pool = get_db_pool()
try:
async with pool.acquire() as connection:
longitudinal = await get_longitudinal_section(connection, project_id, route_id)
return SectionSummaryResponse(
project_id=str(project_id), route_id=route_id, longitudinal=longitudinal
)
except Exception:
logger.exception("B06 종횡단 조회 실패: project_id=%s", project_id)
return JSONResponse(
status_code=500,
content={"status": "error", "message": "종횡단 조회 중 오류가 발생했습니다."},
)
@router.post("/{project_id}/sections/{route_id}/confirm", response_model=SectionConfirmResponse)
async def confirm_sections(
project_id: UUID, route_id: int
) -> SectionConfirmResponse | JSONResponse:
"""경로의 종횡단면을 확정(CONFIRMED)한다."""
pool = get_db_pool()
try:
async with pool.acquire() as connection:
existing = await get_longitudinal_section(connection, project_id, route_id)
if not existing:
return JSONResponse(
status_code=404,
content={"status": "error", "message": "확정할 종횡단이 없습니다."},
)
await connection.begin()
try:
await confirm_sections_for_route(connection, route_id)
async with connection.cursor() as cursor:
await complete_stage(cursor, str(project_id), 3)
await connection.commit()
except Exception:
await connection.rollback()
raise
return SectionConfirmResponse(project_id=str(project_id), route_id=route_id)
except Exception:
logger.exception("B06 종횡단 확정 실패: project_id=%s", project_id)
return JSONResponse(
status_code=500,
content={"status": "error", "message": "종횡단 확정 처리 중 오류가 발생했습니다."},
)