"""D03 화면 계산 거울(PLAN 530-3) — 파이썬 `D03_PlanAnalysis_Analyze` ↔ TS `logic/D03_PlanAnalysis_Plan.ts`. 짝: `pick_mode` · `chain` · `joined_line` ↔ `pickMode` · `chainRows` · `joinedLine`. TS 만: 측점 누가(경계 = 측점 · 간격 배수) · 앞붙임 선(선 위 누가 = 등록 시작 누가) · 받은 지형 판정. ./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_Analyze as analyze # noqa: E402 TS_FILE = ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Plan.ts" TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" _RUNNER = """ import { readFileSync, writeFileSync } from "node:fs"; import * as plan from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Plan.js"; const job = JSON.parse(readFileSync(process.argv[2], "utf8")); const out = job.cases.map((c) => { const rows = plan.chainRows(job.results, c.choices, c.start); return { picks: job.results.map((r) => plan.pickMode(r, c.choices[r.year])), rows, line: plan.joinedLine(job.results, rows), stations: plan.stationChainages(rows, 20), }; }); const lead = plan.leadInLine([[0, 0], [3, 4], [10, 4]], 1234.5); const reach = plan.nodesInReach( [[500, 500], [900, 500], [1700, 500]], [{ x0: 0, y0: 0, x1: 1300, y1: 1000 }, null], ); writeFileSync(process.argv[3], JSON.stringify({ out, lead, reach })); """ _P0, _P1, _P2 = [0.0, 0.0], [300.0, 400.0], [900.0, 400.0] _RESULTS = [ { "year": 2026, "start": _P0, "end": _P1, "routes": { "gradeline": {"planned": [_P0, [100.0, 200.0], _P1], "metrics": {"length_m": 530.0}}, "earthwork": {"error": "길 없음"}, "gentle": {"planned": [_P0, _P1], "metrics": {"length_m": 500.004}}, }, }, { "year": 2027, "start": _P1, "end": _P2, "routes": { "gradeline": {"error": "길 없음"}, "gentle": {"planned": [_P1, [600.0, 500.0], _P2], "metrics": {"length_m": 640.333}}, "weighted": {"planned": [_P1, _P2], "metrics": {"length_m": 600.0}}, }, }, ] _CASES = [ {"choices": {}, "start": 1000.0}, {"choices": {"2026": "gentle", "2027": "weighted"}, "start": 17.25}, {"choices": {"2026": "earthwork", "2027": "follow"}, "start": 0.0}, ] 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") job = tmp_path / "job.json" result = tmp_path / "out.json" job.write_text(json.dumps({"results": _RESULTS, "cases": _CASES}), encoding="utf-8") subprocess.run( # noqa: S603 ["node", str(out / "runner.mjs"), str(job), str(result)], cwd=str(ROOT), check=True, capture_output=True, ) return json.loads(result.read_text(encoding="utf-8")) def test_ts_matches_python_chain(tmp_path: Path) -> None: got = _run_ts(tmp_path) for case, ts in zip(_CASES, got["out"]): choices = {int(k): v for k, v in case["choices"].items()} rows = analyze.chain(_RESULTS, choices, case["start"]) assert ts["picks"] == [analyze.pick_mode(r, choices.get(r["year"])) for r in _RESULTS] assert ts["rows"] == rows assert ts["line"] == analyze.joined_line(_RESULTS, rows) # 측점 = 경계(1530) · 20 배수 · 끝 — 첫 연도 시작 1000 · 끝 2170.333 stations = got["out"][0]["stations"] assert stations[:3] == [1000.0, 1020.0, 1040.0] assert 1530.0 in stations and 1520.0 in stations and 1540.0 in stations assert stations[-2:] == [2160.0, 2170.333] # 시작 17.25 → 첫 규칙 측점 20 assert got["out"][1]["stations"][:2] == [17.25, 20.0] def test_lead_in_and_reach(tmp_path: Path) -> None: got = _run_ts(tmp_path) back = got["lead"][0] assert abs(back[0] + 1234.5 * 0.6) < 1e-9 and abs(back[1] + 1234.5 * 0.8) < 1e-9 assert got["lead"][1:] == [[0, 0], [3, 4], [10, 4]] # 마디 0 = 첫 연도 안 · 마디 1 = 첫 연도 안 + 둘째 연도 DEM 없음 = 밖 · 마디 2 = 밖 assert got["reach"] == [True, False, False]