From d6916225dea964bfd4e97f4af97d1cb0d12731ec Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 00:27:29 +0900 Subject: [PATCH] =?UTF-8?q?feat(b08):=20=EB=85=B8=EC=B2=B4=EB=8B=A4?= =?UTF-8?q?=EC=A7=90=20=EC=B9=B8=20=E2=80=94=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EA=BA=BC=EC=A7=90=20=C2=B7=20=EC=BC=9C=EB=A9=B4=20=ED=86=A0?= =?UTF-8?q?=EA=B3=B5=EC=A7=91=EA=B3=84=20=EB=B3=84=EB=8F=84=20=EC=A4=84(?= =?UTF-8?q?=EC=88=98=EB=9F=89=20=3D=20=EC=84=B1=ED=86=A0=EB=9F=89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 산출 조건 subgrade_compaction_enabled · 기본 False(9-16-2 [주]⑤ 「대규모 성토지로서 층다짐이 필요한 경우」 조건부) - 토공집계 「노체다짐」 줄 · 규격 진동롤러(자주식 10ton) · 매핑 FP-09-16-02 · 갈래 없음 - [주]③ 굴착기+진동롤러 조합 중 굴착기는 성토(포설) 줄이 셈 — 두 번 안 셈(#조합 철회) - 칸 밑에 [주]⑤ 근거 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- .../B08_Quantity_Engine_EarthworkSummary.py | 24 ++++++++++ B08_Quantity/B08_Quantity_Router_Earthwork.py | 4 ++ B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts | 2 + B08_Quantity/B08_Quantity_UI_Page.ts | 22 +++++++++ common_util/common_util_project_settings.py | 3 ++ .../work_item_mapping_2026-01-01.json | 8 ++++ .../tester/test_b08_subgrade_compaction.py | 46 +++++++++++++++++++ ui_template/ui_template_locale_b2.ts | 6 +++ 8 files changed, 115 insertions(+) create mode 100644 resources/tester/test_b08_subgrade_compaction.py diff --git a/B08_Quantity/B08_Quantity_Engine_EarthworkSummary.py b/B08_Quantity/B08_Quantity_Engine_EarthworkSummary.py index fa7f6d26..63b9639e 100644 --- a/B08_Quantity/B08_Quantity_Engine_EarthworkSummary.py +++ b/B08_Quantity/B08_Quantity_Engine_EarthworkSummary.py @@ -72,6 +72,20 @@ class SummaryInput: rock_classes: list[str] = field(default_factory=list) rock_ratios_pct: dict[str, float] = field(default_factory=dict) application_ratios: dict[str, float] = field(default_factory=dict) + # 노체다짐 — 기본 꺼짐(2026-09-13 판정). 켜면 성토 밑에 별도 줄이 선다. + subgrade_compaction_enabled: bool = False + + +#: 노체다짐 줄 — ⭐ 2026-09-13 판정 「별도 줄 · 칸으로 켜고 끔 · 기본 꺼짐」. +#: 성토 단가(포설)에 묻지 않는다 — 묻으면 왜 그 금액인지 설명이 안 되고 켜고 끈 것이 안 보인다. +#: ⚠ [주]③ 「굴착기와 진동롤러 조합」은 작업 방식 설명 — 굴착기는 성토(포설 9-16-1) 줄이 이미 셈. +#: 이 줄에 굴착기를 또 넣으면 두 번 셈(2026-09-14 「#조합」 철회). +SUBGRADE_COMPACTION_SPEC = "진동롤러(자주식 10ton)" +SUBGRADE_COMPACTION_NOTE = ( + "산출 조건에서 켬 — 품셈 9-16-2 [주]⑤ 「대규모 성토지로서 층다짐이 필요한 경우 적용」" + " · 수량 = 성토량(포설과 같은 밑수 ㎥)" + " · [주]③ 굴착기+진동롤러 조합 중 굴착기는 성토(포설) 줄이 셈" +) def _ratio(source: SummaryInput, key: str) -> float: @@ -121,6 +135,16 @@ def build_rows(source: SummaryInput) -> list[SummaryRow]: rows.append(SummaryRow(group="보정량계", amount=earth.get("adjusted_total_m3", 0.0))) rows.append(SummaryRow(group="성토", amount=earth.get("fill_volume_m3", 0.0))) + if source.subgrade_compaction_enabled: + # ⚠ 끄면 줄 자체를 안 낸다 — 빈 줄은 「세야 함」으로 읽힌다(임목파쇄와 같은 자리). + rows.append( + SummaryRow( + group="노체다짐", + spec=SUBGRADE_COMPACTION_SPEC, + amount=earth.get("fill_volume_m3", 0.0), + note=SUBGRADE_COMPACTION_NOTE, + ) + ) # ── 운반 — 수단별. 무대는 집계에 오르되 내역 줄이 아니다 ──────── rows.extend(_haul_rows(source)) diff --git a/B08_Quantity/B08_Quantity_Router_Earthwork.py b/B08_Quantity/B08_Quantity_Router_Earthwork.py index d6cc73e7..78d5b6f5 100644 --- a/B08_Quantity/B08_Quantity_Router_Earthwork.py +++ b/B08_Quantity/B08_Quantity_Router_Earthwork.py @@ -149,6 +149,8 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse: key: application_ratio(settings, key) for key in (settings.get("application_ratios_pct") or {}) }, + # 노체다짐 — 기본 꺼짐. 켠 프로젝트에서만 줄이 선다(2026-09-13 판정). + subgrade_compaction_enabled=bool(settings.get("subgrade_compaction_enabled")), ) ) # 준비공·사방공 — 못 서는 줄도 사유와 함께 남긴다(빈 표는 「빠뜨림」과 구별이 안 됨). @@ -452,6 +454,8 @@ class QuantitySettingsBody(BaseModel): # 임목파쇄 — 기본 꺼짐(확정 5차 5번). 켜면 줄이 서고, 부피를 넣으면 값이 선다. wood_chipping_enabled: bool | None = None wood_chipping_volume_m3: float | None = None + # 노체다짐 — 기본 꺼짐(9-16-2 [주]⑤ 조건부). 켜면 토공집계에 별도 줄. + subgrade_compaction_enabled: bool | None = None # 토량환산계수(다짐) — `{갈래: {"compacted": C, "reason": 사유}}`. # ⚠ **기본값을 복사해 넣지 않는다** — 안 고른 갈래는 키가 없어야 정본이 선다. # 빈 dict 는 「전부 기본값으로 되돌림」이라 통째로 갈아 끼운다. diff --git a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts index a2eb1d0c..36c58ed7 100644 --- a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts +++ b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts @@ -106,6 +106,8 @@ export interface QuantitySettings { wood_chipping_enabled?: boolean | null; /** 파쇄 부피(㎥) — 켜도 이 값이 없으면 줄만 서고 사유가 남는다. */ wood_chipping_volume_m3?: number | null; + /** 노체다짐 — 기본 꺼짐(9-16-2 [주]⑤ 조건부). 켜면 토공집계에 별도 줄. */ + subgrade_compaction_enabled?: boolean | null; } /** 갈래 하나의 「무엇을 골랐나」. 서버 `earthwork_conversion_choices` 와 짝이다. */ diff --git a/B08_Quantity/B08_Quantity_UI_Page.ts b/B08_Quantity/B08_Quantity_UI_Page.ts index c5b2741c..9125dedf 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.ts +++ b/B08_Quantity/B08_Quantity_UI_Page.ts @@ -108,6 +108,7 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr frame_material: draft.frame_material, wood_chipping_enabled: draft.wood_chipping_enabled, wood_chipping_volume_m3: draft.wood_chipping_volume_m3, + subgrade_compaction_enabled: draft.subgrade_compaction_enabled, // 개소는 **통째로** 보낸다 — 지운 항목까지 그대로 가야 되돌릴 길이 있다. ancillary_counts: draft.ancillary_counts, // 토량환산계수 — 고른 갈래만 담긴다. 빈 dict 는 「전부 기본값으로 되돌림」이다. @@ -297,6 +298,8 @@ interface DraftSettings { // 임목파쇄 — **기본 꺼짐**(확정 5차 5번). 켜야 줄이 선다. 근주이식은 칸 자체가 없다. wood_chipping_enabled: boolean; wood_chipping_volume_m3: number | null; + // 노체다짐 — **기본 꺼짐**(9-16-2 [주]⑤ 조건부). 켜면 토공집계에 별도 줄(수량 = 성토량). + subgrade_compaction_enabled: boolean; // 자재별 관급/사급 — 표 안에서 줄마다 고른 값. material_supply: Record; // 갈래별 토량환산계수(다짐) — `compacted` 가 `null` 이면 「안 고름」이라 기본값이 선다. @@ -682,6 +685,24 @@ function buildQuantitySidePanel( ); panel.append(hintRow(L("B08_Quantity_Side_Chipping_Hint"))); + // ── 노체다짐 — ⚠ **기본 꺼짐**(2026-09-13 판정). 9-16-2 [주]⑤ 조건부라 켜야 토공집계에 줄이 선다. + panel.append(field(L("B08_Quantity_Side_SubgradeCompaction"), "")); + panel.append( + selectField( + L("B08_Quantity_SubgradeCompaction_Label"), + draft.subgrade_compaction_enabled ? "on" : "", + [ + { value: "", label: L("B08_Quantity_Chipping_Off") }, + { value: "on", label: L("B08_Quantity_Chipping_On") }, + ], + (value) => { + draft.subgrade_compaction_enabled = value === "on"; + draft.dirty = true; + }, + ), + ); + panel.append(hintRow(L("B08_Quantity_Side_SubgradeCompaction_Hint"))); + // ── 콘크리트 타설 방식 — ⚠ **금액에 바로 걸리는 값**이라 잠정임을 조용히 두지 않는다 ── panel.append(field(L("B08_Quantity_Side_Placing"), "")); panel.append( @@ -979,6 +1000,7 @@ export async function renderB08Quantity(root: HTMLElement): Promise { 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, + subgrade_compaction_enabled: Boolean(stored.subgrade_compaction_enabled), 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 989dc34a..0b312aee 100644 --- a/common_util/common_util_project_settings.py +++ b/common_util/common_util_project_settings.py @@ -157,6 +157,9 @@ def default_settings() -> dict[str, Any]: # 물을 일도 없다. 켜도 **부피는 지어내지 않는다**(아래 칸이 비면 줄만 서고 사유). "wood_chipping_enabled": False, "wood_chipping_volume_m3": None, + # 노체다짐(9-16-2) — **기본 꺼짐**(2026-09-13 판정). [주]⑤ 「대규모 성토지로서 + # 층다짐이 필요한 경우 적용」 조건부라 늘 서지 않는다. 켜면 토공집계에 별도 줄. + "subgrade_compaction_enabled": False, "dataset_versions": {}, }, "estimation": { diff --git a/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json b/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json index c3da4294..6c3bf199 100644 --- a/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json +++ b/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json @@ -70,6 +70,14 @@ "master_name": "노체 > 노체포설", "note": "실무 내역 셋을 다 봤더니 「노체포설·노체다짐」으로 가른 줄이 **어디에도 없다**(2026-09-08 데스크탑 보조). 영월은 「암성토 BACK-HOE(0.7㎥)」 한 줄이고 그 단가산출 안에 적사→성토→다짐 3단계가 들어가며, 울진은 「유용성토」·「사토 및 다짐공」, 봉화는 성토사면다짐만 따로다. ⇒ **성토 본체 한 줄(포설) + 성토면다짐(㎡) 따로**가 실무 양식이라 부모(FP-09-16)에서 자식 포설로 내렸다. 다짐은 그 단가 안 단계로 둔다." }, + { + "group": "노체다짐", + "work_item_code": "FP-09-16-02", + "basis_unit": "㎥", + "basis_source": "산림사업 표준품셈(고시 2025-82) 원문 L5336 「Q=1000×V×W×E×D×f/N= ㎥/시간」 — 포설(9-16-1)과 같은 체적 축이라 성토량을 그대로 밑수로 씀.", + "master_name": "노체 > 노체다짐", + "note": "2026-09-13 브레인 판정 — 별도 줄 · 산출 조건 칸으로 켜고 끔 · 기본 꺼짐([주]⑤ 「대규모 성토지로서 층다짐이 필요한 경우」 조건부). [주]③ 「굴착기와 진동롤러 조합」은 작업 방식 설명 — 굴착기는 포설(9-16-1) 줄, 롤러는 이 줄(B09 B-FP-09-16-02 롤러만). 갈래 없음(2026-09-14 「#조합」 철회 — X-<기계>#조합 잡재료 16% 층과 다른 뜻)." + }, { "group": "성토면다짐", "work_item_code": "FP-09-17-01", diff --git a/resources/tester/test_b08_subgrade_compaction.py b/resources/tester/test_b08_subgrade_compaction.py new file mode 100644 index 00000000..562c765e --- /dev/null +++ b/resources/tester/test_b08_subgrade_compaction.py @@ -0,0 +1,46 @@ +"""노체다짐 칸 — 기본 꺼짐 · 켜면 토공집계 별도 줄 · 수량 = 성토량 (2026-09-13 판정).""" + +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import SummaryInput, build_rows # noqa: E402 + +TOTALS = {"fill_volume_m3": 1234.5} + + +def _groups(enabled: bool) -> dict: + rows = build_rows(SummaryInput(earthwork_totals=TOTALS, subgrade_compaction_enabled=enabled)) + return {row.group: row for row in rows} + + +def test_끄면_줄_자체가_없다() -> None: + assert "노체다짐" not in _groups(False) + + +def test_켜면_성토량으로_선다_근거가_비고에() -> None: + row = _groups(True)["노체다짐"] + assert (row.unit, row.amount, row.in_bill) == ("㎥", 1234.5, True) + assert "[주]⑤" in row.note and "층다짐이 필요한 경우" in row.note + assert "굴착기" not in row.spec # 굴착기는 성토(포설) 줄이 셈 — 두 번 안 셈 + + +def test_인계가_9_16_2_로_잇고_갈래를_안_싣는다() -> None: + from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import build_table + from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff + + table = build_table(SummaryInput(earthwork_totals=TOTALS, subgrade_compaction_enabled=True)) + handed = {r["name"]: r for r in build_handoff(summary_table=table)["work_items"]} + row = handed["노체다짐"] + assert (row["work_item_code"], row["quantity"], row["in_bill"]) == ("FP-09-16-02", 1234.5, True) + assert row["variant_value"] is None and row["blocked_kind"] is None + + +def test_설정_기본은_꺼짐() -> None: + from common_util.common_util_project_settings import default_settings + + assert default_settings()["quantity"]["subgrade_compaction_enabled"] is False diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 3f53bbd3..14cbc9b1 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -711,6 +711,12 @@ export const ui_locales_b2 = { "기본은 안 셉니다(확정 5차 5번) — 현장에 따라 필요하면 켜세요. ⚠ 켜도 부피를 넣어야 값이 섭니다(실무는 부피로 셈)", "Off by default — turn on per site. Volume must be entered for the row to carry a quantity", ], + B08_Quantity_Side_SubgradeCompaction: ["노체다짐", "Subgrade Compaction"], + B08_Quantity_SubgradeCompaction_Label: ["노체다짐을 셀 것인가", "Count subgrade compaction"], + B08_Quantity_Side_SubgradeCompaction_Hint: [ + "기본은 안 셉니다 — 근거: 산림품셈 9-16-2 [주]⑤ 「대규모 성토지로서 층다짐이 필요한 경우 적용한다」. 켜면 토공집계에 「노체다짐」 줄이 성토량(㎥)으로 섭니다. [주]③ 굴착기+진동롤러 조합 중 굴착기는 성토(포설) 줄이 이미 셉니다", + "Off by default — forest standard 9-16-2 note ⑤ applies it only to large fills needing layer compaction. When on, a compaction row equal to the fill volume is added; the excavator of the note ③ pairing is already in the fill (spreading) row", + ], B08_Quantity_Side_Frame: ["규준틀 개소당 재료", "Batter Board Materials (per unit)"], B08_Quantity_Side_Frame_Hint: [ "⚠ 채워진 값은 실무 관측값(제안값)이고 법정 기준이 아닙니다 — 품셈 11-2·11-3 [주]④ 는 「재료량은 설계수량에 따른다」로만 둡니다. 손율은 원문값(비탈 50% · 수평 80%)",