Merge remote-tracking branch 'origin/sub_desktop_1' into sub_laptop_3
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
400 세월교 Φ1000×2련 월류 폭 12 · 유입 날개벽 · 유출 흙막이 찰(높이 빈칸 → 미입력) 3 + 4
|
||||
500 물넘이 포장 월류 폭 8 · H=1.0
|
||||
600 개거 + 노출형 횡단수로 · 700 맹암거(그룹 조합)
|
||||
143-1 — 칸은 그 칸을 낸 조합 사본이 있을 때만 셈: 견본은 칸을 내는 잇기를 모두 두고 연결된 사본 =
|
||||
지정 전부 · 사본 없으면 없음은 test_143_1
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -22,6 +24,7 @@ import pytest
|
||||
|
||||
from B06_Section.B06_Section_Schema import CrossSectionPatch
|
||||
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]
|
||||
@@ -36,7 +39,7 @@ def _item(key: str) -> dict:
|
||||
|
||||
|
||||
POINTS = [
|
||||
{"chainage_m": 300.0, "options": {"pipe_diameter_mm": 800}},
|
||||
{"chainage_m": 300.0, "options": {"pipe_diameter_mm": 800}, "summary_item": _item("SS000100")},
|
||||
{
|
||||
"chainage_m": 300.0,
|
||||
"facility": "box_culvert",
|
||||
@@ -50,7 +53,11 @@ POINTS = [
|
||||
"outlet_revet_after_m": 5,
|
||||
},
|
||||
"summary_item": _item("SS000101"),
|
||||
"group_items": {"gboxbody": _item("SS000102")},
|
||||
"group_items": {
|
||||
"gboxbody": _item("SS000102"),
|
||||
"gboxin": _item("SS000106"),
|
||||
"gboxout": _item("SS000107"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"chainage_m": 340.0,
|
||||
@@ -61,6 +68,8 @@ POINTS = [
|
||||
"wing_in": "없음",
|
||||
"wing_out": "날개벽",
|
||||
},
|
||||
"summary_item": _item("SS000101"),
|
||||
"group_items": {"gboxout": _item("SS000107")},
|
||||
},
|
||||
{
|
||||
"chainage_m": 400.0,
|
||||
@@ -77,21 +86,33 @@ POINTS = [
|
||||
"outlet_revet_before_m": 3,
|
||||
"outlet_revet_after_m": 4,
|
||||
},
|
||||
"group_items": {"gford": _item("SS000103"), "gfordin": _item("SS000104")},
|
||||
"group_items": {
|
||||
"gford": _item("SS000103"),
|
||||
"gfordin": _item("SS000104"),
|
||||
"gfordout": _item("SS000108"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"chainage_m": 500.0,
|
||||
"facility": "ford_pavement",
|
||||
"options": {"ford_width_m": 8, "ford_height_m": 1.0},
|
||||
"summary_item": _item("SS000109"),
|
||||
},
|
||||
]
|
||||
STRUCTURES = [
|
||||
{"structure_id": "s-ditch", "type_id": "open_ditch", "placement": "point", "chainage_m": 600},
|
||||
{
|
||||
"structure_id": "s-ditch",
|
||||
"type_id": "open_ditch",
|
||||
"placement": "point",
|
||||
"chainage_m": 600,
|
||||
"summary_item": _item("SS000110"),
|
||||
},
|
||||
{
|
||||
"structure_id": "s-exposed",
|
||||
"type_id": "cross_drain_exposed",
|
||||
"placement": "point",
|
||||
"chainage_m": 600,
|
||||
"summary_item": _item("SS000111"),
|
||||
},
|
||||
{
|
||||
"structure_id": "s-ud",
|
||||
@@ -123,7 +144,8 @@ def out(tmp_path: Path) -> dict:
|
||||
encoding="utf-8",
|
||||
)
|
||||
names = definition.read()["종류"]["pipe"]["이름"]
|
||||
return summary.pipe_summary(root, DESIGNS, names, 6.0)
|
||||
linked = {owner: {} for owner in snapshots.assignments(root)} # 지정 전부 사본 있음
|
||||
return summary.pipe_summary(root, DESIGNS, names, 6.0, linked=linked)
|
||||
|
||||
|
||||
def _cell(out: dict, at: float, cid: str):
|
||||
|
||||
@@ -40,9 +40,9 @@ def root(tmp_path, monkeypatch) -> Path:
|
||||
return project
|
||||
|
||||
|
||||
def test_기본값은_8_m_와_90_10_파일에_안_씀(root):
|
||||
def test_1본_프로젝트_값_없으면_빈칸_90_10_파일에_안_씀(root):
|
||||
assert options.read_options(root) == {
|
||||
"one_pipe_length_m": 8,
|
||||
"one_pipe_length_m": None, # 143-2 — 시스템 층 8 m 로 안 채움
|
||||
"rock_ratios_pct": {"연암": 90.0, "경암": 10.0},
|
||||
"material_surcharge_pct": {"sand": 12, "gravel": 5, "cement": 3}, # 124
|
||||
}
|
||||
@@ -51,7 +51,7 @@ def test_기본값은_8_m_와_90_10_파일에_안_씀(root):
|
||||
|
||||
|
||||
def test_1본_6_m_저장_뒤_커플링을_다시_셈(root):
|
||||
assert summary.couplings(22, summary.one_pipe_length(root)) == 2 # 올림(22/8) − 1
|
||||
assert summary.couplings(22, summary.one_pipe_length(root)) is None # 프로젝트 값 없음
|
||||
out = options.save_options(root, options.OptionsSave(one_pipe_length_m=6))
|
||||
assert out["one_pipe_length_m"] == 6 == summary.one_pipe_length(root)
|
||||
assert summary.couplings(22, 6) == 3 and summary.couplings(16, 6) == 2
|
||||
@@ -63,6 +63,7 @@ def test_1본_6_m_저장_뒤_커플링을_다시_셈(root):
|
||||
# 다시 저장 = 같은 사본 판 위에 씀
|
||||
options.save_options(root, options.OptionsSave(one_pipe_length_m=8))
|
||||
assert summary.one_pipe_length(root) == 8
|
||||
assert summary.couplings(22, summary.one_pipe_length(root)) == 2 # 올림(22/8) − 1
|
||||
|
||||
|
||||
def test_비율_70_30_저장_뒤_남고_다른_칸은_그대로(root):
|
||||
@@ -79,7 +80,7 @@ def test_비율_70_30_저장_뒤_남고_다른_칸은_그대로(root):
|
||||
assert stored["rock_methods"] == {"연암": "ripping"}
|
||||
assert options.read_options(root)["rock_ratios_pct"]["연암"] == 70.0
|
||||
# 1본 길이는 안 보냈으니 그대로
|
||||
assert options.read_options(root)["one_pipe_length_m"] == 8
|
||||
assert options.read_options(root)["one_pipe_length_m"] is None
|
||||
|
||||
|
||||
def test_비율_읽기_자름_경암만_있을_때():
|
||||
|
||||
@@ -20,6 +20,7 @@ from test_121_b08_material_sheets import SUMMARY, UNITS, _run_ts
|
||||
from B08_Quantity import B08_Quantity_Router_Material as router_mod
|
||||
from B08_Quantity import B08_Quantity_Router_Summary as summary_mod
|
||||
from B08_Quantity.B08_Quantity_Material_Calc import clean_overrides, compute, finish
|
||||
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]
|
||||
@@ -107,7 +108,8 @@ def _ford_summary(tmp_path: Path, designs: list[dict]) -> dict:
|
||||
encoding="utf-8",
|
||||
)
|
||||
names = definition.read()["종류"]["pipe"]["이름"]
|
||||
return summary_mod.pipe_summary(root, designs, names, 6.0)
|
||||
linked = {owner: {} for owner in snapshots.assignments(root)} # 143-1 지정 전부 사본 있음
|
||||
return summary_mod.pipe_summary(root, designs, names, 6.0, linked=linked)
|
||||
|
||||
|
||||
def _cell(out: dict, at: float, cid: str):
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
# -*- 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곳 — B05 · B06 좌측 패널에서 조합 연결 · [저장]" 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_화면_조합_이름_구조물도는_사본에서():
|
||||
ui = (ROOT / "B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8")
|
||||
assert "loadPickerRows" not in ui # 층 목록 GET 안 씀
|
||||
assert "linkedOnly(" in ui
|
||||
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"]]
|
||||
@@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""B08 구조물 집계표 — 관경 · 련 빈칸 = 「미입력」(임의 1000 · 1 로 안 채움 · PLAN 143-2).
|
||||
|
||||
견본: 100 관 관경 빈칸 L16 · 유출 흙막이 높이 빈칸 / 200 세월교 Φ1000 련 빈칸 · 관 길이 9.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
|
||||
ITEM = {"layer": "system", "owner": None, "key": "SS000001", "name": "견본"}
|
||||
POINTS = [
|
||||
{
|
||||
"chainage_m": 100.0,
|
||||
"options": {
|
||||
"pipe_diameter_mm": "",
|
||||
"outlet_type": "기슭막이",
|
||||
"outlet_revet_form": "돌쌓기(메)",
|
||||
"outlet_revet_before_m": 5,
|
||||
"outlet_revet_after_m": 5,
|
||||
},
|
||||
"summary_item": ITEM,
|
||||
"group_items": {"g9lq9uk": ITEM},
|
||||
},
|
||||
{
|
||||
"chainage_m": 200.0,
|
||||
"facility": "ford_bridge",
|
||||
"options": {"pipe_diameter_mm": 1000, "pipe_count": "", "ford_width_m": 12},
|
||||
"summary_item": ITEM,
|
||||
},
|
||||
]
|
||||
DESIGNS = [
|
||||
{"chainage_m": 100.0, "design": {"pipe_length_m": 16}},
|
||||
{"chainage_m": 200.0, "design": {"ford_pipe_length_m": 9}},
|
||||
]
|
||||
|
||||
|
||||
def _run(tmp_path: Path) -> dict:
|
||||
root = tmp_path / "proj"
|
||||
edits = root / "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"
|
||||
)
|
||||
names = definition.read()["종류"]["pipe"]["이름"]
|
||||
linked = {owner: {} for owner in snapshots.assignments(root)}
|
||||
return summary.pipe_summary(root, DESIGNS, names, 6.0, linked=linked)
|
||||
|
||||
|
||||
def _cell(out: dict, at: float, cid: str):
|
||||
row = next(r for r in out["rows"] if abs(r["chainage_m"] - at) < 0.01)
|
||||
return row["values"].get(cid)
|
||||
|
||||
|
||||
def test_관경_빈칸은_미입력_열(tmp_path):
|
||||
out = _run(tmp_path)
|
||||
cols = {c["id"]: c for c in out["columns"]}
|
||||
assert cols["pipe:len|-"]["group"] == ["관 공", "미입력", "관매설"]
|
||||
assert _cell(out, 100, "pipe:len|-") == 16
|
||||
assert _cell(out, 100, "pipe:cp|-") == 2 # 1본 6 m — 관경과 무관
|
||||
assert not any(c.startswith("pipe:len|1000") and _cell(out, 100, c) for c in cols)
|
||||
# 흙막이 빈 높이 = 관경 최소 높이를 모름 → 「미입력」
|
||||
assert cols["pipe:revet|돌쌓기(메)|-"]["group"][2] == "미입력"
|
||||
assert _cell(out, 100, "pipe:revet|돌쌓기(메)|-") == 10
|
||||
|
||||
|
||||
def test_세월교_련_빈칸은_미입력_관_몫_빈칸(tmp_path):
|
||||
out = _run(tmp_path)
|
||||
cols = {c["id"]: c for c in out["columns"]}
|
||||
assert cols["pipe:ford|1000|-|width"]["group"][1] == "Φ1000×미입력련"
|
||||
assert _cell(out, 200, "pipe:ford|1000|-|width") == 12
|
||||
assert _cell(out, 200, "pipe:len|1000") is None # 련 수를 모르면 관 몫도 빈칸
|
||||
assert _cell(out, 200, "pipe:cp|1000") is None
|
||||
assert not [p for p in out["pipes"] if p["column"] == "pipe:cp|1000"]
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""B08 구조물 탭 — 자재 셋 값 없는 줄 숨김 · 표 넷 공용 카드 안(PLAN 143-3).
|
||||
|
||||
숨김은 화면만 — 셈 · 열 · 단위수량은 그대로 · 고친 칸은 원래 줄 번호(`keep`)로 저장 자리를 찾음.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MATERIAL = (ROOT / "B08_Quantity/B08_Quantity_UI_Material.ts").read_text(encoding="utf-8")
|
||||
SUMMARY = (ROOT / "B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_자재_셋_값_없는_줄_숨김():
|
||||
assert MATERIAL.count("const keep = shown(rows, ") == 3 # 총괄 · 콘크리트 · 자재수량
|
||||
assert "row.qty != null || anyValue(row.units)" in MATERIAL # 손 단위수량 있으면 보임
|
||||
assert "row.qty != null || anyValue(row.amounts)" in MATERIAL
|
||||
assert "row.qty != null || row.amount != null" in MATERIAL
|
||||
# 고친 칸 → 원래 줄(숨긴 줄 건너뜀)
|
||||
for target in (
|
||||
"rows[keep[i / 2]].id",
|
||||
"rows[keep[i]].key, key.slice(4)",
|
||||
"rows[keep[i]].panel",
|
||||
):
|
||||
assert target in MATERIAL, target
|
||||
|
||||
|
||||
def test_표_넷_공용_카드_안():
|
||||
assert "createCard({ body: [toolbar, table.root] })" in MATERIAL
|
||||
assert "createCard({ body: parts })" in SUMMARY
|
||||
@@ -0,0 +1,170 @@
|
||||
"""144-2~4 B09 바탕 단가 셈 — 엑셀 검산 대조 · 거울(`B09_Estimation_Calc_Base.py` ↔ `.ts`).
|
||||
|
||||
견본 = 기번3 공통 엑셀(`2024년 간선임도사업(기번3).xlsx`) 같은 입력:
|
||||
노무비목록표 E10~E12 · 환율및기초자료 G6~G8 · 경비목록표 E4 · E5 · E7 · E13 · E16 · E21 ·
|
||||
자재단가대비표 5~12줄 · 중기사용료 1 · 2 · 4 · 11 · 14 · 16 호표(F11 126,000 …)
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import ( # noqa: E402
|
||||
compute_base,
|
||||
round_half,
|
||||
rounddown,
|
||||
roundup,
|
||||
)
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
FACTOR = {"num": 400, "den": 1920, "text": "1/8 × 16/12 × 25/20"}
|
||||
|
||||
LABOR = [
|
||||
{"key": "LB000048", "code": "1048", "name": "건설기계운전사", "price": 267360},
|
||||
{"key": "LB000049", "code": "1049", "name": "화물차운전사", "price": 226709},
|
||||
{"key": "LB000050", "code": "1050", "name": "일반기계운전사", "price": 161142},
|
||||
{"key": "LB000002", "code": "1002", "name": "보통인부", "price": None},
|
||||
]
|
||||
|
||||
|
||||
def _m(key, name, price, loss, fuel, lph, misc, crew_code):
|
||||
return {"key": key, "name": name, "spec": "", "price": price, "loss": loss, "fuel": fuel,
|
||||
"fuel_lph": lph, "misc_pct": misc, "crew": 1 if crew_code else None,
|
||||
"crew_code": crew_code} # fmt: skip
|
||||
|
||||
|
||||
MACHINES = [
|
||||
_m("X00001", "불도우저19ton", 184499, 1811, "경유", 25, 16, "1048"),
|
||||
_m("X00003", "굴삭기(0.2m3)", 62550, 2085, "경유", 5, 21, "1048"),
|
||||
_m("X00005", "굴삭기(0.7m3)", 110926, 2085, "경유", 11.6, 22, "1048"),
|
||||
_m("X00032", "덤프자동덮개", 1546, 2684, None, None, None, None),
|
||||
_m("X00048", "콘크리트믹서0.45m3", 5409, 3039, "휘발유", 3.9, 2, "1050"),
|
||||
_m("X00074", "덤프트럭2.5ton(토사)", 20793, 2967, "경유", 2.9, None, "1049"),
|
||||
_m("X09999", "기준가 없는 기계", None, 2000, "중유", 3, 10, "1048"),
|
||||
]
|
||||
|
||||
|
||||
def _mt(key, name, **prices):
|
||||
return {"key": key, "name": name, "spec": "", "unit": "", "origin": "M01", "prices": prices}
|
||||
|
||||
|
||||
MATERIALS = [
|
||||
_mt("MO1", "휘발유", 관급=1369, 물가정보=0, 물가자료=0),
|
||||
_mt("MO2", "경유", 관급=1272),
|
||||
_mt("M3", "각재", 관급=500661, 물가정보=829341, 물가자료=748503),
|
||||
_mt("M4", "못", 관급=1503, 물가정보=1935, 물가자료=1665),
|
||||
_mt("M8", "화강석", 관급=0, 물가정보=288000, 물가자료=288000),
|
||||
_mt("M9", "사용자 값 줄", 물가자료=1000),
|
||||
_mt("b08:sand", "골재"),
|
||||
]
|
||||
|
||||
INPUTS = {
|
||||
"labor": LABOR,
|
||||
"machines": MACHINES,
|
||||
"materials": MATERIALS,
|
||||
"fuel_keys": {"경유": "MO2", "휘발유": "MO1"},
|
||||
"driver_factor": FACTOR,
|
||||
"fx": [
|
||||
{"key": "FX000001", "name": "USD", "unit": "원/USD", "value": 1414.9, "date": "2026-08-14"}
|
||||
],
|
||||
}
|
||||
USER = {"materials": {"M9": {"applied": 950, "supply": "관급"}, "M4": {"supply": "별도"},
|
||||
"b08:sand": {"applied": None}}} # fmt: skip
|
||||
|
||||
|
||||
def _run_ts(inputs, user):
|
||||
calc = ROOT / "B09_Estimation" / "B09_Estimation_Calc_Base.ts"
|
||||
body = (
|
||||
f"\nconst i = {json.dumps(inputs, ensure_ascii=False)};"
|
||||
f"\nconst u = {json.dumps(user, ensure_ascii=False)};"
|
||||
"\nconsole.log(JSON.stringify(computeBase(i, u)));"
|
||||
)
|
||||
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_끝수_엑셀_자리():
|
||||
assert rounddown(33412.7689, 1) == 33412.7 and rounddown(126000.9) == 126000
|
||||
assert rounddown(-1.25, 1) == -1.2 # 0 쪽으로
|
||||
assert roundup(69849380, -3) == 69850000 and roundup(69850000, -3) == 69850000
|
||||
assert round_half(2.5) == 3 and round_half(0.125, 2) == 0.13 and round_half(-2.5) == -3
|
||||
assert rounddown(0.1 + 0.2, 1) == 0.3 # 떠도는 끝수는 버리지 않음
|
||||
|
||||
|
||||
def test_144_2_노무_경비_환율_기초자료():
|
||||
s = compute_base(INPUTS, {})
|
||||
assert [r["price"] for r in s["labor"]] == [267360, 226709, 161142, None]
|
||||
assert ("labor", "LB000002") in {(g["tab"], g["key"]) for g in s["gaps"]}
|
||||
hours = {d["key"]: d["hour"] for d in s["basis"]["drivers"]}
|
||||
assert hours == {"1048": 55700, "1050": 33571, "1049": 47231} # 기초자료 G6 · G8 · G7
|
||||
assert s["basis"]["drivers"][0]["factor"] == "1/8 × 16/12 × 25/20"
|
||||
assert [r["price"] for r in s["expense"]][:3] == [
|
||||
184499,
|
||||
62550,
|
||||
110926,
|
||||
] # 경비목록표 E4 · E5 · E7
|
||||
assert s["expense"][0]["unit"] == "천원"
|
||||
assert s["basis"]["fx"][0]["value"] == 1414.9
|
||||
|
||||
|
||||
def test_144_3_자재단가대비_최저_사용자_관급():
|
||||
s = compute_base(INPUTS, USER)
|
||||
rows = {r["key"]: r for r in s["compare"]}
|
||||
# 엑셀 O 열 = 최저 · P 열 출처(0 은 뺌 · 같은 값이면 앞 칸 — 화강석 「물정」)
|
||||
assert (rows["MO1"]["applied"], rows["MO1"]["applied_from"]) == (1369, "관급")
|
||||
assert (rows["M3"]["applied"], rows["M3"]["lowest_from"]) == (500661, "관급")
|
||||
assert rows["M4"]["applied"] == 1503 and rows["M4"]["supply"] == "별도"
|
||||
assert (rows["M8"]["applied"], rows["M8"]["applied_from"]) == (288000, "물가정보")
|
||||
assert (rows["M9"]["lowest"], rows["M9"]["applied"], rows["M9"]["applied_from"]) == (
|
||||
1000,
|
||||
950,
|
||||
"사용자",
|
||||
)
|
||||
assert rows["M9"]["supply"] == "관급"
|
||||
assert rows["b08:sand"]["applied"] is None # 출처 없음 · 사용자 빈칸 = 빈칸
|
||||
material = {r["key"]: r["price"] for r in s["material"]}
|
||||
assert material == {r["key"]: r["applied"] for r in s["compare"]} # 재료비목록표 = 적용 단가
|
||||
gaps = [(g["tab"], g["key"]) for g in s["gaps"] if g["tab"] == "compare"]
|
||||
assert gaps == [("compare", "b08:sand")]
|
||||
|
||||
|
||||
def test_144_4_중기사용료_엑셀_검산():
|
||||
s = compute_base(INPUTS, {})
|
||||
got = {r["key"]: (r["sum"], r["labor"], r["material"], r["expense"]) for r in s["machine"]}
|
||||
assert got["X00001"] == (126000, 55700, 36888, 33412) # 중기목록표 4줄
|
||||
assert got["X00003"] == (76436, 55700, 7695, 13041)
|
||||
assert got["X00005"] == (96829, 55700, 18001, 23128)
|
||||
assert got["X00032"] == (414, 0, 0, 414) # 덮개 — 운전사 · 연료 없음
|
||||
assert got["X00048"] == (40659, 33571, 5445, 1643) # 일반기계운전사 · 휘발유 · 잡유 2 %
|
||||
assert got["X00074"][1:3] == (47231, 3688) # 화물차운전사 · 잡품 없음
|
||||
assert got["X09999"] == (None, 55700, None, None) # 기준가 · 중유 단가 없음 = 그 비목 · 합 빈칸
|
||||
block = s["machine_blocks"][0]
|
||||
assert [ln["amount"] for ln in block["lines"]] == [
|
||||
33412.7,
|
||||
55700,
|
||||
31800,
|
||||
5088,
|
||||
] # 중기사용료 F7~F10
|
||||
assert block["lines"][0]["qty"] == 0.1811 and block["lines"][0]["ref"] == {
|
||||
"tab": "expense",
|
||||
"key": "X00001",
|
||||
}
|
||||
why = {g["why"] for g in s["gaps"] if g["key"] == "X09999"}
|
||||
assert why == {"연료(중유) 단가 없음", "기계 기준가 없음", "손료 기준가 · 계수 없음"}
|
||||
|
||||
|
||||
def test_거울_TS_가_같은_값():
|
||||
for user in ({}, USER):
|
||||
assert _run_ts(INPUTS, user) == compute_base(INPUTS, user)
|
||||
@@ -0,0 +1,134 @@
|
||||
"""144-1 B09 뼈대 — 탭 차례 · 저장 자리 · 길 · 빈칸 규칙 · 「연결 없음」 목록 · 단가 굳히기.
|
||||
|
||||
- 탭 = 기번3 공통 엑셀 보이는 탭 차례(목차 · 안내 = 「연결 점검」 한 탭 · INITIAL 안 옮김)
|
||||
- 저장 = 프로젝트 안 `B09_Estimation/v1/`(사용자 값 · 굳힌 단가) · 결과는 저장 안 함
|
||||
- 줄 = 수요 따라(사용자 승인 ①) — 연결된 사본 없으면 노무 · 경비 · 중기 빈 표
|
||||
· B08 자재만 줄 · 출처 빈칸
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402
|
||||
from B09_Estimation import B09_Estimation_Router as router_mod # noqa: E402
|
||||
from B09_Estimation import B09_Estimation_Store as store # noqa: E402
|
||||
|
||||
#: 엑셀 보이는 탭(목차 · 안내 빼고) — 조사 F 1절 차례
|
||||
EXCEL_TABS = [
|
||||
"공사원가계산서", "총괄설계내역서", "설계내역서", "일위대가목록표", "일위대가표",
|
||||
"단가산출근거목록표", "단가산출근거", "환율및기초자료", "중기목록표", "중기사용료",
|
||||
"재료비목록표", "노무비목록표", "경비목록표", "자재단가대비표", "재료비수량금액집계표",
|
||||
"노무비수량금액집계표", "경비수량금액집계표", "중기시간금액집계표",
|
||||
] # fmt: skip
|
||||
|
||||
B08_SHEETS = {
|
||||
"material": {
|
||||
"rows": [
|
||||
{"key": "sand", "group": "골재", "spec": "모래", "unit": "㎥", "amount": 16},
|
||||
{"key": "rubble", "group": "골재", "spec": "막자갈", "unit": "㎥", "amount": None},
|
||||
{"key": "cement", "group": "시멘트", "spec": "40kg/대", "unit": "대", "amount": 244},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_탭_차례가_엑셀과_같다():
|
||||
source = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs.ts").read_text(encoding="utf-8")
|
||||
block = source[
|
||||
source.index("export const TABS") : source.index("];", source.index("export const TABS"))
|
||||
]
|
||||
names = re.findall(r'\["\w+", "([^"]+)"\]', block)
|
||||
assert names[0] == "연결 점검"
|
||||
assert names[1:] == EXCEL_TABS
|
||||
|
||||
|
||||
def test_길_셋():
|
||||
routes = {(r.path, tuple(sorted(r.methods))) for r in router_mod.router.routes}
|
||||
assert ("/api/projects/{project_id}/estimation", ("GET",)) in routes
|
||||
assert ("/api/projects/{project_id}/estimation", ("PUT",)) in routes
|
||||
assert ("/api/projects/{project_id}/estimation/prices", ("POST",)) in routes
|
||||
main = (ROOT / "main.py").read_text(encoding="utf-8")
|
||||
assert "app.include_router(b09_estimation_router" in main
|
||||
|
||||
|
||||
def test_사용자_값_다듬기_저장_자리(tmp_path):
|
||||
raw = {
|
||||
"materials": {"MT1": {"applied": 1200, "supply": "관급", "x": 1}, "MT2": {"applied": None},
|
||||
"MT3": {"supply": "엉뚱"}},
|
||||
"estimate": {"estimated_price": "abc", "profit_adjust": 905980, "work_type": "토목",
|
||||
"rates": {"RT000001": 3.56, "RT000002": None}, "zzz": 1},
|
||||
"other": 1,
|
||||
} # fmt: skip
|
||||
clean = store.write_user(tmp_path, raw)
|
||||
assert clean == {
|
||||
"materials": {"MT1": {"applied": 1200.0, "supply": "관급"}, "MT2": {"applied": None}},
|
||||
"estimate": {"estimated_price": None, "profit_adjust": 905980.0, "work_type": "토목",
|
||||
"rates": {"RT000001": 3.56, "RT000002": None}},
|
||||
} # fmt: skip
|
||||
path = tmp_path / "B09_Estimation" / "v1" / "user_values.json"
|
||||
assert json.loads(path.read_text(encoding="utf-8")) == clean
|
||||
assert store.read_user(tmp_path) == clean
|
||||
assert store.read_book(tmp_path) is None
|
||||
|
||||
|
||||
def test_단가_굳히기_운전사_연료_따라옴():
|
||||
book = prices.refresh_book(["EQ000004"]) # 불도저(무한궤도) 19ton · 조종원 1048 · 경유
|
||||
rows = book["rows"]
|
||||
assert set(rows) == {"EQ000004", "LB000048", "MO000002"}
|
||||
assert rows["EQ000004"]["loss"] == 1811 and rows["EQ000004"]["crew_code"] == "1048"
|
||||
assert rows["LB000048"]["code"] == "1048" and rows["MO000002"]["prices"]["유가"] > 0
|
||||
assert book["driver_factor"] == {"num": 400, "den": 1920, "text": "1/8 × 16/12 × 25/20"}
|
||||
# [저장] = 책에 없는 새 줄만 더함 — 있는 줄 값은 안 바꿈
|
||||
stale = {**book, "rows": {**rows, "LB000048": {**rows["LB000048"], "price": 1}}}
|
||||
grown = prices.extend_book(stale, ["EQ000004", "LB000002"])
|
||||
assert grown["rows"]["LB000048"]["price"] == 1 and "LB000002" in grown["rows"]
|
||||
# 책 밖 줄 = 지금 M01 값 + fresh 표시
|
||||
inputs = prices.build_inputs(stale, ["EQ000004", "LB000002"])
|
||||
fresh = {row["key"]: row.get("fresh", False) for row in inputs["labor"]}
|
||||
assert fresh == {"LB000002": True, "LB000048": False}
|
||||
assert inputs["fuel_keys"] == {"경유": "MO000002"}
|
||||
|
||||
|
||||
def test_연결_없으면_빈_표와_연결_없음(tmp_path, monkeypatch):
|
||||
async def fake_materials(_project_id):
|
||||
return B08_SHEETS
|
||||
|
||||
monkeypatch.setattr(router_mod, "material_sheets", fake_materials)
|
||||
body = asyncio.run(router_mod.payload("p", tmp_path))
|
||||
sheets = body["sheets"]
|
||||
assert sheets["labor"] == [] and sheets["expense"] == [] and sheets["machine"] == []
|
||||
assert [row["key"] for row in sheets["compare"]] == ["b08:sand", "b08:cement"] # 수량 있는 줄만
|
||||
assert all(row["applied"] is None for row in sheets["compare"]) # 출처 없음 = 빈칸(0 아님)
|
||||
assert [(g["tab"], g["key"]) for g in sheets["gaps"]] == [
|
||||
("compare", "b08:sand"),
|
||||
("compare", "b08:cement"),
|
||||
]
|
||||
assert any("연결된 구조물 조합 사본 없음" in n for n in body["notes"])
|
||||
assert body["book"] is None # GET 은 아무것도 쓰지 않음
|
||||
assert not (tmp_path / "B09_Estimation").exists()
|
||||
|
||||
|
||||
def test_연결된_사본이_부른_자원만(tmp_path):
|
||||
"""사본 단가 묶음 통째(고르기 후보 기계 100여)가 아니라 로직이 실제로 부른 줄만."""
|
||||
from B09_Estimation.B09_Estimation_Linked import used_keys
|
||||
from M02_MasterTemplete import M02_Item_Prices as item_prices
|
||||
|
||||
bundle = item_prices.to_json(item_prices.collect(["GC000268"])) # 기계 시간당 기계경비
|
||||
files = {
|
||||
"단가.json": bundle,
|
||||
"일위대가.json": {"줄": {"종류": "일위대가", "담은로직": [{"로직": "GC000268"}]}},
|
||||
"입력값.json": {"GC000268": {"기계": {"값": "0101-0019"}, "지역": {"값": "전국평균"}}},
|
||||
}
|
||||
for name, data in files.items():
|
||||
(tmp_path / name).write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||||
assert used_keys(tmp_path) == ["EQ000004", "MO000002", "LB000048"]
|
||||
(tmp_path / "입력값.json").write_text("{}", encoding="utf-8")
|
||||
assert used_keys(tmp_path) == [] # 입력 빈 로직은 못 돎 — 부른 줄 없음
|
||||
assert used_keys(tmp_path / "없음") == []
|
||||
@@ -32,10 +32,14 @@ def test_read_only():
|
||||
assert "editable: () => false" in DRAWING
|
||||
|
||||
|
||||
def test_copy_first_then_master():
|
||||
# 같은 항목의 프로젝트 복사본이 있으면 복사본 · 없으면 M02 원본 층
|
||||
assert "sameItem(own, row)" in DRAWING and "structureFolder(" in DRAWING
|
||||
assert "readTemplate(layer, part, column, null)" in DRAWING
|
||||
def test_copy_only():
|
||||
# 143-1 — 연결된 사본만(그룹은 `{id}__{그룹}` 사본) · 없으면 「사본 없음」 · M02 층 폴백 없음
|
||||
assert (
|
||||
"request.linked[combo.copyId]" in DRAWING
|
||||
and "sourceOf(request.projectId, copy.폴더)" in DRAWING
|
||||
)
|
||||
assert "readTemplate" not in DRAWING and "FromMaster" not in DRAWING
|
||||
assert "사본 없음" in LOCALE
|
||||
|
||||
|
||||
def test_locale_keys_exist():
|
||||
|
||||
@@ -195,12 +195,15 @@ def test_토공집계표_식() -> None:
|
||||
assert value[("토사운반", "", "19 TON B/D")] == 20
|
||||
assert value[("유용토 소계", "", "")] == pytest.approx(t["diverted_m3"] + 82)
|
||||
assert value[("잔토처리", "", "백호우")] == -98
|
||||
assert value[("성토면다짐", "", "")] == excel_round(101.7 * 0.8)
|
||||
assert value[("초류종자살포", "", "씨드스프레이")] == excel_round(101.7 * 0.5 + 15.3)
|
||||
tree = excel_round(133.2 * 0.5 + 82.8, 2)
|
||||
assert value[("지장목제거", "", "")] == excel_round(tree * 0.8)
|
||||
# 143-2 — 거창 계수(0.8 · 0.5) 자리는 빈칸 + 「비율 미정」 · 구조물 쪽 줄 8 뺌 · 나머지 7 빈칸
|
||||
notes = {r["group"]: r["note"] for r in rows}
|
||||
for group in ("성토면다짐", "초류종자살포", "지장목제거"):
|
||||
assert next(r["value"] for r in rows if r["group"] == group) is None
|
||||
assert notes[group] == "비율 미정"
|
||||
assert value[("표토제거", "", "")] is None
|
||||
assert len(rows) == 33 # 시트 C5:C37
|
||||
assert not {"구조물터파기", "토사다짐", "되메우기", "잔토"} & {r["group"] for r in rows}
|
||||
assert sum(r["value"] is None and r["note"] != "비율 미정" for r in rows) == 7
|
||||
assert len(rows) == 25 # 시트 C5:C37 33 − 구조물 쪽 8
|
||||
|
||||
|
||||
def test_토적표_머리_글자는_사용자_견본대로():
|
||||
|
||||
@@ -56,15 +56,27 @@ def test_장은_시설이_정함() -> None:
|
||||
|
||||
|
||||
def test_박스_울진_산출서_단면() -> None:
|
||||
r = compute(_inputs("box", box_width_m=3.0, box_height_m=1.5, slope_denominator=50))
|
||||
assert SHEET_DEFAULTS["box"]["n"] == 0.013 and SHEET_DEFAULTS["box"]["depth_ratio"] == 0.8
|
||||
# 143-2 — 수심비 처음 값 빈칸(울진 0.8 은 손으로 넣은 값으로만)
|
||||
r = compute(
|
||||
_inputs("box", box_width_m=3.0, box_height_m=1.5, slope_denominator=50, depth_ratio=0.8)
|
||||
)
|
||||
assert SHEET_DEFAULTS["box"]["n"] == 0.013 and SHEET_DEFAULTS["box"]["depth_ratio"] is None
|
||||
assert r["n"] == 0.013
|
||||
assert r["depth"] == pytest.approx(1.2)
|
||||
assert r["section_area"] == pytest.approx(3.6)
|
||||
assert r["perimeter"] == pytest.approx(5.4)
|
||||
assert r["radius"] == pytest.approx(3.6 / 5.4)
|
||||
# 실무 29.236 = 1/n 75 · R 0.67 반올림 — 같은 n 이면 R 반올림 차(0.3 %)만 남음
|
||||
r75 = compute(_inputs("box", box_width_m=3.0, box_height_m=1.5, slope_denominator=50, n=1 / 75))
|
||||
r75 = compute(
|
||||
_inputs(
|
||||
"box",
|
||||
box_width_m=3.0,
|
||||
box_height_m=1.5,
|
||||
slope_denominator=50,
|
||||
n=1 / 75,
|
||||
depth_ratio=0.8,
|
||||
)
|
||||
)
|
||||
assert r75["capacity"] == pytest.approx(29.236, rel=0.004)
|
||||
assert 75 * 0.67 ** (2 / 3) * math.sqrt(1 / 50) * 3.6 == pytest.approx(29.236, abs=0.001)
|
||||
assert r["capacity"] > r75["capacity"] # n 고치면 따라감
|
||||
@@ -79,6 +91,9 @@ def test_박스_내공_경사_빈칸이면_대시() -> None:
|
||||
r = compute(_inputs("box"))
|
||||
assert r["design_flow"] is not None
|
||||
assert r["depth"] is None and r["capacity"] is None and r["ok"] is None
|
||||
# 수심비 빈칸 = 내공 · 경사가 있어도 계산 멈춤(143-2)
|
||||
r = compute(_inputs("box", box_width_m=3.0, box_height_m=1.5, slope_denominator=50))
|
||||
assert r["depth"] is None and r["capacity"] is None and r["ok"] is None
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
78+7 Φ800 L18 · 집수정 ㄷ(U) · 유출 메 H2.0 10 m · 메붙임 20 ㎡ → 커플링 2
|
||||
84+0 Φ800 L16 · 집수정 ㄴ(L) · 유출 메 H2.0 → 커플링 올림(16/6)−1 = 2 · 1본 8 m 면 1
|
||||
89+10 Φ800 L(없음) · 유출 높이 빈칸 → 관경 최소 1.8 · 집수정 기록 없음 → 알림
|
||||
143-1 — 칸은 그 칸을 낸 조합 사본이 있을 때만 셈: 픽스처는 관 · 유입 · 유출 · 흙막이 1 · 돌붙임 1 을
|
||||
모두 잇고 연결된 사본 = 지정 전부(`_run`) · 사본 없으면 없음은 test_143_1
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -17,12 +19,15 @@ 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 import M02_Template_Layers as layers
|
||||
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
|
||||
|
||||
COMBO = {"layer": "system", "owner": None, "key": "SS000011", "name": "횡단배수 배관"}
|
||||
OUT = {"layer": "company", "owner": "3", "key": "SC3-000001", "name": "기슭막이 메쌓기"}
|
||||
WALL = {"side": "outlet", "key": "extra0"}
|
||||
#: 컨테이너 잇기 전부(143-1 — 잇지 않은 칸은 안 셈) · 흙막이 칸 조합 = OUT
|
||||
LINKED = {"gnbf9m9": COMBO, "g9lq9uk": OUT, "gsoiladd1": OUT, "gchuteadd1": COMBO}
|
||||
|
||||
|
||||
def _revet(side: str, form: str, height: float | None) -> dict:
|
||||
@@ -101,7 +106,15 @@ 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]
|
||||
points = [
|
||||
{
|
||||
**p,
|
||||
"source": "user",
|
||||
"summary_item": COMBO,
|
||||
"group_items": {**LINKED, **(p.get("group_items") or {})},
|
||||
}
|
||||
for p in POINTS
|
||||
]
|
||||
(edits / "pipe_points.json").write_text(
|
||||
json.dumps({"points": points}, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
@@ -110,7 +123,8 @@ def root(tmp_path: Path) -> Path:
|
||||
|
||||
def _run(root: Path, one_m: float | None = 6.0) -> dict:
|
||||
names = definition.read()["종류"]["pipe"]["이름"]
|
||||
return summary.pipe_summary(root, DESIGNS, names, one_m)
|
||||
linked = {owner: {} for owner in snapshots.assignments(root)} # 지정 전부 사본 있음
|
||||
return summary.pipe_summary(root, DESIGNS, names, one_m, linked=linked)
|
||||
|
||||
|
||||
def _cell(out: dict, at: float, cid: str):
|
||||
@@ -201,7 +215,7 @@ def test_1본_길이는_프로젝트_층_변수(root, tmp_path, monkeypatch):
|
||||
json.dumps({"변수": {"파형강관_1본_m": 8}}), encoding="utf-8"
|
||||
)
|
||||
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
|
||||
assert summary.one_pipe_length(root) == 8
|
||||
assert summary.one_pipe_length(root) is None # 143-2 — 시스템 층으로 안 채움
|
||||
mine = layers.template_path(layers.project_dir(root), "table", "구조물집계표")
|
||||
mine.parent.mkdir(parents=True)
|
||||
mine.write_text(json.dumps({"변수": {"파형강관_1본_m": 6}}), encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user