fix(B05·B06): 옮긴 관 측점의 숨은 계획선 꼭짓점 제거(192-1)

- 측점 있음 판정 폭 0.5m → 0.005m(측점 스냅 폭) — 0.1m 옮긴 관도 측점 없음으로 셈
- 비정규 측점 병합 때 사라진 측점의 base_pvi · 편집을 지움(0.006m 안 반올림은 옮김) · 계획선 다시 셈 · DB 등급 요약 갱신
- 시험 test_192_1(검증_랩탑메인 사본 재현) · missing_stations 판정 폭

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
This commit is contained in:
2026-10-10 13:36:21 +09:00
co-authored by Claude Opus 5.5
parent 935f12f890
commit 6f2136f592
4 changed files with 233 additions and 26 deletions
+22 -13
View File
@@ -28,7 +28,11 @@ from fastapi import APIRouter
from fastapi.responses import JSONResponse
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
from B05_Profile.B05_Profile_Repository import get_latest_route, get_surface_crs_epsg
from B05_Profile.B05_Profile_Repository import (
get_latest_route,
get_surface_crs_epsg,
update_longitudinal_grade_summary,
)
from B05_Profile.B05_Profile_Router_Confirm import load_sampling_snapshot
from common_util.common_util_storage import resolve_stored_project_path
from config.config_db import run_with_connection
@@ -37,14 +41,10 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/projects", tags=["B06 Section"])
#: 「그 자리에 측점이 있다」고 볼 거리(m).
#: ⚠⚠ **0.5m 다 — 0.05m 가 아니다**(2026-09-09 실측으로 뒤집힌 자리).
#: 측점을 만들 때 **정수 미터가 같은 격자 측점이 있으면 그리로 스냅**한다
#: (`B05_Profile_Engine_Sections_Core` 의 파일명 가드). 그래서 관 440.241 은 측점 440.0
#: 위에 서고, 그 측점은 **구조물 이름표까지 달고 있다**(`structure`).
#: 0.05m 로 보면 그런 자리를 「측점 없음」으로 잘못 세어 **있는 측점을 또 만들라고 한다.**
#: 스냅 폭이 「정수 미터 반올림」이므로 최대 어긋남은 0.5m 다.
STATION_MATCH_TOLERANCE_M = 0.5
#: 「그 자리에 측점이 있다」고 볼 거리(m) — 측점 생성의 스냅 폭과 같음
#: (`B05_Profile_Engine_Sections_Core` · 같은 횡단 이름이 되는 0.005m 안만 한 측점).
#: 넓으면 조금 옮긴 관(769.28 → 769.38)을 「측점 있음」으로 세어 측점이 옛 자리에 남음(192-1).
STATION_MATCH_TOLERANCE_M = 0.005
SNAPSHOT_MISSING = (
"지표 샘플링 조건을 찾을 수 없어 측점을 만들 수 없음 — 1단계(지표 확정)를 마친 뒤"
@@ -167,7 +167,7 @@ async def _regenerate_structure_stations(
stored_options, crs_epsg, longitudinal = await run_with_connection(_load)
def _regenerate() -> int:
def _regenerate() -> tuple[int, dict[str, Any] | None]:
polyline = _load_route_polyline(project_root, str(route["route_data_path"]))
pipes = _load_pipe_points(project_root, polyline)
extras = resolve_extra_stations(project_root, pipes)
@@ -181,13 +181,22 @@ async def _regenerate_structure_stations(
options=_section_options_from_stored(stored_options),
crs=f"EPSG:{crs_epsg}" if crs_epsg is not None else None,
)
summary = None
if stations and longitudinal:
_merge_irregular_into_longitudinal(
summary = _merge_irregular_into_longitudinal(
project_root, str(longitudinal["longitudinal_file_path"]), stations
)
return len(stations)
return len(stations), summary
return await asyncio.to_thread(_regenerate)
made, summary = await asyncio.to_thread(_regenerate)
if summary is not None:
# 옮긴 관 자리의 계획선이 다시 셈됨 — DB 등급 요약도 같이(계획선 저장과 같은 길).
await run_with_connection(
lambda connection: update_longitudinal_grade_summary(
connection, route_id=route["id"], grade_summary=summary
)
)
return made
async def ensure_structure_stations(project_id: UUID) -> int: