Merge remote-tracking branch 'origin/dev' into main_laptop_1

This commit is contained in:
2026-09-14 06:03:26 +09:00
12 changed files with 255 additions and 57 deletions
@@ -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.80.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.80.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」 한 줄이고 그 일위대가가 위 공종을 묶음.",
+2 -1
View File
@@ -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
+5 -6
View File
@@ -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")
+7 -3
View File
@@ -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
+4 -3
View File
@@ -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
+55 -11
View File
@@ -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: