fix(476-1 ①): 추천 [적용] = 늘 새 규칙 초기 종단 — 옛 계획고 · 편집 · L 안 옮김
- run_route_save_chain `initial`(= as_initial) — 끔 · 균형 셈 실패면 `_carry_profile` 안 부름 = 막 쓴 1차 계획선(438 · L 20) 그대로 - 손 노선 [저장]은 그대로 계획고 옮김 - 횡단 손 입력 넘김 판정 한 자리 `carries_cross_inputs`(사용자 답 대기 · 지금 = 넘김) - 시험 test_476_1_apply_initial · 346-3 가짜 _recompute 칸 더함 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@ 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 과 같은 선) ·
|
||||
균형 종단(`apply_profile_target`)은 추천 [적용](`as_initial`)만.
|
||||
추천 [적용](`as_initial`) = 옮기지 않음 — 균형 종단(`apply_profile_target`) 또는 1차 계획선(438).
|
||||
- 초기값은 여기서 안 뜸 — 부르는 쪽이 `as_initial` 일 때만(자동설계 체인 직후 + 추천 [적용]).
|
||||
"""
|
||||
|
||||
@@ -258,6 +258,11 @@ async def _carry_profile(
|
||||
await connection.commit()
|
||||
|
||||
|
||||
def carries_cross_inputs(initial: bool) -> bool:
|
||||
"""횡단 손 입력을 새 측점(같은 자리)에 넘기나 — 추천 [적용]도 넘김(475-3 · 사용자 답 대기 476)."""
|
||||
return True
|
||||
|
||||
|
||||
async def _carry_cross(
|
||||
project_id: UUID,
|
||||
project_root: Path,
|
||||
@@ -358,11 +363,13 @@ async def run_route_save_chain(
|
||||
previous_params: dict[str, Any],
|
||||
profile_target: tuple[list[float], list[float]] | None = None,
|
||||
planned_lines: tuple[list[list[float]], list[list[float]]] | None = None,
|
||||
initial: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""노선 저장 체인 — 성공이면 요약 dict · 실패면 `{"error": 사유}`(파일 · 행은 되살린 뒤).
|
||||
|
||||
`planned_lines` = (옛 · 새 계획 폴리라인) — 바뀐 구간을 화면 planline 단계와 같은 선으로 가름
|
||||
(없으면 풀린 노선 geojson · 점 간격이 달라 앞머리가 일찍 갈림).
|
||||
`initial` = 추천 [적용](`as_initial`) — 옛 계획고 · 편집 · L 안 옮김 · 새 규칙 초기 종단(476).
|
||||
"""
|
||||
import aiomysql
|
||||
|
||||
@@ -465,7 +472,7 @@ async def run_route_save_chain(
|
||||
from B05_Profile.B05_Profile_Router_RouteInitial import apply_profile_target
|
||||
|
||||
await apply_profile_target(project_id, new_route_id, profile_target)
|
||||
else:
|
||||
elif not initial: # [적용] = 막 쓴 1차 계획선(438 · L 20) 그대로(476 · 요구 276)
|
||||
await _carry_profile(
|
||||
project_root,
|
||||
relative,
|
||||
@@ -482,7 +489,7 @@ async def run_route_save_chain(
|
||||
new_route_id,
|
||||
relative,
|
||||
old_long,
|
||||
old_designs,
|
||||
old_designs if carries_cross_inputs(initial) else [],
|
||||
# 옛 관 = 옛 노선 누가거리(B05 가 읽는 자리 · 정본은 다른 선 누가거리일 수 있음 · 459)
|
||||
[pipe for _, pipe in follow_pipes(old_pipes, vertices_of(old_xy))],
|
||||
old_structures,
|
||||
|
||||
@@ -333,6 +333,7 @@ async def _recompute(
|
||||
balanced: RouteInitialRequest | None = None,
|
||||
terrain: Literal["las", "sheet"] | None = None,
|
||||
planned_lines: tuple[list[list[float]], list[list[float]]] | None = None,
|
||||
initial: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""노선을 갈아 끼운 뒤의 재계산 — 초기 업로드 체인과 같은 로직을 그대로 탄다.
|
||||
|
||||
@@ -441,6 +442,7 @@ async def _recompute(
|
||||
previous_params,
|
||||
target,
|
||||
planned_lines,
|
||||
initial,
|
||||
)
|
||||
failure = saved.get("error")
|
||||
marks.append(("재확정 체인", time.perf_counter()))
|
||||
@@ -621,6 +623,7 @@ async def replan_route(project_id: UUID, body: RouteSaveRequest) -> dict[str, An
|
||||
),
|
||||
request.terrain,
|
||||
(old_line, await asyncio.to_thread(_vertices_of, working_path)),
|
||||
as_initial,
|
||||
)
|
||||
if "error" in result:
|
||||
# 노선을 되돌린다 — 실패했는데 새 노선만 남으면 화면·정본이 어긋난 채 굳는다.
|
||||
|
||||
@@ -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) · 초기값 다시 뜸 · 초안 전부 지움 · 페이지 다시 세움 | 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) · 초기값 다시 뜸 · 초안 전부 지움 · 페이지 다시 세움 | 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` |
|
||||
|
||||
## 시설(관 · 구조물 · 추가 측점)
|
||||
|
||||
|
||||
@@ -30,6 +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` | `test_476_1_apply_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` | — |
|
||||
|
||||
@@ -487,7 +487,9 @@ def test_추천_적용은_초기값_다시(api_world):
|
||||
def test_균형_종단은_추천_적용만_손_저장은_계획고_옮김(api_world, monkeypatch):
|
||||
calls: list = []
|
||||
|
||||
async def recompute(project_id, root, stored, balanced=None, terrain=None, lines=None):
|
||||
async def recompute(
|
||||
project_id, root, stored, balanced=None, terrain=None, lines=None, initial=False
|
||||
): # 476 — [적용] = initial 칸 더함
|
||||
calls.append((balanced, lines))
|
||||
return {"route_id": 9, "total_length_m": 10.0}
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ruff: noqa: F811 — 346-3 fixture 를 불러 씀
|
||||
"""476-1 ① 추천 [적용] = 늘 새 규칙 초기 종단(요구 276) — 옛 계획고 · 편집 · L 안 옮김.
|
||||
|
||||
체인은 346-3 시험의 가짜 세계(`chain_world`)로 · 종단 파일은 실제 1차 계획선.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TESTER = Path(__file__).resolve().parent
|
||||
for path in (ROOT, TESTER):
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
from test_346_3_route_save import ( # noqa: E402, F401 — 가짜 세계 fixture
|
||||
NEW,
|
||||
_async,
|
||||
_longitudinal,
|
||||
_run,
|
||||
api_world,
|
||||
chain_world,
|
||||
)
|
||||
|
||||
import B05_Profile.B05_Profile_Repository as b05_repo # noqa: E402
|
||||
import B05_Profile.B05_Profile_Route_Save as save # noqa: E402
|
||||
import B05_Profile.B05_Profile_Router_Replan as replan # noqa: E402
|
||||
import B06_Section.B06_Section_Router as b06_router # noqa: E402
|
||||
from B05_Profile.B05_Profile_Route_Save_Parts import vertices_of # noqa: E402
|
||||
|
||||
LONG = "B06_Section/longitudinal/longitudinal.json"
|
||||
|
||||
|
||||
def _real_profile_world(world, monkeypatch):
|
||||
"""옛 종단 = 손 편집 · L 15 · 새 종횡단 = 실제 1차 계획선 — `_carry_profile` 은 진짜."""
|
||||
total = float(vertices_of(NEW)[-1].chainage_m)
|
||||
old = _longitudinal(total)
|
||||
old["profile_alignment"]["edits"] = {"station_offsets": {"40.000": 0.7}, "curve_radii": {}}
|
||||
old["profile_alignment"]["policy"]["default_curve_length_m"] = 15.0
|
||||
path = world.root / LONG
|
||||
path.write_text(json.dumps(old), "utf-8")
|
||||
fresh = _longitudinal(total)
|
||||
|
||||
async def regenerate(*args, **kwargs):
|
||||
world.order.append("sections")
|
||||
path.write_text(json.dumps(fresh), "utf-8")
|
||||
|
||||
monkeypatch.setattr(b06_router, "regenerate_route_sections", regenerate)
|
||||
monkeypatch.setattr(save, "_carry_profile", _CARRY)
|
||||
monkeypatch.setattr(b05_repo, "update_longitudinal_grade_summary", _async(None))
|
||||
return path, fresh["profile_alignment"]
|
||||
|
||||
|
||||
_CARRY = save._carry_profile
|
||||
|
||||
|
||||
def test_적용_끔이면_계획고_안_옮김(chain_world):
|
||||
result = _run(chain_world, initial=True)
|
||||
assert "error" not in result, result
|
||||
assert "profile" not in chain_world.order # 옛 코드 = 같은 노선 옛 꼭짓점 통째 옮김
|
||||
|
||||
|
||||
def test_적용_같은_노선_끔_새_1차_계획선(chain_world, monkeypatch):
|
||||
path, fresh = _real_profile_world(chain_world, monkeypatch)
|
||||
_run(chain_world, initial=True)
|
||||
alignment = json.loads(path.read_text("utf-8"))["profile_alignment"]
|
||||
assert alignment == fresh # 꼭짓점 · L(정책) · 편집 = 새 1차 계획선(438) 그대로
|
||||
assert not (alignment.get("edits") or {}).get("station_offsets")
|
||||
|
||||
|
||||
def test_손_노선_저장은_계획고_옮김_그대로(chain_world, monkeypatch):
|
||||
path, fresh = _real_profile_world(chain_world, monkeypatch)
|
||||
_run(chain_world)
|
||||
alignment = json.loads(path.read_text("utf-8"))["profile_alignment"]
|
||||
assert alignment["edits"]["station_offsets"].get("40.000") == 0.7 # 앞 구간 손 편집 옮김
|
||||
|
||||
|
||||
def test_적용_켬_균형_셈_실패도_1차_계획선(chain_world, monkeypatch):
|
||||
path, fresh = _real_profile_world(chain_world, monkeypatch)
|
||||
_run(chain_world, profile_target=None, initial=True) # 균형 목표 None(셈 실패)
|
||||
assert json.loads(path.read_text("utf-8"))["profile_alignment"] == fresh
|
||||
|
||||
|
||||
def test_적용은_체인에_initial(api_world, monkeypatch):
|
||||
seen: list = []
|
||||
|
||||
async def recompute(*args, **kwargs):
|
||||
seen.append(args[6] if len(args) > 6 else kwargs.get("initial"))
|
||||
return {"route_id": 9, "total_length_m": 10.0}
|
||||
|
||||
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})))
|
||||
assert seen == [False, True]
|
||||
|
||||
|
||||
def test_횡단_손_입력_넘김_한_자리():
|
||||
# 사용자 답 전 = [적용]도 넘김(475-3) — 답 뒤 이 함수 한 줄
|
||||
assert save.carries_cross_inputs(True) is True and save.carries_cross_inputs(False) is True
|
||||
Reference in New Issue
Block a user