refactor(510-2 서버 자동 관 옛 길 걷음): auto all · add 갈래 걷음(부르는 곳 0 · detail-basins · drainage/live · build_detail · 저장본 열쇠 · computeDetailBasins 셋째 인자) · 전처리 첫 자동 관 그대로 · test_369 옛 all/add 시험 걷음 · test_b05_structures_router 이관 5 = 가짜 함수를 (legacy, standing) 쌍으로 맞춤

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 13:14:13 +09:00
co-authored by Claude Sonnet 5.5
parent 59c2907b73
commit 7b72dffd60
9 changed files with 39 additions and 144 deletions
+1 -1
View File
@@ -148,7 +148,7 @@ def test_칸_없음_파일_없음_DELETE_노선_바뀜은_자동_배치(api) ->
def test_빈_목록과_None_을_가르는_자리() -> None:
src = _read("common_util/common_util_drainage_detail.py")
assert 'if confirmed_chainages is not None and auto != "all":' in src
assert "if confirmed_chainages is not None:" in src
router = _read("B04_PreProcess/B04_PreProcess_Router_Basins.py")
assert "points = requested if requested is not None else stored" in router
assert "requested or stored" not in router
+1 -1
View File
@@ -24,7 +24,7 @@ def setup(tmp_path, monkeypatch):
basins._DETAIL_CACHE.clear()
calls = SimpleNamespace(count=0)
def build(stored_path, context, points, auto=None):
def build(stored_path, context, points):
calls.count += 1
chainages = [p.chainage_m for p in points] if points else [50.0]
return DrainageDetail(), [PipePoint(chainage_m=c, source="stream") for c in chainages]
@@ -52,13 +52,13 @@ def _patch(monkeypatch, analysis_signature, stored):
monkeypatch.setattr(basins, "load_pipe_points", lambda *args: stored)
built = SimpleNamespace(called=False)
def build(stored_path, context, points, auto=None):
def build(stored_path, context, points):
built.called = True
return SimpleNamespace(pipes=[], basins=["유역"]), points or []
monkeypatch.setattr(basins, "_build", build)
monkeypatch.setattr(
basins, "_build_cached", lambda context, points, auto=None: build("x", context, points)
basins, "_build_cached", lambda context, points: build("x", context, points)
)
return built
+10 -79
View File
@@ -1,10 +1,7 @@
"""369 배수 구조물 자리 — 기본 = 세류선 교차만 · [자동 제안] 두 방식(all · add).
"""369 배수 구조물 자리 — 기본 = 세류선 교차만 · 목록 있으면 그대로 · 낡은 분석은 관 자리만.
- 기본(auto 없음) = 세류선 교차 관만 · 100 m 보충 없음 · 관 자리 후보는 셈.
- all = 세류선 + 100 m 보충으로 다시(지금 목록 무시) · 같은 자리(0.05 m) 관은 속성 · id 유지 ·
나머지 = 새 관(등록부 기본값).
- add = 지금 목록 그대로 + 빈 구간 보충 · 더한 관 = 새 관(등록부 기본값).
- 틀린 auto = 400 · 저장본 열쇠에 auto.
- 목록 없음 = 세류선 교차 관만 · 100 m 보충 없음 · 관 자리 후보는 셈.
- 목록 있음 = 그 자리 그대로(보충은 후보만) · [자동 제안] = 510 (`test_510_auto_pipe_propose.py`).
"""
import asyncio
@@ -17,7 +14,6 @@ from B04_PreProcess import B04_PreProcess_Router_Basins as basins
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Grid import GridSpec
from common_util.common_util_drainage_context import DrainageContext
from common_util.common_util_drainage_detail import RoadRouting, build_detail
from common_util.common_util_drainage_pipes import PipePoint
from common_util.common_util_route_geometry import RouteVertex, StructureCandidate
VERTICES = [RouteVertex(0.0, 0.0, 100.0, 0.0), RouteVertex(300.0, 0.0, 90.0, 300.0)]
@@ -41,8 +37,8 @@ def _routing() -> RoadRouting:
)
def _detail(tmp_path, chainages=None, auto=None):
return build_detail(tmp_path / "없음", VERTICES, chainages, _routing(), [], auto)
def _detail(tmp_path, chainages=None):
return build_detail(tmp_path / "없음", VERTICES, chainages, _routing(), [])
def test_기본은_세류선_교차만(tmp_path):
@@ -51,84 +47,19 @@ def test_기본은_세류선_교차만(tmp_path):
assert len(detail.pipe_candidates) == 2 # 0~150 · 150~300 이 100 m 넘음 — 제안만
def test_all_은_세류선_더하기_100m_보충_지금_목록_무시(tmp_path):
detail = _detail(tmp_path, [10.0, 150.02], "all")
reasons = [p.reason for p in detail.pipes]
places = [p.chainage_m for p in detail.pipes]
assert reasons.count("stream") == 1 and reasons.count("spacing") == 2
assert 150.02 in places and 10.0 not in places # 세류선 150 m 0.5 m 안의 지금 관 = 그 자리
gaps = np.diff([0.0, *places, 300.0])
assert gaps.max() <= 100.0 and not detail.pipe_candidates
def test_add_는_지금_목록_더하기_빈_구간_보충(tmp_path):
detail = _detail(tmp_path, [20.0], "add")
places = [p.chainage_m for p in detail.pipes]
assert 20.0 in places and len(places) == 3 # 20 ~ 300 = 280 m → 보충 둘
assert np.diff([0.0, *places, 300.0]).max() <= 100.0
# auto 없음 = 목록 그대로(보충은 후보만)
def test_목록은_그대로_보충은_후보만(tmp_path):
plain = _detail(tmp_path, [20.0])
assert [p.chainage_m for p in plain.pipes] == [20.0] and len(plain.pipe_candidates) == 2
def _build(tmp_path, monkeypatch, points, auto):
monkeypatch.setattr(basins, "drainage_dir", lambda stored: tmp_path / "없음")
return basins._build("x", CONTEXT, points, _routing(), [], auto=auto)
def test_all_같은_자리_관은_속성_id_유지_새_관은_기본값(tmp_path, monkeypatch):
kept = PipePoint(
chainage_m=150.3, # 세류선 150 m 에서 0.3 m — 노선 따라감으로 어긋난 같은 관
source="user",
facility="ford_bridge",
options={"ford_width_m": 4.0},
id="keep",
)
gone = PipePoint(chainage_m=10.0, source="user", options={"pipe_diameter_mm": 600}, id="gone")
_, carried = _build(tmp_path, monkeypatch, [gone, kept], "all")
ford = [p for p in carried if p.id == "keep"]
assert len(ford) == 1 and ford[0].facility == "ford_bridge" and ford[0].source == "user"
assert ford[0].chainage_m == 150.3 # 그 자리 그대로
assert all(p.id != "gone" for p in carried)
fresh = [p for p in carried if p.id != "keep"]
assert len(fresh) == 2 and all(p.source == "spacing" for p in fresh)
assert all((p.options or {}).get("pipe_diameter_mm") for p in fresh) # 등록부 관경
def test_add_더한_관은_기본값_지금_관은_그대로(tmp_path, monkeypatch):
mine = PipePoint(chainage_m=20.0, source="user", id="mine")
_, carried = _build(tmp_path, monkeypatch, [mine], "add")
assert [p.id for p in carried].count("mine") == 1
assert next(p for p in carried if p.id == "mine").options is None # 옛 빈칸 = 미확정 유지
added = [p for p in carried if p.id != "mine"]
assert len(added) == 2 and all(p.source == "spacing" for p in added)
assert all((p.options or {}).get("pipe_diameter_mm") for p in added)
def test_틀린_auto_는_400_열쇠에_auto(tmp_path, monkeypatch):
async def load(project_id):
return CONTEXT, ""
monkeypatch.setattr(basins, "load_drainage_context", load)
monkeypatch.setattr(basins, "drainage_dir", lambda stored: tmp_path)
result = asyncio.run(basins._resolve(uuid4(), {"auto": "spacing"}, use_stored=False))
assert result.status_code == 400
points = [PipePoint(chainage_m=20.0)]
keys = {basins._detail_key(CONTEXT, points, auto) for auto in basins.AUTO_MODES}
assert len(keys) == 3
def test_낡은_분석에_auto_는_409(monkeypatch):
def test_낡은_분석은_관_자리만_낡음으로(monkeypatch):
async def load(project_id):
return CONTEXT, ""
monkeypatch.setattr(basins, "load_drainage_context", load)
monkeypatch.setattr(basins, "analysis_route_signature", lambda stored: "옛 노선")
points = [{"chainage_m": 20.0}]
for auto in ("all", "add"):
result = asyncio.run(
basins._resolve(uuid4(), {"points": points, "auto": auto}, use_stored=False)
)
assert result.status_code == 409 # 말없이 그대로 돌려주지 않음 — 낡음 까닭
kept = asyncio.run(basins._resolve(uuid4(), {"points": points}, use_stored=False))
assert kept[-1] is True # auto 없음 = 관 자리만 낡음으로(그대로)
assert kept[-1] is True # 관 목록 있음 = 관 자리만 낡음으로
none = asyncio.run(basins._resolve(uuid4(), {}, use_stored=False))
assert none.status_code == 409 # 목록 없음 = 낡음 까닭
@@ -32,8 +32,9 @@ def _make_client(tmp_path, monkeypatch, invalidated_calls, *, stale_ok=True, rou
monkeypatch.setattr(router_module, "_invalidate_downstream", fake_invalidate)
async def fake_saved_irregular(project_id, root, route_id):
# 이관 원천 = 저장 종단 파일(452) — 본문 대신 여기서 같은 측점을 줌
return _migrate_payload()["stations"]
# 이관 원천 = 저장 종단 파일(452) — 본문 대신 여기서 같은 측점을 줌 (legacy, standing)
legacy = _migrate_payload()["stations"]
return legacy, [float(item["chainage_m"]) for item in legacy]
monkeypatch.setattr(router_module, "_route_length", fake_route_length)
monkeypatch.setattr(router_module, "_saved_irregular_stations", fake_saved_irregular)
@@ -306,7 +307,7 @@ def test_migrate_keeps_existing_structures(client, tmp_path):
def test_migrate_empty_list_is_noop(client, monkeypatch):
async def none_saved(project_id, root, route_id):
return []
return [], []
monkeypatch.setattr(router_module, "_saved_irregular_stations", none_saved)
response = client.post(