Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
190 lines
8.1 KiB
Python
190 lines
8.1 KiB
Python
"""302-3 인쇄 미리보기 「수량산출」 — B08 탭 표를 화면 없이 PrintSheet 로 모음.
|
||
|
||
견본 = 검증_sub8 서버 GET 응답(구조물 집계표만 손 견본 · `fixtures/b08_302_3_print_quantity.json`).
|
||
`helper_302_3_print_quantity.mjs` 가 화면 코드를 node 로 묶어 같은 응답 · 같은 캐시로
|
||
모으기(`collectQuantitySheets`) 와 B08 화면이 부르는 표 짜기를 나란히 냄 → 쪽 차례 · 이름 · 열 머리 · 줄 대조.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import base64
|
||
import copy
|
||
import json
|
||
import shutil
|
||
import subprocess
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
HELPER = Path(__file__).with_name("helper_302_3_print_quantity.mjs")
|
||
FIXTURE = json.loads(
|
||
(Path(__file__).with_name("fixtures") / "b08_302_3_print_quantity.json").read_text("utf-8")
|
||
)
|
||
PNG = b"\x89PNG\r\n\x1a\n302-3"
|
||
B08 = ROOT / "B08_Quantity"
|
||
|
||
|
||
def _run(responses: dict, drafts: list | None = None, screen: bool = True) -> dict:
|
||
if shutil.which("node") is None:
|
||
pytest.skip("node 가 없음")
|
||
job = {
|
||
"project": "p302",
|
||
"responses": responses,
|
||
"image_b64": base64.b64encode(PNG).decode(),
|
||
"drafts": drafts or [],
|
||
"screen": screen,
|
||
}
|
||
out = subprocess.run(
|
||
["node", str(HELPER), str(ROOT)],
|
||
input=json.dumps(job, ensure_ascii=False),
|
||
cwd=ROOT,
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
timeout=300,
|
||
check=True,
|
||
)
|
||
return json.loads(out.stdout.strip().splitlines()[-1])
|
||
|
||
|
||
def _responses() -> dict:
|
||
data = copy.deepcopy(FIXTURE)
|
||
routes = data["/quantity/milestone"]["data"]["routes"]
|
||
# 경로 그림 둘 — 사용자 그림 · 서버 캡처(auto) 꼴
|
||
data["/quantity/milestone"]["images"] = {
|
||
routes[0]["id"]: "a.png",
|
||
routes[2]["id"]: "auto/b.png",
|
||
}
|
||
return data
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def base() -> dict:
|
||
return _run(_responses())
|
||
|
||
|
||
def _same(sheet: dict, screen: dict) -> None:
|
||
for key in ("heads", "rows", "total", "mergeSame", "frozen"):
|
||
assert sheet[key] == screen[key], (sheet["id"], key)
|
||
|
||
|
||
def test_쪽_차례_이름(base):
|
||
stations = FIXTURE["/quantity/hydro"]["stations"]
|
||
names = [s["name"] for s in base["sheets"]]
|
||
head = ["구조물 집계표", "총괄자재수량집계표", "콘크리트소요자재집계표", "자재수량집계표"]
|
||
assert names[:4] == head
|
||
assert names[4:8] == base["screen"]["earthworkNames"] == [
|
||
"토공집계표", "토적표", "평균운반거리", "종방향 무대운반거리"
|
||
] # fmt: skip
|
||
hydro = names[8:-1]
|
||
assert len(hydro) == len(stations) == 11 # 배수지점 측점마다 한 장
|
||
assert all(n.startswith("수리집수면적표 ") for n in hydro)
|
||
kinds = [n for n, st in zip(hydro, stations) if st["kind"]]
|
||
assert kinds and all(n.endswith(" 세월교") for n in kinds)
|
||
assert names[-1] == "거리이정표"
|
||
assert len({s["id"] for s in base["sheets"]}) == len(names)
|
||
|
||
|
||
def test_표_짜기_B08_화면과_같음(base):
|
||
sheets = {s["id"]: s for s in base["sheets"]}
|
||
screen = base["screen"]
|
||
_same(sheets["summary"], screen["summary"])
|
||
for sid in ("material:material", "material:concrete", "material:quantity",
|
||
"earthwork:summary", "earthwork:sheet"): # fmt: skip
|
||
_same(sheets[sid], screen[sid])
|
||
# 운반 = 화면 표 + 아래 평균운반거리 줄(번호 칸 글)
|
||
for sid in ("earthwork:haul", "earthwork:free"):
|
||
want = screen[sid]
|
||
assert sheets[sid]["heads"] == want["heads"] and sheets[sid]["total"] == want["total"]
|
||
assert sheets[sid]["rows"] == want["rows"] + [{"no": a} for a in want["averages"]]
|
||
assert "평균운반거리" in want["averages"][0]
|
||
_same(sheets["milestone"], {**screen["milestone"]})
|
||
|
||
|
||
def test_견본_수치(base):
|
||
sheets = {s["id"]: s for s in base["sheets"]}
|
||
summary = sheets["summary"]
|
||
assert summary["heads"] == ["측점", "관 공›Φ800m/m›관매설›m", "관 공›Φ800m/m›커플링›ea"]
|
||
assert len(summary["rows"]) == 2 and summary["total"]["pipe:len|800"] == 20.5
|
||
sheet = sheets["earthwork:sheet"]
|
||
assert (
|
||
len(sheet["heads"]) == 35 and len(sheet["rows"]) == 8
|
||
) # 견본 32칸 + 측점 · 거리 · 보정량계
|
||
assert (
|
||
sheet["orientation"] == "landscape"
|
||
and sheets["earthwork:summary"]["orientation"] == "portrait"
|
||
)
|
||
assert sheets["material:concrete"]["rows"] and len(sheets["material:concrete"]["rows"]) == 5
|
||
hydro = next(s for s in base["sheets"] if s["id"].startswith("hydro:"))
|
||
assert hydro["heads"] == ["구분", "항목", "값"]
|
||
value = {r["item"]: r["value"] for r in hydro["rows"]}
|
||
calc = FIXTURE["/quantity/hydro/calc"]
|
||
assert value["Q : 홍수량 (㎥/s)"] == f"{calc['flow']:.2f}"
|
||
assert value["Qd : 설계 홍수량 (㎥/s) = Q × 2.0"] == f"{calc['design_flow']:.2f}"
|
||
assert value["R24 : 확률강우량 (mm/day)"] == f"{calc['r24_mm']:.1f}"
|
||
# 서버 계산 = 화면과 같은 입력(기본 < 장 기본 < 연결 < 저장 < 초안)
|
||
assert base["posts"] == base["screen"]["hydroInputs"]
|
||
|
||
|
||
def test_거리이정표_지도_그림(base):
|
||
milestone = base["sheets"][-1]
|
||
routes = FIXTURE["/quantity/milestone"]["data"]["routes"]
|
||
assert len(milestone["images"]) == 2
|
||
for image, route in zip(milestone["images"], (routes[0], routes[2])):
|
||
head, body = image["src"].split(",", 1)
|
||
assert head == "data:image/png;base64" and base64.b64decode(body) == PNG
|
||
assert image["caption"] == (route["name"].strip() or route["rows"][0]["item"])
|
||
|
||
|
||
def test_캐시_값이면_화면에_보이는_그_값():
|
||
responses = _responses()
|
||
stations = responses["/quantity/hydro"]["stations"]
|
||
key = f"{stations[0]['chainage_m']:.3f}"
|
||
milestone = copy.deepcopy(responses["/quantity/milestone"]["data"])
|
||
milestone["routes"][0]["name"] = "가나상회"
|
||
got = _run(responses, [["b08-hydro", {key: {"area_km2": 0.5}}], ["b08-milestone", milestone]])
|
||
assert got["posts"][0]["area_km2"] == 0.5 == got["screen"]["hydroInputs"][0]["area_km2"]
|
||
hydro = next(s for s in got["sheets"] if s["id"] == f"hydro:{key}")
|
||
assert any(r["value"].startswith("0.5 (") for r in hydro["rows"])
|
||
sheet = got["sheets"][-1]
|
||
assert any(r.get("company") == "가나상회" for r in sheet["rows"])
|
||
_same(sheet, got["screen"]["milestone"])
|
||
|
||
|
||
def test_영역_하나_실패_까닭_한_장():
|
||
responses = _responses()
|
||
del responses["/quantity/earthwork"]
|
||
got = _run(responses, screen=False)
|
||
earth = [s for s in got["sheets"] if s["id"].startswith("earthwork")]
|
||
assert len(earth) == 1 and earth[0]["name"] == "토공"
|
||
assert earth[0]["rows"][0]["note"].startswith("불러오지 못했습니다")
|
||
assert got["sheets"][0]["name"] == "구조물 집계표" and got["sheets"][-1]["name"] == "거리이정표"
|
||
|
||
|
||
def test_B08_화면이_같은_함수를_씀():
|
||
read = lambda name: (B08 / f"B08_Quantity_{name}.ts").read_text("utf-8") # noqa: E731
|
||
assert "summaryTable(view)" in read("UI_Summary") and "loadSummary(projectId)" in read(
|
||
"UI_Summary"
|
||
)
|
||
material = read("UI_Material")
|
||
assert "materialOptions(built)" in material and "loadMaterials(projectId)" in material
|
||
assert "earthworkSheetOptions(table, interval)" in read("UI_Earthwork")
|
||
sheets = read("UI_Earthwork_Sheets")
|
||
assert "earthworkSummaryOptions(table)" in sheets and "haulOptions(groups, caption)" in sheets
|
||
assert "hydroInputs(sheet, station, drafts)" in read("UI_Hydro")
|
||
assert "milestoneTable(data, siteInfo()" in read("UI_Milestone")
|
||
# 표 짜기 파일은 화면 없음
|
||
for name in (
|
||
"Summary_Table",
|
||
"Material_Tables",
|
||
"Earthwork_Tables",
|
||
"Hydro_Table",
|
||
"Milestone_Table",
|
||
):
|
||
text = read(name)
|
||
assert "document." not in text and "createDataTable" not in text, name
|
||
assert len(text.splitlines()) <= 700
|
||
printer = (ROOT / "B09_Estimation/draft/B09_Draft_Print_Quantity.ts").read_text("utf-8")
|
||
assert 'from "./B09_Draft_Print_Types"' in printer and "createDataTable" not in printer
|