- 새 파일 = 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
197 lines
9.5 KiB
Python
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
|