Files
Aislo/resources/tester/test_133_2_design_terrain.py
T
eomsangdonandClaude Opus 5.5 47366d3740 feat(종단보기): 346-3 [노선 반영] 없앰 — [저장]이 노선부터(POST /route/replan · draft = 계약 RouteDraft · 안 바뀌면 안 보냄) · 뒤 차례는 새 노선 · 횡단 초안 측점 열쇠로 넘김 · 결과 한 줄
- design_save 차례 = 노선 → 관 → 구조물 → 계획고(옛 노선 칸 → 새 노선) → 암거 옵션 → 상단측 → 횡단(carryStationDrafts 뒤 저장) · 잠금 막 = [저장] 하나 · 실패 = 멈춤 · 초안 그대로 · 오류 한 줄
- [확정] 중 노선이 바뀌면 노선만 저장하고 다시 세움(새 노선으로 다시 [확정])
- _RouteEdit_Apply = 노선 저장 이음(편집기 등록부 · pendingRouteSave · saveRouteDraft · carryAfterRouteSave · routeSaveSummary · rebuildAfterRouteSave) · 단추 듣기 없음
- 결과 한 줄 = 관 지킴 · 새 교차 관 · 오목 보충 · 구조물 옮김 · 멀어짐 · 노선 밖 고침(서버 cross_dropped + 화면) · 배수유역 낡음
- RE_Chrome [노선 반영] 지움 · [반영 전으로] → [저장 전으로] · replanRoute 본문 = RouteSaveBody
- 시험: test_design_store_save 차례 넷 더함 · 꼴 시험 셋 고쳐 씀 · 323_9 대역

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
2026-10-16 12:14:22 +09:00

298 lines
11 KiB
Python

