feat(b09): 단가산출 Q 식 — f·Q 소수 2자리 확정 뒤 나눔 · D 줄 0.1원·머리 원 미만 절사

- fix2(사사오입 0.01) — 굴착기·도자·다짐 작업량, 암 평균 Q, 표 직접 작업량
- TRUNCATED_KINDS 에 PRICE_BASIS(D) 더함 — B 는 다음 차례
- 골든셋 ③ 단가산출근거 실무 6건 208 호표 전수 일치(제외 소계·% 줄·산근 참조·계약단가 읽기) · 회귀 3,545.5 → 3,545
- 검증 프로젝트 내역 본체 122,924,846 → 122,870,975 · 전체 시험 1606 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
2026-09-14 00:26:45 +09:00
co-authored by Claude Opus 5
parent e5295dc181
commit af23a5d209
7 changed files with 164 additions and 31 deletions
@@ -32,7 +32,7 @@ from __future__ import annotations
import re
from dataclasses import dataclass
from decimal import Decimal
from decimal import ROUND_HALF_UP, Decimal
from typing import Any
from B09_Estimation.B09_Estimation_MachineCost import load_machine_catalog
@@ -75,6 +75,18 @@ class ProductivityError(ValueError):
"""시공능력을 못 세운 경우. 0 이나 가운데값으로 때우지 않는다."""
_HUNDREDTH = Decimal("0.01")
def fix2(value: Decimal) -> Decimal:
"""**소수 2자리로 먼저 확정**(사사오입) — 작업량 Q 와 토량환산계수 f(명세 7장 · STmate 18번 §2.2).
⚠ 원값으로 나누면 어긋남 — `55,700 ÷ 15.708 = 3,545.96` ≠ `55,700 ÷ 15.71 = 3,545.5`.
f 도 `1/1.175 = 0.85` 로 먼저 자른 뒤 곱함(봉화 2024 단가산출근거 제2호표).
"""
return Decimal(value).quantize(_HUNDREDTH, rounding=ROUND_HALF_UP)
def parse_measure(cell: str) -> Decimal | None:
"""계수 셀 하나를 수로 읽는다. **확정값이 아니면 `None`.**
@@ -117,7 +129,12 @@ class CycleFactors:
def formula_text(self) -> str:
return (
f"Q = 3600 ÷ {self.cycle_seconds} × {self.bucket_capacity_m3} × "
f"{self.bucket_coefficient} × {self.volume_factor} × {self.efficiency}"
f"{self.bucket_coefficient} × {fix2(self.volume_factor)} × {self.efficiency}"
+ (
f" = {hourly_output(self)} ㎥/hr (f·Q 소수 2자리 확정)"
if self.cycle_seconds > 0
else ""
)
)
@@ -134,23 +151,23 @@ class FactorGap:
def hourly_output(factors: CycleFactors) -> Decimal:
"""시간당 작업량 `Q` (㎥/hr).
`Q = (3600 ÷ Cm) · q · K · f · E` — 품셈 8-1-4.
`Q = (3600 ÷ Cm) · q · K · f · E` — 품셈 8-1-4. f 와 Q 는 **소수 2자리로 확정**(`fix2`).
"""
if factors.cycle_seconds <= 0:
raise ProductivityError(
f"{factors.work_item_code}: 1싸이클 시간(Cm)이 {factors.cycle_seconds} 입니다."
)
cycles_per_hour = _SECONDS_PER_HOUR / factors.cycle_seconds
output = (
cycles_per_hour
_SECONDS_PER_HOUR
* factors.bucket_capacity_m3
* factors.bucket_coefficient
* factors.volume_factor
* fix2(factors.volume_factor)
* factors.efficiency
/ factors.cycle_seconds
)
if output <= 0:
raise ProductivityError(f"{factors.work_item_code}: 시간당 작업량이 {output} 입니다.")
return output
return fix2(output)
def machine_hours_per_unit(factors: CycleFactors) -> Decimal: