From 9d2c70dc8f435fdee1d8cd827fbfce5c3c745d8d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 19:15:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=EC=B1=84=EC=A7=91=EC=84=9D=20?= =?UTF-8?q?=EA=B3=B5=EC=A0=9C=20=EC=86=8C=EC=9A=94=EB=9F=89=EC=9D=84=20?= =?UTF-8?q?=EB=83=84=20=E2=80=94=20=EB=B9=BC=EC=A7=80=EB=8A=94=20=EC=95=8A?= =?UTF-8?q?=EC=9D=8C=20(=EC=82=AC=EC=9A=A9=EC=9E=90=20=ED=99=95=EC=A0=95?= =?UTF-8?q?=20=E2=91=A1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 돌은 기본 「캔다」(별표2 「야면석 등은 **가급적 현장에서 채취·사용**하도록 운반거리를 조사한다」 · 실무 견적 다섯 권에 야면석 구입 단가 0건). 구조물마다 「구입」으로 바꿀 수 있음. ⚠⚠ **채집석 공제는 사토에서 한 번만 뺀다.** B08 은 소요량(`collected_stone_deduction_m3`, ㎥ **양수**)을 내기만 하고 공제하지 않으며, 빼는 자리는 유토곡선의 사토뿐임 — 실어 내는 몫(spoil_m3 − natural_spoil_m3)에서 먼저 빼고 모자라면 자연방토에서 뺌. (랩탑 메인의 통로에도 같은 문장을 박아 둠 — 두 곳이 같은 말이라야 나중에 누가 봐도 안 갈림.) ⚠ **부호를 넘기지 않음.** 실무 시트가 `−274.66` 이라 그대로 넘기면 두 번 뒤집힘. 밑수 — 면석 몸통(면적×뒷길이×몫) + 고임돌 + 막자갈. **이미 우리 식 안에 있는 값들**이라 새 수치를 지어내지 않음. 목적지는 `haul_deduction` 이라 자재총괄(`material` 만 모음)에 안 섞임. 인계 맨 위에 구조물 합산 한 값으로 실림. 시험 754 통과(새 6건 포함). Co-Authored-By: Claude Opus 5 (1M context) --- B08_Quantity/B08_Quantity_Engine_Handoff.py | 5 ++ .../B08_Quantity_Engine_UnitQuantity.py | 65 ++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff.py b/B08_Quantity/B08_Quantity_Engine_Handoff.py index b24a0e0d..26c8353d 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff.py @@ -183,6 +183,11 @@ def build_handoff( result["basis_unit_warnings"] = verify_unit_matches_basis( work_items, extra=table.declared_units() ) + # ⚠ 채집석 공제 — **양수 ㎥ 로 넘기기만** 한다. 빼는 자리는 유토곡선의 사토뿐이다 + # (2026-09-09 세 창 확정 · 부호를 넘기면 두 번 뒤집힌다). + result["collected_stone_deduction_m3"] = float( + (unit_quantity_table or {}).get("collected_stone_deduction_m3") or 0.0 + ) result["placing_notes"] = placing_notes return result diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index aa7b62c5..d730a4ca 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -165,6 +165,26 @@ STONE_MASONRY = { #: ⚠ 우리에게 **잡석다짐 폭이 아직 없다**(기초잡석은 품셈에 공종만 있고 두께·폭이 없음). #: 그래서 **터파기 폭(평균두께 + 여유 0.2m)을 잠정으로** 쓰고 근거 문구에 그 사실을 적는다. #: 잡석다짐 폭이 정해지면 이 한 줄만 바꾸면 된다. +#: 돌을 **사 오나 캐나** — 기본은 「캔다」(2026-09-09 사용자 확정 ②). +#: 법이 그쪽을 권한다 — 별표2 「석축 등에 필요한 야면석 등은 **가급적 현장에서 +#: 채취·사용**하도록 운반거리를 조사한다」. 실무 견적 다섯 권에도 야면석 **구입 단가가 +#: 0건**이고 울진 일위대가는 「구입/채집」 두 벌을 갖고 있다. +#: ⚠ 구조물마다 바꿀 수 있다 — 저장 제원에 「구입」이라 적힌 구조물만 공제에서 빠진다. +STONE_SUPPLY_KEYS = ("stone_supply", "stone_source") +STONE_SUPPLY_PURCHASED = {"구입", "구입품", "사서", "purchase", "purchased", "buy"} + +#: ⚠⚠ **채집석 공제는 사토에서 한 번만 뺀다** (2026-09-09 세 창 확정 · 랩탑 메인의 통로에도 +#: 같은 문장이 박혀 있다 — 두 곳이 같은 말이라야 나중에 누가 봐도 안 갈린다). +#: +#: 채집석 공제는 사토에서 한 번만 뺀다. +#: B08 은 소요량(collected_stone_deduction_m3, ㎥ 양수)을 내기만 하고 공제하지 않으며, +#: 빼는 자리는 유토곡선의 사토뿐이다 — +#: 실어 내는 몫(spoil_m3 − natural_spoil_m3)에서 먼저 빼고 모자라면 자연방토에서 뺀다. +#: +#: ⚠ **부호를 넘기지 않는다.** 실무 시트가 `−274.66` 으로 적혀 있어 그대로 넘기면 두 번 +#: 뒤집힌다. 우리는 **양수**로 주고, 빼는 것은 받는 쪽이 한다. +COLLECTED_STONE_KEY = "collected_stone_deduction_m3" + BLINDING_THICKNESS_M = 0.10 #: 버림을 뺄 수 있는 칸 — 「기본은 넣고, 빼고 싶으면 뺀다」(사용자 확정 ⑭). #: 저장 제원에 이 칸이 없으면 **넣는 쪽**이 기본이다. @@ -185,7 +205,10 @@ DESTINATION = { "버림콘크리트": "unit_price", "모르터": "unit_price", "거푸집": "unit_price", - "물구멍관": "material", # ⚠ 자재 카탈로그가 이름으로 찾는다 — 공백 없는 한 낱말(B09 규약) + "물구멍관": "material", + # ⚠ 자재도 토공도 아닌 자리 — **유토곡선이 사토에서 뺄 밑수**다. 자재총괄은 + # `material` 만 모으므로 여기 섞이지 않는다. + "채집석": "haul_deduction", # ⚠ 자재 카탈로그가 이름으로 찾는다 — 공백 없는 한 낱말(B09 규약) } # ⚠ 배합 성분 — 산출물에 나타나면 안 된다(㉢). B09 일위대가가 배합표로 분해한다. @@ -235,6 +258,17 @@ class StructureQuantity: billing_quantity: float = 0.0 +def is_collected_stone(options: dict[str, Any]) -> bool: + """이 구조물의 돌을 **캐서 쓰는가**. 정한 적이 없으면 「캔다」(확정 ②).""" + for key in STONE_SUPPLY_KEYS: + raw = options.get(key) + if raw is None: + continue + if str(raw).strip().lower() in STONE_SUPPLY_PURCHASED: + return False + return True + + def wants_blinding(options: dict[str, Any]) -> bool: """버림 콘크리트를 넣을지. **정한 적이 없으면 넣는다**(사용자 확정 ⑭). @@ -519,7 +553,8 @@ def stone_masonry( # 막자갈 = 입적 − (면적 × 뒷길이 × 뒤채움몫 + 고임돌). # 뒤채움 몫은 **돌 종류로 갈린다** — 깬돌·잡석 1/2 · 야면석 1/3 (교본 7-3). wedge = masonry_area * _num(table["wedge_stone_m3_per_m2"]) # None 이면 0 — 막자갈에서 안 뺌 - rubble = volume - (masonry_area * (back_cm / 100.0) * body_ratio + wedge) + stone_body = masonry_area * (back_cm / 100.0) * body_ratio # 면석 몸통 체적 + rubble = volume - (stone_body + wedge) if rubble > 0: components.append( Component( @@ -576,6 +611,22 @@ def stone_masonry( ) ) + # 채집석 — **캐서 쓰는 구조물**의 돌 체적. 사토에서 뺄 밑수이고 **여기서 빼지 않는다.** + if is_collected_stone(options): + collected = max(stone_body + wedge + max(rubble, 0.0), 0.0) + if collected > 0: + components.append( + Component( + "채집석", + "㎥", + collected, + DESTINATION["채집석"], + f"면석 몸통 {stone_body:.3f} + 고임돌 {wedge:.3f} + 막자갈" + f" {max(rubble, 0.0):.3f} ㎥ · 현장 채집분 ·" + " ⚠ 여기서 빼지 않음 — 사토에서 한 번만 뺌", + ) + ) + # 버림 콘크리트 — 기초 바닥에 까는 얇은 층. 빼려면 저장 제원에서 「안 넣음」으로 둔다. base_width = thickness + constants["excavation_extra_m"] blinding = _blinding_component(base_width, length_m, options) @@ -836,5 +887,15 @@ def build_table( "surcharge_applied": False, "mix_components_found": violations, "structure_count": len(quantities), + # ⚠ 사토에서 뺄 밑수 — **양수**로 낸다. 빼는 것은 유토곡선(랩탑 메인) 몫이다. + COLLECTED_STONE_KEY: round( + sum( + component.amount + for item in quantities + for component in item.components + if component.name == "채집석" + ), + 3, + ), "amount_spread": spread_by_unit(totals.values(), value_key="amount"), }