"""244-2 B08 거리이정표 다이어그램 — 방위 자리(8방 · 경위도 방위각 · 빈 방위 차례 · 같은 방위 나란히) · 선 글(도로 구분 km) · 상자 누름 · [+] 항목 추가 길. 자리 계산 = `B08_Quantity_Milestone_Layout.ts` 를 node 로 돌림(화면 = 북 위 · SVG y 아래 = 남). """ 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 B08_Quantity.B08_Quantity_Milestone_Calc import migrate_milestone # noqa: E402 IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) def _layout(data, site=None, mid_km=0.75): data = migrate_milestone(data) # 254 옛 꼴(base · via_base) → bases · to(옛 기준점 = b1) src = (ROOT / "B08_Quantity" / "B08_Quantity_Milestone_Layout.ts").read_text(encoding="utf-8") body = ( f"\nconst d = {json.dumps(data, ensure_ascii=False)};" f"\nconsole.log(JSON.stringify(layoutMilestone(d, {json.dumps(site)}, {mid_km})));" ) result = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript", "-e", IMPORTS.sub("", src) + body], capture_output=True, text=True, encoding="utf-8", timeout=60, ) # fmt: skip assert result.returncode == 0, result.stderr return json.loads(result.stdout) def _route(rid, name, dir=None, via=True, legs=None, lon=None, lat=None): return {"id": rid, "name": name, "address": "", "via_base": via, "dir": dir, "lon": lon, "lat": lat, "legs": legs or [], "rows": [{"item": "", "dest": "start", "note": ""}]} # fmt: skip def _data(routes, base=None): return { "site": {"mid_station_m": None, "mid_road": "비포장"}, "base": base or {"enabled": False, "name": "", "address": "", "dir": None, "legs": []}, "routes": routes, } def _center(layout, bid): box = next(b for b in layout["boxes"] if b["id"] == bid) return box["x"] + box["w"] / 2, box["y"] + box["h"] / 2 def test_공사지_컨테이너_시점_중점_선(): lay = _layout(_data([])) kinds = {b["id"]: b["kind"] for b in lay["boxes"]} assert kinds == {"site": "site", "start": "start", "mid": "mid"} sx, _ = _center(lay, "start") mx, _ = _center(lay, "mid") assert sx < mx # 시점 왼 · 중점 오른 · 한 컨테이너 안 site = next(b for b in lay["boxes"] if b["id"] == "site") for bid in ("start", "mid"): b = next(x for x in lay["boxes"] if x["id"] == bid) assert site["x"] <= b["x"] and b["x"] + b["w"] <= site["x"] + site["w"] assert lay["lines"] == [ {**lay["lines"][0], "from": "start", "to": "mid", "labels": ["비포장\n0.75km"]} # 254 두 줄 ] def test_8방_자리_기준점_없음_공사지_기준(): lay = _layout( _data([_route("e", "동쪽", "E"), _route("n", "북쪽", "N"), _route("sw", "남서", "SW")]) ) cx, cy = _center(lay, "site") ex, ey = _center(lay, "e") nx, ny = _center(lay, "n") wx, wy = _center(lay, "sw") # 254 선 길이 = km 비례(빈 km = 최소 100) — 공사지 테두리(가로 190 · 세로 53)에서 잼 assert ex > cx + 200 and abs(ey - cy) < 1 assert ny < cy - 150 and abs(nx - cx) < 1 assert wx < cx and wy > cy line = next(ln for ln in lay["lines"] if ln["from"] == "e") assert line["to"] == "start" # 기준점 없으면 시점으로 def test_기준점_기준_방위_선_글(): base = {"enabled": True, "name": "병곡리 968-31", "address": "", "dir": "W", "legs": [{"road": "1차포장", "km": 0.6}, {"road": "비포장", "km": 0.8}]} # fmt: skip lay = _layout( _data( [ _route("g", "거창군청", "N", legs=[{"road": "2차포장", "km": 23.8}]), _route("k", "김천역", "E", via=False), ], base, ), # fmt: skip ) cx, cy = _center(lay, "site") bx, by = _center(lay, "b1") assert bx < cx - 200 and abs(by - cy) < 1 # 기준점 = 공사지 서쪽 gx, gy = _center(lay, "g") assert abs(gx - bx) < 1 and gy < by - 200 # 거창군청 = 기준점 북쪽 kx, _ = _center(lay, "k") assert kx > cx + 200 # 기준점 안 거침 = 공사지 동쪽 labels = {(ln["from"], ln["to"]): ln["labels"] for ln in lay["lines"]} assert labels[("g", "b1")] == ["2차포장\n23.8km"] # 254 두 줄(도로 구분 / km) assert labels[("b1", "start")] == ["1차포장\n0.6km", "비포장\n0.8km"] assert labels[("k", "start")] == [] assert next(b for b in lay["boxes"] if b["id"] == "b1")["label"] == "병곡리 968-31" def test_경위도_있으면_방위각이_8방보다_앞섬(): site = {"start_lonlat": [127.8, 35.7], "mid_lonlat": None} lay = _layout(_data([_route("e", "동쪽 좌표", "N", lon=127.9, lat=35.7)]), site) cx, cy = _center(lay, "site") ex, ey = _center(lay, "e") assert ex > cx + 200 and abs(ey - cy) < 1 # 고른 방위 N 보다 좌표(동) 먼저 def test_빈_방위_차례_같은_방위_나란히(): lay = _layout( _data( [_route("a", "가", "E"), _route("b", "나", "E"), _route("c", "다"), _route("d", "라")] ) ) (ax, ay), (bx, by) = _center(lay, "a"), _center(lay, "b") assert abs(ax - bx) < 1 and abs(ay - by) >= 60 # 같은 방위 = 옆으로 나란히 cx, cy = _center(lay, "c") dx, dy = _center(lay, "d") sx, sy = _center(lay, "site") assert cy < sy - 150 and abs(cx - sx) < 1 # 고르지 않음 = 빈 방위 첫째(북) assert (dx, dy) != (cx, cy) and dx < sx # 다음 빈 방위(북서) boxes = [b for b in lay["boxes"] if b["kind"] == "route"] for i, p in enumerate(boxes): # 업체 상자끼리 안 겹침 for q in boxes[i + 1 :]: assert ( p["x"] + p["w"] <= q["x"] or q["x"] + q["w"] <= p["x"] or p["y"] + p["h"] <= q["y"] or q["y"] + q["h"] <= p["y"] ) # fmt: skip def test_화면_상자_누름_더하기_창(): ui = (ROOT / "B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts").read_text(encoding="utf-8") # 244-4 — 상자 누름 = 모달(탭 이동 아님) assert ( "openTab" not in ui and "route: (id) => openPlaceModal(projectId, { add: false, id })," in ui ) # 155 공사지 · 기준점 [+] 없앰 → 다이어그램 [요소 추가] assert "plus(box" not in ui and 'label: "요소 추가"' in ui assert "openPlaceModal(projectId, { add: true }, addRoute)" in ui modal = (ROOT / "B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts").read_text(encoding="utf-8") assert "openModal({" in modal # 149 상자 하나 = 항목 하나 — [+ 줄] · 같은 이름 합침 없음 assert "same.rows.push" not in modal and '"+ 줄"' not in modal page = (ROOT / "B08_Quantity/B08_Quantity_UI_Milestone.ts").read_text(encoding="utf-8") assert "createDiagram(projectId, (init) => addRoute(projectId, init))" in page assert "children: [diagram.root, none, table.root]" in page and "diagram.refresh();" in page for name in ("Milestone_Layout.ts", "UI_Milestone_Diagram.ts"): assert ( len((ROOT / f"B08_Quantity/B08_Quantity_{name}").read_text("utf-8").splitlines()) <= 700 )