"""254-1 B08 거리이정표 여러 기준점(사용자 162-10) — `bases[]` · 경로 `to` · 옛 꼴 옮김 · 기준점 사슬 셈(파이썬 · TS 거울) · 돌기 막음 · 도로 구분(시점에 닿는 기준점 구간 = 1차 · 그 밖 = 2차). 견본 사슬 = 거창군청 → 기준점 A(병곡리) → 기준점 B(신원) → 시점 · 김천역 → B → 시점 · 철근 상회 → 시점(직접). """ import json import re import subprocess import sys from pathlib import Path from uuid import uuid4 ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B08_Quantity import B08_Quantity_Router_Milestone as ms # noqa: E402 from B08_Quantity.B08_Quantity_Haul import with_haul_routes # noqa: E402 from B08_Quantity.B08_Quantity_Milestone_Calc import ( # noqa: E402 base_chain, compute_milestone, migrate_milestone, ) IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) B08 = "B08_Quantity/B08_Quantity_" def _read(rel): return (ROOT / rel).read_text(encoding="utf-8") def _node(files, expr): src = "\n".join(IMPORTS.sub("", _read(f)) for f in files) result = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript", "-e", src + f"\nconsole.log(JSON.stringify({expr}));"], capture_output=True, text=True, encoding="utf-8", timeout=60, ) # fmt: skip assert result.returncode == 0, result.stderr return json.loads(result.stdout) def _leg(road, km): return {"road": road, "km": km} def _base(bid, to, legs, linked=True): return {"id": bid, "name": bid, "address": "", "dir": None, "legs": legs, "to": to, "linked": linked} # fmt: skip def _route(rid, to, legs, dest="start"): return {"id": rid, "name": rid, "address": "", "dir": None, "to": to, "legs": legs, "rows": [{"item": "장비", "dest": dest, "note": ""}]} # fmt: skip CHAIN = { "site": {"mid_station_m": 800, "mid_road": "비포장"}, "bases": [ _base("A", "B", [_leg(None, 4.5), _leg("비포장", 0.3)]), _base("B", "site", [_leg(None, 0.6)]), ], "routes": [ _route("g", "A", [_leg(None, 23.8)]), _route("k", "B", [_leg(None, 61.2)], dest="mid"), _route("c", "site", [_leg(None, 2.0)]), ], } OLD = { "site": {"mid_station_m": None, "mid_road": "비포장"}, "base": {"enabled": True, "name": "병곡리", "address": "주소", "dir": "E", "lon": 127.85, "lat": 35.66, "pos": [10, 20], "legs": [_leg("1차포장", 0.6)]}, "routes": [ {"id": "r1", "name": "거창군청", "address": "", "via_base": True, "dir": None, "legs": [_leg("2차포장", 23.8)], "rows": [{"item": "장비", "dest": "start", "note": ""}]}, {"id": "r2", "name": "김천역", "address": "", "via_base": False, "dir": None, "legs": [_leg(None, 5)], "rows": [{"item": "철근", "dest": "start", "note": ""}]}, ], } # fmt: skip def test_옛_꼴_옮김_거울(): new = migrate_milestone(OLD) assert "base" not in new and [b["id"] for b in new["bases"]] == ["b1"] b1 = new["bases"][0] assert (b1["to"], b1["linked"], b1["name"], b1["pos"]) == ("site", True, "병곡리", [10, 20]) assert b1["legs"] == [_leg("1차포장", 0.6)] and b1["lon"] == 127.85 assert [(r["id"], r["to"], "via_base" in r) for r in new["routes"]] == [ ("r1", "b1", False), ("r2", "site", False), ] # 꺼진 기준점 = bases 없음 · 거침 경로도 시점으로 off = migrate_milestone({**OLD, "base": {**OLD["base"], "enabled": False}}) assert off["bases"] == [] and [r["to"] for r in off["routes"]] == ["site", "site"] assert migrate_milestone(new) == new # 새 꼴 = 그대로 # TS 짝 — 옮긴 꼴 · 표 합이 같음 calc = [B08 + "Milestone_Calc.ts"] ts = _node(calc, f"migrateMilestone({json.dumps(OLD)})") assert ts["bases"][0]["id"] == "b1" and [r["to"] for r in ts["routes"]] == ["b1", "site"] assert ts["bases"][0]["legs"] == b1["legs"] and ts["bases"][0]["pos"] == [10, 20] assert _node(calc, f"computeMilestone({json.dumps(OLD)}, 1500)") == compute_milestone( OLD, 1500.0 ) # 옛 표 합 그대로 — 2차 23.8 + 1차 0.6 (a, b) = compute_milestone(OLD, 1500.0) assert (a["s2"], a["s1"], a["total"]) == (23.8, 0.6, 24.4) and (b["pv"], b["total"]) == (5, 5) def test_서버_읽기_받기_새_꼴(tmp_path, monkeypatch): from fastapi import FastAPI from fastapi.testclient import TestClient # 옛 파일 읽기 = 새 꼴(지우지 않음 · 저장 때 새 꼴) folder = tmp_path / "B08_Quantity" / "milestone" folder.mkdir(parents=True) (folder / "milestone.json").write_text(json.dumps(OLD, ensure_ascii=False), "utf-8") data = ms.read_data(tmp_path) assert [b["id"] for b in data["bases"]] == ["b1"] and data["routes"][0]["to"] == "b1" assert "base" not in data and "via_base" not in data["routes"][0] async def root(_pid): return tmp_path monkeypatch.setattr(ms, "_project_root", root) app = FastAPI() app.include_router(ms.router) client = TestClient(app) url = f"/api/projects/{uuid4()}/quantity/milestone" saved = client.put(url, json=OLD).json() # 옛 꼴 받기 = 새 꼴로 저장 stored = json.loads((folder / "milestone.json").read_text("utf-8")) assert "base" not in stored and stored["bases"][0]["id"] == "b1" assert stored["routes"][0]["to"] == "b1" and "via_base" not in stored["routes"][0] assert saved["table"] == compute_milestone(OLD, None) # 새 꼴 사슬 왕복 · 겹친 id · 공사지 상자 id = 거절 assert client.put(url, json=CHAIN).json()["data"]["bases"][0]["to"] == "B" twice = {**CHAIN, "bases": [*CHAIN["bases"], _base("g", "site", [])]} assert client.put(url, json=twice).status_code == 422 box = {**CHAIN, "bases": [_base("site", "site", [])]} assert client.put(url, json=box).status_code == 422 # 운반 자동 상자 = 시점으로(안 이음) (auto,) = with_haul_routes({"routes": []}, [{"key": "1", "name": "골재"}])["routes"] assert auto["to"] == "site" and auto["linked"] is False and "via_base" not in auto def test_기준점_둘_사슬_셈_거울(): g, k, c = compute_milestone(CHAIN, None) # 거래처 → A = 2차 · A → B(기준점 → 기준점) = 2차 · B → 시점 = 1차 · 비포장은 사용자 값 assert (g["s2"], g["s1"], g["pv"], g["un"]) == (28.3, 0.6, 0, 0.3) assert (g["paved"], g["unpaved"], g["total"]) == (28.9, 0.3, 29.2) # 김천역 → B = 2차 · B → 시점 = 1차 · 목적지 중점 = 시점 → 중점 0.8 비포장 assert (k["s2"], k["s1"], k["un"], k["total"]) == (61.2, 0.6, 0.8, 62.6) # 시점으로 바로 = 포장 assert (c["s2"], c["s1"], c["pv"], c["total"]) == (0, 0, 2.0, 2.0) calc = [B08 + "Milestone_Calc.ts"] assert _node(calc, f"computeMilestone({json.dumps(CHAIN)}, null)") == [g, k, c] # 안 이은 기준점에서 사슬 멈춤(그 구간 · 뒤 구간 안 셈) cut = {**CHAIN, "bases": [CHAIN["bases"][0], {**CHAIN["bases"][1], "linked": False}]} g2, k2, _ = compute_milestone(cut, None) assert (g2["s2"], g2["s1"], g2["un"], g2["total"]) == (28.3, 0, 0.3, 28.6) assert (k2["s2"], k2["total"]) == (61.2, 62.0) assert _node(calc, f"computeMilestone({json.dumps(cut)}, null)") == compute_milestone(cut, None) # 선 자리 — 시점에 닿는 기준점 = base(1차) · 기준점 → 기준점 · 거래처 → 기준점 = via(2차) kinds = _node( calc, f"['g', 'A', 'B', 'c', 'start'].map((f) => segKind({json.dumps(CHAIN)}, f))" ) assert kinds == ["via", "via", "base", "direct", "mid"] def test_돌기_막음(): loop = {**CHAIN, "bases": [_base("A", "B", [_leg(None, 1)]), _base("B", "A", [_leg(None, 2)])]} # 셈 = 방문 집합으로 끊음(A → B → A 멈춤 · 끝없이 안 돎) assert [b["id"] for b in base_chain(loop, "A")] == ["A", "B"] (g, *_) = compute_milestone(loop, None) assert g["total"] == 26.8 calc = [B08 + "Milestone_Calc.ts"] assert _node(calc, f"computeMilestone({json.dumps(loop)}, null)") == compute_milestone( loop, None ) self_loop = {**CHAIN, "bases": [_base("A", "A", [_leg(None, 1)])]} assert [b["id"] for b in base_chain(self_loop, "A")] == ["A"] # 연결 놓을 때 = 돌기면 취소(makesLoop) — A → B 사슬에서 B 를 A 에 이으면 돎 layout = [B08 + "Milestone_Layout.ts"] d = json.dumps(CHAIN) got = _node( layout, f"[makesLoop({d}, 'B', 'A'), makesLoop({d}, 'A', 'B'), makesLoop({d}, 'B', 'site')," f" makesLoop({d}, 'A', 'A'), chainOf({d}, 'A').map((b) => b.id)," f" kmSegments({d}, 'g', [127.8, 35.6]).map((s) => s.text)]", ) assert got[:4] == [True, False, False, True] assert got[4] == ["A", "B"] and got[5] == ["g → A", "A → B", "B → 시점"] def test_화면_기준점_여럿_등록_삭제(): modal = _read(B08 + "UI_Milestone_Modal.ts") assert "fresh.bases.push({" in modal and 'to: "site",' in modal assert "기준위치(있음" not in modal # 254 기준점 여럿 — 고르기 막음 없음 # 지운 기준점에 이은 상자 = 안 이음 assert "if (o.to === base.id)" in modal state = _read(B08 + "UI_Milestone_State.ts") assert "migrateMilestone(" in state and "bases: []," in state page = _read(B08 + "UI_Milestone.ts") assert 'to: "site",' in page assert "data.bases.length > 0" in _read(B08 + "Milestone_Table.ts") # 302-3 표로 옮김 for name in ("Milestone_Calc.ts", "Milestone_Layout.ts", "UI_Milestone_Diagram.ts", "UI_Milestone_Modal.ts", "UI_Milestone_State.ts", "UI_Milestone_LegModal.ts", "UI_Milestone_Route.ts", "UI_Milestone.ts"): # fmt: skip text = _read(B08 + name) assert "data.base." not in text and ".via_base" not in text and ".base.legs" not in text