2026-08-06 사용자 지시 일괄 구현: 반폭 체계: - 초기 샘플 반폭 config 기본 20m(SECTION_CROSS_HALF_WIDTH_M 15→20) — 표시 반폭이 이 안이면 재계산 없이 표시만 자름(기존 crossPlotMetrics) - 재계산 버튼 삭제, [전체 측점 반영]이 반폭 적용 담당: 축소=표시만(즉시), 확대(보유 샘플 폭 초과)=regenerate 후 상세 재로드 - regenerate에 grade_options 재구성 추가 — 계획선(profile_alignment)까지 함께 재계산·저장. 예전 재계산 버튼이 계획 횡단도선·유토곡선·테이블을 지우던 근본 원인 해결(B04 파일입력 파이프라인과 같은 엔진 경로 재활용) - 높이 배율 옵션 폐지(항상 1), 반폭 입력은 표준 횡단면 설정의 [전체 측점 반영] 위로 이동(Standard_Panel extraControl) 개별 반폭(카드): - 카드 하단 ◀/▶/↺(±1m·전역 복귀), 암 경계 그룹 우측 정렬. 숫자 표시 없음 - 개별값 > 전역값 우선(StationWidthControl). 세션 보관, 확정·임시저장 시 cross_patches(design.display_half_width_m)로 영구 저장 → 재접근 복원 - 프리뷰·단건 설계 재계산이 이 필드를 이월해 지우지 않게 보강 - 방위각 표기 삭제 측구 방향 확정 동기화(13측점 보고): - B05 경로확정 uphill 병합 시 저장 횡단 설계도 새 방향으로 재계산·저장 (sync_uphill_overrides_into_designs) — 정본만 갱신하면 B06 표시와 역반영(ditch_side→uphill_side)이 옛 방향으로 순환 덮어쓰던 문제 해결 - 절/성토 역할은 엔진이 지형에서 자동 판정(정상) — 사용자 지정 대상인 측구 방향(ditch_side)이 정확히 동기화됨을 13측점 실측 확인 실측: 축소/확대/개별 조절/저장 복원/확정 동기화/B05 하단 패널 정상, tsc·ruff·prettier 통과, 콘솔 에러 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
206 lines
9.0 KiB
Python
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_wf2_Route.B05_wf2_Route_Router_Confirm import _merge_uphill_overrides_into_longitudinal
|
|
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_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_wf3_ProfileCross.B06_wf3_ProfileCross_Router import _compute_default_designs
|
|
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_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": "종횡단 확정 처리 중 오류가 발생했습니다."},
|
|
)
|