From f9b157f002e67ff832bd02e92521ec2a5037e9a6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 19 Oct 2026 16:56:48 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(476-1=20=E2=91=A3):=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20[=EC=A0=81=EC=9A=A9]=EB=8F=84=20[=EC=A0=80=EC=9E=A5?= =?UTF-8?q?]=EA=B3=BC=20=EA=B0=99=EC=9D=80=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=A0=EA=B8=88=20=C2=B7=20=EB=96=A0=20=EB=91=90?= =?UTF-8?q?=EA=B3=A0=20=EC=8B=A4=ED=8C=A8=EB=A9=B4=20=EB=90=98=EB=8F=8C?= =?UTF-8?q?=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - replan_route as_initial = guard.project_lock 안 · DB 행(지표면 확정 포함) · storage(STORAGE_SCOPE + 배수) 떠 둠 → 실패(409 · 예외)면 되돌림 · 응답 waited - 본체 `_save_route`(손 노선 [저장] = /design/save 가 잠금 · 떠 두기) - 부름 없던 `_write_working_route` 지움(700줄) - 시험 test_476_1_apply_initial 겹침 기다림 · 실패 되돌림 · 손 저장 잠금 밖 · 346-3 가짜 세계에 떠 두기 가짜 Co-Authored-By: Claude Opus 5.5 --- B05_Profile/B05_Profile_Router_Replan.py | 61 ++++++++------ docs/wiki/concepts/design_page/rules.md | 2 +- resources/tester/test_346_3_route_save.py | 6 ++ resources/tester/test_476_1_apply_initial.py | 88 +++++++++++++++++++- 4 files changed, 128 insertions(+), 29 deletions(-) diff --git a/B05_Profile/B05_Profile_Router_Replan.py b/B05_Profile/B05_Profile_Router_Replan.py index 2b008b81d..7142b8ca3 100644 --- a/B05_Profile/B05_Profile_Router_Replan.py +++ b/B05_Profile/B05_Profile_Router_Replan.py @@ -22,7 +22,6 @@ """ import asyncio -import csv import functools import json import logging @@ -49,7 +48,6 @@ from B05_Profile.B05_Profile_Schema import ( # noqa: F401 — 부르는 곳이 ) from common_util.common_util_initial_snapshot import design_route_csv_path from common_util.common_util_route_geometry import ( - densify_route, expected_route_csv_path, planned_route_initial_path, planned_route_working_path, @@ -59,11 +57,6 @@ from common_util.common_util_route_geometry import ( from common_util.common_util_route_polyline import build_planned_polyline from common_util.common_util_storage import resolve_stored_project_path from config.config_db import get_db_pool -from config.config_system import ( - ROUTE_DIRECT_LINK_CELL_FACTOR, - ROUTE_GRID_RES_M, - ROUTE_PLANNED_DENSIFY_SAFETY, -) logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/projects", tags=["B05 Route Design"]) @@ -307,25 +300,6 @@ def _ensure_planned_initial(project_root: Path, radius_m: float) -> dict | None: return summary -def _write_working_route(path: Path, vertices: list[tuple[float, float]]) -> int: - """계획노선(수정본) CSV를 쓴다. 열 이름은 `read_planned_route_csv()`가 아는 것. - - 쓰기 전에 **조밀화**한다 — 정점이 성기면 B05 격자 탐색이 원좌표를 그대로 잇지 못하고 - 제 나름의 길을 찾아 사용자가 그린 선과 달라진다(`densify_route` 주석). 평면 형상은 - 바뀌지 않고 같은 선 위에 점만 더 찍힌다. - """ - dense = densify_route( - vertices, - ROUTE_DIRECT_LINK_CELL_FACTOR * ROUTE_GRID_RES_M * ROUTE_PLANNED_DENSIFY_SAFETY, - ) - path.parent.mkdir(parents=True, exist_ok=True) - with path.open("w", encoding="utf-8", newline="") as file: - writer = csv.writer(file) - writer.writerow(("sequence", "x", "y")) - writer.writerows((index, round(x, 4), round(y, 4)) for index, (x, y) in enumerate(dense)) - return len(dense) - - async def _recompute( project_id: UUID, project_root: Path, @@ -552,7 +526,42 @@ async def replan_route(project_id: UUID, body: RouteSaveRequest) -> dict[str, An """노선 초안을 정본으로 저장한다(346-3 · 요청 · 응답 모양 = `B05_Profile_Route_Save` 머리). 배수유역은 다시 안 셈(낡음 표시) · 초기값은 추천 [적용](`as_initial`)일 때만 다시 뜬다. + 추천 [적용] = 즉시 저장(요구 275 ②) · [저장]과 같은 프로젝트 잠금 · 떠 두고 실패면 되돌림(476 · + 472-6 `common_util_save_guard`) · 응답 `waited` · ⚠ `/design/save` 잠금 안에서 as_initial 금지. """ + if not getattr(body, "as_initial", False): + return await _save_route(project_id, body) + from B06_Section.B06_Section_Router_DesignSave import _restore + from common_util import common_util_save_guard as guard + + lock = guard.project_lock(str(project_id)) + waited = lock.locked() + async with lock: + paths = await _project_paths(project_id) + if paths is None: + return JSONResponse(status_code=404, content=_PROJECT_PATH_MISSING) + pool = get_db_pool() + async with pool.acquire() as connection: # 설계 지형 바꿈(지표면 확정)도 떠 둠 + rows = await guard.snapshot_rows(connection, str(project_id), surface=True) + files = await asyncio.to_thread( + guard.snapshot_storage, paths[0], guard.STORAGE_SCOPE + guard.PIPE_SCOPE + ) + try: + result = await _save_route(project_id, body) + except Exception: + await _restore(project_id, rows, files) + raise + else: + if isinstance(result, JSONResponse) and result.status_code >= 400: + await _restore(project_id, rows, files) + return result + finally: + files.discard() + return {**result, "waited": waited} + + +async def _save_route(project_id: UUID, body: RouteSaveRequest) -> dict[str, Any] | JSONResponse: + """노선 저장 본체(잠금 · 떠 두기는 부르는 쪽 — `replan_route` · `/design/save`).""" as_initial = bool(getattr(body, "as_initial", False)) # 옛 요청 꼴(파이썬 호출)도 받음 try: request = body.resolved() if isinstance(body, RouteSaveRequest) else body diff --git a/docs/wiki/concepts/design_page/rules.md b/docs/wiki/concepts/design_page/rules.md index 113a2e6cf..c98bdd354 100644 --- a/docs/wiki/concepts/design_page/rules.md +++ b/docs/wiki/concepts/design_page/rules.md @@ -52,7 +52,7 @@ source: ["CLAUDE.md §5", "docs/raw/조사/2026-10-19_465_요구272_값안변함 | 내용 | 자리(파일:줄) | 상태 | |---|---|---| -| 저장 밖 정본 쓰기 셋 — 넓힘 `/regenerate` · [측점 만들기] · 추천 [적용] | `B06_Section_Router.py:378-388` · `B06_Section_Router_Stations.py:236-296` · `B05_Profile_UI_RouteEdit_Apply.ts:220-245` | 사용자 확정 · 확인창 · 472-6 협의 | +| 저장 밖 정본 쓰기 셋 — 넓힘 `/regenerate` · [측점 만들기] · 추천 [적용](즉시 저장 · [저장]과 같은 프로젝트 잠금 · 떠 두고 실패면 되돌림 · 476 ④) | `B06_Section_Router.py:378-388` · `B06_Section_Router_Stations.py:236-296` · `B05_Profile_UI_RouteEdit_Apply.ts:220-245` · `B05_Profile_Router_Replan.py` replan_route | 사용자 확정(요구 275 ②) · 확인창 | | `pipe_points.json` 한 [저장]에 두 번 PUT(관 · 관 옵션) | `B06_Section_Api_Culvert_Options.ts:67-95` | 코드 확인 | | 노선 바뀐 [저장]의 관 옵션 차례 = 옛 노선 열쇠(누가거리 · 관이 따라 옮긴 새 자리와 안 맞으면 안 얹힘) | `design_save.ts:145` · `B06_Section_Router_DesignSave.py` `_patched_points` | 추정 · 옛 길과 같음 | | ↶ ↷ 가 세션을 직접 되씀 — 관 · 구조물 · 추가 측점은 되쓴 뒤 가게 듣는 쪽을 부름(`announceDesignStore` · 472-5) · B06 조정창 = 세션 칸이 주인(사본 없음) · 칸이 바뀐 걸음만 상세 얹기 · 카드 · 면적 다시(472-5 5-D) · 암선 · 암 경사 · 상단측 = 세션 칸이 주인(사본 없음 · 472-5) | `B05_Profile_UI_Profile_History.ts` apply · `design_store.ts` announceDesignStore · `B06_Section_UI_Page_Station_Controls.ts` onRestore · `B06_Section_UI_Page_Persist.ts` readCells | 관 · 구조물 = `test_472_5_shop_owner` · 조정창 = `test_472_5d_adjust_owner` · 암선 · 암 경사 = `test_472_5r_rock_owner` | diff --git a/resources/tester/test_346_3_route_save.py b/resources/tester/test_346_3_route_save.py index d5b5fc733..c7a050b63 100644 --- a/resources/tester/test_346_3_route_save.py +++ b/resources/tester/test_346_3_route_save.py @@ -467,6 +467,12 @@ def api_world(monkeypatch, tmp_path): monkeypatch.setattr( route_initial, "refresh_initial_snapshot", lambda *a: calls.append(a) or _async(None)() ) + # 476 ④ — 추천 [적용]은 [저장]과 같은 잠금 · 떠 두기(DB · storage) 안에서 돎 + import common_util.common_util_save_guard as guard + + monkeypatch.setattr(replan, "get_db_pool", lambda: _Pool([])) + monkeypatch.setattr(guard, "snapshot_rows", _async({})) + monkeypatch.setattr(guard, "snapshot_storage", lambda *a: SimpleNamespace(discard=lambda: None)) return calls diff --git a/resources/tester/test_476_1_apply_initial.py b/resources/tester/test_476_1_apply_initial.py index 16c814f9d..24a09168b 100644 --- a/resources/tester/test_476_1_apply_initial.py +++ b/resources/tester/test_476_1_apply_initial.py @@ -13,6 +13,8 @@ import sys from pathlib import Path from uuid import uuid4 +import pytest + ROOT = Path(__file__).resolve().parents[2] TESTER = Path(__file__).resolve().parent for path in (ROOT, TESTER): @@ -96,11 +98,93 @@ def test_적용은_체인에_initial(api_world, monkeypatch): monkeypatch.setattr(replan, "_recompute", recompute) body = save.RouteSaveRequest(vertices=[{"x": 0, "y": 0}, {"x": 5, "y": 5}]) - asyncio.run(replan.replan_route(uuid4(), body)) - asyncio.run(replan.replan_route(uuid4(), body.model_copy(update={"as_initial": True}))) + asyncio.run(replan._save_route(uuid4(), body)) # 잠금 · 떠 두기 밖 본체(④) + asyncio.run(replan._save_route(uuid4(), body.model_copy(update={"as_initial": True}))) assert seen == [False, True] def test_횡단_손_입력_넘김_한_자리(): # 사용자 답 전 = [적용]도 넘김(475-3) — 답 뒤 이 함수 한 줄 assert save.carries_cross_inputs(True) is True and save.carries_cross_inputs(False) is True + + +# ── ④ [적용] = [저장]과 같은 잠금 · 떠 두고 실패면 되돌림 ───────────────────────── + + +class _Files: + def discard(self): + pass + + +@pytest.fixture +def guarded(monkeypatch, tmp_path): + from fastapi.responses import JSONResponse + + import B06_Section.B06_Section_Router_DesignSave as design_save + import common_util.common_util_save_guard as guard + + log: list = [] + + class _Pool: + def acquire(self): + class _A: + async def __aenter__(self): + return None + + async def __aexit__(self, *exc): + return False + + return _A() + + async def save(project_id, body): + rev = int(body.vertices[0].x) + log.append(("start", rev)) + await asyncio.sleep(0.05) + log.append(("end", rev)) + if rev == 9: + return JSONResponse(status_code=409, content={"status": "error", "message": "끊김"}) + return {"status": "success", "route_id": 1} + + async def restore(project_id, rows, files): + log.append(("restore", rows)) + return True + + monkeypatch.setattr(replan, "_project_paths", _async((tmp_path, "p"))) + monkeypatch.setattr(replan, "get_db_pool", lambda: _Pool()) + monkeypatch.setattr(replan, "_save_route", save) + monkeypatch.setattr(guard, "snapshot_rows", lambda *a, **k: log.append("rows") or _async("R")()) + monkeypatch.setattr(guard, "snapshot_storage", lambda *a: log.append("files") or _Files()) + monkeypatch.setattr(design_save, "_restore", restore) + return log + + +def _apply(rev: int, as_initial: bool = True): + body = save.RouteSaveRequest( + vertices=[{"x": rev, "y": 0}, {"x": 5, "y": 5}], as_initial=as_initial + ) + return replan.replan_route(_PID, body) # 같은 프로젝트 = 같은 잠금 + + +_PID = uuid4() + + +def test_적용_겹치면_기다림(guarded): + async def both(): + return await asyncio.gather(_apply(1), _apply(2)) + + first, second = asyncio.run(both()) + assert first["waited"] is False and second["waited"] is True + starts = [entry for entry in guarded if isinstance(entry, tuple) and entry[0] != "restore"] + assert starts == [("start", 1), ("end", 1), ("start", 2), ("end", 2)] # 하나씩 + assert "rows" in guarded and "files" in guarded + + +def test_적용_실패면_되돌림(guarded): + response = asyncio.run(_apply(9)) + assert response.status_code == 409 + assert ("restore", "R") in guarded + + +def test_손_노선_저장은_잠금_밖(guarded): + result = asyncio.run(_apply(1, as_initial=False)) + assert "waited" not in result and "rows" not in guarded # /design/save 가 잠금 · 떠 둠 From 466a2d620415c27f9b76e45d654125a4326a282d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 19 Oct 2026 17:23:21 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(476-1=20=C2=B7=20=EC=9A=94=EA=B5=AC=202?= =?UTF-8?q?77):=20=EC=B6=94=EC=B2=9C=20[=EC=A0=81=EC=9A=A9]=20=3D=20?= =?UTF-8?q?=ED=9A=A1=EB=8B=A8=20=EC=86=90=20=EC=9E=85=EB=A0=A5=EB=8F=84=20?= =?UTF-8?q?=EB=B2=84=EB=A6=BC=20=E2=80=94=20=EC=83=88=20=EB=85=B8=EC=84=A0?= =?UTF-8?q?=20=EA=B8=B0=EC=A4=80=20=EC=B4=88=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - carries_cross_inputs(initial) = not initial — [적용]이면 측점마다 기본 설계(손 노선 [저장]은 같은 자리 넘김 그대로) - 시험 test_476_1_apply_initial 「적용은 횡단 손 입력 버림」(옛 코드 = 넘김) - 위키 rules · operations_b05 줄 Co-Authored-By: Claude Opus 5.5 --- B05_Profile/B05_Profile_Route_Save.py | 8 +++++--- .../concepts/design_page/operations_b05.md | 2 +- docs/wiki/concepts/design_page/rules.md | 2 +- resources/tester/test_476_1_apply_initial.py | 20 ++++++++++++++++--- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/B05_Profile/B05_Profile_Route_Save.py b/B05_Profile/B05_Profile_Route_Save.py index fda51025b..1c5757475 100644 --- a/B05_Profile/B05_Profile_Route_Save.py +++ b/B05_Profile/B05_Profile_Route_Save.py @@ -22,7 +22,8 @@ API — `POST /api/projects/{id}/route/replan`(`B05_Profile_Router_Replan.replan (`B05_Profile_Route_Save_Parts`) · 배수유역 저장분은 노선 지문이 달라 「낡음」(B04 `_resolve`). - 계획고 = 바뀐 자리만 새 지반 첫 계획선 · 같은 자리(옆 1 m 안) 사용자 계획고 지킴 (`B05_Profile_Engine_Grade_Carry` · 옛 · 새 **계획 폴리라인** — 화면 planline 과 같은 선) · - 추천 [적용](`as_initial`) = 옮기지 않음 — 균형 종단(`apply_profile_target`) · 1차 계획선(438). + 추천 [적용](`as_initial`) = 옮기지 않음 — 균형 종단(`apply_profile_target`) · 1차 계획선(438) · + 횡단 손 입력도 안 넘김(요구 277 · 측점마다 기본 설계). - 초기값은 여기서 안 뜸 — 부르는 쪽이 `as_initial` 일 때만(자동설계 체인 직후 + 추천 [적용]). """ @@ -275,8 +276,9 @@ async def _carry_profile( def carries_cross_inputs(initial: bool) -> bool: - """횡단 손 입력을 새 측점(같은 자리)에 넘기나 — [적용]도 넘김(475-3 · 사용자 답 대기 476).""" - return True + """횡단 손 입력을 새 측점(같은 자리 · 475-3)에 넘기나 — 추천 [적용]은 안 넘김(요구 277 · 새 노선 + 기준 초기화 · 종단 편집 · 횡단 손 입력 전부 버림) · 손 노선 [저장]만 넘김.""" + return not initial async def _carry_cross( diff --git a/docs/wiki/concepts/design_page/operations_b05.md b/docs/wiki/concepts/design_page/operations_b05.md index af516d1c1..93fee4507 100644 --- a/docs/wiki/concepts/design_page/operations_b05.md +++ b/docs/wiki/concepts/design_page/operations_b05.md @@ -21,7 +21,7 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "docs/raw/ | 교점 추가 · 삭제 | 편집기 메모리 → `route-draft` | 묶음 전 단계 · 측점 수 바뀜 → 갈아끼움 | 같음 | 같음 | 11 | `B05_Profile_UI_RouteEdit_Input.ts:149-158` · `logic/B05_Profile_UI_RouteEdit_Edits.ts:410-426` | | 곡선 R · L | 편집기 `curveRadius` · `curveArc` → `route-draft` | 묶음 전 단계 · 확폭 · MC 이동 → 곡선 열쇠 옮김(`curveMoves`) | 같음 | `/design/save` `route` 칸 — 넣은 R · L 로 서버가 같은 규칙 | 11 | `B05_Profile_UI_RouteEdit_CurveBar.ts:76-162` · `logic/B05_Profile_Sections_Stations.ts:332-337` · `B05_Profile_Router_Replan.py:582 · 599-600` | | 스냅 켬 · 간격 | 간격 = pref `routeedit-snap-step` · 켬 = `route-draft.stationSnap` | 켬 = 곡선 IP 를 옮김(한 걸음) → 묶음 전 단계 | — | 곡선표 파일 · 서버는 측점만 맞춤(IP 맞춤은 브라우저만) | 3 | `B05_Profile_UI_RouteEdit_SnapStep.ts:74-78` · `logic/design_pipeline_stations.ts:52-59` · `B05_Profile_Engine_Sections_Core.py:457-464` | -| 추천 노선 [적용] | result `routeedit-initial` → `route-draft` | 묶음 한 바퀴 | 미리 셈 `/route/initial` → `route_recommend/*` | **저장 밖** — 바로 `/route/replan`(as_initial) · 종단 = 새 규칙 초기 종단(옛 계획고 · 편집 · L 안 옮김 · 476) · 초기값 다시 뜸 · 초안 전부 지움 · 페이지 다시 세움 | 11 | `B05_Profile_UI_RouteEdit_Initial.ts:139 · 161-181` · `B05_Profile_UI_RouteEdit_Apply.ts:211 · 220-245` · `B05_Profile_Router_Replan.py:659-662` | +| 추천 노선 [적용] | result `routeedit-initial` → `route-draft` | 묶음 한 바퀴 | 미리 셈 `/route/initial` → `route_recommend/*` | **저장 밖** — 바로 `/route/replan`(as_initial) · 종단 = 새 규칙 초기 종단 · 횡단 = 기본 설계(옛 계획고 · 편집 · L · 횡단 손 입력 안 옮김 · 476 · 요구 277) · [저장]과 같은 잠금 · 초기값 다시 뜸 · 초안 전부 지움 · 페이지 다시 세움 | 11 | `B05_Profile_UI_RouteEdit_Initial.ts:139 · 161-181` · `B05_Profile_UI_RouteEdit_Apply.ts:211 · 220-245` · `B05_Profile_Router_Replan.py:659-662` | ## 시설(관 · 구조물 · 추가 측점) diff --git a/docs/wiki/concepts/design_page/rules.md b/docs/wiki/concepts/design_page/rules.md index c98bdd354..854dd1eec 100644 --- a/docs/wiki/concepts/design_page/rules.md +++ b/docs/wiki/concepts/design_page/rules.md @@ -30,7 +30,7 @@ source: ["CLAUDE.md §5", "docs/raw/조사/2026-10-19_465_요구272_값안변함 | 측점 열쇠 = `common_util_station_key` 한 벌 · 노선 바뀜 = 관 · 구조물 좌표(x · y) 따라감 · 누가거리 다시 매김 | `common_util_station_key.ts:32` · `common_util_route_follow.ts:291` | `test_344_3_station_key_mirror` · `test_410_follow_xy` | | 노선 바뀜 넘김 = 열쇠마다 지반 · 측구 종류 · 측구 선택 · 사면 쉬프트 · 소단 · 소단 깊이 · 자리 칸(`SEAT_KEYS` = 암 경계 · 암 절토 경사 · 2단 경사 · 손 상단측 = 단면 유형 · 측구 방향)은 같은 자리(`sameSeat` · 중심 0.01 m · 축 1e-3) 측점만 · 새 자리 = 기본값 · 세션 값은 쓴 때 자리(`@seat`)로 판정(↶ ↷ 유지) · 끌기 갈아끼움 · 미리보기 · Node 넘김 · 서버 노선 저장 한 규칙 | `logic/B06_Section_Station_Swap.ts` SEAT_KEYS · unseated · `logic/B06_Section_Route_Carry.ts` carryKeyedStates · `B05_Profile/logic/B05_Profile_Pipeline_Detail.ts` crossAtSeat · `B05_Profile_Route_Save_Parts.py` carry_cross_inputs | `test_475_3_seat_carry` | | 노선 초안 ↶(버림) = 차례 하나 · 버림 동안 종단도 그리기 멈춤 · 낡음 다시 셈 미룸 → 되돌림 · 저장 기준 맞춤 · 뒤 차례 끝에 한 번 그림(사이 섞인 유토곡선 없음) | `design_pipeline_page.ts:642 · 403` · `B06_Section_Section_Store.ts:316` | `test_472_5c_drop_once` | -| 추천 [적용] 종단 = 새 규칙 초기 종단 — 끔 · 균형 셈 실패 = 1차 계획선(438 · L 20) · 켬 = 균형 종단을 꼭짓점에 얹음(438) · 옛 계획고 · 편집 · L 안 옮김 · 손 노선 저장만 옮김 | `B05_Profile_Route_Save.py` run_route_save_chain `initial` · 미리 보기 = `B05_Profile_Engine_InitialProfile`(같은 조각) | `test_476_1_apply_initial` · `test_476_1_recommend_initial` | +| 추천 [적용] = 새 노선 기준 초기화 — 종단 = 새 규칙 초기 종단(끔 · 균형 셈 실패 = 1차 계획선 438 · L 20 · 켬 = 균형 종단을 꼭짓점에 얹음) · 옛 계획고 · 편집 · L · 횡단 손 입력 안 넘김(요구 277 · 측점마다 기본 설계) · 손 노선 저장만 옮김 · 넘김 | `B05_Profile_Route_Save.py` run_route_save_chain `initial` · carries_cross_inputs · 미리 보기 = `B05_Profile_Engine_InitialProfile`(같은 조각) | `test_476_1_apply_initial` · `test_476_1_recommend_initial` | | B05 · B06 캐시 = 한 벌 · [저장] 한 번에 함께 나감 | `design_shared.ts:395-448` · `design_save.ts:237` | — | | 관 · 구조물 주인 = 가게 칸 하나 — 화면 사본은 가게 알림으로 맞춤(제 쓰기는 같은 값 · 남 고침은 다시 실음) | `design_view_profile.ts` 가게 구독 · `design_view_cross.ts` onShop · `B06_Section_UI_Page_Structures_Panel.ts` seen | `test_472_5_shop_owner` | | 정본 자리 = 값을 낳은 주인 — 노선 시설 = `pipe_points.json` · `structures.json` · 횡단 값 = 횡단 패치 | `B05_Profile_Api_Pipes_Draft.ts:30-37` · `B05_Profile_Structures_Router.py:386-405` · `B06_Section_Section_Store.ts:449` | — | diff --git a/resources/tester/test_476_1_apply_initial.py b/resources/tester/test_476_1_apply_initial.py index 24a09168b..755a259d3 100644 --- a/resources/tester/test_476_1_apply_initial.py +++ b/resources/tester/test_476_1_apply_initial.py @@ -103,9 +103,23 @@ def test_적용은_체인에_initial(api_world, monkeypatch): assert seen == [False, True] -def test_횡단_손_입력_넘김_한_자리(): - # 사용자 답 전 = [적용]도 넘김(475-3) — 답 뒤 이 함수 한 줄 - assert save.carries_cross_inputs(True) is True and save.carries_cross_inputs(False) is True +def test_적용은_횡단_손_입력_버림(chain_world, monkeypatch): + # 요구 277 — 추천 [적용] = 새 노선 기준 초기화(횡단 손 입력 전부 버림) · 손 노선 [저장]은 넘김 + import B06_Section.B06_Section_Repository as b06_repo + + seen: list = [] + + async def carry(project_id, root, route_id, relative, old_long, old_designs, *rest): + seen.append(old_designs) + return 0 + + designs = [{"chainage_m": 20.0, "design": {"rock_boundary_offset_m": -0.7}}] + monkeypatch.setattr(b06_repo, "get_cross_section_designs", _async(designs)) + monkeypatch.setattr(save, "_carry_cross", carry) + _run(chain_world, initial=True) + _run(chain_world) + assert seen == [[], designs] # 옛 코드 = [적용]도 손 입력 넘김 + assert save.carries_cross_inputs(True) is False and save.carries_cross_inputs(False) is True # ── ④ [적용] = [저장]과 같은 잠금 · 떠 두고 실패면 되돌림 ─────────────────────────