"""PLAN 133-2 — 노선 편집 [확인] 때 고른 분석 지형 = 설계 지형(뒤 단계 전부).
수치지형도 고르고 [확인] → 설계 지형 sheet(확정 모델 · stage 1 params) · 종 · 횡단 · 배수 · 유토가
stage 1 params 로 sheet sampler · 라이다로 되돌려 [확인] → 라이다 · [확인] 안 하면 그대로 ·
라이다 없는 프로젝트 그대로 · 체인 실패면 설계 지형도 되돌림.
"""
from __future__ import annotations
import asyncio
import json
from contextlib import asynccontextmanager
from pathlib import Path
from uuid import UUID
import numpy as np
import B03_FileInput.B03_FileInput_Service_Chain as chain
import B04_PreProcess.B04_PreProcess_Service as service
import B05_Profile.B05_Profile_Repository as route_repo
import B05_Profile.B05_Profile_Route_Save as route_save
import common_util.common_util_workflow_state as workflow_state
from B05_Profile import B05_Profile_Router_Replan as replan
from B06_Section import B06_Section_Router_Stations as stations
from common_util.common_util_surface_confirmation import (
LAS_STASH_KEY,
SHEET_ANALYSIS_FILTER,
analysis_terrain_sources,
design_terrain,
get_surface_confirmation_params,
)
from common_util.common_util_surface_sampler import build_surface_sampler
ROOT = Path(__file__).resolve().parents[2]
PID = UUID("1bb0f876-983f-4ca5-9cc7-f59ee1f77e0a")
LAS_ID, SHEET_ID = 582, 584
LAS_Z, SHEET_Z = 100.0, 250.0
LAS_STAGE1 = {"source_filter": "classification", "method": "dtm", "smooth": True}
SHEET_STAGE1 = {"source_filter": SHEET_ANALYSIS_FILTER, "method": "dtm", "smooth": True}
class _Cursor:
def __init__(self, db: _Db):
self.db = db
self.row = None
self.rowcount = 1
async def __aenter__(self):
return self
async def __aexit__(self, *_):
return False
async def execute(self, sql: str, args=()):
if "SELECT params" in sql:
self.row = {"params": json.dumps(self.db.stage1)}
elif "UPDATE project_workflow_stages" in sql:
self.db.stage1 = json.loads(args[0])
self.db.writes += 1
elif "route_start_m" in sql:
self.row = (None, None)
async def fetchone(self):
return self.row
class _Db:
"""stage 1 params 한 행 · 지표면 모델 표 — 실제 DB 대신."""
def __init__(self, stage1: dict, models: list[dict]):
self.stage1 = dict(stage1)
self.models = models
self.writes = 0
def cursor(self, *_):
return _Cursor(self)
async def begin(self):
return None
async def commit(self):
return None
async def rollback(self):
return None
def confirmed(self) -> int | None:
return next((m["id"] for m in self.models if m["status"] == "CONFIRMED"), None)
def _models(las: bool) -> list[dict]:
rows = [
{
"id": SHEET_ID,
"model_type": "dtm",
"status": "COMPLETE" if las else "CONFIRMED",
"generation_params": {"source_filter": SHEET_ANALYSIS_FILTER},
}
]
if las:
rows.insert(
0,
{
"id": LAS_ID,
"model_type": "dtm",
"status": "CONFIRMED",
"generation_params": {"source_filter": "classification"},
},
)
return rows
def _patch_models(monkeypatch, db: _Db) -> None:
async def listed(_connection, _project_id):
return db.models
async def confirm(_connection, _project_id, model_id):
for model in db.models:
if model["status"] == "CONFIRMED":
model["status"] = "COMPLETE"
next(m for m in db.models if m["id"] == model_id)["status"] = "CONFIRMED"
monkeypatch.setattr(service, "list_surface_models", listed)
monkeypatch.setattr(service, "confirm_surface_model", confirm)
def _switch(db: _Db, terrain):
return asyncio.run(service.switch_design_terrain(db, PID, terrain))
def _params(db: _Db) -> dict:
return asyncio.run(get_surface_confirmation_params(db, str(PID)))
def _models_dir(tmp_path: Path) -> Path:
models = tmp_path / "B04_PreProcess" / "models"
models.mkdir(parents=True, exist_ok=True)
for name, z in (("classification", LAS_Z), (SHEET_ANALYSIS_FILTER, SHEET_Z)):
for suffix in ("", "_smooth"):
x = y = np.arange(0.0, 11.0)
np.savez(
models / f"dtm_{name}{suffix}.npz",
x=x,
y=y,
z=np.full((11, 11), z),
valid_mask=np.ones((11, 11), dtype=bool),
)
return models
def _z(models: Path, params: dict) -> float:
sampler = build_surface_sampler(
models, params["source_filter"], params["method"], params["smooth"]
)
z, _ = sampler.sample_xy(np.array([[5.0, 5.0]]))
return float(z[0])
def test_수치지형도_확인_뒤_설계_지형_sheet_라이다로_되돌림(tmp_path, monkeypatch):
db = _Db(LAS_STAGE1, _models(las=True))
_patch_models(monkeypatch, db)
models = _models_dir(tmp_path)
assert _z(models, _params(db)) == LAS_Z
assert _switch(db, "sheet") == (SHEET_ID, "las")
params = _params(db)
assert design_terrain(params) == "sheet" and db.confirmed() == SHEET_ID
assert params[LAS_STASH_KEY] == LAS_STAGE1 # 되돌릴 자리
# 뒤 단계 sampler 는 모두 stage 1 params 를 읽음 → 수치지형도 표고
assert _z(models, params) == SHEET_Z
# 설계 지형이 수치지형도여도 고르기는 둘 다 남음
assert analysis_terrain_sources(params, models) == ["las", "sheet"]
assert _switch(db, "las") == (LAS_ID, "sheet")
params = _params(db)
assert design_terrain(params) == "las" and db.confirmed() == LAS_ID
assert {k: params[k] for k in LAS_STAGE1} == LAS_STAGE1
assert _z(models, params) == LAS_Z
def test_확인_안_하면_그대로(monkeypatch):
db = _Db(LAS_STAGE1, _models(las=True))
_patch_models(monkeypatch, db)
assert _switch(db, None) is None # 고르기 안 보임 · 고른 값 안 실음
assert _switch(db, "las") is None # 지금 설계 지형과 같음
assert db.writes == 0 and db.confirmed() == LAS_ID
def test_라이다_없는_프로젝트_그대로(monkeypatch, tmp_path):
db = _Db(SHEET_STAGE1, _models(las=False))
_patch_models(monkeypatch, db)
assert _switch(db, "las") is None and _switch(db, "sheet") is None
assert db.writes == 0 and db.confirmed() == SHEET_ID
assert analysis_terrain_sources(_params(db), _models_dir(tmp_path)) == ["sheet"]
def _patch_recompute(monkeypatch, tmp_path: Path, db: _Db, failure: str | None):
_patch_models(monkeypatch, db)
seen: dict = {}
class _Pool:
@asynccontextmanager
async def acquire(self):
yield db
async def latest(_connection, _project_id):
return {"id": 237, "surface_model_id": LAS_ID}
async def state(_cursor, _project_id):
return {"stages": []}
async def start(*_args):
return None
async def run_chain(_project_id, _root, _stored, model_id, selection, *_rest):
seen["model_id"], seen["selection"] = model_id, dict(selection)
return {"error": failure} if failure else {"route_id": 238}
points = [{"x": 0.0, "y": 0.0}, {"x": 5.0, "y": 5.0}]
monkeypatch.setattr(replan, "get_db_pool", lambda: _Pool())
monkeypatch.setattr(route_repo, "get_latest_route", latest)
monkeypatch.setattr(chain, "_planned_route_points_in_project_crs", lambda *_a: points)
monkeypatch.setattr(route_save, "run_route_save_chain", run_chain)
monkeypatch.setattr(workflow_state, "get_workflow_state", state)
monkeypatch.setattr(workflow_state, "start_stage", start)
return seen
def test_확인_체인이_수치지형도_모델과_params_로_돎(tmp_path, monkeypatch):
db = _Db(LAS_STAGE1, _models(las=True))
seen = _patch_recompute(monkeypatch, tmp_path, db, None)
result = asyncio.run(replan._recompute(PID, tmp_path, "x", None, "sheet"))
assert "error" not in result
# 옛 노선의 모델 id(라이다)가 아니라 새 설계 지형 모델 · 종 · 횡단 · 배수 · 유토 체인 params
assert seen["model_id"] == SHEET_ID
assert seen["selection"]["source_filter"] == SHEET_ANALYSIS_FILTER
assert design_terrain(_params(db)) == "sheet"
def test_체인_실패면_설계_지형도_되돌림(tmp_path, monkeypatch):
db = _Db(LAS_STAGE1, _models(las=True))
_patch_recompute(monkeypatch, tmp_path, db, "노선 재계산 실패")
result = asyncio.run(replan._recompute(PID, tmp_path, "x", None, "sheet"))
assert result == {"error": "노선 재계산 실패"}
assert design_terrain(_params(db)) == "las" and db.confirmed() == LAS_ID
def test_확인_요청_기본은_설계_지형_그대로():
request = replan.RouteReplanRequest(vertices=[])
assert request.terrain is None
def test_B06_구조물_측점은_옛_지형_조건을_버림(tmp_path, monkeypatch):
snapshot = tmp_path / "B06_Section" / "sampling.json"
snapshot.parent.mkdir(parents=True)
snapshot.write_text(
json.dumps({"filter_key": "classification", "method": "dtm", "smooth": True}),
encoding="utf-8",
)
def stage1(params):
async def run(_fn, _project_id):
return {**params, "contour_interval_m": 1.0}
return run
monkeypatch.setattr(stations, "run_with_connection", stage1(LAS_STAGE1))
same = asyncio.run(stations._sampling_conditions(PID, tmp_path))
assert same["filter_key"] == "classification" and "source" not in same
monkeypatch.setattr(stations, "run_with_connection", stage1(SHEET_STAGE1))
moved = asyncio.run(stations._sampling_conditions(PID, tmp_path))
assert moved["filter_key"] == SHEET_ANALYSIS_FILTER and moved["source"] == "stage1"
def test_뒤_단계는_stage1_params_를_읽음():
"""종 · 횡단 · 배수 · 3D 가 설계 지형 한 곳(stage 1 params)을 보는지 — 배선 고정."""
readers = [
"common_util/common_util_drainage_context.py",
"B06_Section/B06_Section_Router.py",
"B06_Section/B06_Section_Server_Calc_Prebuild.py",
"B06_Section/B06_Section_Router_Stations.py",
"B05_Profile/B05_Profile_Router.py",
"B03_FileInput/B03_FileInput_Service_Chain.py",
"B04_PreProcess/B04_PreProcess_Router.py",
]
for path in readers:
assert "get_surface_confirmation_params" in (ROOT / path).read_text(encoding="utf-8"), path
def test_화면_확인이_고른_지형을_실음():
page = ROOT / "B05_Profile"
apply = (page / "B05_Profile_UI_RouteEdit_Apply.ts").read_text(encoding="utf-8")
main = (page / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8")
# 346-3 — 노선 초안(draft.terrain)에 실림 · [저장]이 보냄
assert "terrain: params.terrain() })" in apply and "terrain: settings.terrain," in apply
assert "pickedTerrain() !== designTerrain" in main # 지형만 바꿔도 [확인]이 다시 셈
assert "analysisTerrain(projectId, designTerrain)" in main # 고른 값 없으면 설계 지형