From 35ba45f572bf579ad85c7f1bc8cd8100a6606abc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 01:09:23 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(b08):=20=EC=9E=84=EB=AA=A9=ED=8F=90?= =?UTF-8?q?=EA=B8=B0=EB=AC=BC=20=EB=BF=8C=EB=A6=AC=20=EC=82=B0=EC=A0=95?= =?UTF-8?q?=EB=B2=95=20=EA=B3=A0=EB=A5=B4=EA=B0=9C=20=C2=B7=20=EC=A7=80?= =?UTF-8?q?=EC=B9=A8=20=CF=80=203.14=20=C2=B7=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=EB=B0=9C=EC=A3=BC=20=EB=8C=80=EC=83=81=20=EC=95=84=EB=8B=98=20?= =?UTF-8?q?=EC=95=88=EB=82=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 환경친화적인 도로건설 지침 부록1 확인: 뿌리분 체적 V = 0.3927D³ × 1,300㎏/㎥ — 고르개로 넣음(기본은 분배비 15/85) - 뿌리분 직경 D 는 지침 표기 「뿌리분의 직경(DBH, m)」이 모호해 흉고직경과 따로 받음 - 지상부 식을 지침 원문대로 π 3.14 로 셈 — 실정보고 예 130.40㎥ 와 바로 맞음 - 분리발주 칸 밑·사유에 「임목폐기물은 분리발주 대상 아님(환경부 회신)」 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- .../B08_Quantity_Engine_Preparation.py | 7 +- ...8_Quantity_Engine_Preparation_TreeWaste.py | 81 +++++++++++++------ B08_Quantity/B08_Quantity_Router_Earthwork.py | 7 ++ B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts | 2 + B08_Quantity/B08_Quantity_UI_Page.ts | 3 + .../B08_Quantity_UI_Side_TreeWaste.ts | 30 +++++++ common_util/common_util_project_settings.py | 3 + resources/tester/test_b08_tree_waste.py | 23 ++++-- ui_template/ui_template_locale_b2.ts | 22 ++++- 9 files changed, 143 insertions(+), 35 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Engine_Preparation.py b/B08_Quantity/B08_Quantity_Engine_Preparation.py index e759e2b6..c2ab2276 100644 --- a/B08_Quantity/B08_Quantity_Engine_Preparation.py +++ b/B08_Quantity/B08_Quantity_Engine_Preparation.py @@ -671,6 +671,7 @@ def build_table( tree_waste: dict[str, Any] | None = None, tree_waste_unit_price_krw_per_ton: Any = None, waste_separate_order: Any = False, + tree_waste_root_method: Any = None, ) -> dict[str, Any]: """화면·인계가 그대로 쓰는 모양. **못 서는 줄도 목록에 남는다.**""" rows = ( @@ -687,7 +688,11 @@ def build_table( + chipping_rows(chipping_enabled, chipping_volume_m3) # 임목폐기물 — 톤 · 비목은 경비(내역 줄 아님 · 2026-09-14 판정). + tree_waste_rows( - slope_totals, tree_waste, tree_waste_unit_price_krw_per_ton, waste_separate_order + slope_totals, + tree_waste, + tree_waste_unit_price_krw_per_ton, + waste_separate_order, + tree_waste_root_method, ) + ancillary_rows(ancillary_counts) ) diff --git a/B08_Quantity/B08_Quantity_Engine_Preparation_TreeWaste.py b/B08_Quantity/B08_Quantity_Engine_Preparation_TreeWaste.py index cd967487..af918aee 100644 --- a/B08_Quantity/B08_Quantity_Engine_Preparation_TreeWaste.py +++ b/B08_Quantity/B08_Quantity_Engine_Preparation_TreeWaste.py @@ -1,13 +1,21 @@ """준비공 — 임목폐기물 처리 한 줄 (톤) · 2026-09-14 판정 「폐기물처리비」. ⭐ 수량 산식 — 한국건설기술연구원 「건설현장 임목폐기물의 재활용 및 자원화 기술개발」(2012) 3.2 - 지상부 WA = k × π × (B/2)² × h × (1+p) × W1 × N (kg) - 뿌리부 WR = WA × 뿌리 분배비 ÷ 지상 분배비 = WA × 15/85 (일반 수목류, 같은 보고서 표) + = 환경친화적인 도로건설 지침 부록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)」으로 + 적어 흉고직경인지 뿌리분 직경인지 갈리지 않는다 — **흉고직경으로 두지 않고 따로 받는다.** 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 중간처리단가 미확보) — 수동 단가를 넣으면 금액이 서되 「미확정」으로 센다(빨간 테두리). 비목은 **경비**라 내역 줄이 아니고 @@ -24,7 +32,11 @@ from B08_Quantity.B08_Quantity_Engine_Preparation_Status import STATUS_PENDING, 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], ...] = ( @@ -34,13 +46,18 @@ TREE_WASTE_INPUTS: tuple[tuple[str, str], ...] = ( ("unit_weight_kg_m3", "단위체적중량(㎏/㎥)"), ) TREE_WASTE_BASIS = ( - "한국건설기술연구원(2012) 3.2 임목 발생량 — k 0.5 · p 0.3(임목)" - " · 뿌리 15 : 지상 85(일반 수목류)" + "환경친화적인 도로건설 지침 부록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 -SEPARATE_ORDER_THRESHOLD_TON = 100.0 +NOT_SEPARATE_NOTE = ( + "임목폐기물은 분리발주 대상 아님(환경부 질의회신 — 임목폐기물처리 실정보고 p4 ·" + " 건설폐기물 업무처리지침: 건설폐기물 아닌 사업장 일반폐기물)" +) EXPENSE_NOTE = ( "비목은 경비(예정가격작성기준 제19조③18호) — 내역 줄이 아니라 원가계산서 폐기물처리비로 감" ) @@ -54,23 +71,37 @@ def _positive(value: Any) -> float | None: return number if number > 0 else None -def tree_waste_tons(area_m2: float, inputs: dict[str, Any]) -> tuple[float, float] | 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`.""" - values = {key: _positive(inputs.get(key)) for key, _ in TREE_WASTE_INPUTS} - if area_m2 <= 0 or any(value is None for value in values.values()): + if area_m2 <= 0 or _missing(inputs, root_method): return None - stems = values["stems_per_1000m2"] * area_m2 / 1000.0 - diameter = values["dbh_cm"] / 100.0 + stems = float(inputs["stems_per_1000m2"]) * area_m2 / 1000.0 + diameter = float(inputs["dbh_cm"]) / 100.0 above_kg = ( STEM_FORM_FACTOR - * math.pi + * PI_AS_WRITTEN * (diameter / 2.0) ** 2 - * values["height_m"] + * float(inputs["height_m"]) * (1.0 + FOLIAGE_ALLOWANCE) - * values["unit_weight_kg_m3"] + * float(inputs["unit_weight_kg_m3"]) * stems ) - return above_kg / 1000.0, above_kg * ROOT_SHARE / ABOVE_SHARE / 1000.0 + 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( @@ -78,11 +109,14 @@ def tree_waste_rows( 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, @@ -93,9 +127,9 @@ def tree_waste_rows( "bill_reason": EXPENSE_NOTE, "separate_order": bool(separate_order), } - tons = tree_waste_tons(area, inputs) + tons = tree_waste_tons(area, inputs, method) if tons is None: - missing = [label for key, label in TREE_WASTE_INPUTS if _positive(inputs.get(key)) is None] + missing = _missing(inputs, method) why = ( f"현장 조사값이 비어 있음 — {' · '.join(missing)}" if missing @@ -106,23 +140,20 @@ def tree_waste_rows( **base, "amount": None, "status": STATUS_PENDING, - "reason": f"{why}. 산식: {TREE_WASTE_BASIS}", + "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}톤(WA × 15/85)", + f"지상부 WA {above:,.2f}톤 + 뿌리부 WR {root:,.2f}톤", TREE_WASTE_BASIS, + root_basis, EXPENSE_NOTE, + NOT_SEPARATE_NOTE, ] - if total >= SEPARATE_ORDER_THRESHOLD_TON: - notes.append( - "⚠ 100톤 이상 — 건설폐기물이면 분리발주 대상(업무처리지침). 다만 임목폐기물은" - " 건설폐기물이 아닌 사업장 일반폐기물로 분류됨(같은 지침) — 산출 조건에서 고를 것" - ) - elif total >= REPORT_THRESHOLD_TON: + if total >= REPORT_THRESHOLD_TON: notes.append("5톤 이상 — 배출자 신고 대상 사업장(폐기물관리법 시행령 제2조8호)") price = _positive(unit_price_krw_per_ton) row = { diff --git a/B08_Quantity/B08_Quantity_Router_Earthwork.py b/B08_Quantity/B08_Quantity_Router_Earthwork.py index 9bff3a17..5cbd698b 100644 --- a/B08_Quantity/B08_Quantity_Router_Earthwork.py +++ b/B08_Quantity/B08_Quantity_Router_Earthwork.py @@ -178,6 +178,7 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse: settings.get("tree_waste") or {}, settings.get("tree_waste_unit_price_krw_per_ton"), settings.get("waste_separate_order"), + settings.get("tree_waste_root_method"), ) method, method_is_default = concrete_placing_method(settings) # ⚠ 금액에 바로 걸리는 값이라 「기본값으로 돌고 있음」을 응답에 실어 화면이 띄우게 한다. @@ -464,6 +465,8 @@ class QuantitySettingsBody(BaseModel): tree_waste: dict[str, float | None] | None = None tree_waste_unit_price_krw_per_ton: float | None = None waste_separate_order: bool | None = None + # 뿌리 산정법 — "root_ball"(뿌리분 체적 × 1,300) · `""` 는 기본(분배비 15/85)으로 되돌림. + tree_waste_root_method: str | None = None # 토량환산계수(다짐) — `{갈래: {"compacted": C, "reason": 사유}}`. # ⚠ **기본값을 복사해 넣지 않는다** — 안 고른 갈래는 키가 없어야 정본이 선다. # 빈 dict 는 「전부 기본값으로 되돌림」이라 통째로 갈아 끼운다. @@ -531,6 +534,9 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) - for key, value in (values["tree_waste"] or {}).items() if isinstance(value, (int, float)) and not isinstance(value, bool) and value > 0 } + if "tree_waste_root_method" in values: + method = values["tree_waste_root_method"] + values["tree_waste_root_method"] = method if method == "root_ball" else None if "topsoil_target" in values: # 기본(노면 + 절토)은 저장하지 않는다 — 「안 정함」과 같게 둬 법 문언이 선다. target = values["topsoil_target"] @@ -571,6 +577,7 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) - "concrete_placing_method", "topsoil_target", "tree_waste", + "tree_waste_root_method", "ancillary_counts", # 고른 계수를 **기본값으로 되돌릴 길**이 있어야 한다 — 병합이면 못 지운다. "conversion_factors_override", diff --git a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts index 22c46169..cbb65fae 100644 --- a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts +++ b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts @@ -114,6 +114,8 @@ export interface QuantitySettings { tree_waste_unit_price_krw_per_ton?: number | null; /** 폐기물 분리발주 — 기본 아님. */ waste_separate_order?: boolean | null; + /** 뿌리 산정법 — 없음이면 분배비 15/85 · `"root_ball"` 은 뿌리분 체적 × 1,300. */ + tree_waste_root_method?: string | null; } /** 갈래 하나의 「무엇을 골랐나」. 서버 `earthwork_conversion_choices` 와 짝이다. */ diff --git a/B08_Quantity/B08_Quantity_UI_Page.ts b/B08_Quantity/B08_Quantity_UI_Page.ts index c23e6924..a6d8289e 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.ts +++ b/B08_Quantity/B08_Quantity_UI_Page.ts @@ -114,6 +114,7 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr tree_waste: draft.tree_waste, tree_waste_unit_price_krw_per_ton: draft.tree_waste_unit_price_krw_per_ton, waste_separate_order: draft.waste_separate_order, + tree_waste_root_method: draft.tree_waste_root_method, // 개소는 **통째로** 보낸다 — 지운 항목까지 그대로 가야 되돌릴 길이 있다. ancillary_counts: draft.ancillary_counts, // 토량환산계수 — 고른 갈래만 담긴다. 빈 dict 는 「전부 기본값으로 되돌림」이다. @@ -309,6 +310,7 @@ interface DraftSettings { tree_waste: Record; tree_waste_unit_price_krw_per_ton: number | null; waste_separate_order: boolean; + tree_waste_root_method: string; // 자재별 관급/사급 — 표 안에서 줄마다 고른 값. material_supply: Record; // 갈래별 토량환산계수(다짐) — `compacted` 가 `null` 이면 「안 고름」이라 기본값이 선다. @@ -1017,6 +1019,7 @@ export async function renderB08Quantity(root: HTMLElement): Promise { tree_waste_unit_price_krw_per_ton: (stored.tree_waste_unit_price_krw_per_ton as number | null) ?? null, waste_separate_order: Boolean(stored.waste_separate_order), + tree_waste_root_method: (stored.tree_waste_root_method as string) ?? "", ancillary_counts: { ...((stored.ancillary_counts ?? {}) as Record), }, diff --git a/B08_Quantity/B08_Quantity_UI_Side_TreeWaste.ts b/B08_Quantity/B08_Quantity_UI_Side_TreeWaste.ts index 52362f39..36999445 100644 --- a/B08_Quantity/B08_Quantity_UI_Side_TreeWaste.ts +++ b/B08_Quantity/B08_Quantity_UI_Side_TreeWaste.ts @@ -16,6 +16,8 @@ export interface TreeWasteDraft { tree_waste: Record; tree_waste_unit_price_krw_per_ton: number | null; waste_separate_order: boolean; + /** 뿌리 산정법 — `""` 분배비 15/85(기본) · "root_ball" 뿌리분 체적 × 1,300. */ + tree_waste_root_method: string; dirty: boolean; } @@ -67,6 +69,34 @@ export function appendTreeWasteFields( } panel.append(h.hintRow(L("B08_Quantity_Side_TreeWaste_Hint"))); + // 뿌리 — 지침 부록1 이 두 방법을 둠. 뿌리분 직경은 흉고직경과 갈라 따로 받음(지침 표기가 모호). + panel.append( + h.selectField( + L("B08_Quantity_TreeWaste_RootMethod"), + draft.tree_waste_root_method, + [ + { value: "", label: L("B08_Quantity_TreeWaste_RootRatio") }, + { value: "root_ball", label: L("B08_Quantity_TreeWaste_RootBall") }, + ], + (value) => { + draft.tree_waste_root_method = value; + draft.dirty = true; + }, + ), + ); + panel.append( + h.optionalNumberField( + L("B08_Quantity_TreeWaste_RootBallDiameter"), + draft.tree_waste["root_ball_diameter_m"] ?? null, + "0.01", + (value) => { + draft.tree_waste["root_ball_diameter_m"] = value; + draft.dirty = true; + }, + ), + ); + panel.append(h.hintRow(L("B08_Quantity_TreeWaste_Root_Hint"))); + const price = h.optionalNumberField( L("B08_Quantity_TreeWaste_Price"), draft.tree_waste_unit_price_krw_per_ton, diff --git a/common_util/common_util_project_settings.py b/common_util/common_util_project_settings.py index fe9bd9f1..80e8ea6d 100644 --- a/common_util/common_util_project_settings.py +++ b/common_util/common_util_project_settings.py @@ -163,6 +163,9 @@ def default_settings() -> dict[str, Any]: # 임목폐기물 — 현장 조사값 넷 `{stems_per_1000m2, dbh_cm, height_m, unit_weight_kg_m3}`. # ⚠ 기본값을 두지 않는다 — 비면 톤이 안 선다(수종·임분마다 달라 지어내지 않음). "tree_waste": {}, + # 뿌리 산정법 — `None` 은 분배비 15/85(기본) · `"root_ball"` 은 뿌리분 체적 × 1,300㎏/㎥ + # (환경친화적인 도로건설 지침 부록1). 뿌리분 직경은 `tree_waste.root_ball_diameter_m`. + "tree_waste_root_method": None, # 처리단가(원/톤) — KCRA 중간처리단가 미확보라 **수동**. 넣으면 금액이 서되 미확정. "tree_waste_unit_price_krw_per_ton": None, # 폐기물 분리발주 — 기본 아님(경비로 원가에 듦 · 2026-09-14 판정). diff --git a/resources/tester/test_b08_tree_waste.py b/resources/tester/test_b08_tree_waste.py index d0aff832..62ce3dd8 100644 --- a/resources/tester/test_b08_tree_waste.py +++ b/resources/tester/test_b08_tree_waste.py @@ -7,7 +7,6 @@ from __future__ import annotations -import math import sys from pathlib import Path @@ -28,8 +27,8 @@ FIELD = {"stems_per_1000m2": 134, "dbh_cm": 16, "height_m": 10.0, "unit_weight_k def test_지상부는_실정보고_부피와_같다() -> None: """W1 = 1,000㎏/㎥ 로 두면 지상부 톤 = 지상부 부피(㎥) — 실정보고 130.40㎥.""" above, root = tree_waste_tons(7450.0, FIELD) - # 실정보고는 π 를 3.14 로 셈 — 같은 π 로 되돌리면 130.40 과 같다. - assert above * 3.14 / math.pi == pytest.approx(130.40, abs=0.01) + # 지침 부록1 식·실정보고 모두 π 를 3.14 로 적음 — 그대로 셈해 130.40 과 같다. + assert above == pytest.approx(130.40, abs=0.01) assert root == pytest.approx(above * 15 / 85) @@ -48,9 +47,9 @@ def test_단가가_없으면_금액이_안_서고_있으면_미확정() -> None: assert priced["amount_krw"] == int(priced["amount"] * 60000) -def test_톤_경계를_알린다() -> None: +def test_톤_경계를_알리고_분리발주_대상_아님을_적는다() -> None: big = tree_waste_rows(SLOPE, FIELD)[0] # 약 153톤 - assert big["amount"] >= 100 and "100톤 이상" in big["reason"] + assert "분리발주 대상 아님" in big["reason"] and "환경부" in big["reason"] small = tree_waste_rows({"tree_removal_fill": 500.0}, FIELD)[0] # 약 10톤 assert 5 <= small["amount"] < 100 and "5톤 이상" in small["reason"] @@ -64,3 +63,17 @@ def test_경비라_내역에_안_서고_제외_사유가_간다() -> None: row = handed["임목폐기물 처리"] assert row["in_bill"] is False and row["blocked_kind"] is None assert "경비" in row["in_bill_reason"] + + +def test_뿌리분_체적법은_D_세제곱_곱하기_1300() -> None: + """지침 부록1 — V = 0.3927 D³ · 뿌리분 중량 = V × 1,300㎏/㎥ (본수만큼).""" + field = {**FIELD, "root_ball_diameter_m": 0.5} + above, root = tree_waste_tons(7450.0, field, "root_ball") + stems = 134 / 1000 * 7450 + assert root == pytest.approx(0.3927 * 0.5**3 * 1300 * stems / 1000) + assert above == pytest.approx(tree_waste_tons(7450.0, FIELD)[0]) + + +def test_체적법인데_뿌리분_직경이_비면_안_선다() -> None: + row = tree_waste_rows(SLOPE, FIELD, root_method="root_ball")[0] + assert row["amount"] is None and "뿌리분 직경" in row["reason"] diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 4d85a535..bb365230 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -717,8 +717,22 @@ export const ui_locales_b2 = { B08_Quantity_TreeWaste_Height: ["평균 수고(m)", "Mean tree height (m)"], B08_Quantity_TreeWaste_UnitWeight: ["단위체적중량(㎏/㎥)", "Unit weight (kg/㎥)"], B08_Quantity_Side_TreeWaste_Hint: [ - "현장 조사값 넷을 넣으면 준비공 「임목폐기물 처리」가 톤으로 섭니다. 산식: 지상부 WA = 0.5 × π × (흉고직경/2)² × 수고 × (1+0.3) × 단위체적중량 × 본수 · 뿌리부 WR = WA × 15/85 · 본수 = 1,000㎡당 본수 × 벌목·지장목제거 면적 — 한국건설기술연구원(2012) 3.2. 단위체적중량 참고: 소나무·침엽수 1,210~1,250 · 단풍·산벚 1,250~1,300 · 느티 1,300~1,340 · 상수리·졸참 1,340 이상(조경설계요람). 기본값 없음 — 수종·임분마다 다름", - "Enter the four field-survey values to raise the tree-waste row in tonnes. WA = 0.5 × π × (DBH/2)² × height × 1.3 × unit weight × stems; roots WR = WA × 15/85 (KICT 2012). No defaults — values differ by species and stand", + "현장 조사값 넷을 넣으면 준비공 「임목폐기물 처리」가 톤으로 섭니다. 산식: 지상부 WA = 0.5 × 3.14 × (흉고직경/2)² × 수고 × (1+0.3) × 단위체적중량 × 본수 · 본수 = 1,000㎡당 본수 × 벌목·지장목제거 면적 — 환경친화적인 도로건설 지침 부록1 · 한국건설기술연구원(2012) 3.2. 단위체적중량 참고: 소나무·침엽수 1,210~1,250 · 단풍·산벚 1,250~1,300 · 느티 1,300~1,340 · 상수리·졸참 1,340 이상(조경설계요람). 기본값 없음 — 수종·임분마다 다름", + "Enter the four field-survey values to raise the tree-waste row in tonnes. WA = 0.5 × 3.14 × (DBH/2)² × height × 1.3 × unit weight × stems (road guideline appendix 1, KICT 2012). No defaults — values differ by species and stand", + ], + B08_Quantity_TreeWaste_RootMethod: ["뿌리부 산정법", "Root estimate"], + B08_Quantity_TreeWaste_RootRatio: [ + "분배비 WA × 15/85 (기본)", + "Share ratio WA × 15/85 (default)", + ], + B08_Quantity_TreeWaste_RootBall: ["뿌리분 체적 × 1,300㎏/㎥", "Root-ball volume × 1,300 kg/㎥"], + B08_Quantity_TreeWaste_RootBallDiameter: [ + "뿌리분 직경 D(m) — 체적법만", + "Root-ball diameter D (m) — volume method", + ], + B08_Quantity_TreeWaste_Root_Hint: [ + "환경친화적인 도로건설 지침 부록1 이 두 방법을 둡니다. 분배비(기본): 뿌리 15 : 지상 85(일반 수목류, 한국건설기술연구원 2012). 뿌리분 체적: V = πD²(D/2 + RH2/3) = 0.3927D³ · 뿌리분 중량 = V × 1,300㎏/㎥ × 본수. ⚠ 지침은 D 를 「뿌리분의 직경(DBH, m)」으로 적어 흉고직경인지 뿌리분 직경인지 갈리지 않음 — 흉고직경으로 두지 않고 따로 받습니다. 참고: 실무 실정보고(한 현장)는 뿌리 14 : 86 · 0.65톤/㎥ 를 씀", + "Road guideline appendix 1 gives two methods: share ratio 15:85 (default), or root-ball volume 0.3927·D³ × 1,300 kg/㎥ × stems. The guideline labels D ambiguously, so it is entered separately", ], B08_Quantity_TreeWaste_Price: ["처리단가(원/톤) — 수동", "Disposal price (KRW/t) — manual"], B08_Quantity_TreeWaste_Price_Hint: [ @@ -729,8 +743,8 @@ export const ui_locales_b2 = { B08_Quantity_TreeWaste_Separate_Off: ["분리발주 아님(기본)", "No (default)"], B08_Quantity_TreeWaste_Separate_On: ["분리발주", "Yes"], B08_Quantity_TreeWaste_Separate_Hint: [ - "기본은 분리발주 아님 — 원가계산서 경비에 듭니다. 켜면 총원가 밖에 서고 총공사비에만 더합니다. 참고: 건설폐기물 위탁처리 100톤 이상은 분리발주 대상(건설폐기물 업무처리지침)이나, 같은 지침은 임목폐기물을 건설폐기물이 아닌 사업장 일반폐기물로 분류함 · 5톤 이상은 배출자 신고 대상(폐기물관리법 시행령 제2조8호)", - "Default: not separate — included in the expense category. When on, it sits outside total cost. Note: the 100 t separate-order rule is for construction waste; tree waste is classified as business general waste. 5 t or more requires a discharge report", + "⚠ 임목폐기물은 분리발주 대상 아님(환경부 질의회신 — 임목폐기물처리 실정보고 p4 · 건설폐기물 업무처리지침: 건설폐기물 아닌 사업장 일반폐기물) — 기본 꺼짐으로 원가계산서 경비에 듭니다. 이 칸은 다른 건설폐기물(위탁처리 100톤 이상 분리발주 대상)이 생길 때 쓰는 칸 — 켜면 총원가 밖에 서고 총공사비에만 더합니다. 5톤 이상은 배출자 신고 대상(폐기물관리법 시행령 제2조8호)", + "Tree waste is not subject to separate ordering (Ministry of Environment reply; classified as business general waste) — off by default, inside the expense category. Use this switch for other construction waste (100 t or more). When on, it sits outside total cost. 5 t or more requires a discharge report", ], B08_Quantity_Side_SubgradeCompaction: ["노체다짐", "Subgrade Compaction"], B08_Quantity_SubgradeCompaction_Label: ["노체다짐을 셀 것인가", "Count subgrade compaction"], From 509660708b406875d6eae8cfdcd13df2e49efc71 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 01:10:58 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(b08):=20=EB=BF=8C=EB=A6=AC=EB=B6=84=20?= =?UTF-8?q?=EC=A7=81=EA=B2=BD=20=EC=B9=B8=20=EC=9D=B4=EB=A6=84=EC=97=90=20?= =?UTF-8?q?=E3=80=8C=ED=9D=89=EA=B3=A0=EC=A7=81=EA=B2=BD=20=EC=95=84?= =?UTF-8?q?=EB=8B=98=E3=80=8D=20=EB=AA=85=EC=8B=9C=20=E2=80=94=20D=20?= =?UTF-8?q?=EB=8A=94=20=EB=BF=8C=EB=A6=AC=EB=B6=84=20=EC=A7=81=EA=B2=BD(20?= =?UTF-8?q?26-09-14=20=ED=8C=90=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- B08_Quantity/B08_Quantity_Engine_Preparation_TreeWaste.py | 4 ++-- ui_template/ui_template_locale_b2.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Engine_Preparation_TreeWaste.py b/B08_Quantity/B08_Quantity_Engine_Preparation_TreeWaste.py index af918aee..9ff72b71 100644 --- a/B08_Quantity/B08_Quantity_Engine_Preparation_TreeWaste.py +++ b/B08_Quantity/B08_Quantity_Engine_Preparation_TreeWaste.py @@ -7,7 +7,7 @@ ① 분배비(기본) 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 본수 — **현장 조사값**이라 칸으로 받는다. @@ -51,7 +51,7 @@ TREE_WASTE_BASIS = ( ) ROOT_RATIO_BASIS = "뿌리 WR = WA × 15/85(분배비 · 일반 수목류)" ROOT_BALL_BASIS = "뿌리 WR = 0.3927 × D³ × 1,300㎏/㎥ × N(뿌리분 체적 · 지침 부록1)" -ROOT_BALL_LABEL = "뿌리분 직경 D(m)" +ROOT_BALL_LABEL = "뿌리분 직경 D(m, 흉고직경 아님 · 굴취 흙덩이 직경)" #: 배출자 신고 경계 — 화면이 알릴 뿐 값을 바꾸지 않는다. REPORT_THRESHOLD_TON = 5.0 NOT_SEPARATE_NOTE = ( diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index bb365230..cae3c143 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -727,8 +727,8 @@ export const ui_locales_b2 = { ], B08_Quantity_TreeWaste_RootBall: ["뿌리분 체적 × 1,300㎏/㎥", "Root-ball volume × 1,300 kg/㎥"], B08_Quantity_TreeWaste_RootBallDiameter: [ - "뿌리분 직경 D(m) — 체적법만", - "Root-ball diameter D (m) — volume method", + "뿌리분 직경 D(m, 흉고직경 아님) — 체적법만", + "Root-ball diameter D (m, not DBH) — volume method", ], B08_Quantity_TreeWaste_Root_Hint: [ "환경친화적인 도로건설 지침 부록1 이 두 방법을 둡니다. 분배비(기본): 뿌리 15 : 지상 85(일반 수목류, 한국건설기술연구원 2012). 뿌리분 체적: V = πD²(D/2 + RH2/3) = 0.3927D³ · 뿌리분 중량 = V × 1,300㎏/㎥ × 본수. ⚠ 지침은 D 를 「뿌리분의 직경(DBH, m)」으로 적어 흉고직경인지 뿌리분 직경인지 갈리지 않음 — 흉고직경으로 두지 않고 따로 받습니다. 참고: 실무 실정보고(한 현장)는 뿌리 14 : 86 · 0.65톤/㎥ 를 씀",