"""토공집계표·프로젝트 설정 검사 — PLAN 8-11·8-13·8-7. 여기서 못 박는 것 셋 · 암 갈래 **개수를 코드에 안 박음** — 공사마다 다르다(울진 2 · 거창 5 · BOM 1). · 반영률 **기본 100 %** — 실무 관측 80/50/80 은 기본값이 아니다(★법대로). · 무대는 **집계에는 오르되 내역 줄이 아니다** — 품셈 1-2-7. """ from __future__ import annotations import json import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import ( # noqa: E402 SummaryInput, build_rows, build_table, haul_check, totals_by_unit, ) from common_util.common_util_project_settings import ( # noqa: E402 APPLICATION_RATIO_KEYS, ROCK_CLASS_SETS, application_ratio, default_settings, load_settings, quantity_settings, rock_classes, save_section, ) def 토적표합계() -> dict[str, float]: return { "cut_soil_volume_m3": 2526.99, "cut_rock_volume_m3": 3512.09, "ditch_soil_volume_m3": 90.51, "ditch_rock_volume_m3": 101.22, "adjusted_total_m3": 6511.06, "fill_volume_m3": 16836.47, "diverted_m3": 5801.21, } def 사면합계() -> dict[str, float]: return { "face_dressing_fill": 13518.6, "face_dressing_cut": 5433.7, "tree_removal_fill": 13518.6, "tree_removal_cut": 5433.7, "bench_cut_fill": 13518.6, } # ── 암 갈래 — 개수를 코드에 박지 않는다 ───────────────────────────── def test_비율이_없으면_암을_쪼개지_않음() -> None: """지어낸 비율로 나누지 않는다 — 「암」 한 줄로 낸다.""" rows = build_rows( SummaryInput( earthwork_totals=토적표합계(), rock_classes=list(ROCK_CLASS_SETS["geochang5"]), rock_ratios_pct={}, ) ) cut = [r for r in rows if r.group == "흙깎기"] assert [r.item for r in cut] == ["토사", "암"] assert cut[1].amount == pytest.approx(3512.09) def test_울진2갈래() -> None: rows = build_rows( SummaryInput( earthwork_totals=토적표합계(), rock_classes=list(ROCK_CLASS_SETS["uljin2"]), rock_ratios_pct={"연암": 20, "발파암": 80}, ) ) cut = [r for r in rows if r.group == "흙깎기"] assert [r.item for r in cut] == ["토사", "연암", "발파암"] assert cut[1].amount == pytest.approx(3512.09 * 0.2) assert cut[2].amount == pytest.approx(3512.09 * 0.8) def test_거창5갈래() -> None: """같은 코드가 갈래 수만 바꿔 선다 — 개수를 박지 않았다는 증거.""" rows = build_rows( SummaryInput( earthwork_totals=토적표합계(), rock_classes=list(ROCK_CLASS_SETS["geochang5"]), rock_ratios_pct={"풍화암": 10, "연암": 60, "보통암": 25, "경암": 5}, ) ) cut = [r for r in rows if r.group == "흙깎기"] assert [r.item for r in cut] == ["토사", "풍화암", "연암", "보통암", "경암"] assert sum(r.amount for r in cut[1:]) == pytest.approx(3512.09) def test_비율_합이_100이_아니어도_총량은_보존() -> None: """설계자가 60/30 만 넣어도 암 총량이 새면 안 된다 — 준 비율끼리 안분한다.""" rows = build_rows( SummaryInput( earthwork_totals=토적표합계(), rock_classes=list(ROCK_CLASS_SETS["uljin2"]), rock_ratios_pct={"연암": 60, "발파암": 30}, ) ) cut = [r for r in rows if r.group == "흙깎기"] assert sum(r.amount for r in cut[1:]) == pytest.approx(3512.09) # 60/30 이 실제로는 66.7/33.3 으로 돈다 — 값이 말없이 바뀌므로 비고에 드러낸다. assert cut[1].amount == pytest.approx(3512.09 * 60 / 90) assert "90" in cut[1].note and "안분" in cut[1].note def test_비율_합이_100이면_비고가_비어_있음() -> None: """제대로 넣었는데 안내가 뜨면 잡음이 된다.""" rows = build_rows( SummaryInput( earthwork_totals=토적표합계(), rock_classes=list(ROCK_CLASS_SETS["uljin2"]), rock_ratios_pct={"연암": 20, "발파암": 80}, ) ) cut = [r for r in rows if r.group == "흙깎기"] assert all(r.note == "" for r in cut[1:]) # ── 반영률 — 기본 100 % ───────────────────────────────────────── def test_반영률_기본은_100퍼센트() -> None: rows = build_rows(SummaryInput(earthwork_totals=토적표합계(), slope_totals=사면합계())) compaction = next(r for r in rows if r.group == "성토면다짐") assert compaction.amount == pytest.approx(13518.6) assert compaction.note == "" # 기본이면 비고에 아무것도 안 적는다 def test_반영률을_주면_곱해지고_비고에_남음() -> None: """실무 시트가 비고란에 적던 그 자리다 — 누가 정한 값인지 보이게 한다.""" rows = build_rows( SummaryInput( earthwork_totals=토적표합계(), slope_totals=사면합계(), application_ratios={"fill_slope_compaction": 0.8}, ) ) compaction = next(r for r in rows if r.group == "성토면다짐") assert compaction.amount == pytest.approx(13518.6 * 0.8) assert "80" in compaction.note def test_초류종자살포는_성토_절토_따로() -> None: rows = build_rows( SummaryInput( earthwork_totals=토적표합계(), slope_totals=사면합계(), application_ratios={"seed_spray_fill": 0.5, "seed_spray_cut": 1.0}, ) ) seed = next(r for r in rows if r.group == "초류종자살포") assert seed.amount == pytest.approx(13518.6 * 0.5 + 5433.7) # ── 무대 — 집계에는 오르되 내역 줄이 아니다 ────────────────────── def test_무대는_내역줄이_아님() -> None: """품셈 1-2-7 — 소운반 20m 는 품에 포함이라 붙일 단가가 없다.""" source = SummaryInput( earthwork_totals=토적표합계(), haul_rows=[ {"equipment": "free_haul", "ground": "토사", "volume_m3": 871, "average_distance_m": 11.94}, {"equipment": "dozer", "ground": "토사", "volume_m3": 1170, "average_distance_m": 43.66}, {"equipment": "dump_truck", "ground": "암", "volume_m3": 1714, "average_distance_m": 318.6}, ], ) rows = build_rows(source) free = next(r for r in rows if r.group.startswith("무대")) assert free.in_bill is False assert "내역 제외" in free.note assert free.amount == pytest.approx(871) assert all(r.in_bill for r in rows if r.group in ("도자운반", "덤프운반")) def test_무대를_내되_검산에_씀() -> None: """무대를 아예 안 내면 `무대+도자+덤프 = 총 운반토량` 검산이 죽는다.""" source = SummaryInput( earthwork_totals=토적표합계(), haul_rows=[ {"equipment": "free_haul", "volume_m3": 1000}, {"equipment": "dozer", "volume_m3": 2000}, {"equipment": "dump_truck", "volume_m3": 2801.21}, ], ) check = haul_check(source, 토적표합계()) assert check["hauled_total_m3"] == pytest.approx(5801.21) assert check["difference_m3"] == pytest.approx(0.0, abs=0.01) def test_평균운반거리가_비고에_남음() -> None: rows = build_rows( SummaryInput( earthwork_totals=토적표합계(), haul_rows=[{"equipment": "dozer", "ground": "토사", "volume_m3": 1170, "average_distance_m": 43.66}], ) ) dozer = next(r for r in rows if r.group == "도자운반") assert "43.66" in dozer.note # ── 표 모양 ───────────────────────────────────────────────────── def test_열_구성은_실무_시트_그대로() -> None: table = build_table(SummaryInput(earthwork_totals=토적표합계())) assert table["columns"] == ["구분", "공종", "규격", "단위", "계", "비고"] def test_단위를_섞어_더하지_않음() -> None: rows = build_rows(SummaryInput(earthwork_totals=토적표합계(), slope_totals=사면합계())) totals = totals_by_unit(rows) assert set(totals) == {"㎥", "㎡"} # ── 프로젝트 설정 ──────────────────────────────────────────────── def test_기본설정_모양(tmp_path: Path) -> None: settings = default_settings() assert settings["schema_version"] == 1 assert set(settings) == {"schema_version", "quantity", "estimation"} quantity = settings["quantity"] # override 는 기본이 None — 「안 정했으면 config 정본을 쓴다」는 뜻. assert quantity["conversion_factors_override"] is None # 도쟈 한계거리 — 안 읽히던 `haul_limits_m_override` 를 갈음(2026-09-13). None 이면 기본 60 m. assert quantity["dozer_haul_limit_m"] is None # 반영률 기본 100. 실무 관측 80/50/80 을 넣지 않는다. assert quantity["application_ratios_pct"] == {key: 100 for key in APPLICATION_RATIO_KEYS} # estimation 은 자리만 — 채우는 것은 B09 몫. # ⚠ 「연도」가 아니라 **판**을 가리킨다 — 제비율은 연중에도 개정된다(현행판 2026-04-13). assert settings["estimation"]["rate_dataset"] is None assert "rate_year" not in settings["estimation"] def test_파일이_없으면_기본값(tmp_path: Path) -> None: assert load_settings(tmp_path) == default_settings() def test_깨진_파일이어도_화면은_서야_함(tmp_path: Path) -> None: (tmp_path / "project_settings.json").write_text("{ 망가짐", encoding="utf-8") assert load_settings(tmp_path) == default_settings() def test_한_구획만_갈아끼움(tmp_path: Path) -> None: """두 페이지가 같은 파일을 쓴다 — 통째로 덮으면 상대 값이 사라진다.""" save_section(tmp_path, "estimation", {"rate_dataset": {"dataset_id": "rates", "effective_date": "2026-04-13"}}) save_section(tmp_path, "quantity", {"rock_class_set": "uljin2"}) stored = json.loads((tmp_path / "project_settings.json").read_text(encoding="utf-8")) # 남의 구획이 살아 있다 — B08 이 저장해도 B09 값이 안 지워진다. assert stored["estimation"]["rate_dataset"]["effective_date"] == "2026-04-13" assert stored["quantity"]["rock_class_set"] == "uljin2" def test_모르는_구획은_거부(tmp_path: Path) -> None: with pytest.raises(ValueError): save_section(tmp_path, "hacked", {}) def test_암갈래는_세트에서(tmp_path: Path) -> None: save_section(tmp_path, "quantity", {"rock_class_set": "uljin2", "rock_classes": None}) settings = quantity_settings(tmp_path) settings["rock_classes"] = None assert rock_classes(settings) == list(ROCK_CLASS_SETS["uljin2"]) def test_반영률_읽기() -> None: settings = default_settings()["quantity"] assert application_ratio(settings, "obstacle_removal") == pytest.approx(1.0) settings["application_ratios_pct"]["obstacle_removal"] = 80 assert application_ratio(settings, "obstacle_removal") == pytest.approx(0.8)