"""준비공 — 임목폐기물 처리 한 줄 (톤) · 2026-09-14 판정 「폐기물처리비」. ⭐ 수량 산식 — 한국건설기술연구원 「건설현장 임목폐기물의 재활용 및 자원화 기술개발」(2012) 3.2 = 환경친화적인 도로건설 지침 부록1 문단 493~508 「3) 임목폐기물」(산출근거: 조경설계요람) 지상부 WA = k × 3.14 × (B/2)² × h × (1+p) × W1 × N (kg · 지침 식이 π 를 3.14 로 적음) 뿌리부 — 지침이 두 방법을 둔다. 고르는 칸. ① 분배비(기본) WR = WA × 15/85 (KICT 일반 수목류 — 2026-09-14 판정 유지) ② 뿌리분 체적 WR = 0.3927 × D³ × 1,300 × N (지침 「V = πD²(D/2 + RH2/3) = 0.3927D³」 · 「UW1 뿌리부분의 단위중량 1,300kg/m3」). ⚠ D 는 지침이 「뿌리분의 직경(DBH, m)」으로 적었으나 2026-09-14 판정으로 **뿌리분 직경**(흉고직경 아님) — 따로 받고 기본값 없음. k 수간 형상계수 0.5 · p 지엽 보합률(임목 0.3) — **산식 상수**라 칸이 없다. B 평균 흉고직경 · h 평균 수고 · W1 수목 단위체적중량 · N 본수 — **현장 조사값**이라 칸으로 받는다. N = 표본지 1,000㎡ 실측 본수 × 대상 면적 ÷ 1,000 (실정보고 예 「134/1000 × 7,450」 모양). 대상 면적 = 벌목·지장목제거 면적(같은 나무를 벤 자리). ⚠ 실무 실정보고(임목폐기물처리, 한 현장)는 뿌리 14:86 · 무게 0.65톤/㎥ · 처리비를 공급가액 뒤(승률 밖)에 두었다 — 판정은 법 문언·공인 자료 쪽(PLAN 10장). 발주처가 요구하면 칸으로 열 자리. ⚠ 금액은 **처리단가가 없어** 못 선다(KCRA 중간처리단가 미확보) — 수동 단가를 넣으면 금액이 서되 「미확정」으로 센다(빨간 테두리). 비목은 **경비**라 내역 줄이 아니고 원가계산서 폐기물처리비 줄로 간다(예정가격작성기준 제19조③18호). """ from __future__ import annotations import math from typing import Any from B08_Quantity.B08_Quantity_Engine_Preparation_Status import STATUS_NEEDS_INPUT, STATUS_READY TREE_WASTE_ITEM = "임목폐기물 처리" STEM_FORM_FACTOR = 0.5 # k FOLIAGE_ALLOWANCE = 0.3 # p — 임목(고립목은 1.0) PI_AS_WRITTEN = 3.14 # 지침 부록1 식 원문 · 실정보고도 3.14 ROOT_SHARE, ABOVE_SHARE = 15.0, 85.0 # 일반 수목류 분배비 ROOT_BALL_FACTOR = 0.3927 # V = 0.3927 D³ (지침 부록1) ROOT_BALL_UNIT_WEIGHT = 1300.0 # ㎏/㎥ (지침 부록1 UW1) ROOT_METHOD_RATIO, ROOT_METHOD_BALL = "ratio", "root_ball" #: 칸 이름 · 화면 이름 · 단위 — 모두 채워야 수량이 선다. TREE_WASTE_INPUTS: tuple[tuple[str, str], ...] = ( ("stems_per_1000m2", "표본지 1,000㎡당 본수"), ("dbh_cm", "평균 흉고직경(㎝)"), ("height_m", "평균 수고(m)"), ("unit_weight_kg_m3", "단위체적중량(㎏/㎥)"), ) TREE_WASTE_BASIS = ( "환경친화적인 도로건설 지침 부록1 · 한국건설기술연구원(2012) 3.2 — k 0.5 · p 0.3(임목) · π 3.14" " · N = 표본지 1,000㎡ 본수 × 벌목·지장목제거 면적" ) ROOT_RATIO_BASIS = "뿌리 WR = WA × 15/85(분배비 · 일반 수목류)" ROOT_BALL_BASIS = "뿌리 WR = 0.3927 × D³ × 1,300㎏/㎥ × N(뿌리분 체적 · 지침 부록1)" ROOT_BALL_LABEL = "뿌리분 직경 D(m, 흉고직경 아님 · 굴취 흙덩이 직경)" #: 배출자 신고 경계 — 화면이 알릴 뿐 값을 바꾸지 않는다. REPORT_THRESHOLD_TON = 5.0 NOT_SEPARATE_NOTE = ( "임목폐기물은 분리발주 대상 아님(환경부 질의회신 — 임목폐기물처리 실정보고 p4 ·" " 건설폐기물 업무처리지침: 건설폐기물 아닌 사업장 일반폐기물)" ) EXPENSE_NOTE = ( "비목은 경비(예정가격작성기준 제19조③18호) — 내역 줄이 아니라 원가계산서 폐기물처리비로 감" ) def _positive(value: Any) -> float | None: try: number = float(value) except (TypeError, ValueError): return None return number if number > 0 else None def _missing(inputs: dict[str, Any], root_method: str) -> list[str]: """비어 있는 칸 이름 — 뿌리분 체적법이면 뿌리분 직경도 든다.""" missing = [label for key, label in TREE_WASTE_INPUTS if _positive(inputs.get(key)) is None] if root_method == ROOT_METHOD_BALL and _positive(inputs.get("root_ball_diameter_m")) is None: missing.append(ROOT_BALL_LABEL) return missing def tree_waste_tons( area_m2: float, inputs: dict[str, Any], root_method: str = ROOT_METHOD_RATIO ) -> tuple[float, float] | None: """(지상부 톤, 뿌리부 톤). 칸이 하나라도 비거나 면적이 0 이면 `None`.""" if area_m2 <= 0 or _missing(inputs, root_method): return None stems = float(inputs["stems_per_1000m2"]) * area_m2 / 1000.0 diameter = float(inputs["dbh_cm"]) / 100.0 above_kg = ( STEM_FORM_FACTOR * PI_AS_WRITTEN * (diameter / 2.0) ** 2 * float(inputs["height_m"]) * (1.0 + FOLIAGE_ALLOWANCE) * float(inputs["unit_weight_kg_m3"]) * stems ) if root_method == ROOT_METHOD_BALL: ball = float(inputs["root_ball_diameter_m"]) root_kg = ROOT_BALL_FACTOR * ball**3 * ROOT_BALL_UNIT_WEIGHT * stems else: root_kg = above_kg * ROOT_SHARE / ABOVE_SHARE return above_kg / 1000.0, root_kg / 1000.0 def tree_waste_rows( slope_totals: dict[str, float] | None, inputs: dict[str, Any] | None, unit_price_krw_per_ton: Any = None, separate_order: Any = False, root_method: Any = None, ) -> list[dict[str, Any]]: """임목폐기물 처리 한 줄 — 값이 없어도 줄은 선다(무엇을 넣으면 풀리는지 적음).""" slope = slope_totals or {} area = float(slope.get("tree_removal_fill", 0.0)) + float(slope.get("tree_removal_cut", 0.0)) inputs = inputs or {} method = ROOT_METHOD_BALL if root_method == ROOT_METHOD_BALL else ROOT_METHOD_RATIO root_basis = ROOT_BALL_BASIS if method == ROOT_METHOD_BALL else ROOT_RATIO_BASIS base = { "group": "준비공", "item": TREE_WASTE_ITEM, "unit": "톤", "work_item_code": None, # 경비 — 내역에 세우지 않는다(인계가 이 칸을 본다). "bill": False, "bill_reason": EXPENSE_NOTE, "separate_order": bool(separate_order), } tons = tree_waste_tons(area, inputs, method) if tons is None: missing = _missing(inputs, method) why = ( f"현장 조사값이 비어 있음 — {' · '.join(missing)}" if missing else "벌목·지장목제거 면적이 0 ㎡ — 사면표가 아직 안 섬" ) return [ { **base, "amount": None, "status": STATUS_NEEDS_INPUT, # 조사값 칸 · 앞 단계 사면표 — 둘 다 사람 몫 "reason": f"{why}. 산식: {TREE_WASTE_BASIS} · {root_basis}", "reference_amount": area, } ] above, root = tons total = above + root notes = [ f"지상부 WA {above:,.2f}톤 + 뿌리부 WR {root:,.2f}톤", TREE_WASTE_BASIS, root_basis, EXPENSE_NOTE, NOT_SEPARATE_NOTE, ] if total >= REPORT_THRESHOLD_TON: notes.append("5톤 이상 — 배출자 신고 대상 사업장(폐기물관리법 시행령 제2조8호)") price = _positive(unit_price_krw_per_ton) row = { **base, "amount": total, "status": STATUS_READY, "reference_amount": area, "above_ground_ton": above, "root_ton": root, "unit_price_krw_per_ton": price, "amount_krw": math.floor(total * price) if price else None, # 수동 단가로 선 금액 — 화면 「미확정 N건」·빨간 테두리. "unconfirmed": 1 if price else 0, } if price: notes.append( f"처리비 {total:,.2f}톤 × {price:,.0f}원 = {math.floor(total * price):,}원" " (⚠ 수동 단가 — 미확정 · KCRA 중간처리단가 미확보)" ) else: notes.append( "처리단가 없음(KCRA 중간처리단가 미확보) — 산출 조건에 수동으로 넣으면 금액이 섬" ) row["reason"] = " · ".join(notes) return [row]