feat(B08): 채집석 공제 소요량을 냄 — 빼지는 않음 (사용자 확정 ②)

돌은 기본 「캔다」(별표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) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 19:15:09 +09:00
co-authored by Claude Opus 5
parent 06f47ab2d0
commit 9d2c70dc8f
2 changed files with 68 additions and 2 deletions
@@ -183,6 +183,11 @@ def build_handoff(
result["basis_unit_warnings"] = verify_unit_matches_basis( result["basis_unit_warnings"] = verify_unit_matches_basis(
work_items, extra=table.declared_units() 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 result["placing_notes"] = placing_notes
return result return result
@@ -165,6 +165,26 @@ STONE_MASONRY = {
#: ⚠ 우리에게 **잡석다짐 폭이 아직 없다**(기초잡석은 품셈에 공종만 있고 두께·폭이 없음). #: ⚠ 우리에게 **잡석다짐 폭이 아직 없다**(기초잡석은 품셈에 공종만 있고 두께·폭이 없음).
#: 그래서 **터파기 폭(평균두께 + 여유 0.2m)을 잠정으로** 쓰고 근거 문구에 그 사실을 적는다. #: 그래서 **터파기 폭(평균두께 + 여유 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 BLINDING_THICKNESS_M = 0.10
#: 버림을 뺄 수 있는 칸 — 「기본은 넣고, 빼고 싶으면 뺀다」(사용자 확정 ⑭). #: 버림을 뺄 수 있는 칸 — 「기본은 넣고, 빼고 싶으면 뺀다」(사용자 확정 ⑭).
#: 저장 제원에 이 칸이 없으면 **넣는 쪽**이 기본이다. #: 저장 제원에 이 칸이 없으면 **넣는 쪽**이 기본이다.
@@ -185,7 +205,10 @@ DESTINATION = {
"버림콘크리트": "unit_price", "버림콘크리트": "unit_price",
"모르터": "unit_price", "모르터": "unit_price",
"거푸집": "unit_price", "거푸집": "unit_price",
"물구멍관": "material", # ⚠ 자재 카탈로그가 이름으로 찾는다 — 공백 없는 한 낱말(B09 규약) "물구멍관": "material",
# ⚠ 자재도 토공도 아닌 자리 — **유토곡선이 사토에서 뺄 밑수**다. 자재총괄은
# `material` 만 모으므로 여기 섞이지 않는다.
"채집석": "haul_deduction", # ⚠ 자재 카탈로그가 이름으로 찾는다 — 공백 없는 한 낱말(B09 규약)
} }
# ⚠ 배합 성분 — 산출물에 나타나면 안 된다(㉢). B09 일위대가가 배합표로 분해한다. # ⚠ 배합 성분 — 산출물에 나타나면 안 된다(㉢). B09 일위대가가 배합표로 분해한다.
@@ -235,6 +258,17 @@ class StructureQuantity:
billing_quantity: float = 0.0 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: def wants_blinding(options: dict[str, Any]) -> bool:
"""버림 콘크리트를 넣을지. **정한 적이 없으면 넣는다**(사용자 확정 ⑭). """버림 콘크리트를 넣을지. **정한 적이 없으면 넣는다**(사용자 확정 ⑭).
@@ -519,7 +553,8 @@ def stone_masonry(
# 막자갈 = 입적 − (면적 × 뒷길이 × 뒤채움몫 + 고임돌). # 막자갈 = 입적 − (면적 × 뒷길이 × 뒤채움몫 + 고임돌).
# 뒤채움 몫은 **돌 종류로 갈린다** — 깬돌·잡석 1/2 · 야면석 1/3 (교본 7-3). # 뒤채움 몫은 **돌 종류로 갈린다** — 깬돌·잡석 1/2 · 야면석 1/3 (교본 7-3).
wedge = masonry_area * _num(table["wedge_stone_m3_per_m2"]) # None 이면 0 — 막자갈에서 안 뺌 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: if rubble > 0:
components.append( components.append(
Component( 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"] base_width = thickness + constants["excavation_extra_m"]
blinding = _blinding_component(base_width, length_m, options) blinding = _blinding_component(base_width, length_m, options)
@@ -836,5 +887,15 @@ def build_table(
"surcharge_applied": False, "surcharge_applied": False,
"mix_components_found": violations, "mix_components_found": violations,
"structure_count": len(quantities), "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"), "amount_spread": spread_by_unit(totals.values(), value_key="amount"),
} }