From 7b72dffd6012bbbd0e3f579e620b97150dad2a9a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 22 Oct 2026 13:14:13 +0900 Subject: [PATCH] =?UTF-8?q?refactor(510-2=20=EC=84=9C=EB=B2=84=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EA=B4=80=20=EC=98=9B=20=EA=B8=B8=20=EA=B1=B7?= =?UTF-8?q?=EC=9D=8C):=20auto=20all=20=C2=B7=20add=20=EA=B0=88=EB=9E=98=20?= =?UTF-8?q?=EA=B1=B7=EC=9D=8C(=EB=B6=80=EB=A5=B4=EB=8A=94=20=EA=B3=B3=200?= =?UTF-8?q?=20=C2=B7=20detail-basins=20=C2=B7=20drainage/live=20=C2=B7=20b?= =?UTF-8?q?uild=5Fdetail=20=C2=B7=20=EC=A0=80=EC=9E=A5=EB=B3=B8=20?= =?UTF-8?q?=EC=97=B4=EC=87=A0=20=C2=B7=20computeDetailBasins=20=EC=85=8B?= =?UTF-8?q?=EC=A7=B8=20=EC=9D=B8=EC=9E=90)=20=C2=B7=20=EC=A0=84=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=B2=AB=20=EC=9E=90=EB=8F=99=20=EA=B4=80=20?= =?UTF-8?q?=EA=B7=B8=EB=8C=80=EB=A1=9C=20=C2=B7=20test=5F369=20=EC=98=9B?= =?UTF-8?q?=20all/add=20=EC=8B=9C=ED=97=98=20=EA=B1=B7=EC=9D=8C=20=C2=B7?= =?UTF-8?q?=20test=5Fb05=5Fstructures=5Frouter=20=EC=9D=B4=EA=B4=80=205=20?= =?UTF-8?q?=3D=20=EA=B0=80=EC=A7=9C=20=ED=95=A8=EC=88=98=EB=A5=BC=20(legac?= =?UTF-8?q?y,=20standing)=20=EC=8C=8D=EC=9C=BC=EB=A1=9C=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- B04_PreProcess/B04_PreProcess_Api_Fetch.ts | 6 +- .../B04_PreProcess_Router_Basins.py | 53 +++-------- .../B04_PreProcess_Router_Watershed_Live.py | 9 +- common_util/common_util_drainage_detail.py | 11 +-- resources/tester/test_312_empty_pipes.py | 2 +- resources/tester/test_342_3_detail_cache.py | 2 +- resources/tester/test_342_3_drainage_stale.py | 4 +- resources/tester/test_369_drainage_auto.py | 89 +++---------------- .../tester/test_b05_structures_router.py | 7 +- 9 files changed, 39 insertions(+), 144 deletions(-) diff --git a/B04_PreProcess/B04_PreProcess_Api_Fetch.ts b/B04_PreProcess/B04_PreProcess_Api_Fetch.ts index 7fafa6703..5f27a3034 100644 --- a/B04_PreProcess/B04_PreProcess_Api_Fetch.ts +++ b/B04_PreProcess/B04_PreProcess_Api_Fetch.ts @@ -624,16 +624,14 @@ export async function fetchDetailPipePoints(projectId: string): Promise { return requestJson(`/projects/${projectId}/drainage/detail-basins`, { method: "POST", - body: JSON.stringify(auto ? { points, auto } : { points }), + body: JSON.stringify({ points }), }); } diff --git a/B04_PreProcess/B04_PreProcess_Router_Basins.py b/B04_PreProcess/B04_PreProcess_Router_Basins.py index 305dfa617..a79e155a1 100644 --- a/B04_PreProcess/B04_PreProcess_Router_Basins.py +++ b/B04_PreProcess/B04_PreProcess_Router_Basins.py @@ -73,8 +73,6 @@ router = APIRouter(prefix="/api/projects", tags=["B04 Surface Drainage Basins"]) _NO_ANALYSIS = "배수유역 분석 결과가 없습니다. [유역 분석]을 먼저 실행하세요." # 노선 지문이 분석 때와 다름(옛 저장분은 지문 없음) — 옛 격자로 나누면 틀린 유역이 나온다(342-3). # 머리 「배수유역 낡음」 = 화면 `STALE_PREFIX`(B05 Drainage_Live · 369) 짝. -# [자동 제안] 방식(369) — None = 지금 목록 그대로(없으면 세류선 교차만) · all · add -AUTO_MODES = (None, "all", "add") STALE_MESSAGE = ( "배수유역 낡음 — 노선이 분석 때와 다릅니다. 유역을 다시 분석해야 세부 유역을 나눕니다." ) @@ -86,22 +84,20 @@ def _build( points: list[PipePoint] | None, routing: Any = None, upstream_lonlat: list | None = None, - auto: str | None = None, ) -> tuple[DrainageDetail | None, list[PipePoint]]: """세부유역을 계산하고, 그 결과에 쓰인 관 목록을 함께 돌려준다. `points`가 None 이면 B04 기본 관(세류선 교차)만. 있으면 그 자리를 그대로 쓰되, 관이 왜 거기 있는지(기본/자동/수동)는 넘겨받은 목록의 표시를 유지한다 — 계산기는 확정 좌표만 보므로 그대로 - 두면 전부 "수동"이 된다. `auto`([자동 제안] · 369) = "all" 자리 새로 · "add" 빈 구간 보충 — - 같은 자리(0.05 m) 관만 속성 · id 를 이어받고 나머지는 새 관(등록부 기본값). + 두면 전부 "수동"이 된다. 원본에 없던 자리의 새 관은 등록부 기본값을 받는다. """ directory = drainage_dir(stored_path) chainages = [point.chainage_m for point in points] if points is not None else None - detail = build_detail(directory, context.vertices, chainages, routing, upstream_lonlat, auto) + detail = build_detail(directory, context.vertices, chainages, routing, upstream_lonlat) if detail is None: return None, [] if points is not None: - _retag(detail.pipes, points, keep_new=auto is not None) + _retag(detail.pipes, points) rebuilt = _rebuilt(detail) # 원본에 없던 자리 = 이번에 새로 놓인 관(자동 배치 · 간격 보충) — 등록부 기본값을 받을 관 fresh = [ @@ -142,14 +138,11 @@ _DETAIL_CACHE_SIZE = 4 DETAIL_CACHE_FILENAME = "04_detail_cache.pkl" -def _detail_key( - context: DrainageContext, points: list[PipePoint] | None, auto: str | None = None -) -> str: +def _detail_key(context: DrainageContext, points: list[PipePoint] | None) -> str: directory = drainage_dir(context.stored_path) digest = hashlib.sha1(usedforsecurity=False) listed = None if points is None else [point.as_dict() for point in points] - # auto 를 열쇠에 — 같은 관 목록이라도 [자동 제안] 결과와 섞이지 않게(369 · 옛 저장본도 비껴감) - digest.update(json.dumps([listed, auto], sort_keys=True, ensure_ascii=False).encode()) + digest.update(json.dumps(listed, sort_keys=True, ensure_ascii=False).encode()) for vertex in context.vertices: digest.update(f"{vertex.x:.3f},{vertex.y:.3f},{vertex.z:.3f};".encode()) routing = STAGES["road_routing"] @@ -180,10 +173,10 @@ def _remember(key: str, value: tuple[DrainageDetail, list[PipePoint]]) -> None: def _build_cached( - context: DrainageContext, points: list[PipePoint] | None, auto: str | None = None + context: DrainageContext, points: list[PipePoint] | None ) -> tuple[DrainageDetail | None, list[PipePoint]]: """`_build` + 저장본 — 관 목록은 사본으로 돌려준다(저장이 좌표를 채워도 저장본은 그대로).""" - key = _detail_key(context, points, auto) + key = _detail_key(context, points) hit = _DETAIL_CACHE.get(key) if hit is None: try: @@ -193,7 +186,7 @@ def _build_cached( except (OSError, pickle.PickleError, EOFError, AttributeError, KeyError, TypeError): hit = None if hit is None: - detail, carried = _build(context.stored_path, context, points, auto=auto) + detail, carried = _build(context.stored_path, context, points) if detail is None: return None, [] hit = (detail, carried) @@ -289,20 +282,8 @@ def ford_height_m(flow_m3s: float | None, width_m: float) -> float | None: return math.ceil(depth * 100 - 1e-9) / 100 -def _retag( - pipes: list[StructureCandidate], points: list[PipePoint], keep_new: bool = False -) -> None: - """확정 좌표로 되돌아온 관에 원래의 생성 사유를 다시 붙인다. - - `keep_new`([자동 제안]) = 같은 자리(0.05 m) 관만 · 새로 놓인 관은 계산기 사유 - (stream · spacing) 그대로. - """ - if keep_new: - picks = carry_indices([pipe.chainage_m for pipe in pipes], [p.chainage_m for p in points]) - for pipe, pick in zip(pipes, picks): - if pick is not None: - pipe.reason = points[pick].source - return +def _retag(pipes: list[StructureCandidate], points: list[PipePoint]) -> None: + """확정 좌표로 되돌아온 관에 원래의 생성 사유를 다시 붙인다.""" by_chainage = {round(point.chainage_m, 2): point.source for point in points} for pipe in pipes: source = by_chainage.get(round(pipe.chainage_m, 2)) @@ -481,16 +462,11 @@ async def _resolve( 돌려준다 — 관 목록도 없으면(자동 생성 자리) 409(342-3). 마지막 값 = 낡음. 빈 목록(`[]`)은 「관 0개」로 그대로 쓴다 — 자동 생성은 요청에 칸이 없고 저장분도 없을(None) 때만(312). - 요청 `auto`([자동 제안] · 369) = "all" · "add" — `_build` 참고 · 없으면 None. """ context, reason = await load_drainage_context(project_id) if context is None: return JSONResponse(status_code=404, content={"status": "error", "message": reason}) - auto = (payload or {}).get("auto") - if auto not in AUTO_MODES: - message = f"auto 는 {', '.join(m for m in AUTO_MODES if m)} 중 하나입니다." - return JSONResponse(status_code=400, content={"status": "error", "message": message}) try: raw = (payload or {}).get("points") requested = None if raw is None else parse_pipe_points(raw) @@ -504,7 +480,7 @@ async def _resolve( saved = requested is None and stored is not None if analysis_route_signature(context.stored_path) != signature: - if points is None or auto is not None: # 자동 자리 = 지금 노선 분석이 있어야(369) + if points is None: return JSONResponse( status_code=409, content={"status": "stale", "message": STALE_MESSAGE} ) @@ -513,11 +489,9 @@ async def _resolve( # [저장]은 정본으로 다시 셈 — 저장본은 GET · POST 만 if fresh: - result = await asyncio.to_thread( - lambda: _build(context.stored_path, context, points, auto=auto) - ) + result = await asyncio.to_thread(lambda: _build(context.stored_path, context, points)) else: - result = await asyncio.to_thread(_build_cached, context, points, auto) + result = await asyncio.to_thread(_build_cached, context, points) detail, resolved = result if detail is None: return JSONResponse(status_code=404, content={"status": "error", "message": _NO_ANALYSIS}) @@ -549,7 +523,6 @@ async def post_detail_basins( `points` 칸 없이 부르면 저장분을 무시하고 기본 관(세류선 교차)만으로 되돌린다 — [배관 배치 초기화] · 노선이 바뀌어 전체 분석을 다시 돌린 직후의 경로다. 빈 목록은 관 0개(312). - `auto` = [자동 제안](369) — "all" 전체 자동 · "add" 지금 관 + 빈 구간 보충. """ resolved = await _resolve(project_id, payload, use_stored=False) if isinstance(resolved, JSONResponse): diff --git a/B04_PreProcess/B04_PreProcess_Router_Watershed_Live.py b/B04_PreProcess/B04_PreProcess_Router_Watershed_Live.py index 1ec330489..2bd8f3510 100644 --- a/B04_PreProcess/B04_PreProcess_Router_Watershed_Live.py +++ b/B04_PreProcess/B04_PreProcess_Router_Watershed_Live.py @@ -22,7 +22,7 @@ from fastapi.responses import JSONResponse from B04_PreProcess.B04_PreProcess_Engine_Watershed_Analyze import preview_stages from B04_PreProcess.B04_PreProcess_Engine_Watershed_Expand import AnalysisStopped from B04_PreProcess.B04_PreProcess_Engine_Watershed_Export import drainage_dir -from B04_PreProcess.B04_PreProcess_Router_Basins import AUTO_MODES, _build, _payload +from B04_PreProcess.B04_PreProcess_Router_Basins import _build, _payload from B04_PreProcess.B04_PreProcess_Router_Watershed import _prepare, router from common_util.common_util_drainage_context import _open_sampler, load_drainage_context from common_util.common_util_drainage_detail import RoadRouting @@ -79,7 +79,7 @@ async def post_drainage_live( project_id: UUID, payload: dict[str, Any] | None = None ) -> dict[str, Any] | JSONResponse: """몸통 = {generation, route: RouteDraft | null(저장 노선), - points: 관 목록 | null(세류선 교차만), auto: [자동 제안] 방식(all · add · 369) | 없음}.""" + points: 관 목록 | null(세류선 교차만)}.""" body = payload or {} generation = int(body.get("generation") or 0) key = str(project_id) @@ -88,9 +88,6 @@ async def post_drainage_live( def superseded() -> bool: return _LATEST_GENERATION.get(key) != generation - auto = body.get("auto") - if auto not in AUTO_MODES: - return JSONResponse(status_code=400, content={"status": "error", "message": "auto 틀림"}) try: raw = body.get("points") points = None if raw is None else parse_pipe_points(raw) @@ -143,7 +140,7 @@ async def post_drainage_live( [list(to_lonlat(x, y)) for x, y in line.coords] for line in preview.region.split.upstream ] detail, carried = await asyncio.to_thread( - lambda: _build(context.stored_path, live, points, routing, upstream, auto=auto) + lambda: _build(context.stored_path, live, points, routing, upstream) ) if detail is None: return JSONResponse( diff --git a/common_util/common_util_drainage_detail.py b/common_util/common_util_drainage_detail.py index ac93ca754..bd883621d 100644 --- a/common_util/common_util_drainage_detail.py +++ b/common_util/common_util_drainage_detail.py @@ -172,25 +172,20 @@ def build_detail( confirmed_chainages: list[float] | None = None, routing: RoadRouting | None = None, upstream_lonlat: list[list[list[float]]] | None = None, - auto: str | None = None, ) -> DrainageDetail | None: """B04 분석 결과를 읽어 관 자리를 정하고 세부유역을 나눈다. `confirmed_chainages`를 주면 그 위치를 관으로 확정하고(사용자 편집 · 빈 목록 = 관 0개), None 이면 B04의 기본 관(세류선 교차)만. 어느 쪽이든 격자 해석은 하지 않는다. - `auto`([자동 제안] · 369) — None = 위 그대로 · "all" = 기본 관 + 최대 간격 보충으로 다시 - (확정 목록 무시) · "add" = 확정 목록 + 빈 구간 보충. 관 자리 후보는 어느 길이든 셈. + 관 자리 후보(`pipe_candidates`)는 어느 길이든 셈 — [자동 제안](510)이 이 후보를 씀. `routing` · `upstream_lonlat` 을 주면 파일 대신 그것(「지금 노선으로 셈」 메모리 결과 · 347-2). """ routing = routing or read_road_routing(directory) if routing is None or len(vertices) < 2: return None - if confirmed_chainages is not None and auto != "all": + if confirmed_chainages is not None: pipes = pipes_from_chainages(vertices, confirmed_chainages) - if auto == "add": - pipes = [*pipes, *suggest_pipe_positions(vertices, pipes, routing.strength_curve)] - pipes.sort(key=lambda pipe: pipe.chainage_m) else: # 저장분의 기본 관은 누가거리만 신뢰한다 — 좌표는 현재 노선 위로 다시 찍는다. base = [ @@ -203,7 +198,7 @@ def build_detail( for pipe in routing.base_pipes ] # 자동 관 자리 = 정수 m · 일반 측점 1 m 안이면 그 측점(234-1) — 확정본(위)은 단추로만 - placed = place_pipes(vertices, base, routing.strength_curve, fill=auto is not None) + placed = place_pipes(vertices, base, routing.strength_curve, fill=False) pipes = snap_candidates(placed, vertices) if confirmed_chainages: kept = pipes_from_chainages(vertices, confirmed_chainages) diff --git a/resources/tester/test_312_empty_pipes.py b/resources/tester/test_312_empty_pipes.py index f83c0f428..83ceb81ce 100644 --- a/resources/tester/test_312_empty_pipes.py +++ b/resources/tester/test_312_empty_pipes.py @@ -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 diff --git a/resources/tester/test_342_3_detail_cache.py b/resources/tester/test_342_3_detail_cache.py index 661d712aa..8ea80935b 100644 --- a/resources/tester/test_342_3_detail_cache.py +++ b/resources/tester/test_342_3_detail_cache.py @@ -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] diff --git a/resources/tester/test_342_3_drainage_stale.py b/resources/tester/test_342_3_drainage_stale.py index c252f6c35..18d096861 100644 --- a/resources/tester/test_342_3_drainage_stale.py +++ b/resources/tester/test_342_3_drainage_stale.py @@ -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 diff --git a/resources/tester/test_369_drainage_auto.py b/resources/tester/test_369_drainage_auto.py index e342a71ea..d8a5e1d59 100644 --- a/resources/tester/test_369_drainage_auto.py +++ b/resources/tester/test_369_drainage_auto.py @@ -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 # 목록 없음 = 낡음 까닭 diff --git a/resources/tester/test_b05_structures_router.py b/resources/tester/test_b05_structures_router.py index 40f5bbdce..467b5b0e3 100644 --- a/resources/tester/test_b05_structures_router.py +++ b/resources/tester/test_b05_structures_router.py @@ -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(