feat(B05 평면): 예정노선 변경 = 예정노선 페이지 긋기 · 고르기 · 메뉴 · 속성 판 그대로(한 선) · 원본 옅은 점선 켬끔 (PLAN 626-3 · 요구 535-1 · 536 · 535-3)

- B05 「예정노선 변경」 켬끔 = D03 createPlanDraw · createPlanEdit · createPlanMenu · createPlanProps 를 한 선 network 로 얹음(같은 함수 · 단추 · 단축키 · 노드 끌기 · 고른 토막 끌어 꺾임점 · Delete · [다시 긋기]) — 갈래 · 둘째 선 · 연도 · 지점은 막음/숨김
- 한 번 고침(긋기 끝 · 놓음 · 지움) → 켠 뒤 첫 고침만 확인창 → `/route/follow` → 편집기 한 걸음 · 다 지우면 새로 그을 때까지 평면 그대로
- 도구줄 「예정노선(원본)」 켬끔 — 파일입력 예정노선(안 덮음)을 옅은 점선으로(고친 것과 다를 때만)
- D03 덧 인자 `oneLine`(UI_Draw · UI_DrawPanel 연도 마커 · UI_Menu · UI_Props) — 기본값 = 지금 그대로 · 긋기 칸 · 속성 판 꼴 CSS 를 `D03_PlanAnalysis_UI_DrawParts.css` 로 옮겨 그 TS 가 불러옴(D03 꼴 같음)
- 옛 자체 긋기 셈 `logic/B05_Profile_ExpectedDraw.ts` 지움(D03 셈으로 갈음) · 시험 test_626_3 다시 씀(5)
- 낡은 글 고침 — 설계 지형 바꿈 = 지형 칩(`/route/design-terrain`) · 「추천 [적용] 때만」 글 걷음(Router_Terrain · Router_Replan · Api_Replan · RouteEdit_Apply · 동작 그대로)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 22:42:36 +09:00
co-authored by Claude Opus 5.5
parent 8a7d972ce8
commit e250517adf
18 changed files with 451 additions and 580 deletions
+41 -97
View File
@@ -1,116 +1,31 @@
"""626-3 B05 평면 예정노선 그리기 → 다시 추적 → 편집기 한 걸음 → [저장](요구 530 · 531 · 결정 D3 · D4 · D9 · D10).
"""626-3 B05 평면 예정노선 변경 → 다시 추적 → 편집기 한 걸음 → [저장](요구 530 · 531 · 535-1 · 536 · 결정 D3 · D4 · D9).
- 셈(TS `logic/B05_Profile_ExpectedDraw.ts` · tsc 로 옮겨 Node) — 구간 다시 긋기 갈음(앞뒤 그대로 · 거꾸로 그음 뒤집음 ·
시점 · 종점 자리) · 다음 점 방식 넷(D03 셈 그대로) · 선 위 자리 · 첫 구간 정방향 앞 점.
- 예정노선 페이지(D03) 긋기 · 고르기 · 메뉴 · 속성 판 함수를 그대로 부름(`oneLine` = 한 선 · 갈래 · 연도 · 지점 막음/숨김) ·
D03 덧 인자는 기본값 = 지금 그대로.
- 지킴(글 대조) — 다시 추적 = `/route/follow` 만(저장 0) · `/route/replan` · `as_initial` · `expected_route.csv` 안 부름 ·
결과 = 편집기 `restoreSnapshot(…, true)` 한 걸음 · [저장] = 손 편집 같은 길(`bindRouteApply`) + 초안 `expected` ·
걸음 사진 · 노선 초안에 고친 예정노선 실림(되돌리기 · 세션 되살림) · 같은 선 판정에 예정노선 · 긋기 방식 칸 공용 ·
판 켜면 캔버스 누름을 먼저 받음(재기 · 계획선 고르기 막음) · RouteEdit 700 줄 안.
걸음 사진 · 노선 초안에 고친 예정노선 실림(되돌리기 · 세션 되살림) · 같은 선 판정에 예정노선 · 켠 동안 캔버스 누름을
먼저 받음(재기 · 계획선 고르기 막음) · 원본 켬끔 · RouteEdit 700 줄 안.
./venv/Scripts/python.exe -m pytest resources/tester/test_626_3_expected_edit.py -q
"""
from __future__ import annotations
import json
import re
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
B05 = ROOT / "B05_Profile"
TS_FILES = [
ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Draw.ts",
ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Fan.ts",
B05 / "logic" / "B05_Profile_ExpectedDraw.ts",
]
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
_RUNNER = """
import { writeFileSync } from "node:fs";
import * as d from "./B05_Profile/logic/B05_Profile_ExpectedDraw.js";
const line = [[0, 0], [10, 0], [20, 0], [30, 0], [40, 0]];
const plane = (x, y) => 0.65 * x;
const out = {};
out.forward = d.respliceLine(line, 12, 28, [[15, 5], [25, 5]]);
out.backward = d.respliceLine(line, 28, 12, [[25, 5], [15, 5]]);
out.head = d.respliceLine(line, 0, 20, [[10, 3]]);
out.tail = d.respliceLine(line, 30, 40, []);
out.near = d.nearestOnLine(line, [17, 4]);
out.leadStart = d.leadBefore(line, 0);
out.lead = d.leadBefore(line, 12);
out.free = d.pickNext("free", [0, 0], [3, 4], 20, 9, plane, null);
out.length = d.pickNext("length", [0, 0], [3, 4], 20, 9, plane, null);
const both = d.pickNext("both", [0, 0], [0, 50], 20, 9, plane, [0, -20]);
out.both = { point: both.point, nodes: both.fan.nodes.length, arcs: both.fan.arcs.length, lengthOnly: both.lengthOnly };
const between = d.pickNext("both", [0, 0], [-3, 50], 20, 9, plane, [0, -20], true);
out.between = { point: between.point, lengthOnly: between.lengthOnly };
out.none = d.pickNext("both", [0, 0], [0, 50], 20, 9, () => NaN, null);
out.slopeNone = d.pickNext("slope", [0, 0], [0, 50], 20, 9, () => 0, null);
writeFileSync(process.argv[2], JSON.stringify(out));
"""
@pytest.fixture(scope="module")
def r(tmp_path_factory: pytest.TempPathFactory) -> dict:
tmp = tmp_path_factory.mktemp("expected_draw")
out = tmp / "js"
subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(TSC), *map(str, TS_FILES), "--outDir", str(out), "--rootDir", str(ROOT)]
+ ["--module", "esnext", "--target", "es2022", "--moduleResolution", "bundler"]
+ ["--ignoreConfig"],
cwd=str(ROOT),
check=True,
capture_output=True,
)
for emitted in out.rglob("*.js"):
emitted.write_text(
re.sub(r'(from\s+"\.\.?/[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
encoding="utf-8",
)
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
result = tmp / "out.json"
subprocess.run( # noqa: S603
["node", str(out / "runner.mjs"), str(result)],
cwd=str(ROOT),
check=True,
capture_output=True,
)
return json.loads(result.read_text(encoding="utf-8"))
D03 = ROOT / "D03_PlanAnalysis"
def _src(path: Path) -> str:
return path.read_text(encoding="utf-8")
def test_구간_다시_긋기_앞뒤_그대로_가운데만_갈음(r):
assert r["forward"] == [[0, 0], [10, 0], [12, 0], [15, 5], [25, 5], [28, 0], [30, 0], [40, 0]]
assert r["backward"] == r["forward"] # 거꾸로 그어도 같은 선
assert r["head"] == [[0, 0], [10, 3], [20, 0], [30, 0], [40, 0]] # 시점부터 · 같은 점 하나
assert r["tail"] == [[0, 0], [10, 0], [20, 0], [30, 0], [40, 0]] # 속 점 없음 = 곧은 갈음
def test_선_위_자리_첫_구간_앞_점(r):
assert r["near"] == {"atM": 17, "point": [17, 0]}
assert r["leadStart"] is None and r["lead"] == [7, 0]
def test_다음_점_방식_넷_D03_셈_그대로(r):
assert r["free"]["point"] == [3, 4] and r["free"]["fan"] is None
assert [round(v, 6) for v in r["length"]["point"]] == [12, 16]
both = r["both"]
assert both["nodes"] == 4 and both["arcs"] == 2 and not both["lengthOnly"]
assert abs((both["point"][0] ** 2 + both["point"][1] ** 2) ** 0.5 - 20) < 1e-6
between = r["between"]
assert between["lengthOnly"] and abs(sum(v * v for v in between["point"]) ** 0.5 - 20) < 1e-6
assert r["none"]["point"] is None and "자리 없음" in r["none"]["note"]
assert r["slopeNone"]["point"] is None and "자리 없음" in r["slopeNone"]["note"]
def test_다시_추적_follow_만_저장_0_초기값_추천_안_건드림():
ui = _src(B05 / "B05_Profile_UI_ExpectedDraw.ts")
ui = _src(B05 / "B05_Profile_UI_ExpectedDraw.ts").split("*/", 1)[1] # 머리 글 뺀 몸
assert "/route/follow" in ui and "askConfirm(" in ui and "flattenServerPlan(" in ui
for banned in (
"/route/replan",
@@ -152,18 +67,47 @@ def test_걸음_사진_초안_같은_선에_예정노선():
assert "expected: list[tuple[float, float]] | None = None" in server
def test_방식_칸_공용_판_켜면_캔버스_누름_먼저_받음():
def test_D03_긋기_고르기_메뉴_속성판_그대로_한_선():
ui = _src(B05 / "B05_Profile_UI_ExpectedDraw.ts")
assert 'from "../A00_Common/map/map_draw_kind"' in ui and "createDrawKindFields(" in ui
assert "paintPreview(" in ui and "pickNext(" in ui # D03 그림 · 셈 그대로
for call in ("createPlanDraw({", "createPlanEdit(", "createPlanMenu({", "createPlanProps({"):
assert call in ui, call
assert ui.count("oneLine: true,") == 3 # 긋기 · 메뉴 · 속성 판
assert "draw.key(event) || editor.key(event)" in ui # 같은 단축키
assert (
'keyIs(event, "plan.undo") || keyIs(event, "plan.redo")' in ui
) # 되돌리기 = 페이지 사진첩
assert '"pointerdown",' in ui and "event.stopImmediatePropagation()" in ui
assert re.search(
r"\n true,\n \);", ui
) # 잡기 단계(capture) — 재기 · 계획선 고르기보다 먼저
assert 'keyIs(event, "plan.drawKind")' in ui and 'keyIs(event, "plan.drawFinish")' in ui
assert "createFloatPanel(" in ui and 'createElement("button")' not in ui
assert "event.buttons & 6" in ui # 오른쪽 · 가운데 끌기 = 지도 이동 그대로
assert 'createElement("button")' not in ui
# D03 덧 인자 — 기본값 = 지금 그대로
draw = _src(D03 / "D03_PlanAnalysis_UI_Draw.ts")
assert (
"oneLine?: boolean;" in draw
and "params.oneLine ? oneLineBlock(target.kind, network) : null" in draw
)
assert "marks: !params.oneLine," in draw
assert "markButton.hidden = opts.marks === false;" in _src(
D03 / "D03_PlanAnalysis_UI_DrawPanel.ts"
)
menu = _src(D03 / "D03_PlanAnalysis_UI_Menu.ts")
assert "if (params.oneLine && oneLineBlock(aim, params.network())) return;" in menu
assert 'if (!params.oneLine) items.push(["여기서 연도 구역 나눔"' in menu
props = _src(D03 / "D03_PlanAnalysis_UI_Props.ts")
assert "!!params.oneLine ||" in props and "...(params.oneLine ? [] :" in props
tools = _src(ROOT / "A00_Common" / "map" / "map_toolbar.ts")
assert re.search(r'key: "expected-draw",\s+does: "[^"]+",\s+pages: B05', tools)
assert re.search(r'key: "expected-original", does: "[^"]+", pages: B05', tools)
render = _src(B05 / "B05_Profile_UI_RouteEdit_Render.ts")
# 맨 위 = 구조물 마커 뒤 · 회전 끝 앞
assert render.index("scene.overAll?.(context);") < render.index("context.restore(); // 회전 끝")
def test_원본_예정노선_안_덮음_옅은_점선_켬끔():
ui = _src(B05 / "B05_Profile_UI_ExpectedDraw.ts")
assert "showOriginal && original.length > 1 && line() !== original" in ui
assert "expected_route.csv" in ui.split("*/")[0] # 머리 = 원본 안 덮음 적음
for banned in ("write_route_csv", 'expected_route.csv"', "/route/initial"):
assert banned not in ui.split("*/", 1)[1], banned