feat(B05·B06): 배수관 측점 정본화 · 코리도 단차 제거 · 인접 측점 기슭막이 링크
3D 예상형상은 종단·횡단 확정 뒤의 최종 산출물이고 3D 쪽 편집 경로는 없다 (2026-08-24 사용자 확정). 그런데 구조물(비정규) 측점은 횡단 정본 자체가 없어 조회할 때마다 프리뷰 기본값이 재계산됐고, 조정창 조작값은 세션에서 사라졌다. 정본 경로를 열고, 그 위에서 코리도 단차를 걷어냈다. ① 구조물 측점 정본화 — 확정·임시저장이 종단 정본 stations 중 DB 행이 없는 측점을 대상에 넣어 upsert. cross_sections 19 → 23행. ② 조정창 조작값 정본화 — 기슭막이 4축(x·d·h·m)·다단 단 수를 design.revet_adjust / extra_wall_counts로 저장하고, 3D culvertLayoutOf가 그 값을 기하 입력으로 쓴다. 카드 우선순위는 세션 → 정본 → 자동. ③ 노폭 연장의 노견 편입 — PIECE_COLS.shoulder 2→3으로 원 노견 끝 꺾임을 남기고, 조기 return을 없애 확폭이 구조물 구간 내내 유지되게 했다. ④ 종단 기울기 반영 — 구조물 구간 복사 단면에 계획고 차이를 전량 얹는다. 노견 표고 단차 0.49~0.61m 제거. ⑤ 접속 테이퍼 — 구간 경계를 제어점으로 세워 "복사 vs 보간" 두 규칙을 하나로 통합. 노폭 3.0m·성토비탈 8.1m 단차 제거. 접속 길이 = 인접 측점까지. 제어점 생성은 B05_Profile_UI_Corridor_Frames.ts로 분리(Build 699 → 635줄). ⑥ 인접 측점 링크 — 기슭막이 연장(기본 10m)이 덮는 옆 측점 횡단도에도 소유 측점 단면을 계획고 차이만 얹어 그린다. 요소별 연장 판정(관·집수정 제외), 표시 전용. BUILD_VERSION 11 → 14 (저장 코리도 만료). tsc/ruff/prettier 통과, pytest 192 passed (기존 실패 1건 유지). 작업이력·자체검증 기록: docs/raw/PLAN.md §4-8. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -23,6 +24,7 @@ from B05_Profile.B05_Profile_Repository import confirm_route as confirm_route_st
|
||||
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_chainages,
|
||||
get_cross_section_designs,
|
||||
get_cross_sections_missing_design_chainages,
|
||||
get_longitudinal_section,
|
||||
@@ -44,6 +46,42 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/projects", tags=["B06 Profile Cross"])
|
||||
|
||||
|
||||
def _rowless_station_chainages(
|
||||
project_root: Path, longitudinal_file_path: str, known: list[float]
|
||||
) -> list[float]:
|
||||
"""종단 정본 stations 중 **cross_sections 행이 없는** 측점의 chainage 목록.
|
||||
|
||||
구조물(비정규) 측점은 B05 확정이 횡단 파일만 쓰고 DB 행을 만들지 않는다
|
||||
(`generate_irregular_sections`). 그래서 확정해도 설계가 정본으로 남지 않고 조회할
|
||||
때마다 프리뷰 기본값이 다시 계산됐다 — 3D·수량이 확정 결과가 아니게 된다
|
||||
(2026-08-24 사용자 확정: 3D는 종단·횡단 확정 뒤의 최종 산출물). 여기서 골라내
|
||||
확정 대상에 넣으면 `update_cross_section_design`의 upsert가 행을 만든다.
|
||||
"""
|
||||
root = project_root.resolve()
|
||||
path = (root / longitudinal_file_path).resolve()
|
||||
if root not in path.parents or not path.is_file():
|
||||
return []
|
||||
try:
|
||||
longitudinal = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return []
|
||||
stations = longitudinal.get("stations") if isinstance(longitudinal, dict) else None
|
||||
if not isinstance(stations, list):
|
||||
return []
|
||||
result: list[float] = []
|
||||
for station in stations:
|
||||
if not isinstance(station, dict):
|
||||
continue
|
||||
try:
|
||||
chainage = float(station.get("chainage_m"))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if any(abs(chainage - value) < 0.01 for value in known):
|
||||
continue
|
||||
result.append(chainage)
|
||||
return result
|
||||
|
||||
|
||||
async def _apply_section_edits(
|
||||
connection: aiomysql.Connection,
|
||||
route_id: int,
|
||||
@@ -83,6 +121,13 @@ async def _apply_section_edits(
|
||||
patch["inlet_structure"] = patch_item.inlet_structure
|
||||
if patch_item.basin_adjust is not None:
|
||||
patch["basin_adjust"] = patch_item.basin_adjust.model_dump()
|
||||
# 기슭막이 4축·다단 단 수 — 세션 전용이던 값을 정본에 남긴다(2026-08-24).
|
||||
if patch_item.revet_adjust is not None:
|
||||
patch["revet_adjust"] = {
|
||||
role: adjust.model_dump() for role, adjust in patch_item.revet_adjust.items()
|
||||
}
|
||||
if patch_item.extra_wall_counts is not None:
|
||||
patch["extra_wall_counts"] = patch_item.extra_wall_counts.model_dump()
|
||||
if patch:
|
||||
await merge_cross_section_design_patch(
|
||||
connection, route_id=route_id, chainage_m=patch_item.chainage_m, patch=patch
|
||||
@@ -97,20 +142,48 @@ async def save_sections(
|
||||
) -> SectionConfirmResponse | JSONResponse:
|
||||
"""편집 중인 종횡단을 **확정하지 않고** 영구저장소에만 남긴다(임시 저장).
|
||||
|
||||
저장 내용은 확정과 같지만 경로 상태·워크플로 단계를 건드리지 않는다. 미지정 측점을
|
||||
기본값으로 채우지도 않는다 — 임시 저장은 **사용자가 실제로 손댄 것만** 남기는 게 맞다.
|
||||
저장 내용은 확정과 같지만 경로 상태·워크플로 단계를 건드리지 않는다. 지반유형 미지정
|
||||
측점을 기본값으로 채우지도 않는다 — 임시 저장은 **사용자가 실제로 손댄 것만** 남긴다.
|
||||
|
||||
다만 **행 자체가 없는 측점**(구조물 등 비정규)은 예외다. 임시저장이 곧 캐시를
|
||||
영구저장소에 내리는 시점인데(2026-08-24 사용자 확정), 행이 없으면 그 측점 패치가
|
||||
통째로 버려져 조작값이 사라진다. 그래서 여기서 정본 행을 만든다.
|
||||
"""
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
if not await get_longitudinal_section(connection, project_id, route_id):
|
||||
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)
|
||||
known = await get_cross_section_chainages(connection, route_id)
|
||||
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
rowless = await asyncio.to_thread(
|
||||
_rowless_station_chainages,
|
||||
project_root,
|
||||
str(existing["longitudinal_file_path"]),
|
||||
known,
|
||||
)
|
||||
default_designs: list[tuple[float, dict[str, Any]]] = []
|
||||
if rowless:
|
||||
default_designs = await asyncio.to_thread(
|
||||
_compute_default_designs,
|
||||
project_root,
|
||||
str(existing["longitudinal_file_path"]),
|
||||
rowless,
|
||||
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, [])
|
||||
await _apply_section_edits(
|
||||
connection, route_id, request, default_designs, project_id
|
||||
)
|
||||
await connection.commit()
|
||||
except Exception:
|
||||
await connection.rollback()
|
||||
@@ -155,11 +228,21 @@ async def confirm_sections(
|
||||
)
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
missing = await get_cross_sections_missing_design_chainages(connection, route_id)
|
||||
known = await get_cross_section_chainages(connection, route_id)
|
||||
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
# 행 자체가 없는 측점(구조물 등 비정규)도 확정 대상에 넣는다 — 정본이 없으면
|
||||
# 조회 때마다 프리뷰가 다시 계산돼 3D·수량이 확정 결과가 아니게 된다(2026-08-24).
|
||||
missing = missing + await asyncio.to_thread(
|
||||
_rowless_station_chainages,
|
||||
project_root,
|
||||
str(existing["longitudinal_file_path"]),
|
||||
known,
|
||||
)
|
||||
|
||||
# 미지정 측점을 기본값으로 계산해 채운다 (계산 불가 측점은 조용히 건너뜀).
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user