feat(b08): 옹벽 구멍 셋 — 터파기 기초분 · 원문 값 · 물빼기 칸이 표에 닿음(전후 대조)
① 터파기·되메우기·잔토 — 소광리 「옹벽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
This commit is contained in:
@@ -221,7 +221,9 @@ def structure_earthwork_rows(
|
||||
for structure in unit_quantity_table.get("structures") or []:
|
||||
options = structure.get("options") or {}
|
||||
height = float(structure.get("height_m") or options.get("height_m") or 0.0)
|
||||
band = _depth_band(height + _foundation_depth(options))
|
||||
# 단면으로 판 깊이가 있으면 그것(옹벽은 기초분만 팜 · 비탈분 제외) — 없으면 직고 + 기초 깊이.
|
||||
dug = structure.get("trench_depth_m")
|
||||
band = _depth_band(float(dug) if dug else height + _foundation_depth(options))
|
||||
ground = structure.get("ground_type") or None
|
||||
ground_basis = str(structure.get("ground_type_basis") or "")
|
||||
for component in structure.get("components") or []:
|
||||
|
||||
@@ -151,23 +151,101 @@ def billing_of(
|
||||
return str(found.get("unit") or "개소"), scale
|
||||
|
||||
|
||||
#: 단면으로 터파기를 세운 줄의 사유 — 표 사유·구조물도 그림이 **같은 말**(브레인 판정 2026-09-14).
|
||||
SECTION_TRENCH_NOTE = "터파기는 기초 폭 + 양쪽 여유로 수직 — 지반선이 제원에 없어 비탈분 제외"
|
||||
|
||||
|
||||
def earthwork_missing_note(entry: dict[str, Any]) -> str | None:
|
||||
"""관측 줄에 토공 줄이 없으면 한 줄 — 표 사유·구조물도 그림이 **같은 말**을 씀."""
|
||||
"""관측 줄에 토공 줄이 없고 단면으로도 못 세우면 한 줄 — 표 사유·구조물도 그림이 같은 말."""
|
||||
if any(item.get("destination") == "earthwork" for item in entry.get("components") or []):
|
||||
return None
|
||||
if (entry.get("section") or {}).get("trench"):
|
||||
return SECTION_TRENCH_NOTE
|
||||
return "⚠ 터파기·되메우기·잔토가 안 섬 — 관측 원단위 표에 그 줄이 없음(값을 지어내지 않음)"
|
||||
|
||||
|
||||
def weep_option_note(entry: dict[str, Any], options: dict[str, Any]) -> str | None:
|
||||
"""제원의 물빼기 칸을 채웠는데 관측 값이 붙박이라 **표에 안 닿을 때** 한 줄(표·그림 같은 말)."""
|
||||
weep = (entry.get("section") or {}).get("weep")
|
||||
keys = ("weep_hole_diameter_mm", "weep_hole_area_m2")
|
||||
if not weep or all(options.get(key) in (None, "") for key in keys):
|
||||
def weep_component(
|
||||
entry: dict[str, Any], options: dict[str, Any], scale: float, scale_note: str
|
||||
) -> dict[str, Any] | None:
|
||||
"""물구멍관 — 단면이 있는 관측 줄은 **실무 식에 제원 칸을 넣어** 다시 셈(붙박이 값 대신).
|
||||
|
||||
식 `벽 높이 ÷ 개소당 벽면적 × 관 길이`(소광리 「옹벽2.0」 T31). 관 길이는 실무 관측값이라
|
||||
`basis_kind` 는 그대로 `observed`. 규격 `Ø지름` 을 달아 돌쌓기 물구멍관과 (이름+규격)으로 합침.
|
||||
"""
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_StoneSpec import weep_hole_spec
|
||||
|
||||
section = entry.get("section") or {}
|
||||
weep = section.get("weep")
|
||||
if not weep:
|
||||
return None
|
||||
return (
|
||||
f"⚠ 물빼기 지름·면적 칸이 이 표에 안 닿음 — 관측 원단위 붙박이(Ø{weep['diameter_mm']}"
|
||||
f" · {weep['area_m2']:g}㎡당 1개소 · 관 {weep['pipe_length_m']:g} m)"
|
||||
area, diameter, spec_basis = weep_hole_spec(options)
|
||||
height, length = section["wall"]["height_m"], weep["pipe_length_m"]
|
||||
per_unit = height / area * length
|
||||
return {
|
||||
"name": "물구멍관",
|
||||
"unit": "m",
|
||||
"amount": per_unit * scale,
|
||||
"destination": "material",
|
||||
"basis": (
|
||||
f"관측 원단위 식(소광리 「옹벽2.0」 T31) 벽 높이 {height:g} ÷ {area:g}㎡/개소"
|
||||
f" × 관 {length:g} m = {per_unit:.3f}/{entry.get('unit')} × {scale_note}"
|
||||
f" · 관 길이는 관측값 · {spec_basis}"
|
||||
),
|
||||
"basis_kind": BASIS_OBSERVED,
|
||||
"source": str(entry.get("source") or ""),
|
||||
"spec": f"Ø{diameter}",
|
||||
}
|
||||
|
||||
|
||||
def section_trench_components(
|
||||
entry: dict[str, Any], rubble_thickness_m: float, scale: float
|
||||
) -> tuple[list[dict[str, Any]], float] | None:
|
||||
"""단면이 있는 관측 줄의 터파기·되메우기·잔토(`destination: earthwork`)와 파는 깊이(m).
|
||||
|
||||
⚠ 브레인 판정 — 기초 폭 + 양쪽 여유(0.3, 같은 파일 식생옹벽블럭 기초 터파기)로 **수직** ·
|
||||
깊이 = 기초 + 버림 + 기초잡석 · **비탈분(벽 높이 몫)은 안 셈**(지반선이 제원에 없음).
|
||||
⚠ 되메우기 = 터파기 − 그 안에 든 것(기초·전단키 콘크리트 + 버림 + 기초잡석) — 잔토 = 든 것.
|
||||
"""
|
||||
section = entry.get("section") or {}
|
||||
trench = section.get("trench")
|
||||
if not trench:
|
||||
return None
|
||||
footing, key, blinding = section["footing"], section["key"], section["blinding"]
|
||||
clearance = trench["clearance_m"]
|
||||
width = footing["width_m"] + 2 * clearance
|
||||
depth = footing["thickness_m"] + blinding["thickness_m"] + rubble_thickness_m
|
||||
key_below = max(key["depth_m"] - blinding["thickness_m"] - rubble_thickness_m, 0.0)
|
||||
excavation = width * depth + key["width_m"] * key_below
|
||||
layer_width = footing["width_m"] + 2 * blinding["overhang_m"] - key["width_m"]
|
||||
filled = (
|
||||
footing["width_m"] * footing["thickness_m"]
|
||||
+ key["width_m"] * key["depth_m"]
|
||||
+ layer_width * (blinding["thickness_m"] + rubble_thickness_m)
|
||||
)
|
||||
backfill = excavation - filled
|
||||
basis = (
|
||||
f"(기초 폭 {footing['width_m']:g} + 여유 {clearance:g}×2) × 깊이 {depth:.2f}"
|
||||
f"(기초 {footing['thickness_m']:g} + 버림 {blinding['thickness_m']:g}"
|
||||
f" + 기초잡석 {rubble_thickness_m:g})"
|
||||
f" · {SECTION_TRENCH_NOTE} · 여유는 소광리 식생옹벽블럭 기초 터파기"
|
||||
)
|
||||
rows = [
|
||||
("터파기", excavation, basis),
|
||||
("되메우기", backfill, f"터파기 − 든 것 {filled:.3f}(기초·전단키 + 버림 + 기초잡석)"),
|
||||
("잔토처리", filled, "터파기 − 되메우기"),
|
||||
]
|
||||
return [
|
||||
{
|
||||
"name": name,
|
||||
"unit": "㎥",
|
||||
"amount": amount * scale,
|
||||
"destination": "earthwork",
|
||||
"basis": f"{text} = {amount:.3f}/{entry.get('unit')}",
|
||||
"basis_kind": BASIS_DERIVED,
|
||||
"source": str(entry.get("source") or ""),
|
||||
}
|
||||
for name, amount, text in rows
|
||||
], depth
|
||||
|
||||
|
||||
def expand_observed(
|
||||
@@ -192,9 +270,18 @@ def expand_observed(
|
||||
return [], [f"{type_label(type_id)}의 연장·면적이 0 이라 물량을 내지 않았습니다"]
|
||||
|
||||
source_key = str(found.get("source") or "")
|
||||
options = structure.get("options") or {}
|
||||
components: list[dict[str, Any]] = []
|
||||
for item in found.get("components") or []:
|
||||
note = str(item.get("basis_note") or "")
|
||||
weep = (
|
||||
weep_component(found, options, scale, scale_note)
|
||||
if item["name"] == "물구멍관"
|
||||
else None
|
||||
)
|
||||
if weep is not None:
|
||||
components.append(weep)
|
||||
continue
|
||||
components.append(
|
||||
{
|
||||
"name": item["name"],
|
||||
@@ -209,10 +296,7 @@ def expand_observed(
|
||||
}
|
||||
)
|
||||
notes = [f"관측 원단위 적용 — {found.get('source_note') or source_key}"]
|
||||
for note in (
|
||||
earthwork_missing_note(found),
|
||||
weep_option_note(found, structure.get("options") or {}),
|
||||
):
|
||||
if note:
|
||||
notes.append(note)
|
||||
earthwork_note = earthwork_missing_note(found)
|
||||
if earthwork_note:
|
||||
notes.append(earthwork_note)
|
||||
return components, notes
|
||||
|
||||
@@ -236,10 +236,7 @@ def _wall_figure(
|
||||
x 는 앞굽 끝이 0(앞면 왼쪽), y 는 기초 밑면이 0. 버림·기초잡석은 기초 밑, 전단키가 뚫고 내려감.
|
||||
⚠ 기초잡석 두께는 표의 기초잡석 줄과 같은 산출 조건 값(비면 기본) — 폭은 버림과 같음.
|
||||
"""
|
||||
from B08_Quantity.B08_Quantity_Engine_ObservedUnit import (
|
||||
earthwork_missing_note,
|
||||
weep_option_note,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_ObservedUnit import earthwork_missing_note
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import RUBBLE_BASE_THICKNESS_M
|
||||
|
||||
section = entry["section"]
|
||||
@@ -297,8 +294,26 @@ def _wall_figure(
|
||||
shapes.append(
|
||||
_path([(toe + front * ratio, weep_y), (toe + w_bottom - back * ratio, weep_y)], "guide")
|
||||
)
|
||||
# 터파기 — 표 줄과 같은 식(기초 폭 + 양쪽 여유 · 수직 · 기초 윗면에서 기초잡석 밑까지). 점선.
|
||||
trench = section.get("trench")
|
||||
clearance = trench["clearance_m"] if trench else 0.0
|
||||
dig_bottom = -(b_t + rubble)
|
||||
if trench:
|
||||
shapes.append(
|
||||
_path(
|
||||
[
|
||||
(-clearance, f_t),
|
||||
(-clearance, dig_bottom),
|
||||
(f_w + clearance, dig_bottom),
|
||||
(f_w + clearance, f_t),
|
||||
],
|
||||
"guide",
|
||||
dash=True,
|
||||
)
|
||||
)
|
||||
|
||||
right = f_w + over + 0.2
|
||||
right = max(f_w + over, f_w + clearance) + 0.2
|
||||
weep_area, weep_dia, _weep_basis = weep_hole_spec(sheet.get("options") or {})
|
||||
blinding_width = f_w + 2 * over - k_w
|
||||
shapes += [
|
||||
_text(
|
||||
@@ -316,8 +331,8 @@ def _wall_figure(
|
||||
"left",
|
||||
),
|
||||
_text(
|
||||
f"물구멍 Ø{weep['diameter_mm']} — {weep['area_m2']:g}㎡당 1개소"
|
||||
f" · 관 {weep['pipe_length_m']:g} m",
|
||||
f"물구멍 Ø{weep_dia} — {weep_area:g}㎡당 1개소"
|
||||
f" · 관 {weep['pipe_length_m']:g} m(관측값)",
|
||||
(right, weep_y),
|
||||
"left",
|
||||
),
|
||||
@@ -341,11 +356,19 @@ def _wall_figure(
|
||||
"left",
|
||||
)
|
||||
)
|
||||
if trench:
|
||||
shapes.append(
|
||||
_text(
|
||||
f"터파기 폭 {f_w + 2 * clearance:.2f} × 깊이 {f_t - dig_bottom:.2f} m"
|
||||
f" (기초 {f_w:g} + 여유 {clearance:g}×2)",
|
||||
(right, f_t + 0.25),
|
||||
"left",
|
||||
)
|
||||
)
|
||||
below = -max(k_d, b_t + rubble) - 0.36
|
||||
notes = [
|
||||
f"단면 — 관측 원단위 {entry.get('source_note') or ''}의 수량이 나온 그 단면",
|
||||
earthwork_missing_note(entry),
|
||||
weep_option_note(entry, sheet.get("options") or {}),
|
||||
]
|
||||
for index, note in enumerate(note for note in notes if note):
|
||||
shapes.append(_text(note, (0.0, below - index * 0.16), "left"))
|
||||
|
||||
@@ -448,6 +448,33 @@ def _rubble_base_component(
|
||||
)
|
||||
|
||||
|
||||
def _append_section_trench(
|
||||
quantity: StructureQuantity,
|
||||
structure: dict[str, Any],
|
||||
observed: ObservedUnitTable,
|
||||
thickness_m: float | None,
|
||||
) -> None:
|
||||
"""단면이 실린 관측 줄(옹벽)의 터파기·되메우기·잔토 — 기초잡석과 **같은 두께**로 깊이를 잡음."""
|
||||
from B08_Quantity.B08_Quantity_Engine_ObservedUnit import scale_for, section_trench_components
|
||||
|
||||
keys = OBSERVED_SPEC_KEYS.get(quantity.type_id)
|
||||
if not keys or not quantity.components:
|
||||
return
|
||||
options = structure.get("options") or {}
|
||||
entry = observed.find(
|
||||
quantity.type_id, {key: options[key] for key in keys if options.get(key) is not None}
|
||||
)
|
||||
if entry is None:
|
||||
return
|
||||
scale, _note = scale_for(entry, structure)
|
||||
thickness = RUBBLE_BASE_THICKNESS_M if thickness_m is None else float(thickness_m)
|
||||
found = section_trench_components(entry, max(thickness, 0.0), scale)
|
||||
if found is None:
|
||||
return
|
||||
rows, quantity.trench_depth_m = found
|
||||
quantity.components.extend(Component(**row) for row in rows)
|
||||
|
||||
|
||||
def build_table(
|
||||
structures: Iterable[dict[str, Any]],
|
||||
names: dict[str, str] | None = None,
|
||||
@@ -482,6 +509,7 @@ def build_table(
|
||||
rubble = _rubble_base_component(quantity.components, rubble_base_thickness_m)
|
||||
if rubble is not None:
|
||||
quantity.components.append(rubble)
|
||||
_append_section_trench(quantity, item, observed, rubble_base_thickness_m)
|
||||
quantities.append(quantity)
|
||||
if use_templates:
|
||||
# 늦게 부름 — 양식 모듈이 이 모듈을 부르므로 맨 위에서 부르면 맞물림.
|
||||
@@ -531,6 +559,8 @@ def build_table(
|
||||
# ⚠ 여기서 새로 만드는 값이 아니라 측점 설계값(`design.ground_type`)을 옮긴 것이다.
|
||||
"ground_type": item.ground_type,
|
||||
"ground_type_basis": item.ground_type_basis,
|
||||
# 파는 깊이 — 단면으로 터파기를 세운 종류만(옹벽 기초분). 비면 직고 + 기초 깊이.
|
||||
"trench_depth_m": item.trench_depth_m,
|
||||
# 양식 있음/없음 — 화면이 가림(비면 지금 전개).
|
||||
"library_item": item.library_item,
|
||||
"notes": item.notes,
|
||||
|
||||
@@ -104,6 +104,8 @@ class StructureQuantity:
|
||||
#: 품셈 9-13 구조물터파기의 **토질 축**이 이 값으로 갈린다. 못 가르면 `None` 이다.
|
||||
ground_type: str | None = None
|
||||
ground_type_basis: str = ""
|
||||
#: 파는 깊이(m) — 단면으로 터파기를 세운 종류만(옹벽 기초분). 비면 인계가 직고 + 기초 깊이로 봄.
|
||||
trench_depth_m: float | None = None
|
||||
#: 양식으로 성분을 세웠으면 그 양식 이름(PLAN 3장 ④-2). 비면 지금 전개 값.
|
||||
library_item: str = ""
|
||||
|
||||
|
||||
@@ -34,33 +34,41 @@
|
||||
"source": "uljin_library",
|
||||
"source_note": "§7 옹벽류 — 반중력식옹벽 H=2.0",
|
||||
"section": {
|
||||
"note": "단면 치수(m) — 위 수량이 나온 그 단면. 출처 소광리 07-구조도 「옹벽2.0」 탭 산출식(T22 기초 0.4×1.6+0.3×0.35 · T23 벽체 (0.3+0.45)÷2×1.6 · T25 버림 0.1×(1.8−0.35) · T26·T27 유로폼 √(1.6²+0.032²)+√(1.6²+0.118²) · T31 파이프 1.6÷2×0.4)과 같은 탭 일반도(앞굽 950·뒷굽 200·키 자리 뒷굽 끝에서 500). 원문 1.345·0.145·3.205 를 라이브러리가 1.35·0.15·3.2 로 적음",
|
||||
"note": "단면 치수(m) — 아래 수량이 나온 그 단면. 출처 소광리 07-구조도 「옹벽2.0」 탭 산출식(T22 기초 0.4×1.6+0.3×0.35 · T23 벽체 (0.3+0.45)÷2×1.6 · T25 버림 0.1×(1.8−0.35) · T26·T27 유로폼 √(1.6²+0.032²)+√(1.6²+0.118²) · T31 파이프 1.6÷2×0.4)과 같은 탭 일반도(앞굽 950·뒷굽 200·키 자리 뒷굽 끝에서 500)",
|
||||
"footing": { "width_m": 1.6, "thickness_m": 0.4, "toe_m": 0.95 },
|
||||
"wall": { "height_m": 1.6, "top_m": 0.3, "front_batter_m": 0.118, "back_batter_m": 0.032 },
|
||||
"key": { "width_m": 0.35, "depth_m": 0.3, "from_heel_m": 0.5 },
|
||||
"blinding": { "thickness_m": 0.1, "overhang_m": 0.1 },
|
||||
"weep": { "area_m2": 2.0, "diameter_mm": 50, "pipe_length_m": 0.4 }
|
||||
"weep": {
|
||||
"pipe_length_m": 0.4,
|
||||
"note": "소광리 「옹벽2.0」 T31 「파이프 φ50 (1.6 ÷ 2) × 0.4」 — 벽 높이 ÷ 개소당 벽면적 × 관 길이. 관 길이 0.4 는 **실무 관측값**(품셈에 없음) · 개소당 벽면적·지름은 제원 칸(비우면 2㎡ · Ø50)"
|
||||
},
|
||||
"trench": {
|
||||
"clearance_m": 0.3,
|
||||
"note": "소광리 「옹벽2.0」 탭에 터파기 줄이 없음 → 같은 파일 「식생옹벽블럭(H=2.0)」 F31 기초 터파기 밑폭 = 기초 폭 + 0.6(양쪽 0.3). 브레인 판정(2026-09-14) — 수직 · 비탈분 제외(지반선이 제원에 없음)"
|
||||
}
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"name": "콘크리트",
|
||||
"unit": "㎥",
|
||||
"amount": 1.35,
|
||||
"amount": 1.345,
|
||||
"destination": "unit_price",
|
||||
"basis_note": "기초 0.75 + 벽체 0.60"
|
||||
"basis_note": "기초 0.745 + 벽체 0.60 · 원문 값(라이브러리 표기 1.35)"
|
||||
},
|
||||
{
|
||||
"name": "버림콘크리트",
|
||||
"unit": "㎥",
|
||||
"amount": 0.15,
|
||||
"destination": "unit_price"
|
||||
"amount": 0.145,
|
||||
"destination": "unit_price",
|
||||
"basis_note": "원문 값(라이브러리 표기 0.15)"
|
||||
},
|
||||
{
|
||||
"name": "유로폼",
|
||||
"unit": "㎡",
|
||||
"amount": 3.2,
|
||||
"amount": 3.205,
|
||||
"destination": "unit_price",
|
||||
"basis_note": "배면+전면"
|
||||
"basis_note": "배면+전면 · 원문 값(라이브러리 표기 3.20)"
|
||||
},
|
||||
{
|
||||
"name": "합판거푸집",
|
||||
|
||||
@@ -345,7 +345,7 @@
|
||||
"from_components": [
|
||||
"기초잡석"
|
||||
],
|
||||
"why": "관측 원단위에는 기초잡석 줄이 없으나 **버림 폭이 곧 잡석다짐 폭**이라(KCS 34 50 05) 버림 물량에서 두께 비로 나온다 — 버림 0.15㎥/m ÷ 0.1 = 폭 1.5m, 두께 0.2m(사용자 확정 3차 ②) ⇒ 0.30㎥/m."
|
||||
"why": "관측 원단위에는 기초잡석 줄이 없으나 **버림 폭이 곧 잡석다짐 폭**이라(KCS 34 50 05) 버림 물량에서 두께 비로 나온다 — 버림 0.145㎥/m ÷ 0.1 = 폭 1.45m(1.8 − 전단키 0.35), 두께 0.2m(사용자 확정 3차 ②) ⇒ 0.29㎥/m(원문 값 · 2026-09-14 라이브러리 반올림 0.15 에서 바꿈)."
|
||||
}
|
||||
],
|
||||
"why": "품셈 12장에 「옹벽」 공종이 없음. 실무 내역은 「반중력식옹벽 H=2.0」 한 줄이고 그 일위대가가 위 공종을 묶음.",
|
||||
|
||||
@@ -51,7 +51,8 @@ def test_횟수별_비율을_여기서_곱하지_않을것() -> None:
|
||||
euroform = next(
|
||||
c for s in table["structures"] for c in s["components"] if c["name"] == "유로폼"
|
||||
)
|
||||
assert euroform["amount"] == 32.0 # 3.20 ㎡/m × 10m — 46.1 % 를 곱하지 않았다
|
||||
# 3.205 ㎡/m × 10m — 46.1 % 를 곱하지 않았다(2026-09-14 원문 값 3.205 · 전 라이브러리 3.20 → 32.0)
|
||||
assert abs(euroform["amount"] - 32.05) < 1e-9
|
||||
# 비율표는 데이터에 있되 값에 안 걸린다.
|
||||
assert load_formwork_table().reuse_ratio_pct["plywood"]["3"] == 46.1
|
||||
|
||||
|
||||
@@ -791,11 +791,10 @@ def 조각(row: dict, code_fragment: str) -> dict:
|
||||
def test_조각마다_수량과_단위가_실릴것() -> None:
|
||||
"""코드만 보내면 받는 쪽이 상세 줄을 못 세운다."""
|
||||
row = build_handoff(unit_quantity_table=옹벽단위())["work_items"][0]
|
||||
assert 조각(row, "12-01-01")["quantity"] == pytest.approx(
|
||||
15.0
|
||||
) # 콘크리트 1.35 + 버림 0.15, ×10m
|
||||
# 콘크리트 1.345 + 버림 0.145, ×10m — 2026-09-14 원문 값(전 라이브러리 1.35 + 0.15 → 15.0)
|
||||
assert 조각(row, "12-01-01")["quantity"] == pytest.approx(14.9)
|
||||
assert 조각(row, "12-04")["quantity"] == pytest.approx(6.0)
|
||||
assert 조각(row, "12-38")["quantity"] == pytest.approx(32.0)
|
||||
assert 조각(row, "12-38")["quantity"] == pytest.approx(32.05)
|
||||
|
||||
|
||||
def test_철근은_ton_으로_환산될것() -> None:
|
||||
@@ -862,12 +861,12 @@ def test_기초잡석은_버림_폭에서_선다() -> None:
|
||||
|
||||
그 조각이 **채워졌다**(2026-09-09 확정 3차 ②). 버림 폭이 곧 잡석다짐 폭이라
|
||||
(KCS 34 50 05) 두께 비만 곱하면 관측 원단위 구조물에도 값이 선다 —
|
||||
버림 1.5㎥ × (0.2 ÷ 0.1) = 3.0㎥(= 0.30㎥/m × 10m).
|
||||
버림 1.45㎥ × (0.2 ÷ 0.1) = 2.9㎥(= 0.29㎥/m × 10m · 2026-09-14 원문 버림 0.145, 전 3.0).
|
||||
「못 채운 조각은 0 이 아니라 사유와 함께」라는 규칙 자체는 다른 조각 시험이 지킨다.
|
||||
"""
|
||||
row = build_handoff(unit_quantity_table=옹벽단위())["work_items"][0]
|
||||
gravel = 조각(row, "12-25")
|
||||
assert gravel["quantity"] == 3.0
|
||||
assert gravel["quantity"] == pytest.approx(2.9)
|
||||
assert not gravel.get("not_ready")
|
||||
|
||||
|
||||
|
||||
@@ -51,8 +51,9 @@ def 성분(result, name: str):
|
||||
def test_규격이_맞으면_연장만큼_곱해짐() -> None:
|
||||
"""`H=2.0 옹벽 10m` 는 같은 단면이 10m 이어진 것 — 개수를 세는 것이지 규격을 늘리는 게 아니다."""
|
||||
result = expand(옹벽(length=10.0))
|
||||
assert 성분(result, "콘크리트").amount == pytest.approx(13.5) # 1.35 ㎥/m × 10
|
||||
assert 성분(result, "유로폼").amount == pytest.approx(32.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:
|
||||
@@ -114,8 +115,11 @@ def test_표에도_근거가_실림() -> None:
|
||||
관측이 아니라 `derived` 다(2026-09-09 확정 3차 ②). 근거가 갈려 실리는 것이 맞다."""
|
||||
table = build_table([옹벽()])
|
||||
components = [c for s in table["structures"] for c in s["components"]]
|
||||
kinds = {c["basis_kind"] for c in components if c["name"] != "기초잡석"}
|
||||
# 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
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
⚠ 겨누는 것 여섯
|
||||
① 두께는 **0.2 m**(사용자 확정) — 품셈 12-25 는 ㎥당 품만 주고 두께를 안 정함
|
||||
② 폭을 다시 세지 않는다 — **버림 폭이 곧 잡석다짐 폭**(KCS 34 50 05)이라 두께 비만 곱함
|
||||
③ ⚠ 그래서 **관측 원단위로 오는 옹벽에도 값이 선다**(버림 0.15㎥/m ⇒ 잡석 0.30㎥/m)
|
||||
③ ⚠ 그래서 **관측 원단위로 오는 옹벽에도 값이 선다**(버림 0.145㎥/m ⇒ 잡석 0.29㎥/m
|
||||
· 2026-09-14 원문 값 — 전 라이브러리 반올림 0.15 ⇒ 0.30)
|
||||
④ 두께는 화면에서 바꿀 수 있다
|
||||
⑤ ⚠ 자재총괄에 안 섞인다 — 운반·부설·다짐 품이 붙는 **공종**이다
|
||||
⑥ ⚠ 묶음으로 서는 구조물은 줄을 또 세우지 않는다(옹벽 조각이 이미 셈)
|
||||
@@ -86,7 +87,7 @@ def test_버림_두께_비로_나온다() -> None:
|
||||
|
||||
def test_관측_원단위_구조물에도_선다() -> None:
|
||||
"""⚠ 옹벽은 관측 원단위로 오는데 그 표에 기초잡석 줄이 없다 — 버림에서 나온다."""
|
||||
assert abs(성분(옹벽(), "기초잡석")["amount"] - 3.0) < 1e-9 # 0.30㎥/m × 10m
|
||||
assert abs(성분(옹벽(), "기초잡석")["amount"] - 2.9) < 1e-9 # 0.29㎥/m × 10m
|
||||
|
||||
|
||||
def test_두께를_바꾸면_따라간다() -> None:
|
||||
@@ -112,4 +113,4 @@ def test_묶음_구조물은_줄을_또_세우지_않는다() -> None:
|
||||
assert all(row["name"] != "기초잡석" for row in rows)
|
||||
parts = rows[0]["composite_parts"]
|
||||
gravel = next(part for part in parts if part["code"] == "FP-12-25")
|
||||
assert gravel["quantity"] == 3.0
|
||||
assert abs(gravel["quantity"] - 2.9) < 1e-9
|
||||
|
||||
@@ -130,15 +130,58 @@ def test_옹벽_단면_치수가_표의_관측_수량을_그대로_낸다() -> N
|
||||
form = math.hypot(wall["height_m"], wall["back_batter_m"]) + math.hypot(
|
||||
wall["height_m"], wall["front_batter_m"]
|
||||
)
|
||||
weep = section["weep"]
|
||||
pipe = wall["height_m"] / weep["area_m2"] * weep["pipe_length_m"]
|
||||
assert round(concrete, 2) == amounts["콘크리트"] # 1.345
|
||||
assert round(blind, 2) == amounts["버림콘크리트"] # 0.145
|
||||
assert round(form, 1) == amounts["유로폼"] # 3.205
|
||||
assert round(pipe, 2) == amounts["물구멍관"]
|
||||
pipe = wall["height_m"] / 2.0 * section["weep"]["pipe_length_m"]
|
||||
# 원문 값 그대로(2026-09-14 브레인 판정 — 라이브러리 반올림 1.35·0.15·3.2 에서 바꿈)
|
||||
assert abs(concrete - amounts["콘크리트"]) < 1e-9 # 1.345
|
||||
assert abs(blind - amounts["버림콘크리트"]) < 1e-9 # 0.145
|
||||
assert abs(form - amounts["유로폼"]) < 5e-4 # 3.2047 → 3.205
|
||||
assert abs(pipe - amounts["물구멍관"]) < 1e-9 # 0.32 (제원 칸 비었을 때)
|
||||
assert footing["thickness_m"] + wall["height_m"] == 2.0
|
||||
|
||||
|
||||
def _wall_table(length: float = 10.0, rubble=None, **options) -> dict:
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table
|
||||
|
||||
structure = {
|
||||
"structure_id": "w1",
|
||||
"type_id": "retaining_wall",
|
||||
"start_m": 0.0,
|
||||
"end_m": length,
|
||||
"options": {"form": "반중력식", "height_m": 2.0, "length_m": length, **options},
|
||||
}
|
||||
table = build_table([structure], {"retaining_wall": "옹벽"}, rubble_base_thickness_m=rubble)
|
||||
return table["structures"][0]
|
||||
|
||||
|
||||
def _amounts(structure: dict) -> dict:
|
||||
return {row["name"]: row for row in structure["components"]}
|
||||
|
||||
|
||||
def test_옹벽_터파기는_기초_폭과_양쪽_여유로_수직이고_토공으로만_간다() -> None:
|
||||
wall = _wall_table()
|
||||
rows = _amounts(wall)
|
||||
# 폭 1.6 + 0.3×2 = 2.2 · 깊이 기초 0.4 + 버림 0.1 + 기초잡석 0.2 = 0.7 → 1.54/m
|
||||
assert abs(rows["터파기"]["amount"] - 15.4) < 1e-9
|
||||
# 든 것 = 기초·전단키 0.745 + (버림 + 잡석) 1.45 × 0.3 = 1.18 → 되메우기 0.36/m
|
||||
assert abs(rows["잔토처리"]["amount"] - 11.8) < 1e-9
|
||||
assert abs(rows["되메우기"]["amount"] - 3.6) < 1e-9
|
||||
assert {rows[k]["destination"] for k in ("터파기", "되메우기", "잔토처리")} == {"earthwork"}
|
||||
assert abs(rows["기초잡석"]["amount"] - 2.9) < 1e-9 # 원문 버림 0.145 × 2
|
||||
assert wall["trench_depth_m"] == 0.7
|
||||
assert "비탈분 제외" in rows["터파기"]["basis"]
|
||||
# 잡석을 얇게 두면 전단키가 그 밑으로 나와 그 몫을 더 팜: 2.2×0.6 + 0.35×0.1
|
||||
thin = _amounts(_wall_table(rubble=0.1))
|
||||
assert abs(thin["터파기"]["amount"] - 13.55) < 1e-9
|
||||
|
||||
|
||||
def test_옹벽_물구멍관은_제원_칸이_닿고_돌쌓기와_같은_규격으로_합쳐진다() -> None:
|
||||
plain = _amounts(_wall_table())["물구멍관"]
|
||||
assert plain["spec"] == "Ø50" and abs(plain["amount"] - 3.2) < 1e-9
|
||||
assert plain["basis_kind"] == "observed" and "관측값" in plain["basis"]
|
||||
given = _amounts(_wall_table(weep_hole_area_m2=2.5, weep_hole_diameter_mm=75))["물구멍관"]
|
||||
assert given["spec"] == "Ø75" and abs(given["amount"] - 1.6 / 2.5 * 0.4 * 10) < 1e-9
|
||||
|
||||
|
||||
def test_옹벽_그림은_기초잡석_두께를_산출_조건에서_받고_사유는_표와_같다() -> None:
|
||||
shapes = build_figure(_wall_sheet(), rubble_thickness_m=0.3)
|
||||
texts = _texts(shapes)
|
||||
@@ -146,14 +189,15 @@ def test_옹벽_그림은_기초잡석_두께를_산출_조건에서_받고_사
|
||||
assert "버림 T=0.10 · 폭 1.80 − 키 0.35 = 1.45 m" in texts
|
||||
wall = _paths(shapes)[0]["points"]
|
||||
assert [0.95, 0.4] in wall and [1.068, 2.0] in wall and [1.368, 2.0] in wall
|
||||
# 표 사유와 같은 말 — 터파기 줄이 없다는 것
|
||||
# 표 사유와 같은 말 — 터파기를 어떻게 셌나(비탈분 제외)
|
||||
_rows, table_notes = expand_observed(
|
||||
"retaining_wall",
|
||||
{"form": "반중력식", "height_m": 2.0},
|
||||
_wall_sheet() | {"options": {"length_m": 10}},
|
||||
)
|
||||
missing = [note for note in table_notes if "터파기" in note]
|
||||
assert missing and missing[0] in texts
|
||||
trench = [note for note in table_notes if "터파기" in note]
|
||||
assert trench and trench[0] in texts and "비탈분 제외" in trench[0]
|
||||
assert "터파기 폭 2.20 × 깊이 0.80 m (기초 1.6 + 여유 0.3×2)" in texts # 잡석 0.3
|
||||
assert not any(text.startswith("기초잡석") for text in _texts(build_figure(_wall_sheet(), 0)))
|
||||
|
||||
|
||||
@@ -161,8 +205,8 @@ def test_옹벽_못_그리는_장은_표와_같은_까닭() -> None:
|
||||
assert "형식" in figure_reason({**_wall_sheet(), "options": {"height_m": 2.0}})
|
||||
other = figure_reason(_wall_sheet(height_m=1.6) | {"height_m": 1.6})
|
||||
assert other.startswith("그림 없음 — ") and "자료에 없습니다" in other
|
||||
note = [t for t in _texts(build_figure(_wall_sheet(weep_hole_area_m2=3))) if "안 닿음" in t]
|
||||
assert note and "Ø50" in note[0]
|
||||
label = [t for t in _texts(build_figure(_wall_sheet(weep_hole_area_m2=3))) if "물구멍" in t]
|
||||
assert label == ["물구멍 Ø50 — 3㎡당 1개소 · 관 0.4 m(관측값)"]
|
||||
|
||||
|
||||
def test_양식_장은_양식이_푼_뒷길이로_그린다() -> None:
|
||||
|
||||
Reference in New Issue
Block a user