① 터파기·되메우기·잔토 — 소광리 「옹벽2.0」에 줄이 없어 브레인 판정대로 기초 폭 + 양쪽 여유 0.3(같은 파일 식생옹벽블럭 기초 터파기)로 수직 · 깊이 기초 0.4 + 버림 0.1 + 기초잡석 · 비탈분 제외(지반선이 제원에 없음) · earthwork 로만 m당 터파기 1.54 · 되메우기 0.36(터파기 − 기초·전단키·버림·잡석) · 잔토 1.18 · 인계 심도 구분은 판 깊이(0.7 m → 0~1m) ② 관측 값을 원문으로 — 콘크리트 1.35→1.345 · 버림 0.15→0.145 · 유로폼 3.2→3.205 ⇒ 기초잡석 0.30→0.29 ③ 물구멍관 — 실무 관측 식(「옹벽2.0」 T31 벽 높이 ÷ 개소당 면적 × 관 0.4)에 제원 칸이 닿음 · 규격 Ø50 이 붙어 자재총괄에서 돌쌓기 물구멍관과 한 줄(33.924 + 3.2 → 37.124 m) · 관 길이는 관측값으로 표시 - 검증 프로젝트 내역 본체 122,848,989 → 124,202,681원(+1,353,692 = 구조물터파기 심도 0~1m 15.4㎥ 1,341,216 + 되메우기 21.0→24.6㎥ +12,476) - 그림: 터파기 점선·「터파기 폭 2.20 × 깊이 0.70」 · 표 사유와 같은 말 · 옛 값을 박은 시험 8건을 원문 값으로(전 값은 주석) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
201 lines
8.5 KiB
Python
201 lines
8.5 KiB
Python
"""관측 원단위표 검사 — 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
|