feat(B03): 계획노선 shapefile 의 CSV 정본화 + LAS 없는 도엽 서피스 좌표계 교정

- 자동설계 체인이 초기값 스냅샷에 `planned_route.csv` 를 함께 남김. 값은 체인이
  이미 만든 정점(사업지 좌표계·트림·조밀화 후)이라 재계산 없음.
- `load_design_route()` 는 surface_params 가 주어진 호출에 한해 그 CSV 정본을 읽음.
  트림 전 원본이 필요한 호출(도엽 범위)은 종전 경로 유지. 지표면·노선이 바뀌면
  `discard_initial_snapshot()` 이 폴더째 지워 경로가 저절로 닫힘.
- `run_sheet_surface_analysis()` 가 노선을 원본 좌표계로 읽어 도엽 서피스를 딴 자리에
  만들던 문제 교정 — `load_design_route()` 로 창구 통일, 도엽 확보 PRJ 탐색도 지형
  PRJ 폴더로 교정. shapefile 노선(5179) + 지형 PRJ(5176) 조합에서 트림이 노선을
  통째로 지우던 원인.

검증: tmp/tests 158 passed(신규 4건 포함), 공용 브라우저 실사용 경로로 용화 shapefile
한 벌 업로드 → 자동 체인 완주 → CSV 정본 643점, 원본 경로와 최대 좌표차 0.00007m.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-03 08:17:21 +09:00
co-authored by Claude Opus 5
parent e065e5701e
commit 56b9bf5280
4 changed files with 63 additions and 11 deletions
+33 -2
View File
@@ -9,6 +9,7 @@ CLAUDE.md 5장(조작·데이터 흐름 정책)의 **초기값** 층이다. 자
`pipe_points.json` 편집분이 그대로 남아 초기값과 다른 결과가 나온다(2026-08-29).
"""
import csv
import json
import shutil
from pathlib import Path
@@ -19,6 +20,10 @@ import aiomysql
# 스냅샷 폴더는 워크플로우 단계가 아니므로 PROJECT_STORAGE_LAYOUT_V2에 넣지 않는다.
SNAPSHOT_DIRNAME = "initial_snapshot"
_DB_DUMP_NAME = "db.json"
# 설계가 쓰는 계획노선 정본 — shapefile로 온 노선도 여기서는 CSV 한 벌이다.
# 사업지(.prj) 좌표계로 옮기고 지표면 밖을 잘라 조밀화까지 끝낸 값이라, 설계 계통은
# 이 파일만 읽으면 매번 같은 노선을 본다(2026-09-03 사용자 확정).
DESIGN_ROUTE_CSV_NAME = "planned_route.csv"
# 초기 설계 체인이 도는 동안만 존재하는 마커(진입 차단 판정용).
DESIGNING_LOCK_NAME = "initial_design.lock"
# 초기 설계 체인이 실패로 끝났음을 남기는 마커.
@@ -40,6 +45,11 @@ def snapshot_dir(project_root: Path) -> Path:
return Path(project_root) / SNAPSHOT_DIRNAME
def design_route_csv_path(project_root: Path) -> Path:
"""설계용 계획노선 CSV 정본의 자리."""
return snapshot_dir(Path(project_root)) / DESIGN_ROUTE_CSV_NAME
def designing_lock_path(project_root: Path) -> Path:
"""초기 설계 체인이 도는 동안만 존재하는 마커.
@@ -158,14 +168,24 @@ def _is_json_native(value: Any) -> bool:
async def save_initial_snapshot(
connection: aiomysql.Connection, project_root: Path, route_id: int
connection: aiomysql.Connection,
project_root: Path,
route_id: int,
design_route_points: list[dict[str, float]] | None = None,
) -> None:
"""자동설계 체인 성공 직후 한 번 부른다. 이미 있으면 덮어쓰지 않는다."""
"""자동설계 체인 성공 직후 한 번 부른다. 이미 있으면 덮어쓰지 않는다.
`design_route_points`는 체인이 이미 만들어 둔 계획노선 정점(사업지 좌표계·트림·조밀화
후)이다. 받으면 CSV 정본으로 함께 남긴다 — 노선이 shapefile로 왔더라도 설계 계통이
읽는 것은 이 CSV 한 벌이다.
"""
root = Path(project_root)
target = snapshot_dir(root)
if has_initial_snapshot(root):
return
target.mkdir(parents=True, exist_ok=True)
if design_route_points:
_write_design_route_csv(target / DESIGN_ROUTE_CSV_NAME, design_route_points)
for tree in _FILE_TREES:
_copy_tree(root / tree, target / tree.replace("/", "__"))
@@ -181,6 +201,17 @@ async def save_initial_snapshot(
(target / _DB_DUMP_NAME).write_text(json.dumps(dump, ensure_ascii=False), encoding="utf-8")
def _write_design_route_csv(path: Path, points: list[dict[str, float]]) -> None:
"""계획노선 정점을 CSV로 적는다. 열 이름은 `read_planned_route_csv()`가 아는 것으로."""
with path.open("w", encoding="utf-8", newline="") as file:
writer = csv.writer(file)
writer.writerow(("sequence", "x", "y"))
writer.writerows(
(index, round(point["x"], 4), round(point["y"], 4))
for index, point in enumerate(points)
)
def wipe_edited_masters(project_root: Path) -> list[str]:
"""사용자 편집 정본을 걷어낸다 — 재계산으로 **진짜 초기값**을 만들기 위한 사전 정리.