feat(b08): 노체다짐 칸 — 기본 꺼짐 · 켜면 토공집계 별도 줄(수량 = 성토량)

- 산출 조건 subgrade_compaction_enabled · 기본 False(9-16-2 [주]⑤ 「대규모 성토지로서 층다짐이 필요한 경우」 조건부)
- 토공집계 「노체다짐」 줄 · 규격 진동롤러(자주식 10ton) · 매핑 FP-09-16-02 · 갈래 없음
- [주]③ 굴착기+진동롤러 조합 중 굴착기는 성토(포설) 줄이 셈 — 두 번 안 셈(#조합 철회)
- 칸 밑에 [주]⑤ 근거

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
2026-09-14 00:27:29 +09:00
co-authored by Claude Opus 5
parent 9f72ad19b8
commit d6916225de
8 changed files with 115 additions and 0 deletions
@@ -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))
@@ -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 는 「전부 기본값으로 되돌림」이라 통째로 갈아 끼운다.
@@ -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` 와 짝이다. */
+22
View File
@@ -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<string, SupplyChoice>;
// 갈래별 토량환산계수(다짐) — `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<void> {
frame_material: { ...((stored.frame_material ?? {}) as Record<string, number | null>) },
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<string, number | null>),
},
@@ -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": {
@@ -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",
@@ -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
+6
View File
@@ -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%)",