- 집수정 조정값(BasinAdjust: 내공 폭·높이, 좌우 lateralM, 상하 slopeM) 신설. 측점별 세션 보관 + 백엔드 design.basin_adjust 저장 경로(Schema·Router) 연동 - 조정창에 집수정 조정 항목 추가, I형 관 시작점을 벽 안쪽 변과 원지반의 교차점으로 산출(이분법), 집수정 성토(되메움)선 5° → 성토 비탈 1:1.2로 통일 - 설계선 트림 수정: 배수관 레이아웃이 사면 폴리라인을 준 쪽은 그 시작점(노견)에서 설계선을 끊는다 — 종전에는 트림 경계에서 끝점 표고를 벽 상단으로 강제 스냅해 집수정을 좌우로 옮기면 마지막 구간이 수직으로 튀는 선이 남았다(사용자 지적) - basinApproachPoints의 길이 0 중복 꼭짓점 제거 검증: 4+4.3(집수정 I형 좌우 2.9m 이동)·13+15.7·5+0.0·2+0.0에서 급경사 세그먼트 0건 · tsc --noEmit 통과 · ruff check 통과 · prettier 적용 · pytest tmp/tests/ 148 passed, 7 skipped · B06_Section 700줄 초과 0건. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
223 lines
10 KiB
Python
223 lines
10 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_Repository import confirm_route as confirm_route_status
|
|
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, start_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_item.inlet_structure is not None:
|
|
patch["inlet_structure"] = patch_item.inlet_structure
|
|
if patch_item.basin_adjust is not None:
|
|
patch["basin_adjust"] = patch_item.basin_adjust.model_dump()
|
|
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),
|
|
mark_stage_complete: bool = True,
|
|
) -> SectionConfirmResponse | JSONResponse:
|
|
"""경로의 종횡단면을 확정(CONFIRMED)한다.
|
|
|
|
지반유형을 지정하지 않은 측점은 기본값(토사/좌절토)으로 자동 채운 뒤 확정한다.
|
|
표준 횡단면 설정값이 함께 오면 longitudinal_sections.data.options에 저장한다.
|
|
|
|
B05·B06은 한 흐름이라 사용자 [확정]은 stage 2(종단)와 3(횡단)을 함께 닫는다
|
|
(B05 단독 확정 폐지, 2026-08-08 워크플로우 재정의). `mark_stage_complete=False`는
|
|
자동 계산 체인용 — 데이터만 확정하고 stage 3을 IN_PROGRESS(검토 대기)로 남긴다.
|
|
"""
|
|
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:
|
|
if mark_stage_complete:
|
|
# B05 단독 확정 폐지 보완 — 재탐색 후 임시저장 없이 바로 확정해도
|
|
# 경로 상태(DRAFT)가 남지 않도록 여기서 함께 CONFIRMED로 닫는다.
|
|
await confirm_route_status(connection, route_id)
|
|
await complete_stage(cursor, str(project_id), 2)
|
|
await complete_stage(cursor, str(project_id), 3)
|
|
else:
|
|
await start_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": "종횡단 확정 처리 중 오류가 발생했습니다."},
|
|
)
|