Files
Aislo/resources/tester/test_244_1_b08_milestone.py
T
eomsangdonandClaude Opus 5.5 5e25f418cc refactor(b08): 탭 표 자료 읽기 · 표 짜기를 화면 없는 파일로 꺼냄 — 구조물 집계표 · 자재 · 토공 · 수리 입력 · 거리이정표 · 화면은 같은 함수를 씀 · 동작 그대로 (302-3)
- 새 파일 = B08_Quantity_Summary_Table · Material_Tables · Earthwork_Tables · Hydro_Table · Milestone_Table
- 화면 UI 파일 = 불러오기 · 그리기만 · 옛 B08 시험 = 옮긴 글 자리 따라 읽는 파일 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
2026-10-13 08:03:23 +09:00

197 lines
9.5 KiB
Python

"""244-1 B08 거리이정표 — 표 합 거울(파이썬 · TS) · 저장 왕복(임시 storage) · 그림 ·
하위 탭 추가 · 삭제 · 중점 기본.
견본 = 실무 「거리이정표」 시트(거창군청 → 병곡리 968-31 → 시점 · 김천역 → 임도중점)
장비 = 2차 23.8 + 1차 0.6 + 비포장 0.8 → 포장 24.4 · 비포장 0.8 · 합계 25.2.
"""
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 compute_milestone # noqa: E402
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
def _leg(road, km):
return {"road": road, "km": km}
SAMPLE = {
"site": {"mid_station_m": None, "mid_road": "비포장"},
"base": {"enabled": True, "name": "병곡리 968-31", "address": "", "dir": "E",
"legs": [_leg("1차포장", 0.6), _leg("비포장", 0.8)]},
"routes": [
{"id": "r1", "name": "거창군청", "address": "", "via_base": True, "dir": "W",
"legs": [_leg("2차포장", 23.8)],
"rows": [{"item": "장비(20톤)", "dest": "start", "note": ""},
{"item": "장비(8톤)", "dest": "start", "note": "같은 길"}]},
{"id": "r2", "name": "김천역", "address": "", "via_base": False, "dir": "N",
"legs": [_leg("2차포장", 61.2), _leg("1차포장", 2.05), _leg("비포장", None)],
"rows": [{"item": "철근", "dest": "mid", "note": ""}]},
],
} # fmt: skip
def _run_ts(data, mid_default):
calc = ROOT / "B08_Quantity" / "B08_Quantity_Milestone_Calc.ts"
body = (
f"\nconst d = {json.dumps(data, ensure_ascii=False)};"
f"\nconsole.log(JSON.stringify(computeMilestone(d, {json.dumps(mid_default)})));"
)
script = IMPORTS.sub("", calc.read_text(encoding="utf-8")) + body
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", script],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def test_견본_표_합():
rows = compute_milestone(SAMPLE, 1500.0)
a, b, c = rows
assert (a["s2"], a["s1"], a["un"]) == (23.8, 0.6, 0.8)
assert (a["paved"], a["unpaved"], a["total"]) == (24.4, 0.8, 25.2)
assert b["item"] == "장비(8톤)" and b["total"] == 25.2 and b["company"] == "거창군청"
# 기준점 안 거침 = 거래처 → 시점 = 포장(152 · 옛 2차 · 1차 값도 자리로) · 목적지 중점 = 시점 → 중점
# (기본 1500 ÷ 1000) 비포장 더함 · 빈 km = 0
assert (c["s2"], c["s1"], c["pv"], c["un"], c["total"]) == (0, 0, 63.25, 1.5, 64.75)
# 사용자 중점 측점 · 기준점 없음 = 공통 구간 안 셈
no_base = {**SAMPLE, "base": {**SAMPLE["base"], "enabled": False},
"site": {"mid_station_m": 420, "mid_road": "1차포장"}} # fmt: skip
a2, _, c2 = compute_milestone(no_base, 1500.0)
# 기준점 없음 = 2차 · 1차 없이 포장(152) · 시점 → 중점 1차포장 값 = 포장
assert a2["total"] == 23.8 and (a2["s2"], a2["pv"]) == (0, 23.8)
assert (c2["s1"], c2["pv"], c2["un"]) == (0, 63.67, 0)
def test_거울_TS():
for mid in (1500.0, None):
assert _run_ts(SAMPLE, mid) == compute_milestone(SAMPLE, mid)
odd = {**SAMPLE, "site": {"mid_station_m": 1234.5678, "mid_road": "비포장"}}
assert _run_ts(odd, 10.0) == compute_milestone(odd, 10.0)
def _route_csv(root: Path, points):
path = root / "B05_Profile" / "route" / "planned_route.csv"
path.parent.mkdir(parents=True)
lines = ["sequence,x,y"] + [f"{i},{x},{y}" for i, (x, y) in enumerate(points)]
path.write_text("\n".join(lines), encoding="utf-8")
def test_중점_기본_노선_절반(tmp_path):
assert ms.site_info(tmp_path, None)["mid_default_m"] is None # 노선 전 = 빈칸
_route_csv(tmp_path, [(208000, 367000), (209000, 367000), (209000, 367500)])
site = ms.site_info(tmp_path, None)
assert site["length_m"] == 1500 and site["mid_default_m"] == 750
lon, lat = site["start_lonlat"]
assert 124 < lon < 132 and 33 < lat < 39 # 작업 좌표계(폴백 5186) → 경위도
assert site["mid_lonlat"] != site["start_lonlat"]
def _client(tmp_path, monkeypatch):
from fastapi import FastAPI
from fastapi.testclient import TestClient
async def root(_project_id):
return tmp_path
monkeypatch.setattr(ms, "_project_root", root)
app = FastAPI()
app.include_router(ms.router)
return TestClient(app), f"/api/projects/{uuid4()}/quantity/milestone"
def test_저장_왕복_그림(tmp_path, monkeypatch):
client, base = _client(tmp_path, monkeypatch)
empty = client.get(base).json()
assert empty["data"]["routes"] == [] and empty["table"] == [] and empty["images"] == {}
_route_csv(tmp_path, [(0, 0), (3000, 0)])
saved = client.put(base, json=SAMPLE).json()
assert saved["data"] == ms.Milestone.model_validate(SAMPLE).model_dump()
assert saved["table"] == compute_milestone(SAMPLE, 1500.0) # 서버가 다시 셈
stored = json.loads((tmp_path / "B08_Quantity/milestone/milestone.json").read_text("utf-8"))
assert stored["routes"][0]["name"] == "거창군청" and "table" not in stored
assert client.get(base).json() == saved
# 그림 — 경로 하나 = 그림 하나(교체) · 내려받기 · 지우기
img = base + "/image"
up = client.post(img, params={"route": "r1"}, files={"file": ("a.PNG", b"png1", "image/png")})
assert up.json() == {"name": "r1.png"}
client.post(img, params={"route": "r1"}, files={"file": ("b.jpg", b"jpg2", "image/jpeg")})
assert client.get(base).json()["images"] == {"r1": "r1.jpg"}
assert client.get(img, params={"route": "r1"}).content == b"jpg2"
assert (
client.post(img, params={"route": "r1"}, files={"file": ("x.pdf", b"1")}).status_code == 400
)
assert client.get(img, params={"route": "../x"}).status_code == 400
client.post(img, params={"route": "r2"}, files={"file": ("c.png", b"png3", "image/png")})
client.delete(img, params={"route": "r2"})
assert client.get(img, params={"route": "r2"}).status_code == 404
# 경로 삭제 뒤 [저장] = 그 경로 그림도 지움
client.post(img, params={"route": "r2"}, files={"file": ("c.png", b"png3", "image/png")})
one = {**SAMPLE, "routes": SAMPLE["routes"][:1]}
assert client.put(base, json=one).json()["images"] == {"r1": "r1.jpg"}
# 겹친 id · 나쁜 값 = 거절
twice = {**SAMPLE, "routes": [SAMPLE["routes"][0]] * 2}
assert client.put(base, json=twice).status_code == 422
bad = {**SAMPLE, "site": {"mid_station_m": -1, "mid_road": "비포장"}}
assert client.put(base, json=bad).status_code == 422
def _read(rel):
return (ROOT / rel).read_text(encoding="utf-8")
def test_메인_탭_하위_탭_추가_삭제():
page = _read("B08_Quantity/B08_Quantity_UI_Page.ts")
assert '{ id: "milestone", label: "B08_Quantity_Tab_Milestone" }' in page
assert 'if (tab === "milestone" && projectId) return renderMilestone(view, projectId);' in page
assert 'B08_Quantity_Tab_Milestone: ["거리이정표"' in _read(
"ui_template/ui_template_locale_b4.ts"
)
ui = _read("B08_Quantity/B08_Quantity_UI_Milestone.ts")
assert 'tab("table", "거리이정표")' in ui and "routes.map((r, i) => tab(r.id," in ui
# 244-4 — 하위 탭 줄 [+ 추가] · [삭제] 없음(탭은 [+] 등록 · 항목 정보 [삭제]로만)
assert 'label: "+ 추가"' not in ui and "removeCurrent" not in ui
modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts")
assert "fresh.routes = fresh.routes.filter((r) => r.id !== route!.id);" in modal
assert "editOnDouble: true" in ui and "milestoneTable(data, siteInfo()" in ui
assert "computeMilestone(data, midDefaultM)" in _read(
"B08_Quantity/B08_Quantity_Milestone_Table.ts"
)
route = _read("B08_Quantity/B08_Quantity_UI_Milestone_Route.ts")
assert "createDataTable" not in route and "uploadImage(projectId, routeId" in route
# 조작 = 캐시 · [저장] = 좌측 패널 한 줄에서 PUT · 자동 저장 없음
state = _read("B08_Quantity/B08_Quantity_UI_Milestone_State.ts")
assert 'writeState("b08-milestone"' in state and 'clearState("b08-milestone"' in state
assert state.count('method: "PUT"') == 1 and "setTimeout" not in state
options = _read("B08_Quantity/B08_Quantity_UI_Options.ts")
assert 'card("milestone"' not in options and "milestoneCardBody" not in options # 244-6
assert "await saveMilestone(projectId);" in options
registry = _read("A00_Common/b_page_state.ts")
assert '"b08-milestone": { bucket: "draft", scope: "project" }' in registry
main = _read("main.py")
assert "app.include_router(b08_milestone_router, dependencies=protected_with_company)" in main
# 짝 표기
assert "짝 = `B08_Quantity_Milestone_Calc.ts`" in _read(
"B08_Quantity/B08_Quantity_Milestone_Calc.py"
)
assert "짝 = `B08_Quantity_Milestone_Calc.py`" in _read(
"B08_Quantity/B08_Quantity_Milestone_Calc.ts"
)
for name in ("UI_Milestone.ts", "UI_Milestone_Route.ts", "UI_Milestone_State.ts",
"Router_Milestone.py"): # fmt: skip
rel = f"B08_Quantity/B08_Quantity_{name}"
assert len(_read(rel).splitlines()) <= 700