"""관측 원단위표 검사 — PLAN 8-6·8-8 · 2026-09-07 3자 승인. 이 일감의 위험은 계산이 아니라 **관측값을 늘려 쓰는 것**이다. · 관측값은 그 규격에서만 맞다 — 벽·기초는 높이에 비례하지 않는다. · 규격이 표에 없으면 가까운 값을 갖다 쓰지 않고 「원단위 미확보」로 드러낸다. · 두 근거(식에서 나온 값 / 관측값)가 한 표에 섞이므로 줄마다 근거를 단다. """ from __future__ import annotations 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_ObservedUnit import ( # noqa: E402 BASIS_OBSERVED, NOTE_UNIT_MISSING, ObservedUnitTable, expand_observed, load_observed_table, scale_for, ) from B08_Quantity.B08_Quantity_Engine_UnitQuantity import ( # noqa: E402 BASIS_DERIVED, build_table, expand, ) def 옹벽(height: float = 2.0, length: float = 10.0, form: str = "반중력식") -> dict: return { "structure_id": "w1", "type_id": "retaining_wall", "start_m": 100.0, "end_m": 100.0 + length, "options": {"form": form, "height_m": height, "length_m": length}, } def 성분(result, name: str): return next(c for c in result.components if c.name == name) # ── 규격이 맞을 때만 쓴다 ─────────────────────────────────────────── def test_규격이_맞으면_연장만큼_곱해짐() -> None: """`H=2.0 옹벽 10m` 는 같은 단면이 10m 이어진 것 — 개수를 세는 것이지 규격을 늘리는 게 아니다.""" result = expand(옹벽(length=10.0)) # 원문 값 1.345 ㎥/m × 10 · 유로폼 3.205(2026-09-14 · 전 라이브러리 1.35 → 13.5 · 3.20 → 32.0) assert 성분(result, "콘크리트").amount == pytest.approx(13.45) assert 성분(result, "유로폼").amount == pytest.approx(32.05) def test_높이가_다르면_비례로_늘리지_않고_미확보() -> None: """⚠ 이 시험이 이 파일의 핵심 — H=1.6 은 표에 없다. 1.35 × 0.8 로 만들지 않는다.""" result = expand(옹벽(height=1.6)) assert result.components == [] # ⚠ 사용자에게 뜨는 말로 잰다 — 키 이름이 새면 안 된다(㉑). assert any("자료에 없습니다" in note for note in result.notes) def test_형식이_다르면_미확보() -> None: result = expand(옹벽(form="캔틸레버식")) assert result.components == [] # ⚠ 사용자에게 뜨는 말로 잰다 — 키 이름이 새면 안 된다(㉑). assert any("자료에 없습니다" in note for note in result.notes) def test_미확보_알림에_있는_규격을_같이_알려줄것() -> None: """「없다」만 말하면 사용자가 무엇을 고쳐야 할지 모른다. ⚠ 규격도 **사람 말**로 보인다 — `form`·`height_m` 이 아니라 「옹벽 형식」·「높이(m)」.""" result = expand(옹벽(height=1.6)) note = next(n for n in result.notes if "자료에 있는 규격" in n) assert "옹벽 형식 반중력식" in note and "높이(m) 2.0" in note assert "form" not in note and "height_m" not in note def test_규격이_비어_있으면_고르지_못함을_알림() -> None: result = expand({"type_id": "retaining_wall", "options": {"length_m": 5.0}}) assert result.components == [] assert result.notes # ── 근거를 줄마다 단다 ────────────────────────────────────────────── def test_관측값에는_observed_근거가_붙음() -> None: result = expand(옹벽()) for component in result.components: assert component.basis_kind == BASIS_OBSERVED assert "관측 원단위" in component.basis assert component.source def test_식에서_나온_값은_derived_로_남음() -> None: """두 근거가 한 표에 섞이므로 갈라 보여야 한다.""" stone = expand( { "type_id": "masonry_wet", "start_m": 0.0, "end_m": 10.0, "options": {"height_m": 1.5, "length_m": 10.0}, } ) assert all(c.basis_kind == BASIS_DERIVED for c in stone.components) def test_표에도_근거가_실림() -> None: """⚠ 관측 줄에 **파생 줄 하나가 섞인다** — 기초잡석은 버림 폭에서 나오는 값이라 관측이 아니라 `derived` 다(2026-09-09 확정 3차 ②). 근거가 갈려 실리는 것이 맞다.""" table = build_table([옹벽()]) components = [c for s in table["structures"] for c in s["components"]] # 2026-09-14 — 단면으로 세운 터파기·되메우기·잔토도 파생 줄(브레인 판정 ①). derived = {"기초잡석", "터파기", "되메우기", "잔토처리"} kinds = {c["basis_kind"] for c in components if c["name"] not in derived} assert kinds == {BASIS_OBSERVED} assert {c["basis_kind"] for c in components if c["name"] in derived} == {BASIS_DERIVED} 잡석 = next(c for c in components if c["name"] == "기초잡석") assert 잡석["basis_kind"] == BASIS_DERIVED # ── 이중계상 규칙은 그대로 ────────────────────────────────────────── def test_관측값에도_배합이_섞이지_않음() -> None: """㉢ — 콘크리트 ㎥ 까지만. 시멘트·모래로 쪼개는 것은 B09 몫이다.""" assert build_table([옹벽()])["mix_components_found"] == [] def test_터파기는_토공으로_되메우기는_토공으로() -> None: """관측 원단위의 터파기도 내역 줄이 아니라 토공 합산이다.""" table = load_observed_table() entry = table.find("pipe_inlet_basin", {"inlet_basin_form": "돌집수정 ㄷ형"}) destinations = {c["name"]: c["destination"] for c in entry["components"]} assert destinations["터파기"] == "earthwork" assert destinations["잔토처리"] == "earthwork" assert destinations["콘크리트"] == "unit_price" def test_할증은_여전히_자재총괄_몫() -> None: assert build_table([옹벽()])["surcharge_applied"] is False # ── 표 자체 ───────────────────────────────────────────────────────── def test_실제_데이터판이_읽힘() -> None: table = load_observed_table() assert table.effective_date assert table.find("retaining_wall", {"form": "반중력식", "height_m": 2.0}) def test_숫자와_글자_규격을_같이_본다() -> None: """입력 폼이 `"800"` 을 문자열로 준다 — 그렇다고 `2.0` 과 `1.6` 을 같다고 보면 안 된다.""" table = load_observed_table() assert table.find( "pipe_inlet_basin", { "inlet_basin_form": "□형(기본형)", "inlet_basin_material": "콘크리트", "pipe_diameter_mm": 800, }, ) assert table.find("retaining_wall", {"form": "반중력식", "height_m": 1.9999}) is None def test_BOX암거는_미확보로_남음() -> None: """⚠ 두께가 저장돼 있지 않아 식도 못 세우고 관측값도 없다 — 지어내면 콘크리트·거푸집· 철근으로 번져 나간다. 사용자 확정 대기.""" table = load_observed_table() assert table.find("box_culvert", {"body_width_m": 3.0, "body_height_m": 1.2}) is None reasons = [item["type_id"] for item in table.not_found["items"]] assert "box_culvert" in reasons def test_파일이_없으면_전부_미확보() -> None: empty = ObservedUnitTable() components, notes = expand_observed("retaining_wall", {"form": "반중력식"}, 옹벽(), empty) assert components == [] # 자료가 통째로 없으면 「표준 물량 자료가 없다」로 말한다. assert "표준 물량 자료가 아직 없습니다" in notes[0] def test_곱할_연장이_0이면_내지_않음() -> None: result = expand(옹벽(length=0.0)) assert result.components == [] def test_개소당_원단위는_안_곱해짐() -> None: """집수정은 1개소가 1개소다 — 연장을 곱하면 값이 부푼다.""" table = load_observed_table() entry = table.find("pipe_inlet_basin", {"inlet_basin_form": "돌집수정 ㄴ형"}) scale, note = scale_for(entry, {"options": {"length_m": 10.0}}) assert scale == 1.0 assert "개소" in note