Files
Aislo/resources/tester/test_530_3_plan_logic.py
T
eomsangdonandClaude Opus 5.5 cdcdd5f597 feat(562-3 · 563-1): D03 손 긋기 화면 — 도구 여섯 · 그리기 · 거리재기 · 구역 연도 · 횡단 판 · 예상 코리도 걷음
- UI_Map: 바탕(위성 · 등고선) + 겹(그리기 · 거리재기) · 도구줄 부품(되돌리기 · 다시하기 · 3D · 좌 · 우 회전 · 거리재기) · B05 createMapRotation · 왼쪽 끌기 이동 끔(가운데 단추 · 휠 방향 = 설정)
- 새 UI_Draw: 시점에서 점 찍어 직선 · 미리보기 길이 · 기울기 · 제약 칸(자유 · 길이 고정 · 기울기 고정 · 둘 다 · 20 m · 9 %) · 종점 붙기 · 갈래 · 구역 마크 · Enter · Esc · Backspace
- 새 UI_Measure: 두 점 길이 · 기울기 · 높이 차(지형 heightAt)
- logic/Plan: 선 · 갈래 · 조각 꼴 다루기로 다시 씀(옛 갈래 넷 · 이은 누가 걷음)
- UI_Page · Api: 지형 범위 카카오 지도 미리보기 → [이 범위 받기] · [범위 밖 더 받기] · GET · PUT plan/network · [초기화] · 조각 연도 고름 · 캐시 판 2
- UI_3D: 지형 + 그은 선(선 고름) · 예상 코리도 · 체크 칸 걷음
- 지움: UI_Cross · UI_Edit · logic/Expect · logic/Edit · test_544_1(예상 횡단 · 예상 코리도 셈이 화면에서 걷혀 지킬 대상 없음)
- 시험: 530_3 = 손 긋기 꼴 다루기 + 서버 derive 대조로 다시 씀 · 553_4 D03 부품 단언 새 화면으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:16:08 +09:00

147 lines
5.6 KiB
Python

"""D03 손 긋기 꼴 다루기(PLAN 562-3 · 563-1) — TS `logic/D03_PlanAnalysis_Plan.ts`.
조각이 선을 빈틈없이 덮음 · 마크 놓기 · 지우기 · 갈래 차례 밀기 · 갈래 붙은 점 못 뺌 · 서버 꼴 읽기 ·
화면이 보내는 꼴(선 · 마크 · 조각)을 서버 셈(`D03_PlanAnalysis_Network.derive`)이 그대로 받음.
./venv/Scripts/python.exe -m pytest resources/tester/test_530_3_plan_logic.py -q
"""
from __future__ import annotations
import json
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from D03_PlanAnalysis import D03_PlanAnalysis_Network as server # noqa: E402
TS_FILE = ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Plan.ts"
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
_RUNNER = """
import { writeFileSync } from "node:fs";
import * as plan from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Plan.js";
const Y = 2026;
let n = plan.emptyNetwork([0, 0]);
n = plan.appendVertex(n, 0, [100, 0]);
n = plan.appendVertex(n, 0, [100, 100]);
n = plan.normalizePieces(n, Y);
const one = n;
n = plan.normalizePieces(plan.splitPiece(n, "main", 50), Y);
const split = n;
const tooClose = plan.splitPiece(n, "main", 50.5);
n = plan.normalizePieces(plan.setPieceYear(n, 1, 2027), Y);
n = plan.addBranch(n, 0, 1);
n = plan.appendVertex(n, 1, [160, 0]);
n = plan.normalizePieces(n, Y);
const branched = n;
const inserted = plan.insertVertex(n, 0, 0, [40, 0]);
const blocked = plan.dropLastVertex(inserted, 0);
const dropped = plan.dropLastVertex(n, 1);
const guard = plan.dropLastVertex(plan.dropLastVertex(n, 0), 0);
const removed = plan.normalizePieces(plan.removeMark(n, "main", 50.4, 1), Y);
const grown = plan.normalizePieces(plan.appendVertex(n, 0, [100, 150]), Y);
const read = plan.fromServer(
n.lines,
[{ line: "main", at_m: 50 }, { line: "main", at_m: 150 }],
[{ id: "p9", line: "main", from_m: 0, to_m: 50, year: 2028 }],
Y,
);
writeFileSync(process.argv[2], JSON.stringify({
one, split, tooClose, branched, inserted, blocked, dropped, guard, removed, grown, read,
marks: plan.marksOf(branched),
reached: plan.reachedEnd(n, [100, 100]),
}));
"""
def _run_ts(tmp_path: Path) -> dict:
out = tmp_path / "js"
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(TSC),
str(TS_FILE),
"--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_path / "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"))
def _spans(network: dict, line: str = "main") -> list[tuple[float, float, int]]:
return [(p["from_m"], p["to_m"], p["year"]) for p in network["pieces"] if p["line"] == line]
def test_pieces_cover_and_split(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
assert _spans(got["one"]) == [(0, 200, 2026)]
assert _spans(got["split"]) == [(0, 50, 2026), (50, 200, 2026)]
assert [p["id"] for p in got["split"]["pieces"]] == ["p1", "p2"] # 앞 조각이 id 를 지님
assert got["tooClose"] == got["split"] # 마크 1 m 안 = 안 놓음
assert _spans(got["removed"]) == [(0, 200, 2026)] # 마크 지움 = 앞 조각 연도
assert _spans(got["grown"]) == [(0, 50, 2026), (50, 250, 2027)] # 선이 늘면 끝 조각이 늚
assert got["reached"] is True
def test_branch_order_and_guard(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
branch = got["branched"]["lines"][1]
assert branch["branch"] == {"line": "main", "vertex": 1} and branch["vertices"][0] == [100, 0]
assert _spans(got["branched"], branch["id"]) == [(0, 60, 2026)]
# 본선 앞 구간에 꺾임점을 끼우면 갈래가 붙은 차례가 한 칸 밀림
assert got["inserted"]["lines"][0]["vertices"][1] == [40, 0]
assert got["inserted"]["lines"][1]["branch"]["vertex"] == 2
assert got["blocked"]["lines"][0]["vertices"][-1] == [100, 0] # 끝 점은 갈래 없음 → 빠짐
assert "갈래가 붙은 점" in got["guard"] # 갈래가 붙은 점은 못 뺌
assert got["dropped"]["lines"][1]["vertices"] == [[100, 0]]
def test_from_server_fills_marks(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
# 조각 없는 구역(마크만) = 처음 연도 · 새 id
assert _spans(got["read"]) == [(0, 50, 2028), (50, 150, 2026), (150, 200, 2026)]
assert got["read"]["pieces"][0]["id"] == "p9"
assert len({p["id"] for p in got["read"]["pieces"]}) == 4 # 갈래 조각 포함 겹침 없음
def test_server_accepts_screen_body(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
body = {**got["branched"], "marks": got["marks"], "epsg": 5186}
derived = server.derive(body, 2026)
assert [(p["line"], p["from_m"], p["to_m"], p["year"]) for p in derived["pieces"]] == [
("main", 0.0, 50.0, 2026),
("main", 50.0, 200.0, 2027),
("branch-1", 0.0, 60.0, 2026),
]