"""토공 보조 탭 셋 — 토공집계표 · 평균운반거리 · 종방향 무대운반거리 (PLAN 69-2). 근거 = 거창 실무 워크북(`00.25년_산불_거창(…)내역+수량.xlsx`)의 같은 이름 시트 칸 · 식 그대로. 평균운반거리 덤프트럭 · 불도우저 각 토량(Q) · 거리(L) · Q×L · 합계 · 평균 = ROUND(ΣQ×L ÷ ΣQ, 2) 종방향 무대운반거리 무대운반 토량(Q) · 거리(L) · Q×L · 합계 · 평균(같은 식) 토공집계표 토적표 합계 줄 + 위 두 시트 ΣQ 를 시트 식대로 묶음 Q · L 출처 = 저장된 유토곡선 배분(`mass_haul.haul_plan` — B07 토적도가 그리는 것과 같은 정본) · 장비 띠(`blocks[].bands[]`)와 떨어진 구간 운반(`transfers[]`)의 `equipment` 로 가름 (`free_haul` = 무대 · `dozer` = 불도우저 · `dump_truck` = 덤프트럭). 암 갈래 — 「연암」 · 「경암」 줄 = 암 전체 × 비율(좌측 패널 · 기본 90 : 10 · PLAN 122). 풍화암 · 보통암 · 표토제거 · 법면보호공 · 운반성토 줄은 값이 없어 비움(None) · 구조물 쪽 줄(su2 시트 구조물터파기 · 토사다짐 · 되메우기 · 잔토)은 뺌 · 거창 계수(성토면다짐 0.8 · 초류종자 성토 0.5 · 지장목 0.8)는 근거가 없어 값 빈칸 + 「비율 미정」(PLAN 143-2). 짝 — `rock_split` · `split_rock_columns` ↔ `B08_Quantity_Options_Calc.ts` `rockSplit` · `splitRockColumns` · `splitRockSummary`(거울 시험 `test_122_b08_options_mirror`). """ from __future__ import annotations from decimal import ROUND_HALF_UP, Decimal from typing import Any from common_util.common_util_project_settings import SOFT_ROCK_PCT_DEFAULT #: 운반 시트 열 — 장비 키 HAUL_EQUIPMENT = ("dump_truck", "dozer", "free_haul") #: 암 칸 → 연암 칸 · 경암 칸(토적표 · 합계) · 칸 꼬리 셋 ROCK_SPLIT = ( ("cut_rock", "cut_soft_rock", "cut_hard_rock"), ("ditch_rock", "ditch_soft_rock", "ditch_hard_rock"), ) ROCK_SUFFIXES = ("area_m2", "volume_m3", "adjusted_m3") def excel_round(value: float, digits: int = 0) -> float: """엑셀 ROUND — 반은 0 에서 먼 쪽(파이썬 round 는 짝수 쪽).""" step = Decimal(1).scaleb(-digits) return float(Decimal(str(value)).quantize(step, rounding=ROUND_HALF_UP)) def rock_split(value: float, soft_pct: float) -> tuple[float, float]: """암 → (연암, 경암) — 연암 = 암 × 비율(0~100 자름) · 경암 = 암 − 연암.""" soft = value * min(max(soft_pct, 0.0), 100.0) / 100.0 return soft, value - soft def split_rock_columns(record: dict[str, Any], soft_pct: float) -> dict[str, Any]: """줄 · 합계의 암 칸마다 연암 · 경암 칸을 더함(숫자인 칸만).""" for base, soft_key, hard_key in ROCK_SPLIT: for suffix in ROCK_SUFFIXES: value = record.get(f"{base}_{suffix}") if isinstance(value, (int, float)) and not isinstance(value, bool): soft, hard = rock_split(float(value), soft_pct) record[f"{soft_key}_{suffix}"] = soft record[f"{hard_key}_{suffix}"] = hard return record def haul_lists(haul_plan: dict[str, Any] | None) -> dict[str, dict[str, Any]]: """장비별 `{rows: [{q, l, ql}], q, ql, average_m}` — 시트 한 줄 = 띠 · 운반 하나.""" items: dict[str, list[dict[str, float]]] = {key: [] for key in HAUL_EQUIPMENT} plan = haul_plan if isinstance(haul_plan, dict) else {} bands = [band for block in plan.get("blocks") or [] for band in block.get("bands") or []] for item in [*bands, *(plan.get("transfers") or [])]: rows = items.get(str(item.get("equipment"))) q, dist = item.get("volume_m3"), item.get("haul_distance_m") if rows is None or not isinstance(q, (int, float)) or not isinstance(dist, (int, float)): continue rows.append({"q": float(q), "l": float(dist), "ql": float(q) * float(dist)}) out: dict[str, dict[str, Any]] = {} for key, rows in items.items(): q = sum(row["q"] for row in rows) ql = sum(row["ql"] for row in rows) out[key] = { "rows": rows, "q": q, "ql": ql, "average_m": excel_round(ql / q, 2) if q > 0 else None, } return out def _row(group: str, kind: str, spec: str, unit: str, value: float | None, note: str = ""): return {"group": group, "kind": kind, "spec": spec, "unit": unit, "value": value, "note": note} def summary_rows( totals: dict[str, float], haul: dict[str, dict[str, Any]], soft_pct: float = SOFT_ROCK_PCT_DEFAULT, ) -> list[dict[str, Any]]: """토공집계표 줄 — 시트 C5:G37 차례 · 식 그대로(셀 주소는 줄 끝 주석) · 암 = 연암 % 로 나눔.""" t = totals cut_soft, cut_hard = rock_split(t["cut_rock_volume_m3"], soft_pct) ditch_soft, ditch_hard = rock_split(t["ditch_rock_volume_m3"], soft_pct) r0 = excel_round free_q, dump_q, dozer_q = haul["free_haul"]["q"], haul["dump_truck"]["q"], haul["dozer"]["q"] diverted = t["diverted_m3"] m3 = "㎥" unsure = "비율 미정" # 거창 계수 자리 — 값을 확인한 뒤 다시 잇기(143-2) return [ _row("흙깍기", "토사", "기계(굴삭기)", m3, r0(t["cut_soil_volume_m3"])), # 토적표!E137 _row("흙깍기", "풍화암", "굴삭기 + 브레카", m3, None), _row("흙깍기", "연암", "굴삭기 + 브레카", m3, r0(cut_soft)), _row("흙깍기", "보통암", "굴삭기 + 브레카", m3, None), _row("흙깍기", "경암", "발파 + 굴삭기", m3, r0(cut_hard)), _row("측구 터파기", "토사", "기계", m3, r0(t["ditch_soil_volume_m3"])), # 토적표!K137 _row("측구 터파기", "풍화암", "굴삭기 + 브레카", m3, None), _row("측구 터파기", "연암", "굴삭기 + 브레카", m3, r0(ditch_soft)), _row("측구 터파기", "보통암", "굴삭기 + 브레카", m3, None), _row("측구 터파기", "경암", "발파 + 굴삭기", m3, r0(ditch_hard)), _row("보정량계", "", "", m3, r0(t["adjusted_total_m3"])), # 토적표!P137 _row("성토", "", "", m3, r0(t["fill_volume_m3"])), # 토적표!R137 _row("유용토 소계", "", "", m3, diverted + free_q), # G18+G19 _row("횡방향유용토", "", "백호우", m3, diverted, "유용토"), # 토적표!S137 _row("종방향유용토", "", "백호우", m3, free_q, "유용토"), # 종방향무대운반거리!B25 _row("토사운반", "", "BH+DT+BH", m3, dump_q), # 평균운반거리!B42 _row("토사운반", "", "19 TON B/D", m3, dozer_q), # 평균운반거리 불도우저 ΣQ _row("잔토처리", "", "백호우", m3, r0(t["balance_m3"])), # 토적표!T137 _row("층따기", "", "백호우", m3, t["benching_area_m2"]), # 토적표!W137 _row("성토면다짐", "", "", "㎡", None, unsure), # 거창 = 성토면 × 0.8 _row("표토제거", "", "", "㎡", None), _row("초류종자살포", "", "씨드스프레이", "㎡", None, unsure), # 거창 = 성토 × 0.5 + 절토 _row("법면보호공", "", "", "㎡", None), _row("지장목제거", "", "", "㎡", None, unsure), # 거창 = (성토 × 0.5 + 절토) × 0.8 _row("운반성토", "", "BH+DT+BH", m3, None), ]