- 원문 본문 식(채워 넣는 서식)을 데이터로: Qt = T/γt×L · n = Qt/(0.7×K) · ㎝t = t1+t2+t3+t4+t5 · Q = 60×Qt×f×E/㎝t - 브레인 판정: n 의 Qt 계산값(원문 표기 10 버림) · 발파암 E 0.9(원문 누락) · Es 운반 줄 값 · 거리별 갈래 - 거리는 B08 인계(유토곡선·사토장)에서 — 내역이 조립에 넘기고, 호표 한 장은 코드 #L…m 에서 되읽음 - 거리 없는 줄은 0 원 아니라 「운반거리 미입력」 · 갈래는 범위 별칭 리핑암 → 암절취 - 배정 프로젝트 164.23m: 토사 3,249원/㎥ · 암절취 8,882원/㎥ · 같은 인계 대조로 바뀐 줄은 이 둘뿐(+1,437,981원) - 시험 5건 · 전체 1761 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
213 lines
8.9 KiB
Python
213 lines
8.9 KiB
Python
"""B09 원가계산 — **덤프트럭 운반** 시공능력 (산림사업 표준품셈 10-12 「2. 운반」 · 2026-09-14).
|
||
|
||
원문(고시 2025-82호 10-12-1·2·3 「2. 운반」)은 표가 아니라 **채워 넣는 식 서식**이라 공종 마스터에
|
||
안 실렸다. 값은 원문 본문에서 읽어 여기 한 곳에 둔다(식을 데이터로).
|
||
|
||
Qt = T / γt × L 덤프 1대 적재토량(㎥, 흐트러진 상태) · T 15ton
|
||
n = Qt / (q × K) 적재기계 싸이클 횟수 · q 버켓 0.7㎥
|
||
t1 = ㎝s × n / (60 × Es) 적재 대기(분) · ㎝s 20초
|
||
t2 = (D/V1 + D/V2) × 60 왕복(분) · D 운반거리 km · V1 5 · V2 6 km/hr
|
||
㎝t = t1 + t2 + t3 + t4 + t5 t3 적하 1.1 · t4 대기 0.9 · t5 덮개 0.5 (분)
|
||
Q = 60 × Qt × f × E / ㎝t ㎥/hr (자연상태) · f = 1/L
|
||
|
||
⚠ **원문이 서로 어긋나는 자리 — 고른 값과 버린 값을 사유에 나란히 둔다**(2026-09-14 브레인 판정).
|
||
사용자가 뒤집을 수 있어야 한다.
|
||
① n 의 Qt — 원문 표기 「10/(0.7×K)」(셋 다 10) ↔ **계산값 T/γt×L**(토사 10.26·암절취 8.44·
|
||
발파암 10.16). 원문이 `q` 한 글자를 적재토량·버켓용량 두 뜻으로 써 10 은 토사 어림수
|
||
복사로 봄.
|
||
② 발파암 운반 줄에 E 누락 — **0.9**(덤프 작업효율 · 같은 절 토사·암절취 0.9). 빼면 Q 를 못 구함.
|
||
④ Es(적재기계 작업효율 · t1) ≠ E0(적재 식) — **운반 줄 값 그대로**(토사 0.85 ·
|
||
암 0.35).
|
||
⚠ 운반거리 D 는 원문에 없다 — **설계자 값**(B08 유토곡선·사토장). 없으면 줄을 안 세우고
|
||
까닭(0 원 금지).
|
||
⚠ 적재(「1. 적재」 굴착기 Q1)는 **별도 줄**이다 — 이 식의 t1 은 덤프가 기다리는 시간이지
|
||
굴착기 품이 아님(흙깎기 9-3-2 ㎝ 20초·135° 는 깎아 옆에 둠 — 22초·180° 싣기와 다른 일).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from dataclasses import dataclass
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
from B09_Estimation.B09_Estimation_MachineProductivity import fix2
|
||
|
||
#: 「[주] 장비는 덤프트럭(15ton)을 적용한다」 — 기계 카탈로그 덤프트럭 15.
|
||
DUMP_TRUCK_CODE = "0602-0150"
|
||
DUMP_PARENT = "FP-10-12"
|
||
|
||
_TRUCK_TON = Decimal(15)
|
||
_BUCKET_M3 = Decimal("0.7")
|
||
_LOADER_CYCLE_SEC = Decimal(20)
|
||
_V_LOADED_KMH = Decimal(5)
|
||
_V_EMPTY_KMH = Decimal(6)
|
||
_T3_UNLOAD, _T4_WAIT, _T5_COVER = Decimal("1.1"), Decimal("0.9"), Decimal("0.5")
|
||
_SIXTY = Decimal(60)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class DumpMaterial:
|
||
"""10-12 절 하나의 재료 값 — 원문 「2. 운반」 줄."""
|
||
|
||
work_item_code: str
|
||
label: str
|
||
unit_weight: Decimal # γt ton/㎥
|
||
loose_factor: Decimal # L 토량환산계수
|
||
bucket_factor: Decimal # K
|
||
loader_efficiency: Decimal # Es
|
||
truck_efficiency: Decimal # E
|
||
notes: tuple[str, ...] = ()
|
||
|
||
|
||
_COMMON_NOTE = (
|
||
"n 의 Qt = 계산값 T/γt×L(원문 표기 「10/(0.7×K)」의 10 은 토사 어림수 복사로 봄 — 버림)"
|
||
)
|
||
_ES_NOTE = "Es = 운반 줄 값(적재 식의 E0 와 다른 기호)"
|
||
|
||
DUMP_MATERIALS: dict[str, DumpMaterial] = {
|
||
"FP-10-12-01": DumpMaterial(
|
||
"FP-10-12-01", "토사", Decimal("1.9"), Decimal("1.3"), Decimal("0.9"), Decimal("0.85"),
|
||
Decimal("0.9"), (_COMMON_NOTE, _ES_NOTE),
|
||
),
|
||
"FP-10-12-02": DumpMaterial(
|
||
"FP-10-12-02", "암절취", Decimal("2.4"), Decimal("1.35"), Decimal("0.55"), Decimal("0.35"),
|
||
Decimal("0.9"), (_COMMON_NOTE + " · 원문 10 이면 n 1.18배", _ES_NOTE),
|
||
),
|
||
"FP-10-12-03": DumpMaterial(
|
||
"FP-10-12-03", "발파암", Decimal("2.4"), Decimal("1.625"), Decimal("0.55"), Decimal("0.35"),
|
||
Decimal("0.9"),
|
||
(_COMMON_NOTE, _ES_NOTE, "E 원문 누락 — 0.9 적용(같은 절 토사·암절취 · 덤프 작업효율)"),
|
||
),
|
||
} # fmt: skip
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class DumpHaul:
|
||
"""덤프 운반 한 벌 — 재료 + 운반거리."""
|
||
|
||
material: DumpMaterial
|
||
distance_m: Decimal
|
||
|
||
@property
|
||
def truck_load_m3(self) -> Decimal:
|
||
return _TRUCK_TON / self.material.unit_weight * self.material.loose_factor
|
||
|
||
@property
|
||
def loader_cycles(self) -> Decimal:
|
||
return self.truck_load_m3 / (_BUCKET_M3 * self.material.bucket_factor)
|
||
|
||
@property
|
||
def cycle_minutes(self) -> Decimal:
|
||
wait = _LOADER_CYCLE_SEC * self.loader_cycles / (_SIXTY * self.material.loader_efficiency)
|
||
km = self.distance_m / Decimal(1000)
|
||
travel = (km / _V_LOADED_KMH + km / _V_EMPTY_KMH) * _SIXTY
|
||
return wait + travel + _T3_UNLOAD + _T4_WAIT + _T5_COVER
|
||
|
||
@property
|
||
def volume_factor(self) -> Decimal:
|
||
return fix2(Decimal(1) / self.material.loose_factor)
|
||
|
||
@property
|
||
def hourly_output(self) -> Decimal:
|
||
"""Q (㎥/hr) — f 와 Q 는 소수 2자리로 먼저 확정(명세 7장)."""
|
||
return fix2(
|
||
_SIXTY
|
||
* self.truck_load_m3
|
||
* self.volume_factor
|
||
* self.material.truck_efficiency
|
||
/ self.cycle_minutes
|
||
)
|
||
|
||
@property
|
||
def formula_text(self) -> str:
|
||
m = self.material
|
||
return (
|
||
f"Qt = 15/{m.unit_weight}×{m.loose_factor} = {self.truck_load_m3:.2f}㎥ · "
|
||
f"n = {self.truck_load_m3:.2f}/(0.7×{m.bucket_factor}) = {self.loader_cycles:.2f}회 · "
|
||
f"㎝t = {self.cycle_minutes:.2f}분(L={self.distance_m}m) · "
|
||
f"Q = 60×{self.truck_load_m3:.2f}×{self.volume_factor}×{m.truck_efficiency}"
|
||
f"/{self.cycle_minutes:.2f} = {self.hourly_output}㎥/hr"
|
||
)
|
||
|
||
|
||
def dump_title_code(work_item_code: str, distance_m: Decimal) -> str:
|
||
"""운반거리별 갈래 — 사토장이 여럿이면 거리마다 호표가 갈림(STmate 도 그 모양)."""
|
||
return f"B-{work_item_code}#L{distance_m.normalize():f}m"
|
||
|
||
|
||
def attach_dump_hauls(build: Any, master: dict[str, Any], distances_m: tuple[Decimal, ...]) -> None:
|
||
"""거리마다 덤프 운반 일위대가를 세운다 — X(덤프트럭 15ton) → D → B.
|
||
|
||
시간당 사용료가 안 섰으면 세우지 않고 까닭을 남긴다(0 원 일위대가 금지).
|
||
"""
|
||
from B09_Estimation.B09_Estimation_PriceBook import PriceKind, PriceTitle
|
||
|
||
if not distances_m:
|
||
return
|
||
hourly = f"X-{DUMP_TRUCK_CODE}"
|
||
names = {
|
||
str(node.get("work_item_code")): str(node.get("name") or "")
|
||
for node in master.get("work_items", [])
|
||
}
|
||
for code, material in DUMP_MATERIALS.items():
|
||
if hourly not in build.book.titles:
|
||
build.component_gaps[code] = "덤프트럭(15ton) 시간당 사용료가 아직 안 섰습니다"
|
||
continue
|
||
for distance in distances_m:
|
||
haul = DumpHaul(material, distance)
|
||
title_code = dump_title_code(code, distance)
|
||
if title_code in build.book.titles:
|
||
continue
|
||
build.book.add_title(
|
||
PriceTitle(
|
||
code=title_code,
|
||
kind=PriceKind.UNIT_PRICE,
|
||
name=f"{names.get(DUMP_PARENT) or '덤프운반'} "
|
||
f"{names.get(code) or material.label}",
|
||
spec=f"L={distance.normalize():f}m",
|
||
unit="㎥",
|
||
)
|
||
)
|
||
build.book.add_output_detail(
|
||
title_code,
|
||
hourly,
|
||
Decimal(1) / haul.hourly_output,
|
||
haul.formula_text + " · " + " · ".join(material.notes),
|
||
output=haul.hourly_output,
|
||
)
|
||
|
||
|
||
def dump_haul_distances(payload: dict[str, Any]) -> tuple[str, ...]:
|
||
"""B08 인계에서 덤프 운반 거리(m) 갈래 — 거리가 없는 줄은 안 셈(그 줄은 「운반거리 미입력」)."""
|
||
found = {
|
||
str(Decimal(str(row["haul_distance_m"])))
|
||
for row in payload.get("work_items") or []
|
||
if str(row.get("work_item_code") or "").startswith(DUMP_PARENT)
|
||
and row.get("haul_equipment") == "dump_truck"
|
||
and row.get("haul_distance_m") not in (None, "")
|
||
}
|
||
return tuple(sorted(found, key=Decimal))
|
||
|
||
|
||
_RE_DUMP_CODE = re.compile(rf"^[BD]-{DUMP_PARENT}-0[1-3]#L(\d+(?:\.\d+)?)m$")
|
||
|
||
|
||
def dump_distance_from_code(code: str) -> tuple[str, ...]:
|
||
"""호표 한 장을 여는 쪽 — 코드 `#L164.23m` 에서 거리를 되읽음(인계를 다시 안 셈)."""
|
||
found = _RE_DUMP_CODE.match(code)
|
||
return (found.group(1),) if found else ()
|
||
|
||
|
||
def dump_child_for(ground: str | None, edition: str) -> str | None:
|
||
"""인계 갈래(토사·리핑암·발파암) → 10-12 잎. 모르는 갈래는 `None` — 가까운 것을 안 고름."""
|
||
from common_util.common_util_aliases import alias_target, load_aliases
|
||
|
||
wanted = str(ground or "").strip()
|
||
if not wanted:
|
||
return None
|
||
alias = alias_target(load_aliases("variant"), wanted, DUMP_PARENT, edition) or wanted
|
||
return next(
|
||
(code for code, material in DUMP_MATERIALS.items() if material.label == alias), None
|
||
)
|