Files
Aislo/B06_Section/B06_Section_Router_Confirm.py
T
eomsangdonandClaude Fable 5 54954a05e5 refactor(B05,B06): B05_wf2_Route -> B05_Profile, B06_wf3_ProfileCross -> B06_Section 동시 개명
- 한몸으로 동작하는 두 페이지라 한 커밋으로 처리 (상호 참조 다수)
- B05 37파일 + B06 20파일 접두사 개명 (git mv, 이력 보존)
- 참조 치환 91파일: import 경로, 라우트 슬러그(b05-profile/b06-section),
  라우트 키(B05_PROFILE/B06_SECTION), B03 자동 체인, storage 상수, pyproject 제외 경로
- 로직 변경 없음. typecheck·백엔드 import 검증 통과

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 10:03:11 +09:00

206 lines
9.0 KiB
Python

"""B06 종횡단 **저장·확정** 라우터.
`_Router.py`가 700줄을 넘겨 조회·계산(그쪽)과 저장·확정(여기)을 갈랐다.
임시 저장과 확정은 **저장하는 내용이 같다**(표준횡단 설정 · 유토곡선 · 측점별 암 경계선).
다른 것은 뒤처리뿐이다 — 확정만 미지정 측점을 기본값으로 채우고, 경로 상태를 CONFIRMED로
바꾸고, 워크플로 단계를 닫고, 측구 방향을 B05 종단 정본에 역반영한다. 그래서 공통 저장을
`_apply_section_edits()` 하나로 두고 두 엔드포인트가 함께 쓴다.
"""
import asyncio
import logging
from pathlib import Path
from typing import Any
from uuid import UUID
import aiomysql
from fastapi import APIRouter, Body
from fastapi.responses import JSONResponse
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
from B05_Profile.B05_Profile_Router_Confirm import _merge_uphill_overrides_into_longitudinal
from B06_Section.B06_Section_Repository import (
confirm_sections_for_route,
get_cross_section_designs,
get_cross_sections_missing_design_chainages,
get_longitudinal_section,
merge_cross_section_design_patch,
merge_longitudinal_section_data,
merge_longitudinal_section_options,
update_cross_section_design,
)
from B06_Section.B06_Section_Router import _compute_default_designs
from B06_Section.B06_Section_Schema import (
SectionConfirmRequest,
SectionConfirmResponse,
)
from common_util.common_util_storage import resolve_stored_project_path
from common_util.common_util_workflow_state import complete_stage
from config.config_db import get_db_pool
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/projects", tags=["B06 Profile Cross"])
async def _apply_section_edits(
connection: aiomysql.Connection,
route_id: int,
request: SectionConfirmRequest | None,
default_designs: list[tuple[float, dict[str, Any]]],
project_id: UUID | None = None,
) -> None:
"""임시 저장과 확정이 **함께 쓰는** 저장 본체. 트랜잭션은 호출한 쪽이 연다."""
for chainage_m, design in default_designs:
await update_cross_section_design(
connection,
route_id=route_id,
chainage_m=chainage_m,
design=design,
project_id=project_id,
)
if request and request.standard_cross_section:
await merge_longitudinal_section_options(
connection,
route_id=route_id,
options_patch={"standard_cross_section": request.standard_cross_section},
)
# 유토곡선 결과는 생성 옵션이 아니므로 data.options가 아니라 최상위 키에 둔다.
if request and request.mass_haul:
await merge_longitudinal_section_data(
connection, route_id=route_id, data_patch={"mass_haul": request.mass_haul}
)
# 프론트 세션 보관값(암 경계선 오프셋 등)을 측점별 design에 병합.
if request and request.cross_patches:
for patch_item in request.cross_patches:
patch: dict[str, Any] = {}
if patch_item.rock_boundary_offset_m is not None:
patch["rock_boundary_offset_m"] = patch_item.rock_boundary_offset_m
if patch_item.display_half_width_m is not None:
patch["display_half_width_m"] = patch_item.display_half_width_m
if patch:
await merge_cross_section_design_patch(
connection, route_id=route_id, chainage_m=patch_item.chainage_m, patch=patch
)
@router.post("/{project_id}/sections/{route_id}/save", response_model=SectionConfirmResponse)
async def save_sections(
project_id: UUID,
route_id: int,
request: SectionConfirmRequest | None = Body(default=None),
) -> SectionConfirmResponse | JSONResponse:
"""편집 중인 종횡단을 **확정하지 않고** 영구저장소에만 남긴다(임시 저장).
저장 내용은 확정과 같지만 경로 상태·워크플로 단계를 건드리지 않는다. 미지정 측점을
기본값으로 채우지도 않는다 — 임시 저장은 **사용자가 실제로 손댄 것만** 남기는 게 맞다.
"""
pool = get_db_pool()
try:
async with pool.acquire() as connection:
if not await get_longitudinal_section(connection, project_id, route_id):
return JSONResponse(
status_code=404,
content={"status": "error", "message": "저장할 종횡단이 없습니다."},
)
await connection.begin()
try:
await _apply_section_edits(connection, route_id, request, [])
await connection.commit()
except Exception:
await connection.rollback()
raise
return SectionConfirmResponse(
project_id=str(project_id), route_id=route_id, confirmed=False
)
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,
request: SectionConfirmRequest | None = Body(default=None),
) -> SectionConfirmResponse | JSONResponse:
"""경로의 종횡단면을 확정(CONFIRMED)한다.
지반유형을 지정하지 않은 측점은 기본값(토사/좌절토)으로 자동 채운 뒤 확정한다.
표준 횡단면 설정값이 함께 오면 longitudinal_sections.data.options에 저장한다.
"""
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": "확정할 종횡단이 없습니다."},
)
stored_path = await get_project_storage_relative_path(connection, project_id)
missing = await get_cross_sections_missing_design_chainages(connection, route_id)
# 미지정 측점을 기본값으로 계산해 채운다 (계산 불가 측점은 조용히 건너뜀).
default_designs: list[tuple[float, dict[str, Any]]] = []
if missing:
project_root = Path(resolve_stored_project_path(stored_path))
default_designs = await asyncio.to_thread(
_compute_default_designs,
project_root,
str(existing["longitudinal_file_path"]),
missing,
request.standard_cross_section if request else None,
)
async with pool.acquire() as connection:
await connection.begin()
try:
await _apply_section_edits(
connection, route_id, request, default_designs, project_id
)
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
# 측구 방향(design.ditch_side) 변경을 B05 종단 정본 stations.uphill_side에 역반영한다(E-7).
# 파일 기반·비치명적: 실패해도 확정은 유지한다.
try:
async with pool.acquire() as connection:
designs = await get_cross_section_designs(connection, route_id)
overrides = [
{"chainage_m": record["chainage_m"], "side": record["design"]["ditch_side"]}
for record in designs
if isinstance(record.get("design"), dict)
and record["design"].get("ditch_side") in ("left", "right")
]
if overrides:
await asyncio.to_thread(
_merge_uphill_overrides_into_longitudinal,
Path(resolve_stored_project_path(stored_path)),
str(existing["longitudinal_file_path"]),
overrides,
)
except Exception:
logger.exception(
"B06 측구 방향 B05 역반영 실패 (확정은 유지): project_id=%s route_id=%s",
project_id,
route_id,
)
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": "종횡단 확정 처리 중 오류가 발생했습니다."},
)