"""254-2 B08 거리이정표 다이어그램(사용자 162-4 · 5 · 6 · 10) — 기준점 상자 연결 점 · 주소 · 연결 점 왼쪽 위 안쪽 · 항목 글 = 거래처 글 · 주소 줄바꿈 + 상자 높이 · 선 글 두 줄 · 선 길이 = 거리 비례(로그 · 최소 · 최대). """ import json import math import re import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[2] IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) B08 = "B08_Quantity/B08_Quantity_" LONG = "경상남도 거창군 북상면 병곡리 968-31" def _read(rel): return (ROOT / rel).read_text(encoding="utf-8") def _node(expr): src = IMPORTS.sub("", _read(B08 + "Milestone_Layout.ts")) result = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript", "-"], input=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(km): return {"road": None, "km": km} def _route(rid, dir, km, to="site", address=""): return {"id": rid, "name": rid, "address": address, "dir": dir, "to": to, "legs": [_leg(km)] if km is not None else [], "rows": [{"item": "장비", "dest": "start", "note": ""}]} # fmt: skip def _base(bid, to, km, address="", lon=None, lat=None): return {"id": bid, "name": bid, "address": address, "dir": None, "to": to, "lon": lon, "lat": lat, "legs": [_leg(km)], "linked": True} # fmt: skip def _data(bases, routes): return {"site": {"mid_station_m": None, "mid_road": "비포장"}, "bases": bases, "routes": routes} def _lay(data): return _node(f"layoutMilestone({json.dumps(data, ensure_ascii=False)}, null, 0.5)") def _boxes(lay): return {b["id"]: b for b in lay["boxes"]} def test_선_길이_거리_비례_최소_최대(): kms = [0, 1, 10, 23.8, 100, 1e6, -3] got = _node(f"{json.dumps(kms)}.map(lineLen)") want = [min(320, max(100, 100 + 90 * math.log10(1 + max(0, k)))) for k in kms] assert all(abs(a - b) < 1e-9 for a, b in zip(got, want)) assert got[0] == 100 and got[5] == 320 and got[6] == 100 # 최소 · 최대(너무 멀지 않게) assert got[0] < got[1] < got[2] < got[3] < got[4] # 동쪽 거래처 셋 — 공사지 테두리 → 상자 테두리 = 그 구간 선 길이 lay = _lay(_data([], [_route("a", "E", 1), _route("b", "S", 60), _route("c", "W", None)])) boxes = _boxes(lay) site = boxes["site"] a, b, c = boxes["a"], boxes["b"], boxes["c"] assert abs((a["x"]) - (site["x"] + site["w"]) - got[1]) < 1e-6 assert abs(b["y"] - (site["y"] + site["h"]) - _node("lineLen(60)")) < 1e-6 assert abs(site["x"] - (c["x"] + c["w"]) - 100) < 1e-6 # 빈 km = 최소 def test_기준점_사슬_자리_주소_높이(): bases = [_base("A", "B", 4.5, address=LONG), _base("B", "site", 0.6, address="거창읍")] lay = _lay(_data(bases, [_route("g", "N", 23.8, to="A")])) boxes = _boxes(lay) a, b, g = boxes["A"], boxes["B"], boxes["g"] assert (a["kind"], b["kind"]) == ("base", "base") # 기준점 상자 = 주소(줄바꿈) · 높이 = 이름 + 주소 줄 assert a["notes"] == ["경상남도 거창군 북상면", "병곡리 968-31"] and a["tip"] == LONG assert a["h"] == 3 * 15 + 13 and b["notes"] == ["거창읍"] and b["h"] == 58 # B = 공사지 서쪽(좌표 없음) · A = B 둘레(B 에 이음) · g = A 북쪽 site = boxes["site"] assert b["x"] + b["w"] < site["x"] and a["x"] + a["w"] < b["x"] + b["w"] assert g["y"] + g["h"] < a["y"] lines = {(ln["from"], ln["to"]) for ln in lay["lines"]} assert {("g", "A"), ("A", "B"), ("B", "start"), ("start", "mid")} == lines # 아주 긴 주소 = 최대 3줄 · 끝 줄 줄임표 many = " ".join([LONG] * 4) rows = _node(f'wrap("{many}", 132, 10)') assert len(rows) == 3 and rows[-1].endswith("…") assert all(_node(f'textW("{r}", 10)') <= 132 for r in rows) def test_선_글_두_줄(): lay = _lay(_data([_base("B", "site", 0.6)], [_route("g", "N", 23.8, to="B")])) line = next(ln for ln in lay["lines"] if ln["from"] == "B") assert line["labels"] == ["1차포장\n0.6km"] or line["labels"] == ["도로 구분 고르기\n0.6km"] assert line["lh"] == 2 * 16 + 2 # 두 줄 높이 two = _node("legText([{road: '1차포장', km: 0.6}, {road: '비포장', km: null}])") assert two == ["1차포장\n0.6km", "비포장\n-km"] ui = _read(B08 + "UI_Milestone_Diagram.ts") assert 'for (const row of label.split("\\n"))' in ui # 줄마다 tspan assert "span.dataset.b08MilestoneLeg = `${ln.from}:${i}`;" in ui # 두 줄 다 그 구간 def test_연결_점_왼쪽_위_안쪽_기준점도_돌기_취소(): ui = _read(B08 + "UI_Milestone_Diagram.ts") dot = ui[ui.index("const connectDot") :] dot = dot[: dot.index("return dot;")] # 왼쪽 위 안쪽 — 가운데 (12, 12) · 반지름 9 + 테두리 2 = 상자 안(373) assert "const cx = box.x + 12;" in dot and "const cy = box.y + 12;" in dot and "r: 9" in dot assert ( 'if (box.kind === "route" || box.kind === "base") g.append(connectDot(box, shift));' in ui ) assert 'if (box.kind === "base" && makesLoop(fresh, box.id, t))' in dot assert "if (id === box.id) return null;" in dot # 자기 = 취소 assert "route.to = t;" in dot and "route.linked = true;" in dot css = _read(B08 + "UI_Milestone.css") items = css[css.index(".b08-milestone__items {") :] assert "font-size: 13px;" in items[: items.index("}")] # 항목 = 거래처 글 assert "items: 13" in _read(B08 + "Milestone_Layout.ts") for name in ("Milestone_Layout.ts", "UI_Milestone_Diagram.ts"): assert len(_read(B08 + name).splitlines()) <= 700, name