From 962bbfe9718662c95ecc46b18ec60af01744abe1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 12:16:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=EC=9E=84=EB=AA=A9=ED=8C=8C?= =?UTF-8?q?=EC=87=84=20=EC=BC=9C=EB=8A=94=20=EC=B9=B8=20(=ED=99=95?= =?UTF-8?q?=EC=A0=95=205=EC=B0=A8=205=EB=B2=88)=20=E2=80=94=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EA=BA=BC=EC=A7=90=20=C2=B7=20=EA=B7=BC=EC=A3=BC?= =?UTF-8?q?=EC=9D=B4=EC=8B=9D=EC=9D=80=20=EC=B9=B8=20=EC=97=86=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 「기본 안 셈」이되 「현장에 따라 파쇄가 적용될 필요 있음」이라 **임목파쇄만** 켤 수 있게 둠 - ⚠ 안 켠 프로젝트에는 **줄 자체가 없음** — 빈 칸이 「세야 함」으로 읽히지 않게 (부대시설은 법정 의무라 늘 서는 것과 다른 자리) - 켜도 부피는 지어내지 않음 — 넣어야 값이 서고 FP-08-11(이동식 임목 파쇄)이 붙음 - 근주이식(FP-14-02)은 **칸도 안 만듦** — 켤 자리가 없으면 물을 일도 없음 - 설정 `wood_chipping_enabled`·`wood_chipping_volume_m3` + 화면 칸 둘 - tmp/tests/test_b08_wood_chipping.py 신설(5건) Co-Authored-By: Claude Opus 5 (1M context) --- .../B08_Quantity_Engine_Preparation.py | 45 +++++++++++++++++++ B08_Quantity/B08_Quantity_Router_Earthwork.py | 7 +++ B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts | 4 ++ B08_Quantity/B08_Quantity_UI_Page.ts | 37 +++++++++++++++ common_util/common_util_project_settings.py | 5 +++ ui_template/ui_template_locale_b2.ts | 9 ++++ 6 files changed, 107 insertions(+) diff --git a/B08_Quantity/B08_Quantity_Engine_Preparation.py b/B08_Quantity/B08_Quantity_Engine_Preparation.py index fc1c3537..9c1a82c0 100644 --- a/B08_Quantity/B08_Quantity_Engine_Preparation.py +++ b/B08_Quantity/B08_Quantity_Engine_Preparation.py @@ -246,6 +246,48 @@ ROOT_STEPS_NOTE = ( ) +#: ⭐ 2026-09-09 **사용자 확정 5차 5번** — 근주이식·임목파쇄는 **기본 안 셈**. +#: ⚠ 다만 「**현장에 따라 파쇄가 적용될 필요 있음**」이라 **임목파쇄만 켤 수 있는 칸**을 둔다. +#: **기본은 꺼짐**이고, **근주이식은 칸도 안 만든다**(켤 자리가 없으면 물을 일도 없다). +#: ⚠ 켜도 **부피는 지어내지 않는다** — 실무는 부피(영월 78㎥ @56,124)로 세고, 그 부피를 +#: 우리가 든 곳이 없다. 켜면 줄이 서고 **수량 칸이 비어 사유로 드러난다.** +CHIPPING_ITEM = "임목파쇄" +CHIPPING_CODE = "FP-08-11" # 이동식 임목 파쇄 +CHIPPING_OFF_NOTE = ( + "기본 안 셈(확정 5차 5번) — 현장에 따라 필요하면 산출 조건에서 켤 것." + " 근주이식(FP-14-02)은 칸도 두지 않음" +) +CHIPPING_ON_NOTE = ( + "켜져 있음(확정 5차 5번) — ⚠ **파쇄할 부피(㎥)를 든 곳이 없어** 물량이 안 섬." + " 실무는 부피로 셈(영월 78㎥). 산출 조건에 부피를 넣으면 값이 섬" +) + + +def chipping_rows(enabled: Any, volume_m3: Any) -> list[dict[str, Any]]: + """임목파쇄 — **켰을 때만** 줄이 선다. 끄면 줄 자체를 안 낸다. + + ⚠ 끈 상태에서 줄을 세우면 **빈 칸이 「세야 함」으로 읽힌다**(부대시설과 다른 자리 — + 그쪽은 법정 의무라 줄이 늘 서고, 이쪽은 **셀지 말지가 설계 판단**이다). + """ + if not bool(enabled): + return [] + try: + amount = float(volume_m3) if volume_m3 not in (None, "") else None + except (TypeError, ValueError): + amount = None + return [ + { + "group": "준비공", + "item": CHIPPING_ITEM, + "unit": "㎥", + "amount": amount, + "status": STATUS_READY if amount and amount > 0 else STATUS_PENDING, + "reason": CHIPPING_ON_NOTE if not amount else "산출 조건에서 넣은 부피 (확정 5차 5번)", + "work_item_code": CHIPPING_CODE, + } + ] + + def _root_steps_rows( slope: dict[str, float], stand_volume_class: str | None ) -> list[dict[str, Any]]: @@ -588,6 +630,8 @@ def build_table( ancillary_counts: dict[str, Any] | None = None, topsoil_haul_distance_m: float | None = None, stand_volume_class: str | None = None, + chipping_enabled: Any = False, + chipping_volume_m3: Any = None, ) -> dict[str, Any]: """화면·인계가 그대로 쓰는 모양. **못 서는 줄도 목록에 남는다.**""" rows = ( @@ -599,6 +643,7 @@ def build_table( stand_volume_class, ) + erosion_rows(structures, names) + + chipping_rows(chipping_enabled, chipping_volume_m3) + ancillary_rows(ancillary_counts) ) return { diff --git a/B08_Quantity/B08_Quantity_Router_Earthwork.py b/B08_Quantity/B08_Quantity_Router_Earthwork.py index 8fd45e00..cc53eb50 100644 --- a/B08_Quantity/B08_Quantity_Router_Earthwork.py +++ b/B08_Quantity/B08_Quantity_Router_Earthwork.py @@ -142,6 +142,9 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse: settings.get("topsoil_haul_distance_m"), # 임목축적 등급 — 품셈 9-21 제근이 소·중·밀로 갈리는 축(본수가 아니다). settings.get("stand_volume_class"), + # 임목파쇄 — 기본 꺼짐. 켠 프로젝트에서만 줄이 선다(확정 5차 5번). + settings.get("wood_chipping_enabled"), + settings.get("wood_chipping_volume_m3"), ) method, method_is_default = concrete_placing_method(settings) # ⚠ 금액에 바로 걸리는 값이라 「기본값으로 돌고 있음」을 응답에 실어 화면이 띄우게 한다. @@ -409,6 +412,9 @@ class QuantitySettingsBody(BaseModel): stand_volume_class: str | None = None # 규준틀 개소당 재료 — `{자재명: 수량}`. 비우면 제안값(실무 관측)이 선다. frame_material: dict[str, Any] | None = None + # 임목파쇄 — 기본 꺼짐(확정 5차 5번). 켜면 줄이 서고, 부피를 넣으면 값이 선다. + wood_chipping_enabled: bool | None = None + wood_chipping_volume_m3: float | None = None #: `None` 이 「안 정함」을 뜻하는 칸 — 저장에서 **버리지 않고 그대로 덮어쓴다**. @@ -419,6 +425,7 @@ NULLABLE_SETTING_KEYS = ( "spoil_site_distance_m", "rubble_base_thickness_m", "topsoil_haul_distance_m", + "wood_chipping_volume_m3", ) diff --git a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts index 7eb74241..95c9548b 100644 --- a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts +++ b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts @@ -83,6 +83,10 @@ export interface QuantitySettings { stand_volume_class?: string | null; /** 규준틀 개소당 재료 — `{자재명: 수량}`. 비우면 제안값(실무 관측)이 선다. */ frame_material?: Record; + /** 임목파쇄 — 기본 꺼짐(확정 5차 5번). 켜야 줄이 선다. */ + wood_chipping_enabled?: boolean | null; + /** 파쇄 부피(㎥) — 켜도 이 값이 없으면 줄만 서고 사유가 남는다. */ + wood_chipping_volume_m3?: number | null; } export interface EarthworkTable { diff --git a/B08_Quantity/B08_Quantity_UI_Page.ts b/B08_Quantity/B08_Quantity_UI_Page.ts index d710a543..3c5bc69c 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.ts +++ b/B08_Quantity/B08_Quantity_UI_Page.ts @@ -94,6 +94,8 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr topsoil_haul_distance_m: draft.topsoil_haul_distance_m, stand_volume_class: draft.stand_volume_class, frame_material: draft.frame_material, + wood_chipping_enabled: draft.wood_chipping_enabled, + wood_chipping_volume_m3: draft.wood_chipping_volume_m3, // 개소는 **통째로** 보낸다 — 지운 항목까지 그대로 가야 되돌릴 길이 있다. ancillary_counts: draft.ancillary_counts, }), @@ -284,6 +286,9 @@ interface DraftSettings { stand_volume_class: string; // 규준틀 개소당 재료 — 비우면 **제안값(실무 관측)**이 선다. 값이 아니라 「고칠 수 있음」이 요점. frame_material: Record; + // 임목파쇄 — **기본 꺼짐**(확정 5차 5번). 켜야 줄이 선다. 근주이식은 칸 자체가 없다. + wood_chipping_enabled: boolean; + wood_chipping_volume_m3: number | null; // 자재별 관급/사급 — 표 안에서 줄마다 고른 값. material_supply: Record; dirty: boolean; @@ -588,6 +593,36 @@ function buildQuantitySidePanel( } panel.append(hintRow(L("B08_Quantity_Side_Frame_Hint"))); + // ── 임목파쇄 — ⚠ **기본 꺼짐**(확정 5차 5번). 「셀지 말지가 설계 판단」이라 켜야 줄이 선다. + // ⚠ 근주이식은 **칸 자체를 안 만든다** — 켤 자리가 없으면 물을 일도 없다. + panel.append(field(L("B08_Quantity_Side_Chipping"), "")); + panel.append( + selectField( + L("B08_Quantity_Chipping_Label"), + draft.wood_chipping_enabled ? "on" : "", + [ + { value: "", label: L("B08_Quantity_Chipping_Off") }, + { value: "on", label: L("B08_Quantity_Chipping_On") }, + ], + (value) => { + draft.wood_chipping_enabled = value === "on"; + draft.dirty = true; + }, + ), + ); + panel.append( + optionalNumberField( + L("B08_Quantity_Chipping_Volume"), + draft.wood_chipping_volume_m3, + "1", + (value) => { + draft.wood_chipping_volume_m3 = value; + draft.dirty = true; + }, + ), + ); + panel.append(hintRow(L("B08_Quantity_Side_Chipping_Hint"))); + // ── 콘크리트 타설 방식 — ⚠ **금액에 바로 걸리는 값**이라 잠정임을 조용히 두지 않는다 ── panel.append(field(L("B08_Quantity_Side_Placing"), "")); panel.append( @@ -846,6 +881,8 @@ export async function renderB08Quantity(root: HTMLElement): Promise { topsoil_haul_distance_m: (stored.topsoil_haul_distance_m as number | null) ?? null, stand_volume_class: (stored.stand_volume_class as string) ?? "", frame_material: { ...((stored.frame_material ?? {}) as Record) }, + wood_chipping_enabled: Boolean(stored.wood_chipping_enabled), + wood_chipping_volume_m3: (stored.wood_chipping_volume_m3 as number | null) ?? null, ancillary_counts: { ...((stored.ancillary_counts ?? {}) as Record), }, diff --git a/common_util/common_util_project_settings.py b/common_util/common_util_project_settings.py index 805a72fe..28a97660 100644 --- a/common_util/common_util_project_settings.py +++ b/common_util/common_util_project_settings.py @@ -139,6 +139,11 @@ def default_settings() -> dict[str, Any]: # 품셈 11-2·11-3 [주]④ 가 「재료량은 설계수량에 따른다」라 값을 안 주는 자리이고, # 세는 것 자체는 확정이라 **제안값 + 고칠 수 있게**로 둔다(확정 ⑨·⑩ 과 같은 틀). "frame_material": {}, + # 임목파쇄 — **기본 꺼짐**(확정 5차 5번). 현장에 따라 켤 수 있는 칸. + # ⚠ 근주이식은 칸도 두지 않는다 — 「셀지 말지가 설계 판단」이라 켤 자리가 없으면 + # 물을 일도 없다. 켜도 **부피는 지어내지 않는다**(아래 칸이 비면 줄만 서고 사유). + "wood_chipping_enabled": False, + "wood_chipping_volume_m3": None, "dataset_versions": {}, }, "estimation": { diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 7e43adb6..553bda58 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -686,6 +686,15 @@ export const ui_locales_b2 = { "제근(품셈 9-21)이 이 등급으로 갈립니다 — ⚠ 본수가 아니라 축적이고, 산림조사부·영림계획에서 옮겨 적는 값입니다", "Root removal (9-21) splits by this class — stand volume, not tree count; copied from the forest survey", ], + B08_Quantity_Side_Chipping: ["임목파쇄", "Wood Chipping"], + B08_Quantity_Chipping_Label: ["임목파쇄를 셀 것인가", "Count wood chipping"], + B08_Quantity_Chipping_Off: ["안 셈(기본)", "No (default)"], + B08_Quantity_Chipping_On: ["셈", "Yes"], + B08_Quantity_Chipping_Volume: ["파쇄 부피(㎥)", "Chipping volume (㎥)"], + B08_Quantity_Side_Chipping_Hint: [ + "기본은 안 셉니다(확정 5차 5번) — 현장에 따라 필요하면 켜세요. ⚠ 켜도 부피를 넣어야 값이 섭니다(실무는 부피로 셈)", + "Off by default — turn on per site. Volume must be entered for the row to carry a quantity", + ], B08_Quantity_Side_Frame: ["규준틀 개소당 재료", "Batter Board Materials (per unit)"], B08_Quantity_Side_Frame_Hint: [ "⚠ 채워진 값은 실무 관측값(제안값)이고 법정 기준이 아닙니다 — 품셈 11-2·11-3 [주]④ 는 「재료량은 설계수량에 따른다」로만 둡니다. 손율은 원문값(비탈 50% · 수평 80%)",