Files
Aislo/resources/tester/test_254_2_b08_milestone_diagram.py
T
eomsangdonandClaude Opus 5.5 2584325b16 feat(B08): 거리이정표 다이어그램 — 기준점 연결 점 · 주소 줄바꿈 · 선 글 두 줄 · 거리 비례 선 길이(254-2)
- 연결 점 = 거래처 · 기준점 상자 왼쪽 위 안쪽 · 기준점 = 시점 · 다른 기준점에 놓음(돌기면 취소 + 알림)
- 기준점 상자 주소 · 주소 줄바꿈(최대 3줄) + 상자 높이 = 내용 · 항목 글 13px
- 선 글 = 구간마다 두 줄(도로 구분 / km) · 처음 자리 선 길이 = 100 + 90·log10(1 + km)(100 ~ 320)
- 밀어 냄 = 놓인 상자 · 놓인 선 둘 다 피함
- 시험 = test_254_2 새로 · 244 · 250 기대값(두 줄 · 짧아진 선) · 244_9 node 입력 = stdin

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
2026-10-11 14:07:14 +09:00

130 lines
5.8 KiB
Python

"""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;")]
# 왼쪽 위 안쪽 — 가운데 (10, 10) · 반지름 6 + 테두리 2 = 상자 안
assert "const cx = box.x + 10;" in dot and "const cy = box.y + 10;" in dot and "r: 6" 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