"""597-2 예정노선 지점 칸 손 고침 계산 — 이름 규칙 · 보이는 이름 · 경위도 읽기 · 더함 · 이름 바꿈 · 경위도로 옮김 (요구 463 7 · 8) — TS `logic/D03_PlanAnalysis_PointEdit.ts`. ./venv/Scripts/python.exe -m pytest resources/tester/test_597_point_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] TS_FILE = ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_PointEdit.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"; import * as pe from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_PointEdit.js"; import * as tmx from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Tm.js"; const Y = 2026; const tm = tmx.tmParams(5187); // 지점 (100,0) 에서 시작하는 선 · 지점 둘 let n = plan.newLine(plan.emptyNetwork(), [100, 0]); n = plan.normalizePieces(plan.appendVertex(n, 0, [200, 0]), Y); const state = { points: [[100, 0], [500, 500]], names: ["", "가"], network: n }; const named = pe.setLineName(pe.setLineName(n, n.lines[0].id, ' 본선A '), 'none', 'x'); const cleared = pe.setLineName(named, n.lines[0].id, ' '); const added = pe.addPoint(state, [7, 8]); const full = { points: Array.from({ length: 40 }, (_, i) => [i, 0]), names: [], network: n }; const [lon, lat] = tmx.tmToLonLat(tm, 100, 0); const moved = pe.movePointTo(state, 0, tm, lon + 0.001, lat); const back = tmx.tmToLonLat(tm, ...moved.points[0]); writeFileSync(process.argv[2], JSON.stringify({ errors: ["", "가나다", "a/b", "a" + String.fromCharCode(92) + "b", "a:b", "a*b", "a?b", 'a"b', "ab", "a|b", "x".repeat(20), "x".repeat(21)] .map((s) => plan.nameError(s)), labels: [pe.pointLabel(["", " ", "가"], 0), pe.pointLabel(["", " ", "가"], 1), pe.pointLabel(["", " ", "가"], 2), pe.pointLabel([], 3)], parse: [pe.parseLonLat("128.5", "36.9"), pe.parseLonLat("", "36.9"), pe.parseLonLat("128.5", "abc"), pe.parseLonLat("181", "0"), pe.parseLonLat("0", "-91")], added: [added.points, added.names, added.network === n], lineName: [named.lines[0].name, plan.lineNames(named.lines), cleared.lines[0].name, plan.lineNames(cleared.lines)], full: pe.addPoint(full, [1, 1]) === full, renamed: [pe.renamePoint(state, 1, " 나 ").names, pe.renamePoint({ ...state, names: [] }, 1, "다").names], moved: { points: moved.points, lines: moved.network.lines.map((l) => l.vertices), back, expect: [lon + 0.001, lat], untouched: moved.points[1], names: moved.names }, })); """ @pytest.fixture(scope="module") def got(tmp_path_factory: pytest.TempPathFactory) -> dict: tmp_path = tmp_path_factory.mktemp("pe") 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, ) # fmt: skip 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 test_name_rule(got: dict) -> None: ok, bad = got["errors"][:2], got["errors"][2:11] assert ok == [None, None] # 빈 이름 · 한글 = 됨 assert all(msg and "쓸 수 없습니다" in msg for msg in bad) # 파일 이름 못 쓰는 글자 아홉 assert got["errors"][11] is None # 20 자 assert "20 자" in got["errors"][12] # 21 자 = 막음 def test_label_blank_falls_back(got: dict) -> None: assert got["labels"] == ["지점 1", "지점 2", "가", "지점 4"] def test_parse_lonlat(got: dict) -> None: ok, no_lon, bad_lat, big_lon, big_lat = got["parse"] assert ok == [128.5, 36.9] assert no_lon.startswith("경도") and bad_lat.startswith("위도") assert big_lon.startswith("경도") and big_lat.startswith("위도") def test_add_and_limit(got: dict) -> None: points, names, network_same = got["added"] assert points == [[100, 0], [500, 500], [7, 8]] and names == ["", "가", ""] assert network_same # 더해도 노선 그대로 assert got["full"] # 40 개 = 그대로 def test_rename(got: dict) -> None: assert got["renamed"][0] == ["", "나"] # 앞뒤 빈칸 뺌 assert got["renamed"][1] == ["", "다"] # 이름 목록이 짧아도 차례 맞춤 def test_move_by_lonlat_drags_node(got: dict) -> None: moved = got["moved"] assert moved["back"] == pytest.approx(moved["expect"], abs=1e-7) # 경위도 왕복 assert moved["lines"][0][0] == moved["points"][0] # 그 자리 노선 노드도 같이 assert moved["lines"][0][1] == [200, 0] # 다른 노드는 그대로 assert moved["untouched"] == [500, 500] and moved["names"] == ["", "가"] def test_line_name(got: dict) -> None: named, names, cleared, back = got["lineName"] assert named == "본선A" and names == ["본선A"] # 앞뒤 빈칸 뺌 · 이름 우선 assert cleared is None and back == ["본선"] # 빈 값 = 차례 이름으로