"""BOX암거 전개식 — **m당 × 연장** (2026-09-15 브레인 12장 D 판정 ①~⑤). 근거 둘 — 구조가 같다 ① 산림과임업기술(임도) 5장 「나) 구조물도에 의한 수량산출(예)」 · 그림 5-2-70 Box(2.0×1.5×2련×6.03m) — 구체 = (외곽 − 유수구 + 헌치) × L · 기초 = 폭 × 0.1 × L · 거푸집 = 외벽·유수구·받침판·귀면 · 동바리 = 유수구 단면 × L · 철근 「상세도에서」(값 없음) ② 실무 울진 기번3 `5. 구조도(기번3).xlsx` 「암거수량집계표(2×2)」 m당 — 레미콘 2.84 · 버림 0.28 · 유로폼 11.132 · 합판6회 0.2 · 철근 SD40 H13 0.179 + H16 0.157t · 동바리 3.92 · 비계 18.72 두께 제안값은 등록부 칸(예제 벽 0.3 · 판 0.25 · 헌치 0.2 · 버림 0.1 · KDS 300㎜ 병기). ⚠ 1련만 — 등록부에 련수 칸이 없음(예제 식은 련수로 늘어나나 그림·칸이 1련). ⚠ 철근은 **크기별 식이 없다** — 울진 2×2 관측값만 선다(판정 ②). ⚠ 비계 시종점 두 면은 **개소당 한 번** — 원문은 m당 칸에 넣어 연장을 곱하면 부풀어 오름. """ from __future__ import annotations import math from typing import Any from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import Component, _num, routed #: 버림 폭 여유(m, 한쪽) — 예제 5.1 = 외곽 4.9 + 0.1×2 · 울진 2.8 = 외곽 2.6 + 0.1×2 (둘이 같음). BLINDING_EXTRA_M = 0.1 #: 철근 관측값(kg/m) — 울진 기번3 「암거수량집계표(2×2)」 SD40 · 내공 2.0×2.0 · 벽·판 0.3. REBAR_OBSERVED_2X2 = (("D13", 179.0), ("D16", 157.0)) REBAR_SIZE_ONLY = ( "철근 — 크기별 식 없음 · 관측값은 2×2 뿐(울진 기번3 구조도 SD40 H13 0.179 + H16 0.157t/m ·" " 벽·판 0.3) — 이 크기는 철근이 안 섬(2026-09-15 브레인 판정 ②)" ) SOURCES = ( "산식 = 산림과임업기술(임도) 5장 수량산출 예제(그림 5-2-70) · 실무 울진 기번3 구조도" " 「암거수량집계표(2×2)」" ) NOTE_FORMWORK = ( "거푸집 — 본체 유로폼(외벽 · 내벽 헌치 뺀 높이 · 상판 밑면 · 귀면 넷) · 버림 옆면 합판" " (울진 2×2 모양 · 예제는 합판 3회)" ) NOTE_SHORING_SCAFFOLD = ( "동바리(유수구 단면 − 헌치)·비계(외벽 양면 + 시종점 두 면)는 **실무 관측 모양**" "(울진 2×2 「강재 3개월」) — 품셈 12-20 강관동바리 · 12-19 강관비계로 이음 ·" " 시종점 두 면은 개소당 한 번(원문은 m당 칸)" ) NOTE_WING = "날개벽 수량 근거 없음(교본·실무 모두 · 판정 ⑤) — 날개벽 줄 안 섬" NOTE_RUBBLE = ( "기초잡석 — 근거 둘(예제·울진 집계표)에는 잡석 줄이 없음 — 사용자 확정 3차 ② 로 세움" "(버림이 선 구조물엔 잡석 · 옹벽과 같은 규칙)" ) #: 구체 터파기 — 울진 「암거구체 토공(2×2)」: 밑폭 = 버림 폭 + 여유 0.5×2 · 옆면 1:0.5. TRENCH_CLEARANCE_M = 0.5 TRENCH_SIDE_SLOPE = 0.5 NOTE_TRENCH_MISSING = ( "평균 터파기고 칸이 비어 구체 터파기·되메우기가 안 섬 — 칸을 적으면 토공으로 섬" ) NOTE_TRENCH = ( "구체 터파기 — 밑폭·옆면은 울진 「암거구체 토공(2×2)」(여유 0.5 · 1:0.5)" " · 깊이 = 평균 터파기고 칸 · 되메우기 = 터파기 − 든 것(기초잡석·버림·구체)" " — 원문은 잔토에 구체 전체 · 되메우기 한쪽만이라 옹벽 식(터파기 − 든 것)으로 맞춤" " · 토공집계로만 감(이중계상 방지)" ) def box_culvert(options: dict[str, Any]) -> tuple[list[Component], list[str]]: """BOX암거 한 개소 — (성분, 사유). 연장이 없으면 성분 없이 사유만.""" length = _num(options.get("length_m")) if length <= 0: return [], ["BOX암거 연장(계류 방향)이 없어 수량이 안 섬 — 연장 칸을 적을 것(기본값 없음)"] width, height = _num(options.get("body_width_m")), _num(options.get("body_height_m")) wall, slab = _num(options.get("wall_thickness_m")), _num(options.get("slab_thickness_m")) haunch, blinding = _num(options.get("haunch_m")), _num(options.get("blinding_thickness_m")) if min(width, height, wall, slab) <= 0: return [], ["BOX암거 본체 폭·높이·두께 중 빈 칸이 있어 수량이 안 섬"] outer_w, outer_h = width + 2 * wall, height + 2 * slab chamfer = math.sqrt(2) * haunch per_m: list[tuple[str, str, float, str, str]] = [ ( "콘크리트", "㎥", outer_w * outer_h - width * height + haunch**2 / 2 * 4, f"{outer_w:g}×{outer_h:g} − {width:g}×{height:g} + {haunch:g}²÷2×4", "", ), ( "버림콘크리트", "㎥", (outer_w + 2 * BLINDING_EXTRA_M) * blinding, f"({outer_w:g} + {BLINDING_EXTRA_M:g}×2) × {blinding:g}", "", ), ( "유로폼", "㎡", 2 * outer_h + 2 * (height - 2 * haunch) + (width - 2 * haunch) + 4 * chamfer, f"외벽 {outer_h:g}×2 + 내벽 ({height:g} − {haunch:g}×2)×2" f" + 상판 밑 ({width:g} − {haunch:g}×2) + 귀면 √({haunch:g}²+{haunch:g}²)×4", "", ), ("합판거푸집", "㎡", 2 * blinding, f"버림 옆면 {blinding:g}×2", ""), ( "동바리", "㎥", width * height - 2 * haunch**2, f"{width:g}×{height:g} − {haunch:g}²÷2×4", "", ), ("비계", "㎡", 2 * outer_h, f"외벽 양면 {outer_h:g}×2", ""), ] notes: list[str] = [SOURCES, NOTE_FORMWORK, NOTE_SHORING_SCAFFOLD, NOTE_WING, NOTE_RUBBLE] if math.isclose(width, 2.0) and math.isclose(height, 2.0): per_m += [ ("이형철근", "kg", kg, "울진 기번3 구조도 2×2 관측(SD40)", size) for size, kg in REBAR_OBSERVED_2X2 ] notes.append("철근은 관측값(울진 2×2 · 벽·판 0.3) — 두께를 바꿔도 따라 안 바뀜") else: notes.append(REBAR_SIZE_ONLY) components = [ Component( name, unit, amount * length, destination, f"{basis} = m당 {amount:.4g} × 연장 {length:g}m", spec=spec, ) for name, unit, amount, basis, spec in per_m if (destination := routed(name, notes)) ] ends = 2 * outer_w * outer_h for component in components: if component.name == "비계": component.amount += ends component.basis += ( f" + 시종점 두 면 {outer_w:g}×{outer_h:g}×2 = {ends:.4g}(개소당 한 번)" ) if _num(options.get("trench_depth_m")) <= 0: notes.append(NOTE_TRENCH_MISSING) return components, notes def box_trench( options: dict[str, Any], rubble_thickness_m: float ) -> tuple[list[Component], float] | None: """구체 터파기·되메우기·잔토(토공 축) — 평균 터파기고가 비면 `None`. 기초잡석 두께는 원단위표와 **같은 값**을 받음(`build_table` 의 두께) — 두 벌로 안 셈. """ depth, length = _num(options.get("trench_depth_m")), _num(options.get("length_m")) outer_w = _num(options.get("body_width_m")) + 2 * _num(options.get("wall_thickness_m")) outer_h = _num(options.get("body_height_m")) + 2 * _num(options.get("slab_thickness_m")) blinding = _num(options.get("blinding_thickness_m")) if depth <= 0 or length <= 0 or outer_w <= 0: return None base_w = outer_w + 2 * BLINDING_EXTRA_M bottom = base_w + 2 * TRENCH_CLEARANCE_M top = bottom + 2 * TRENCH_SIDE_SLOPE * depth excavation = (bottom + top) / 2 * depth body = min(max(depth - rubble_thickness_m - blinding, 0.0), outer_h) filled = base_w * (rubble_thickness_m + blinding) + outer_w * body notes: list[str] = [] rows = [ ( "터파기", excavation, f"(밑폭 {bottom:.2f}(버림 폭 {base_w:g} + 여유 {TRENCH_CLEARANCE_M:g}×2) + 윗폭" f" {top:.2f}(1:{TRENCH_SIDE_SLOPE:g})) ÷ 2 × 깊이 {depth:g} · {NOTE_TRENCH}", ), ( "되메우기", excavation - filled, f"터파기 − 든 것 {filled:.3f}(버림 폭 × (잡석 {rubble_thickness_m:g}" f" + 버림 {blinding:g}) + 구체 폭 {outer_w:g} × {body:.2f})", ), ("잔토처리", filled, "터파기 − 되메우기"), ] return [ Component(name, "㎥", amount * length, destination, f"{basis} × 연장 {length:g}m") for name, amount, basis in rows if (destination := routed(name, notes)) ], depth