- 서버 pipe_summary 응답 unlinked = 셈한 칸에 사본 없는 시설 · 칸 전부 + 연결 여부(몸 = Summary_Sheet) - 같은 측점 박스 + 관 = 종류로 가름 · 세월교 필수 잇기(gford · gfordin · gfordout) 칸 포함 - 화면 = 그 측점 줄 「· 미연결」 · 알림 글 「「미연결」 줄을 눌러 조합 연결」 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
225 lines
9.6 KiB
Python
225 lines
9.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""B08 구조물 집계표 — 조합 사본 없으면 없음(PLAN 143-1).
|
||
|
||
견본(관 지점 넷 + 세월교 둘 + 개거 하나):
|
||
100 관 Φ800 L16 · 사본 있음 · 유출 흙막이 잇기 사본 없음 → 관 칸만 · 유출 흙막이 칸 없음
|
||
200 관 Φ800 L18 · 지정 있으나 사본 폴더가 다른 항목 → 없음
|
||
300 관 Φ800 L20 · 지정 있으나 사본 폴더 없음 → 없음
|
||
400 관 Φ800 L22 · 지정 없음 → 없음
|
||
500 세월교 Φ1000×2련 · 본체 잇기(gford) 사본 있음 → 관 몫 · 월류 폭 셈
|
||
600 세월교 · 잇기 없음 → 관 몫 · pipes 에도 없음
|
||
700 개거 · 지정 없음 → 「사본 없는 시설」 에 셈
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from B08_Quantity import B08_Quantity_Router_Summary as summary
|
||
from M02_MasterTemplete import M02_Item_Snapshot as snapshots
|
||
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
|
||
|
||
def _item(key: str) -> dict:
|
||
return {"layer": "system", "owner": None, "key": key, "name": key}
|
||
|
||
|
||
def _soil() -> dict:
|
||
return {
|
||
"outlet_type": "기슭막이",
|
||
"outlet_revet_form": "돌쌓기(메)",
|
||
"outlet_revet_height_m": 2,
|
||
"outlet_revet_before_m": 5,
|
||
"outlet_revet_after_m": 5,
|
||
}
|
||
|
||
|
||
POINTS = [
|
||
{
|
||
"chainage_m": 100.0,
|
||
"options": {"pipe_diameter_mm": 800, **_soil()},
|
||
"summary_item": _item("SS000001"),
|
||
"group_items": {"g9lq9uk": _item("SS000002")},
|
||
},
|
||
{"chainage_m": 200.0, "options": {"pipe_diameter_mm": 800}, "summary_item": _item("SS000001")},
|
||
{"chainage_m": 300.0, "options": {"pipe_diameter_mm": 800}, "summary_item": _item("SS000001")},
|
||
{"chainage_m": 400.0, "options": {"pipe_diameter_mm": 800}},
|
||
{
|
||
"chainage_m": 500.0,
|
||
"facility": "ford_bridge",
|
||
"options": {"pipe_diameter_mm": 1000, "pipe_count": 2, "ford_width_m": 12},
|
||
"group_items": {"gford": _item("SS000003")},
|
||
},
|
||
{
|
||
"chainage_m": 600.0,
|
||
"facility": "ford_bridge",
|
||
"options": {"pipe_diameter_mm": 1000, "pipe_count": 1, "ford_width_m": 8},
|
||
},
|
||
]
|
||
STRUCTURES = [
|
||
{"structure_id": "s-ditch", "type_id": "open_ditch", "placement": "point", "chainage_m": 700},
|
||
]
|
||
DESIGNS = [
|
||
{"chainage_m": 100.0, "design": {"pipe_length_m": 16}},
|
||
{"chainage_m": 200.0, "design": {"pipe_length_m": 18}},
|
||
{"chainage_m": 300.0, "design": {"pipe_length_m": 20}},
|
||
{"chainage_m": 400.0, "design": {"pipe_length_m": 22}},
|
||
{"chainage_m": 500.0, "design": {"ford_pipe_length_m": 9}},
|
||
{"chainage_m": 600.0, "design": {"ford_pipe_length_m": 7}},
|
||
]
|
||
|
||
|
||
def _snapshot(root: Path, owner: str, item: dict, parts: tuple[str, ...] = ()) -> None:
|
||
folder = snapshots.structures_dir(root) / snapshots.folder_name(owner)
|
||
folder.mkdir(parents=True)
|
||
origin = {**item, "경로": ["횡단배수", item["key"], "이름"]}
|
||
(folder / snapshots.MANIFEST_NAME).write_text(
|
||
json.dumps({"원본": origin}, ensure_ascii=False), encoding="utf-8"
|
||
)
|
||
for name in parts:
|
||
(folder / name).write_text("{}", encoding="utf-8")
|
||
|
||
|
||
@pytest.fixture
|
||
def root(tmp_path: Path) -> Path:
|
||
found = tmp_path / "proj"
|
||
edits = found / "B04_PreProcess/drainage/edits"
|
||
edits.mkdir(parents=True)
|
||
points = [{**p, "source": "user"} for p in POINTS]
|
||
(edits / "pipe_points.json").write_text(
|
||
json.dumps({"points": points}, ensure_ascii=False), encoding="utf-8"
|
||
)
|
||
route = found / "B05_Profile/route"
|
||
route.mkdir(parents=True)
|
||
(route / "structures.json").write_text(
|
||
json.dumps({"revision": 1, "structures": STRUCTURES}, ensure_ascii=False),
|
||
encoding="utf-8",
|
||
)
|
||
_snapshot(found, "pipe@100.0", _item("SS000001"), ("도면.json", "일위대가.json"))
|
||
_snapshot(found, "pipe@200.0", _item("SS000009")) # 다른 항목 사본
|
||
_snapshot(found, "pipe@500.0__gford", _item("SS000003"))
|
||
return found
|
||
|
||
|
||
def _run(root: Path) -> dict:
|
||
names = definition.read()["종류"]["pipe"]["이름"]
|
||
return summary.pipe_summary(root, DESIGNS, names, 6.0)
|
||
|
||
|
||
def _cell(out: dict, at: float, cid: str):
|
||
row = next((r for r in out["rows"] if abs(r["chainage_m"] - at) < 0.01), None)
|
||
return None if row is None else row["values"].get(cid)
|
||
|
||
|
||
def test_연결된_사본은_지정_교집합_같은_항목(root):
|
||
linked = snapshots.linked_snapshots(root)
|
||
assert set(linked) == {"pipe@100.0", "pipe@500.0__gford"}
|
||
one = linked["pipe@100.0"]
|
||
assert one["항목"]["key"] == "SS000001" and one["폴더"] == "pipe@100.0"
|
||
assert one["부분"] == ["도면", "일위대가"] # 있는 파일만
|
||
assert one["경로"] == ["횡단배수", "SS000001", "이름"]
|
||
|
||
|
||
def test_사본_없는_칸은_안_셈(root):
|
||
out = _run(root)
|
||
assert _cell(out, 100, "pipe:len|800") == 16
|
||
assert _cell(out, 100, "pipe:cp|800") == 2
|
||
# 유출 흙막이 잇기는 사본 없음 → 그 칸만 없음(열도 안 섬)
|
||
assert not any(c["id"].startswith("pipe:revet") for c in out["columns"])
|
||
# 다른 항목 사본 · 사본 없음 · 지정 없음 → 줄 없음
|
||
assert [round(r["chainage_m"]) for r in out["rows"]] == [100, 500]
|
||
assert out["total"]["pipe:len|800"] == 16
|
||
|
||
|
||
def test_세월교_관_몫은_본체_잇기_사본으로(root):
|
||
out = _run(root)
|
||
assert _cell(out, 500, "pipe:len|1000") == 18 # 9 × 2련
|
||
assert _cell(out, 500, "pipe:ford|1000|2|width") == 12
|
||
assert _cell(out, 600, "pipe:len|1000") is None
|
||
assert [(round(p["chainage_m"]), p["column"]) for p in out["pipes"]] == [
|
||
(100, "pipe:cp|800"),
|
||
(500, "pipe:cp|1000"),
|
||
(500, "pipe:cp|1000"),
|
||
]
|
||
width = next(c for c in out["columns"] if c["id"] == "pipe:ford|1000|2|width")
|
||
assert width["combos"] == ["system||SS000003"]
|
||
|
||
|
||
def test_사본_없는_시설_알림_응답에_사본(root):
|
||
out = _run(root)
|
||
# 100(유출 흙막이) · 200 · 300 · 400 · 500(기본 날개벽 잇기) · 600 · 개거 700 = 7곳
|
||
assert not any("wing" in c["id"] for c in out["columns"])
|
||
assert "조합 사본 없는 시설 7곳 — 「미연결」 줄을 눌러 조합 연결" in out["notes"]
|
||
assert not any("관 연장 없음" in n for n in out["notes"])
|
||
assert set(out["linked"]) == {"pipe@100.0", "pipe@500.0__gford"}
|
||
|
||
|
||
def test_미연결_시설_칸_전부_연결_여부(root):
|
||
"""154-1 — 셈한 칸 기준 미연결 시설 일곱 · 시설마다 칸 전부(본체 · 그룹) + 연결 여부."""
|
||
out = _run(root)
|
||
got = {(round(u["chainage_m"]), u["type_id"]): u for u in out["unlinked"]}
|
||
assert sorted(got) == [
|
||
(100, "pipe"), (200, "pipe"), (300, "pipe"), (400, "pipe"),
|
||
(500, "ford_bridge"), (600, "ford_bridge"), (700, "open_ditch"),
|
||
] # fmt: skip
|
||
one = got[(100, "pipe")]
|
||
assert one["id"] == "pipe@100.0" and one["facility"] == "pipe" and one["name"] == "배관"
|
||
body = one["slots"][0]
|
||
assert body["copy"] == "pipe@100.0" and body["group"] is None and body["linked"] is True
|
||
outlet = next(s for s in one["slots"] if s["copy"] == "pipe@100.0__g9lq9uk")
|
||
assert outlet["linked"] is False and outlet["item"]["key"] == "SS000002" and outlet["group"]
|
||
assert not got[(200, "pipe")]["slots"][0]["linked"] # 다른 항목 사본
|
||
assert got[(400, "pipe")]["slots"][0]["item"] is None
|
||
ford = got[(500, "ford_bridge")]
|
||
assert ford["facility"] == "ford_bridge"
|
||
assert any(s["copy"] == "pipe@500.0__gford" and s["linked"] for s in ford["slots"])
|
||
ditch = got[(700, "open_ditch")]
|
||
assert ditch["id"] == "s-ditch" and ditch["facility"] is None
|
||
assert ditch["slots"][0] == {"copy": "s-ditch", "group": None, "item": None, "linked": False}
|
||
|
||
|
||
def test_화면_조합_이름_구조물도는_사본에서():
|
||
ui = (ROOT / "B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8")
|
||
assert "loadPickerRows" not in ui # 층 목록 GET 안 씀
|
||
assert "linkedOnly(" in ui
|
||
assert "unlinkedAt(" in ui and "B08_Quantity_Summary_Unlinked" in ui # 154-1 미연결 줄
|
||
drawing = (ROOT / "B08_Quantity/B08_Quantity_UI_Drawing.ts").read_text(encoding="utf-8")
|
||
assert "readTemplate" not in drawing # 층 폴백 없음
|
||
assert "B08_Quantity_Sheets_NoCopy" in drawing
|
||
|
||
|
||
def test_화면_사본_있는_조합만_사본_id():
|
||
"""`linkedOnly` — 본체 · 그룹마다 사본 있는 것만 · 하나도 없으면 시설째 뺌 · `combosAt` 사본 id."""
|
||
import re
|
||
import subprocess
|
||
|
||
imports = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||
sources = [
|
||
ROOT / "common_util" / "common_util_summary_item.ts",
|
||
ROOT / "B08_Quantity" / "B08_Quantity_Summary_Data.ts",
|
||
]
|
||
things = [
|
||
{"structure_id": "s1", "chainage_m": 40, "summary_item": _item("SS1"),
|
||
"group_items": {"g1": _item("SS3"), "g2": _item("SS4")}},
|
||
{"structure_id": "s2", "chainage_m": 80, "summary_item": _item("SS2")},
|
||
] # fmt: skip
|
||
linked = {"s1__g1": {}, "s1": {}}
|
||
body = (
|
||
f"\nconst kept = linkedOnly({json.dumps(things)}, {json.dumps(linked)});"
|
||
"\nconsole.log(JSON.stringify([kept.map((t) => [t.structure_id, Object.keys(t.group_items)]),"
|
||
" combosAt(kept, 40).map((c) => c.copyId)]));"
|
||
)
|
||
script = "\n".join(imports.sub("", p.read_text(encoding="utf-8")) for p in sources) + 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
|
||
assert json.loads(result.stdout) == [[["s1", ["g1"]]], ["s1", "s1__g1"]]
|