Files
Aislo/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Router.py
T
2026-07-18 23:59:28 +09:00

196 lines
8.6 KiB
Python

"""B06 종횡단 생성 FastAPI 라우터."""
import asyncio
import json
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_Engine_Sections_Core import SectionGenerationOptions
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Repository import (
confirm_sections_for_route,
count_cross_sections,
get_confirmed_route_context,
get_longitudinal_section,
)
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Schema import (
SectionConfirmResponse,
SectionContextResponse,
SectionDetailResponse,
SectionOptionDefaults,
SectionSummaryResponse,
)
from common_util.common_util_storage import resolve_stored_project_path
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
from common_util.common_util_workflow_state import complete_stage
from config.config_db import get_db_pool
from config.config_system import SECTION_VERTICAL_EXAGGERATION
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/projects", tags=["B06 Profile Cross"])
@router.get("/{project_id}/sections/context", response_model=SectionContextResponse)
async def get_section_context(project_id: UUID) -> SectionContextResponse | JSONResponse:
"""최신 확정 경로와 B04 확정값, config 기반 생성 기본값을 반환한다."""
pool = get_db_pool()
try:
async with pool.acquire() as connection:
route_context = await get_confirmed_route_context(connection, project_id)
surface_params = await get_surface_confirmation_params(connection, str(project_id))
defaults = SectionGenerationOptions()
return SectionContextResponse(
project_id=str(project_id),
route_id=route_context["route_id"] if route_context else None,
filter_key=surface_params["source_filter"] if route_context else None,
method=surface_params["method"] if route_context else None,
smooth=bool(surface_params["smooth"]) if route_context else None,
crs_epsg=route_context["crs_epsg"] if route_context else None,
defaults=SectionOptionDefaults(
station_interval_m=defaults.station_interval_m,
cross_half_width_m=defaults.cross_half_width_m,
cross_sample_interval_m=defaults.cross_sample_interval_m,
long_sample_interval_m=defaults.long_sample_interval_m,
vertical_exaggeration=SECTION_VERTICAL_EXAGGERATION,
),
)
except Exception:
logger.exception("B06 종횡단 컨텍스트 조회 실패: project_id=%s", project_id)
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)
cross_section_count = await count_cross_sections(connection, route_id)
data = longitudinal.get("data") if longitudinal else None
length_m = data.get("length_m") if isinstance(data, dict) else None
return SectionSummaryResponse(
project_id=str(project_id),
route_id=route_id,
longitudinal=longitudinal,
length_m=length_m,
cross_section_count=cross_section_count,
)
except Exception:
logger.exception("B06 종횡단 조회 실패: project_id=%s", project_id)
return JSONResponse(
status_code=500,
content={"status": "error", "message": "종횡단 조회 중 오류가 발생했습니다."},
)
def _read_section_detail(project_root: Path, longitudinal_file_path: str) -> dict:
"""검증된 프로젝트 루트 안의 종단 및 횡단 JSON을 읽는다."""
root = project_root.resolve()
longitudinal_path = (root / longitudinal_file_path).resolve()
if root not in longitudinal_path.parents:
raise ValueError("종단면 파일 경로가 프로젝트 저장소를 벗어났습니다.")
if not longitudinal_path.is_file():
raise FileNotFoundError("종단면 상세 파일을 찾을 수 없습니다.")
stage_root = longitudinal_path.parent.parent
cross_dir = stage_root / "cross_sections"
if not cross_dir.is_dir():
raise FileNotFoundError("횡단면 상세 파일을 찾을 수 없습니다.")
longitudinal = json.loads(longitudinal_path.read_text(encoding="utf-8"))
cross_sections = [
json.loads(path.read_text(encoding="utf-8"))
for path in sorted(cross_dir.glob("cross_*.json"))
]
if not isinstance(longitudinal, dict) or not all(
isinstance(section, dict) for section in cross_sections
):
raise ValueError("종횡단 상세 파일 형식이 올바르지 않습니다.")
return {"longitudinal": longitudinal, "cross_sections": cross_sections}
@router.get("/{project_id}/sections/{route_id}/detail", response_model=SectionDetailResponse)
async def get_section_detail(
project_id: UUID, route_id: int
) -> SectionDetailResponse | JSONResponse:
"""경로의 SVG 렌더링용 종단·횡단 원시 샘플을 반환한다."""
pool = get_db_pool()
try:
async with pool.acquire() as connection:
longitudinal = await get_longitudinal_section(connection, project_id, route_id)
if not longitudinal:
return JSONResponse(
status_code=404,
content={"status": "error", "message": "종횡단 상세 결과가 없습니다."},
)
stored_path = await get_project_storage_relative_path(connection, project_id)
project_root = Path(resolve_stored_project_path(stored_path))
detail = await asyncio.to_thread(
_read_section_detail,
project_root,
str(longitudinal["longitudinal_file_path"]),
)
return SectionDetailResponse(**detail)
except FileNotFoundError as exc:
return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)})
except (OSError, ValueError, json.JSONDecodeError) as exc:
logger.warning(
"B06 종횡단 상세 파일 조회 실패: project_id=%s route_id=%s error=%s",
project_id,
route_id,
exc,
)
return JSONResponse(
status_code=500,
content={"status": "error", "message": "종횡단 상세 파일을 읽지 못했습니다."},
)
except Exception:
logger.exception(
"B06 종횡단 상세 조회 실패: project_id=%s route_id=%s", project_id, route_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": "종횡단 확정 처리 중 오류가 발생했습니다."},
)