"""244-12 B08 거리이정표 다이어그램(사용자 142~144번) — ① 넉넉한 고정 바탕(공사지만 있어도 거래처 · 기준점 자리까지 · 넘는 상자가 생길 때만 넓힘) ② 시점 → 중점 선(공사지 바탕 위) · 시점 · 중점 상자 작은 글씨 주소 · 좌표(위도, 경도 소수 5자리) · 중점 주소 = 지금 중점 측점(?mid_m=) ③ 거래처 상자 = 항목 · 이름 · 주소(줄임표 + 호버). """ 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_Milestone_Calc import migrate_milestone # noqa: E402 IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) LONG = "경상남도 거창군 북상면 병곡리 968-31" def _read(rel): return (ROOT / rel).read_text(encoding="utf-8") def _node(expr): src = _read("B08_Quantity/B08_Quantity_Milestone_Layout.ts") result = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript", "-e", IMPORTS.sub("", 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 _route(rid, dir=None, via=True, address="", item=""): return {"id": rid, "name": rid, "address": address, "via_base": via, "dir": dir, "lon": None, "lat": None, "legs": [], "rows": [{"item": item, "dest": "start", "note": ""}]} # fmt: skip def _data(routes, base=False): return { "site": {"mid_station_m": None, "mid_road": "비포장"}, "base": {"enabled": base, "name": "", "address": "", "dir": None, "legs": []}, "routes": routes, } def _clipped(note, box_w): # 줄임표 · 안쪽 여백(양쪽 8) 안 · 앞부분 그대로 assert note.endswith("…") and LONG.startswith(note[:-1]) assert _node(f'textW("{note}", 10)') <= box_w - 16 def _lay(data, site=None): data = migrate_milestone(data) # 254 옛 꼴 → bases · to(옛 기준점 = b1) return _node(f"layoutMilestone({json.dumps(data, ensure_ascii=False)}, {json.dumps(site)}, 1)") def test_바탕_고정_범위_넘을_때만_넓힘(): base_view = _node("BASE_VIEW") assert _lay(_data([]))["view"] == base_view # 공사지만 있어도 넉넉한 바탕 vx, vy, vw, vh = base_view assert vw >= 900 and vh >= 800 # 공사지 둘레 8방 거래처 · 기준점(서) — 바탕 그대로(다시 줌아웃 없음) dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] for lay in (_lay(_data([_route(d, d, via=False) for d in dirs])), _lay(_data([], base=True))): assert lay["view"] == base_view for b in lay["boxes"]: assert vx <= b["x"] and b["x"] + b["w"] <= vx + vw assert vy <= b["y"] and b["y"] + b["h"] <= vy + vh # 기준점 너머 거래처 = 바탕 넘음 → 그쪽만 넓힘 lay = _lay(_data([_route("far", "W")], base=True)) x, y, w, h = lay["view"] far = next(b for b in lay["boxes"] if b["id"] == "far") assert x < vx and x <= far["x"] - 48 and x + w == vx + vw and (y, h) == (vy, vh) def test_시점_중점_선_상자_주소_좌표(): site = {"start_lonlat": [127.812345678, 35.7], "mid_lonlat": [127.9, 35.712341], "start_address": LONG, "mid_address": None} # fmt: skip lay = _lay(_data([]), site) boxes = {b["id"]: b for b in lay["boxes"]} note, coord = boxes["start"]["notes"] _clipped(note, boxes["start"]["w"]) assert coord == "35.70000, 127.81235" assert boxes["start"]["tip"] == LONG # 호버 = 줄이지 않은 주소 assert boxes["mid"]["notes"] == ["35.71234, 127.90000"] # 주소 없음 = 좌표만 assert _lay(_data([]))["boxes"][1]["notes"] == [] # 좌표도 없음 = 빈 줄 line = next(ln for ln in lay["lines"] if ln["from"] == "start") assert line["to"] == "mid" and line["labels"] == ["비포장\n1km"] # 254 두 줄 s, m = boxes["start"], boxes["mid"] assert m["x"] - (s["x"] + s["w"]) >= 100 # 선 글 자리 for b in (s, m): # 상자 = 공사지 안 site_box = boxes["site"] assert site_box["x"] <= b["x"] and b["x"] + b["w"] <= site_box["x"] + site_box["w"] assert site_box["y"] + 20 < b["y"] and b["y"] + b["h"] <= site_box["y"] + site_box["h"] def test_거래처_상자_항목_이름_주소(): lay = _lay(_data([_route("g", "N", via=False, address=LONG, item="장비"), _route("k", "E", via=False, address="거창읍")])) # fmt: skip boxes = {b["id"]: b for b in lay["boxes"]} assert (boxes["g"]["items"], boxes["g"]["label"]) == ("장비", "g") # 254 긴 주소 = 줄바꿈(낱말 사이) + 상자 세로 늘림(줄 × 15 + 13 · 최소 58) assert boxes["g"]["notes"] == ["경상남도 거창군 북상면", "병곡리 968-31"] assert boxes["g"]["h"] == 4 * 15 + 13 and boxes["k"]["h"] == 58 assert boxes["g"]["tip"] == LONG and boxes["k"]["notes"] == ["거창읍"] assert _node('clip("가나다", 100, 10)') == "가나다" assert _node(f'clip("{LONG}", 108, 10)') == "경상남도 거창군 북상…" # 한글 10 · 빈칸 6 · … 6 def test_화면_선_순서_글_줄_중점_주소(): ui = _read("B08_Quantity/B08_Quantity_UI_Milestone_Diagram.ts") # 선 = 공사지 바탕 뒤 · 다른 상자 앞(시점 → 중점 선 보임) assert 'if (box.kind === "site") shift.append(lineLayer);' in ui assert '...(box.notes ?? []).map((n): [string, string] => [n, "b08-milestone__note"])' in ui assert 'if (box.tip) g.append(svg("title", {}, box.tip));' in ui assert "siteAddress(projectId, data.site.mid_station_m ?? site?.mid_default_m ?? null)" in ui state = _read("B08_Quantity/B08_Quantity_UI_Milestone_State.ts") assert "site-address${query}" in state and "?mid_m=${midM}" in state assert ".b08-milestone__note {" in _read("B08_Quantity/B08_Quantity_UI_Milestone.css") for name in ("Milestone_Layout.ts", "UI_Milestone_Diagram.ts", "UI_Milestone_State.ts"): assert len(_read(f"B08_Quantity/B08_Quantity_{name}").splitlines()) <= 700 def test_중점_주소_지금_측점(tmp_path, monkeypatch): from fastapi import FastAPI from fastapi.testclient import TestClient async def root(_pid): return tmp_path async def project(_pid): return {"region": ""} async def where(lon, lat): return f"주소 {lon:.5f}" monkeypatch.setattr(ms, "_project_root", root) monkeypatch.setattr(ms, "get_project_by_id", project) monkeypatch.setattr(ms, "reverse_geocode", where) app = FastAPI() app.include_router(ms.router) client = TestClient(app) path = tmp_path / "B05_Profile" / "route" / "planned_route.csv" path.parent.mkdir(parents=True) path.write_text("sequence,x,y\n0,208000,367000\n1,209000,367000", encoding="utf-8") url = f"/api/projects/{uuid4()}/quantity/milestone/site-address" half = client.get(url).json() # 저장본 없음 = 기본(연장 ÷ 2) near = client.get(url, params={"mid_m": 100}).json() assert near["start_lonlat"] == half["start_lonlat"] assert near["start_lonlat"][0] < near["mid_lonlat"][0] < half["mid_lonlat"][0] assert near["mid_address"] == f"주소 {near['mid_lonlat'][0]:.5f}" assert client.get(url, params={"mid_m": -1}).status_code == 422 def _hits(a, b, gap=0): return (a["x"] < b["x"] + b["w"] + gap and b["x"] < a["x"] + a["w"] + gap and a["y"] < b["y"] + b["h"] + gap and b["y"] < a["y"] + a["h"] + gap) # fmt: skip def _seg_hits(x1, y1, x2, y2, r): t0, t1, dx, dy = 0.0, 1.0, x2 - x1, y2 - y1 for p, q in ((-dx, x1 - r["x"]), (dx, r["x"] + r["w"] - x1), (-dy, y1 - r["y"]), (dy, r["y"] + r["h"] - y1)): # fmt: skip if p == 0: if q < 0: return False continue t = q / p t0, t1 = (max(t0, t), t1) if p < 0 else (t0, min(t1, t)) if t0 > t1: return False return True def test_여유_상자_선_글_겹침_0(): """④(145번) — 몰린 꼴(기준점 거침 셋 같은 방위 · 방위 없음 둘 · 공사지 동쪽 셋 · 서쪽 = 기준점 자리).""" legs = [{"road": "2차포장", "km": 23.8}] routes = [{**_route(f"n{i}", "N", address=LONG, item="장비"), "legs": legs} for i in range(3)] routes += [{**_route(f"f{i}"), "legs": legs} for i in range(2)] routes += [{**_route(f"e{i}", "E", via=False), "legs": legs} for i in range(3)] routes += [{**_route("w", "W", via=False), "legs": legs}] # 기준점과 같은 방위 = 밀려남 data = _data(routes, base=True) data["base"]["legs"] = [{"road": "1차포장", "km": 0.6}, {"road": "비포장", "km": 0.8}] lay = _lay(data) gap = _node("GAP") boxes = lay["boxes"] inside = {("site", "start"), ("site", "mid")} for i, a in enumerate(boxes): # 상자 사이 여유(공사지 안 시점 · 중점만 품음) for b in boxes[i + 1 :]: if (a["id"], b["id"]) not in inside: assert not _hits(a, b, gap - 0.01), (a["id"], b["id"]) for ln in lay["lines"]: # 선 끝 = 어느 상자 안도 아님(바깥 → 시점 = 공사지 테두리에서 끊김) for x, y in ((ln["x1"], ln["y1"]), (ln["x2"], ln["y2"])): for b in boxes: if not (b["id"] == "site" and ln["from"] == "start"): assert not (b["x"] < x < b["x"] + b["w"] and b["y"] < y < b["y"] + b["h"]), ( ln["from"], ln["to"], b["id"], x, y, ) rects = {ln["from"]: _node(f"labelRect({json.dumps(ln, ensure_ascii=False)})") for ln in lay["lines"] if ln["labels"]} # fmt: skip for k, r in rects.items(): # 선 글 ↔ 다른 선 for o in lay["lines"]: if o["from"] != k: assert not _seg_hits(o["x1"], o["y1"], o["x2"], o["y2"], r), (k, o["from"]) keys = list(rects) for i, k in enumerate(keys): # 선 글 ↔ 상자 · 다른 선 글 for b in boxes: if not (b["kind"] == "site" and k == "start"): assert not _hits(rects[k], b), (k, b["id"]) for j in keys[i + 1 :]: assert not _hits(rects[k], rects[j]), (k, j)