feat(344-2): 첫 계획선 · 관 자리 투영 · 구조물 좌표 따라감 · 계획고 옮김 짝

- 첫 계획선(계획고 = 지반고) base_pvi TS 짝 = B05_Profile/logic/B05_Profile_Grade_Ground.ts(1순위만 · 못 서는 땅은 null = 서버로 다시 셈)
- 관 자리 투영 TS 짝 · 구조물 따라감 py · ts 짝 = common_util_route_follow(정수 m · 구간 끝 밂 · 0 ~ 연장 자름 · 20 m 넘으면 멀어짐 표시 · 상수 STRUCTURE_ROUTE_DRIFT_MAX_M)
- 구조물 x · y 저장(저장 때 노선 위 자리 · 자리가 그대로면 옛 좌표 지킴 · 옛 자료는 읽을 때 채움) · 노선 [확인] 때 구조물 정본이 좌표를 따라감
- 노선 바뀜 때 계획고 옮김 = 바뀐 구간만 새 지반 첫 계획선 · 앞 그대로 · 뒤 길이 차만큼 밂(B05_Profile_Engine_Grade_Carry.py ↔ logic/B05_Profile_Grade_Carry.ts)
- 거울 시험 test_344_2_route_pairs(+ helper cjs) · buildCurves · evaluateAt 내보냄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
2026-10-16 11:19:54 +09:00
co-authored by Claude Opus 5.5
parent 73a251db26
commit bb63b30aae
14 changed files with 1137 additions and 3 deletions
@@ -42,6 +42,7 @@ from common_util.common_util_drainage_pipes import (
pipe_points_path_in,
read_pipe_points_file,
)
from common_util.common_util_route_follow import fill_structure_coordinates, follow_structures
from common_util.common_util_storage import resolve_stored_project_path
from common_util.common_util_structure_station import plan_stations
from config.config_db import get_db_pool
@@ -96,6 +97,61 @@ async def _route_length(project_id: UUID) -> float | None:
return None
async def _route_xy(project_id: UUID, root: str) -> list[list[float]] | None:
"""최신 노선 정점(x · y) — 구조물 좌표를 채울 선. 없으면 None(좌표 없이 진행)."""
from B05_Profile.B05_Profile_Engine_Sections import _load_route_polyline
try:
pool = get_db_pool()
async with pool.acquire() as connection:
latest = await get_latest_route(connection, project_id)
path = (latest or {}).get("route_data_path")
return (
await asyncio.to_thread(_load_route_polyline, Path(root), str(path)) if path else None
)
except Exception:
logger.exception(
"B05 구조물 좌표: 노선을 못 읽음(좌표 없이 진행): project_id=%s", project_id
)
return None
def follow_structures_to_route(
project_root: Path, old_route_data_path: str | None, new_xy: list[dict[str, float]]
) -> dict[str, int]:
"""노선이 바뀔 때 구조물이 땅 위 자리를 따라감(344-2) → 정본 저장. {옮김, 멀어짐} 수.
좌표가 없는 옛 자료는 옛 노선(아직 안 바뀐 경로 파일)으로 먼저 채운다. 실패는 비치명 —
구조물은 옛 누가거리 그대로 남는다(지금까지와 같음).
"""
from B05_Profile.B05_Profile_Engine_Sections import _load_route_polyline
root = str(project_root)
revision, items = load_structures(root)
if not items:
return {"moved": 0, "far": 0}
if old_route_data_path:
try:
fill_structure_coordinates(
items, _load_route_polyline(project_root, old_route_data_path), only_missing=True
)
except (OSError, ValueError):
logger.warning("B05 구조물 따라감: 옛 노선을 못 읽어 좌표 없는 구조물은 그대로")
before = [(item.chainage_m, item.start_m, item.end_m) for item in items]
result = follow_structures(items, [(point["x"], point["y"]) for point in new_xy])
moved = sum(
before[index] != (item.chainage_m, item.start_m, item.end_m)
for index, item in enumerate(items)
)
far = sum(1 for _, flag in result if flag)
if moved:
# 범위는 따라감이 0 ~ 새 연장 안으로 이미 자름 — DB 연장(옛 노선)으로 재지 않음
save_structures(root, items, base_revision=revision)
if far:
logger.warning("B05 구조물 %d건이 새 노선에서 멀어짐(버리지 않음)", far)
return {"moved": moved, "far": far}
def _migrate_pipe_facilities(
root: str, facilities: list[PipePoint], migrated_before: set[str]
) -> tuple[int, set[str]]:
@@ -160,6 +216,11 @@ async def read_structures(project_id: UUID) -> StructureListResponse | JSONRespo
if root is None:
return JSONResponse(status_code=404, content=_PROJECT_PATH_MISSING)
revision, structures = load_structures(root)
# 옛 자료(좌표 없음)는 지금 노선 위 기준 자리로 채워 보냄 — 파일은 다음 [저장] 때(344-2)
if any(item.x is None for item in structures):
xy = await _route_xy(project_id, root)
if xy:
fill_structure_coordinates(structures, xy, only_missing=True)
return StructureListResponse(
project_id=str(project_id),
revision=revision,
@@ -287,6 +348,14 @@ async def write_structures(
if root is None:
return JSONResponse(status_code=404, content=_PROJECT_PATH_MISSING)
_, previous = load_structures(root)
# 땅 위 자리(344-2) — 기준 측점의 노선 위 좌표 · 자리가 그대로면 옛 좌표 지킴
xy = await _route_xy(project_id, root)
if xy:
fill_structure_coordinates(
payload.structures,
xy,
previous={item.structure_id: item for item in previous if item.structure_id},
)
revision = save_structures(
root,
payload.structures,