feat(B08): 토공 탭 — 보조 탭 넷(토공집계표 · 토적표 · 평균운반거리 · 종방향 무대운반거리) · 토적표 견본 칸 (PLAN 69-2 · 69-3)
- 토적표 칸 = 사용자 견본 32칸 · 보정량계 = 절토 보정량만 · 면 계열(면고르기 · 지장목제거 · 제근 · 노면정리) 거리 = B07 횡단 수량 함수 · 면적 = 앞뒤 평균 × 측점 거리 - 토공집계표 · 운반 시트 = 거창 실무 워크북 같은 이름 시트 칸 · 식 · Q · L = 저장된 유토곡선 배분(서버 · 읽기만) - 시험 test_69_b08_earthwork_tabs.py · 보정량계 시험 정함대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
This commit is contained in:
@@ -10,8 +10,15 @@
|
||||
|
||||
측점 · 거리 · 절토[토사·암 각 (단면적·입적·보정량)] · 측구터파기[토사·암 각 3칸]
|
||||
· 보정량계 · 성토[단면적·입적] · 유용토 · 차인토량 · 누가토량
|
||||
· 면고르기[성토면·절토면] · 지장목제거[성토·절토] · 제근 · 노면정리 각 (거리·면적)
|
||||
|
||||
사면 계열(층따기·면고르기·법면보호공·지장목제거)은 사면길이가 아직 없어 일감 3에서 붙인다.
|
||||
보정량계 = 절토 보정량만 (PLAN 69 정함 · 사용자 견본 120+18 행 36.2 + 55.2 = 91.4)
|
||||
측구터파기 보정량은 칸에 두되 보정량계에 넣지 않는다.
|
||||
|
||||
면 계열 (PLAN 69-3) — 측점 「거리」 = B07 횡단도 수량과 같은 값
|
||||
면고르기 · 지장목제거 = `B07_DesignDetail_Engine_Cross_Quantity.derived_cells` 그대로 ·
|
||||
노면정리 = 그 측점 노면 폭(설계 `roadbed_width_m`) · 제근 = B07 에 값이 없어 빈 칸.
|
||||
면적 = (앞 거리 + 현 거리) ÷ 2 × 측점 거리 — 첫 측점은 거리만. 층따기는 토공집계표용.
|
||||
|
||||
평균단면적법 (신규 문서 5장 「다. 공사수량의 산출」)
|
||||
체적 = (앞 측점 단면적 + 현 측점 단면적) ÷ 2 × 두 측점 사이 거리.
|
||||
@@ -39,6 +46,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Iterable
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cross_Quantity import derived_cells
|
||||
from config.config_system_design import EARTHWORK_CONVERSION_FACTORS
|
||||
|
||||
# 절토 암을 어느 환산계수로 볼지 — 측점의 `cut_rock_kind` 를 그대로 쓴다.
|
||||
@@ -57,6 +65,17 @@ _FALLBACK_NOTE = "측구 가름값이 설계에 없어 절토 토사:암 면적
|
||||
ROCK_KINDS = ("ripping_rock", "blasting_rock")
|
||||
|
||||
|
||||
#: 면 계열 칸 — 화면 차례 · `benching`(층따기)은 토공집계표에만 씀
|
||||
FACE_KEYS = (
|
||||
"grading_fill",
|
||||
"grading_cut",
|
||||
"tree_removal_fill",
|
||||
"tree_removal_cut",
|
||||
"grubbing",
|
||||
"road_surface",
|
||||
"benching",
|
||||
)
|
||||
|
||||
#: 계수 묶음의 모양 — `{지반유형: {"compacted": C}}`.
|
||||
Factors = dict[str, dict[str, float]]
|
||||
|
||||
@@ -82,6 +101,8 @@ class StationArea:
|
||||
ditch_rock_area_m2: float | None = None
|
||||
ditch_split_basis: str | None = None
|
||||
cut_rock_kind: str | None = None
|
||||
# 면 계열 그 측점 거리(m) — `None` = B07 에 값 없음
|
||||
faces: dict[str, float | None] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_design(cls, chainage_m: float, design: dict[str, Any]) -> "StationArea":
|
||||
@@ -93,8 +114,12 @@ class StationArea:
|
||||
value = design.get(key)
|
||||
return float(value) if isinstance(value, (int, float)) else None
|
||||
|
||||
cells = derived_cells(float(chainage_m), design)
|
||||
faces = {key: cells.get(key) for key in FACE_KEYS}
|
||||
faces["road_surface"] = optional("roadbed_width_m")
|
||||
return cls(
|
||||
chainage_m=float(chainage_m),
|
||||
faces=faces,
|
||||
cut_soil_area_m2=num("cut_soil_area_m2"),
|
||||
cut_rock_area_m2=num("cut_rock_area_m2"),
|
||||
fill_area_m2=num("fill_area_m2"),
|
||||
@@ -137,6 +162,8 @@ class EarthworkRow:
|
||||
# 절토 암 칸이 어느 갈래(환산계수)로 셌나 — 화면이 리핑암 · 발파암 열로 가름
|
||||
cut_rock_kind: str = _DEFAULT_ROCK_KIND
|
||||
notes: list[str] = field(default_factory=list)
|
||||
# 면 계열 — `{키: {"length_m": 거리, "area_m2": 면적}}` · 값 없으면 None
|
||||
faces: dict[str, dict[str, float | None]] = field(default_factory=dict)
|
||||
|
||||
|
||||
def _split_ditch(area: StationArea) -> tuple[float, float, str]:
|
||||
@@ -224,12 +251,9 @@ def build_rows(
|
||||
row.ditch_soil_adjusted_m3 = row.ditch_soil_volume_m3 * soil_factor
|
||||
row.ditch_rock_adjusted_m3 = row.ditch_rock_volume_m3 * rock_factor
|
||||
|
||||
row.adjusted_total_m3 = (
|
||||
row.cut_soil_adjusted_m3
|
||||
+ row.cut_rock_adjusted_m3
|
||||
+ row.ditch_soil_adjusted_m3
|
||||
+ row.ditch_rock_adjusted_m3
|
||||
)
|
||||
row.faces = _face_cells(previous, station)
|
||||
# 보정량계 = 절토 보정량만(PLAN 69 정함) — 측구터파기는 칸에만
|
||||
row.adjusted_total_m3 = row.cut_soil_adjusted_m3 + row.cut_rock_adjusted_m3
|
||||
# 유용토 = 그 측점에서 절취분과 성토분이 서로 만나는 몫.
|
||||
row.diverted_m3 = min(row.adjusted_total_m3, row.fill_volume_m3)
|
||||
row.balance_m3 = row.adjusted_total_m3 - row.fill_volume_m3
|
||||
@@ -242,6 +266,22 @@ def build_rows(
|
||||
return rows
|
||||
|
||||
|
||||
def _face_cells(
|
||||
previous: StationArea | None, station: StationArea
|
||||
) -> dict[str, dict[str, float | None]]:
|
||||
"""면 계열 거리 · 면적 — 면적 = 앞뒤 평균 거리 × 측점 거리 · 첫 측점은 거리만."""
|
||||
out: dict[str, dict[str, float | None]] = {}
|
||||
for key in FACE_KEYS:
|
||||
length = station.faces.get(key)
|
||||
before = previous.faces.get(key) if previous else None
|
||||
area = None
|
||||
if previous is not None and (length is not None or before is not None):
|
||||
distance = station.chainage_m - previous.chainage_m
|
||||
area = ((before or 0.0) + (length or 0.0)) / 2.0 * distance
|
||||
out[key] = {"length_m": length, "area_m2": area}
|
||||
return out
|
||||
|
||||
|
||||
def totals(rows: list[EarthworkRow]) -> dict[str, float]:
|
||||
"""합계 행. 단면적은 합이 뜻이 없어 싣지 않는다(실무 토적표도 비워 둔다)."""
|
||||
keys = (
|
||||
@@ -264,6 +304,9 @@ def totals(rows: list[EarthworkRow]) -> dict[str, float]:
|
||||
mine = [row for row in rows if row.cut_rock_kind == kind]
|
||||
out[f"cut_{kind}_volume_m3"] = sum(row.cut_rock_volume_m3 for row in mine)
|
||||
out[f"cut_{kind}_adjusted_m3"] = sum(row.cut_rock_adjusted_m3 for row in mine)
|
||||
out[f"ditch_{kind}_volume_m3"] = sum(row.ditch_rock_volume_m3 for row in mine)
|
||||
for key in FACE_KEYS:
|
||||
out[f"{key}_area_m2"] = sum(row.faces.get(key, {}).get("area_m2") or 0.0 for row in rows)
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
"""토공 보조 탭 셋 — 토공집계표 · 평균운반거리 · 종방향 무대운반거리 (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` = 덤프트럭).
|
||||
|
||||
암 갈래 — 리핑암 = 「연암」 줄 · 발파암 = 「경암」 줄. 풍화암 · 보통암 · 구조물 쪽 줄(su2 시트)은
|
||||
값이 없어 비움(None).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from typing import Any
|
||||
|
||||
#: 운반 시트 열 — 장비 키
|
||||
HAUL_EQUIPMENT = ("dump_truck", "dozer", "free_haul")
|
||||
|
||||
|
||||
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 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]]) -> list[dict[str, Any]]:
|
||||
"""토공집계표 줄 — 시트 C5:G37 차례 · 식 그대로(셀 주소는 줄 끝 주석)."""
|
||||
t = totals
|
||||
r0 = excel_round
|
||||
free_q, dump_q, dozer_q = haul["free_haul"]["q"], haul["dump_truck"]["q"], haul["dozer"]["q"]
|
||||
diverted = t["diverted_m3"]
|
||||
tree = r0(t["tree_removal_fill_area_m2"] * 0.5 + t["tree_removal_cut_area_m2"], 2)
|
||||
m3 = "㎥"
|
||||
return [
|
||||
_row("흙깍기", "토사", "기계(굴삭기)", m3, r0(t["cut_soil_volume_m3"])), # 토적표!E137
|
||||
_row("흙깍기", "풍화암", "굴삭기 + 브레카", m3, None),
|
||||
_row("흙깍기", "연암", "굴삭기 + 브레카", m3, r0(t["cut_ripping_rock_volume_m3"])),
|
||||
_row("흙깍기", "보통암", "굴삭기 + 브레카", m3, None),
|
||||
_row("흙깍기", "경암", "발파 + 굴삭기", m3, r0(t["cut_blasting_rock_volume_m3"])),
|
||||
_row("측구 터파기", "토사", "기계", m3, r0(t["ditch_soil_volume_m3"])), # 토적표!K137
|
||||
_row("측구 터파기", "풍화암", "굴삭기 + 브레카", m3, None),
|
||||
_row("측구 터파기", "연암", "굴삭기 + 브레카", m3, r0(t["ditch_ripping_rock_volume_m3"])),
|
||||
_row("측구 터파기", "보통암", "굴삭기 + 브레카", m3, None),
|
||||
_row("측구 터파기", "경암", "발파 + 굴삭기", m3, r0(t["ditch_blasting_rock_volume_m3"])),
|
||||
_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("성토면다짐", "", "", "㎡", r0(t["grading_fill_area_m2"] * 0.8), "성토면80%반영"),
|
||||
_row("표토제거", "", "", "㎡", None),
|
||||
_row(
|
||||
"초류종자살포",
|
||||
"",
|
||||
"씨드스프레이",
|
||||
"㎡",
|
||||
r0(t["grading_fill_area_m2"] * 0.5 + t["grading_cut_area_m2"]),
|
||||
"성토면50%반영+절토면",
|
||||
),
|
||||
_row("법면보호공", "", "", "㎡", None),
|
||||
# 토적표!AI138 = ROUND(성토 × 0.5 + 절토, 2) → × 0.8
|
||||
_row("지장목제거", "", "", "㎡", r0(tree * 0.8), "80%반영"),
|
||||
_row("구조물터파기", "토사", "기계(굴삭기)", m3, None, "구조물 수량"),
|
||||
_row("구조물터파기", "풍화암", "굴삭기 + 브레카", m3, None),
|
||||
_row("구조물터파기", "연암", "굴삭기 + 브레카", m3, None, "구조물 수량"),
|
||||
_row("구조물터파기", "보통암", "굴삭기 + 브레카", m3, None),
|
||||
_row("구조물터파기", "경암", "발파 + 굴삭기", m3, None),
|
||||
_row("토사다짐", "", "", m3, None, "구조물 수량"),
|
||||
_row("되메우기", "", "", m3, None, "구조물 수량"),
|
||||
_row("잔토", "", "", m3, None, "현장내 성토(유용토)"),
|
||||
_row("운반성토", "", "BH+DT+BH", m3, None),
|
||||
]
|
||||
@@ -1,6 +1,9 @@
|
||||
"""B08 3탭 토적표 길 (PLAN 61-3) — 읽기만, 아무 파일 · DB 에도 쓰지 않음.
|
||||
"""B08 토공 탭 길 (PLAN 61-3 · 69-2) — 읽기만, 아무 파일 · DB 에도 쓰지 않음.
|
||||
|
||||
GET /api/projects/{id}/quantity/earthwork 측점별 횡단 설계 단면적 → 평균단면적법 토적표
|
||||
GET /api/projects/{id}/quantity/earthwork
|
||||
토적표 + 토공집계표 + 평균운반거리 · 종방향 무대운반거리
|
||||
|
||||
운반 Q · L = 종단 레코드에 저장된 유토곡선 배분(`mass_haul.haul_plan`) — B07 토적도와 같은 정본.
|
||||
|
||||
단면적 = B06 횡단 설계 저장값(`cross_sections.data.design`) · 노선 = 확정 노선(없으면 최신 노선) ·
|
||||
토량환산계수 = 유토곡선과 같은 `mixed_conversion_factors(quantity_settings)`.
|
||||
@@ -17,9 +20,11 @@ from fastapi import APIRouter, HTTPException
|
||||
from B06_Section.B06_Section_Repository import (
|
||||
get_confirmed_route_context,
|
||||
get_cross_section_designs,
|
||||
get_longitudinal_section,
|
||||
get_workflow_route_context,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_Earthwork import StationArea, build_table
|
||||
from B08_Quantity.B08_Quantity_Engine_Earthwork_Summary import haul_lists, summary_rows
|
||||
from B08_Quantity.B08_Quantity_Router_Hydro import _project_root
|
||||
from common_util.common_util_project_settings import mixed_conversion_factors, quantity_settings
|
||||
from config.config_db import get_db_pool
|
||||
@@ -38,6 +43,17 @@ async def earthwork_table(project_id: UUID) -> dict[str, Any]:
|
||||
raise HTTPException(status_code=404, detail="노선이 없습니다 — 노선 · 횡단 설계부터.")
|
||||
route_id = int(context["route_id"])
|
||||
rows = await get_cross_section_designs(connection, route_id)
|
||||
section = await get_longitudinal_section(connection, project_id, route_id)
|
||||
factors = await asyncio.to_thread(lambda: mixed_conversion_factors(quantity_settings(root)))
|
||||
stations = [StationArea.from_design(row["chainage_m"], row["design"]) for row in rows]
|
||||
return {**build_table(stations, factors), "route_id": route_id, "confirmed": confirmed}
|
||||
table = build_table(stations, factors)
|
||||
mass_haul = ((section or {}).get("data") or {}).get("mass_haul") or {}
|
||||
haul = haul_lists(mass_haul.get("haul_plan"))
|
||||
return {
|
||||
**table,
|
||||
"summary": summary_rows(table["totals"], haul),
|
||||
"haul": haul,
|
||||
"haul_saved": bool(mass_haul.get("haul_plan")),
|
||||
"route_id": route_id,
|
||||
"confirmed": confirmed,
|
||||
}
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
/* =============================================================================
|
||||
* B08_Quantity_UI_Earthwork.ts
|
||||
* 3탭 토적표(PLAN 61-3) — 줄 = 측점 · 열 = 옛 토적 엔진 출력 칸 그대로 · 맨 아래 합계 줄.
|
||||
* 토공 탭(PLAN 61-3 · 69-2 · 69-3) — 보조 탭 넷 = 토공집계표 · 토적표 · 평균운반거리 · 종방향 무대운반거리.
|
||||
*
|
||||
* GET …/quantity/earthwork (서버 단독 · 읽기만) — 평균단면적법 · 보정량 = 입적 × 다짐 환산계수
|
||||
* 절토 암은 그 측점이 센 갈래(`cut_rock_kind`)대로 리핑암 · 발파암 열에 놓음.
|
||||
* 표 모양은 1탭 집계표 것(`b08-summary__*`)을 다시 씀.
|
||||
* GET …/quantity/earthwork 한 번(서버 단독 · 읽기만) — 네 장이 같은 응답을 나눠 그림.
|
||||
* 토적표 = 줄 측점 · 칸 = 사용자 견본(절토 토사 · 암석 · 측구터파기 토사 · 암석 각 단면적 · 입적 ·
|
||||
* 보정량 · 보정량계 · 성토 · 유용토 · 차인토량 · 누가토량 · 면고르기 · 지장목제거 · 제근 · 노면정리 각
|
||||
* 거리 · 면적) · 맨 아래 합계 줄. 첫 측점은 앞이 없어 입적 · 면적이 「-」.
|
||||
* 표 모양은 1탭 집계표 것(`b08-summary__*`)을 다시 씀 · 나머지 셋은 `B08_Quantity_UI_Earthwork_Sheets`.
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { createButton, createCard } from "@ui/ui_template_elements";
|
||||
import { stationLabel } from "../common_util/common_util_station";
|
||||
import { stationInterval } from "./B08_Quantity_UI_Summary";
|
||||
import {
|
||||
renderEarthworkSummary,
|
||||
renderFreeHaul,
|
||||
renderHaulDistance,
|
||||
} from "./B08_Quantity_UI_Earthwork_Sheets";
|
||||
|
||||
type Face = { length_m: number | null; area_m2: number | null };
|
||||
|
||||
interface EarthworkRow {
|
||||
chainage_m: number;
|
||||
@@ -34,31 +44,70 @@ interface EarthworkRow {
|
||||
balance_m3: number;
|
||||
cumulative_m3: number;
|
||||
notes: string[];
|
||||
faces: Record<string, Face>;
|
||||
}
|
||||
|
||||
interface EarthworkTable {
|
||||
export interface HaulRow {
|
||||
q: number;
|
||||
l: number;
|
||||
ql: number;
|
||||
}
|
||||
|
||||
export interface HaulList {
|
||||
rows: HaulRow[];
|
||||
q: number;
|
||||
ql: number;
|
||||
average_m: number | null;
|
||||
}
|
||||
|
||||
export interface SummaryRow {
|
||||
group: string;
|
||||
kind: string;
|
||||
spec: string;
|
||||
unit: string;
|
||||
value: number | null;
|
||||
note: string;
|
||||
}
|
||||
|
||||
export interface EarthworkTable {
|
||||
rows: EarthworkRow[];
|
||||
totals: Record<string, number>;
|
||||
conversion_factors: Record<string, { compacted: number }>;
|
||||
station_count: number;
|
||||
route_id: number;
|
||||
confirmed: boolean;
|
||||
summary: SummaryRow[];
|
||||
haul: Record<"dump_truck" | "dozer" | "free_haul", HaulList>;
|
||||
haul_saved: boolean;
|
||||
}
|
||||
|
||||
const ROCKS = ["ripping_rock", "blasting_rock"] as const;
|
||||
const GROUPS: Array<[string, number]> = [
|
||||
["절토 토사", 3],
|
||||
["절토 리핑암", 3],
|
||||
["절토 발파암", 3],
|
||||
["측구터파기 토사", 3],
|
||||
["측구터파기 암", 3],
|
||||
];
|
||||
const ROCK_KIND_LABEL: Record<string, string> = { ripping_rock: "리핑암", blasting_rock: "발파암" };
|
||||
const TRIPLE = ["단면적", "입적", "보정량"];
|
||||
const FACE_GROUPS: Array<[string, Array<[string, string]>]> = [
|
||||
[
|
||||
"면고르기",
|
||||
[
|
||||
["grading_fill", "성토면"],
|
||||
["grading_cut", "절토면"],
|
||||
],
|
||||
],
|
||||
[
|
||||
"지장목제거",
|
||||
[
|
||||
["tree_removal_fill", "성토"],
|
||||
["tree_removal_cut", "절토"],
|
||||
],
|
||||
],
|
||||
["제근", [["grubbing", ""]]],
|
||||
["노면정리", [["road_surface", ""]]],
|
||||
];
|
||||
const FACE_KEYS = FACE_GROUPS.flatMap(([, faces]) => faces.map(([key]) => key));
|
||||
const DASH = "-";
|
||||
|
||||
const num = (value: number | undefined | null): string =>
|
||||
value == null || !Number.isFinite(value) ? "" : value.toFixed(2);
|
||||
|
||||
function cell(tag: "th" | "td", text: string, span?: { row?: number; col?: number }) {
|
||||
export function cell(tag: "th" | "td", text: string, span?: { row?: number; col?: number }) {
|
||||
const node = document.createElement(tag);
|
||||
node.textContent = text;
|
||||
if (span?.row) node.rowSpan = span.row;
|
||||
@@ -66,41 +115,63 @@ function cell(tag: "th" | "td", text: string, span?: { row?: number; col?: numbe
|
||||
return node;
|
||||
}
|
||||
|
||||
function note(text: string): HTMLElement {
|
||||
export function note(text: string): HTMLElement {
|
||||
const p = document.createElement("p");
|
||||
p.className = "ui-general-block__empty";
|
||||
p.textContent = text;
|
||||
return p;
|
||||
}
|
||||
|
||||
/** 표 한 장 — 1탭 집계표 모양 · 공용 컨테이너(카드) 안 */
|
||||
export function tableCard(
|
||||
info: HTMLElement[],
|
||||
className: string,
|
||||
parts: HTMLElement[],
|
||||
): HTMLElement {
|
||||
const el = document.createElement("table");
|
||||
el.className = `ui-general-block__table b08-summary__table ${className}`;
|
||||
el.append(...parts);
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "ui-general-block__table-wrap b08-summary__wrap";
|
||||
wrap.append(el);
|
||||
return createCard({ body: [...info, wrap] });
|
||||
}
|
||||
|
||||
/** 머리 석 줄 — 견본 차례 · 32칸 */
|
||||
function head(): HTMLTableSectionElement {
|
||||
const station = cell("th", "측점", { row: 3 });
|
||||
station.classList.add("b08-summary__station");
|
||||
const top = document.createElement("tr");
|
||||
top.append(cell("th", "측점", { row: 2 }), cell("th", "거리", { row: 2 }));
|
||||
for (const [label, span] of GROUPS) top.append(cell("th", label, { col: span }));
|
||||
top.append(
|
||||
cell("th", "보정량계", { row: 2 }),
|
||||
cell("th", "성토", { col: 2 }),
|
||||
cell("th", "유용토", { row: 2 }),
|
||||
cell("th", "차인토량", { row: 2 }),
|
||||
cell("th", "누가토량", { row: 2 }),
|
||||
station,
|
||||
cell("th", "거리", { row: 3 }),
|
||||
cell("th", "절토", { col: 6 }),
|
||||
cell("th", "측구터파기", { col: 6 }),
|
||||
cell("th", "보정량계", { row: 3 }),
|
||||
cell("th", "성토", { col: 2, row: 2 }),
|
||||
cell("th", "유용토", { row: 3 }),
|
||||
cell("th", "차인토량", { row: 3 }),
|
||||
cell("th", "누가토량", { row: 3 }),
|
||||
...FACE_GROUPS.map(([label, faces]) =>
|
||||
cell("th", label, { col: faces.length * 2, row: faces.length > 1 ? 1 : 2 }),
|
||||
),
|
||||
);
|
||||
top.firstElementChild?.classList.add("b08-summary__station");
|
||||
const mid = document.createElement("tr");
|
||||
for (let i = 0; i < 2; i += 1)
|
||||
mid.append(cell("th", "토사", { col: 3 }), cell("th", "암석", { col: 3 }));
|
||||
for (const [, faces] of FACE_GROUPS) {
|
||||
if (faces.length > 1) mid.append(...faces.map(([, label]) => cell("th", label, { col: 2 })));
|
||||
}
|
||||
const sub = document.createElement("tr");
|
||||
for (const _ of GROUPS) sub.append(...TRIPLE.map((t) => cell("th", t)));
|
||||
for (let i = 0; i < 4; i += 1) sub.append(...TRIPLE.map((t) => cell("th", t)));
|
||||
sub.append(cell("th", "단면적"), cell("th", "입적"));
|
||||
for (const _ of FACE_KEYS) sub.append(cell("th", "거리"), cell("th", "면적"));
|
||||
const thead = document.createElement("thead");
|
||||
thead.append(top, sub);
|
||||
thead.append(top, mid, sub);
|
||||
return thead;
|
||||
}
|
||||
|
||||
/** 절토 암 세 칸 — 그 측점 갈래 열에만 · 다른 갈래 열은 빈 칸 */
|
||||
function rockCells(row: EarthworkRow, kind: string): string[] {
|
||||
return row.cut_rock_kind === kind
|
||||
? [num(row.cut_rock_area_m2), num(row.cut_rock_volume_m3), num(row.cut_rock_adjusted_m3)]
|
||||
: ["", "", ""];
|
||||
}
|
||||
|
||||
function bodyRow(row: EarthworkRow, interval: number): HTMLTableRowElement {
|
||||
function bodyRow(row: EarthworkRow, first: boolean, interval: number): HTMLTableRowElement {
|
||||
const tr = document.createElement("tr");
|
||||
tr.dataset.b08Earthwork = String(row.chainage_m);
|
||||
const station = cell(
|
||||
@@ -109,55 +180,55 @@ function bodyRow(row: EarthworkRow, interval: number): HTMLTableRowElement {
|
||||
);
|
||||
station.className = "b08-summary__station";
|
||||
if (row.notes.length) station.title = row.notes.join("\n");
|
||||
// 첫 측점은 앞이 없어 입적 · 보정량 · 면적이 없음(견본 「-」)
|
||||
const v = (value: number): string => (first ? DASH : num(value));
|
||||
const triple = (area: number, volume: number, adjusted: number) => [
|
||||
num(area),
|
||||
v(volume),
|
||||
v(adjusted),
|
||||
];
|
||||
const values = [
|
||||
num(row.distance_m),
|
||||
num(row.cut_soil_area_m2),
|
||||
num(row.cut_soil_volume_m3),
|
||||
num(row.cut_soil_adjusted_m3),
|
||||
...ROCKS.flatMap((kind) => rockCells(row, kind)),
|
||||
num(row.ditch_soil_area_m2),
|
||||
num(row.ditch_soil_volume_m3),
|
||||
num(row.ditch_soil_adjusted_m3),
|
||||
num(row.ditch_rock_area_m2),
|
||||
num(row.ditch_rock_volume_m3),
|
||||
num(row.ditch_rock_adjusted_m3),
|
||||
num(row.adjusted_total_m3),
|
||||
...triple(row.cut_soil_area_m2, row.cut_soil_volume_m3, row.cut_soil_adjusted_m3),
|
||||
...triple(row.cut_rock_area_m2, row.cut_rock_volume_m3, row.cut_rock_adjusted_m3),
|
||||
...triple(row.ditch_soil_area_m2, row.ditch_soil_volume_m3, row.ditch_soil_adjusted_m3),
|
||||
...triple(row.ditch_rock_area_m2, row.ditch_rock_volume_m3, row.ditch_rock_adjusted_m3),
|
||||
v(row.adjusted_total_m3),
|
||||
num(row.fill_area_m2),
|
||||
num(row.fill_volume_m3),
|
||||
num(row.diverted_m3),
|
||||
num(row.balance_m3),
|
||||
num(row.cumulative_m3),
|
||||
v(row.fill_volume_m3),
|
||||
v(row.diverted_m3),
|
||||
v(row.balance_m3),
|
||||
v(row.cumulative_m3),
|
||||
...FACE_KEYS.flatMap((key) => {
|
||||
const face = row.faces?.[key];
|
||||
return [num(face?.length_m), first ? DASH : num(face?.area_m2)];
|
||||
}),
|
||||
];
|
||||
tr.append(station, ...values.map((v) => cell("td", v)));
|
||||
const cells = values.map((text) => cell("td", text));
|
||||
// 암석 칸 — 어느 갈래(환산계수)로 셌는지 마우스를 올려 봄
|
||||
if (row.cut_rock_area_m2 > 0) cells[4].title = ROCK_KIND_LABEL[row.cut_rock_kind] ?? "";
|
||||
tr.append(station, ...cells);
|
||||
return tr;
|
||||
}
|
||||
|
||||
/** 합계 줄 — 단면적은 합이 뜻이 없어 비움(옛 엔진 `totals` 와 같음) · 누가토량 = 마지막 줄 */
|
||||
/** 합계 줄 — 단면적 · 거리는 합이 뜻이 없어 비움(옛 엔진 `totals` 와 같음) · 누가토량 = 마지막 줄 */
|
||||
function totalRow(table: EarthworkTable): HTMLTableRowElement {
|
||||
const t = table.totals;
|
||||
const last = table.rows[table.rows.length - 1];
|
||||
const triple = (key: string) => ["", num(t[`${key}_volume_m3`]), num(t[`${key}_adjusted_m3`])];
|
||||
const values = [
|
||||
num(t.distance_m),
|
||||
"",
|
||||
num(t.cut_soil_volume_m3),
|
||||
num(t.cut_soil_adjusted_m3),
|
||||
...ROCKS.flatMap((kind) => [
|
||||
"",
|
||||
num(t[`cut_${kind}_volume_m3`]),
|
||||
num(t[`cut_${kind}_adjusted_m3`]),
|
||||
]),
|
||||
"",
|
||||
num(t.ditch_soil_volume_m3),
|
||||
num(t.ditch_soil_adjusted_m3),
|
||||
"",
|
||||
num(t.ditch_rock_volume_m3),
|
||||
num(t.ditch_rock_adjusted_m3),
|
||||
...triple("cut_soil"),
|
||||
...triple("cut_rock"),
|
||||
...triple("ditch_soil"),
|
||||
...triple("ditch_rock"),
|
||||
num(t.adjusted_total_m3),
|
||||
"",
|
||||
num(t.fill_volume_m3),
|
||||
num(t.diverted_m3),
|
||||
num(t.balance_m3),
|
||||
num(last?.cumulative_m3),
|
||||
...FACE_KEYS.flatMap((key) => ["", num(t[`${key}_area_m2`])]),
|
||||
];
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "b08-earthwork__total";
|
||||
@@ -168,8 +239,38 @@ function totalRow(table: EarthworkTable): HTMLTableRowElement {
|
||||
return tr;
|
||||
}
|
||||
|
||||
function renderSheet(host: HTMLElement, table: EarthworkTable, interval: number): void {
|
||||
if (!table.rows.length) {
|
||||
host.replaceChildren(note("측점 단면적이 아직 없습니다 — 횡단 설계를 먼저 마치세요."));
|
||||
return;
|
||||
}
|
||||
const c = (kind: string): string => num(table.conversion_factors[kind]?.compacted);
|
||||
const info = note(
|
||||
`평균단면적법 · 노선 ${table.route_id}${table.confirmed ? "(확정)" : "(확정 전 최신 노선)"} · ` +
|
||||
`측점 ${table.station_count} · 다짐 환산계수 토사 ${c("soil")} · 리핑암 ${c("ripping_rock")} · ` +
|
||||
`발파암 ${c("blasting_rock")} · 보정량계 = 절토 보정량 · 면 거리 = B07 횡단도 수량 · ` +
|
||||
`단위 m · ㎡ · ㎥ · * = 사유 있음(마우스를 올림)`,
|
||||
);
|
||||
const tbody = document.createElement("tbody");
|
||||
tbody.append(...table.rows.map((row, i) => bodyRow(row, i === 0, interval)));
|
||||
const tfoot = document.createElement("tfoot");
|
||||
tfoot.append(totalRow(table));
|
||||
const card = tableCard([info], "b08-earthwork__table", [head(), tbody, tfoot]);
|
||||
card.querySelector("table")!.dataset.b08Earthwork = "table";
|
||||
host.replaceChildren(card);
|
||||
}
|
||||
|
||||
const SHEETS = [
|
||||
{ id: "summary", label: "토공집계표" },
|
||||
{ id: "sheet", label: "토적표" },
|
||||
{ id: "haul", label: "평균운반거리" },
|
||||
{ id: "free", label: "종방향 무대운반거리" },
|
||||
] as const;
|
||||
|
||||
type SheetId = (typeof SHEETS)[number]["id"];
|
||||
|
||||
export async function renderEarthwork(host: HTMLElement, projectId: string): Promise<void> {
|
||||
host.replaceChildren(note("토적표를 만드는 중…"));
|
||||
host.replaceChildren(note("토공 수량을 만드는 중…"));
|
||||
let table: EarthworkTable;
|
||||
let interval = 20;
|
||||
try {
|
||||
@@ -186,29 +287,34 @@ export async function renderEarthwork(host: HTMLElement, projectId: string): Pro
|
||||
table = body;
|
||||
interval = gap;
|
||||
} catch (error) {
|
||||
host.replaceChildren(note(`토적표를 불러오지 못했습니다 — ${String(error)}`));
|
||||
host.replaceChildren(note(`토공 수량을 불러오지 못했습니다 — ${String(error)}`));
|
||||
return;
|
||||
}
|
||||
if (!table.rows.length) {
|
||||
host.replaceChildren(note("측점 단면적이 아직 없습니다 — 횡단 설계를 먼저 마치세요."));
|
||||
return;
|
||||
}
|
||||
const c = (kind: string): string => num(table.conversion_factors[kind]?.compacted);
|
||||
const info = note(
|
||||
`평균단면적법 · 노선 ${table.route_id}${table.confirmed ? "(확정)" : "(확정 전 최신 노선)"} · ` +
|
||||
`측점 ${table.station_count} · 다짐 환산계수 토사 ${c("soil")} · 리핑암 ${c("ripping_rock")} · ` +
|
||||
`발파암 ${c("blasting_rock")} · 단위 ㎡ · ㎥ · * = 사유 있음(마우스를 올림)`,
|
||||
);
|
||||
const tbody = document.createElement("tbody");
|
||||
tbody.append(...table.rows.map((row) => bodyRow(row, interval)));
|
||||
const tfoot = document.createElement("tfoot");
|
||||
tfoot.append(totalRow(table));
|
||||
const el = document.createElement("table");
|
||||
el.className = "ui-general-block__table b08-summary__table b08-earthwork__table";
|
||||
el.dataset.b08Earthwork = "table";
|
||||
el.append(head(), tbody, tfoot);
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "ui-general-block__table-wrap b08-summary__wrap";
|
||||
wrap.append(el);
|
||||
host.replaceChildren(info, wrap);
|
||||
|
||||
const tabs = document.createElement("div");
|
||||
tabs.className = "b08-quantity__tabs b08-earthwork__tabs";
|
||||
tabs.setAttribute("role", "tablist");
|
||||
const view = document.createElement("div");
|
||||
const select = (picked: SheetId): void => {
|
||||
tabs.replaceChildren(
|
||||
...SHEETS.map(({ id, label }) => {
|
||||
const button = createButton({
|
||||
label,
|
||||
variant: id === picked ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => id !== picked && select(id),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(id === picked));
|
||||
button.dataset.b08EarthworkSheet = id;
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
if (picked === "sheet") renderSheet(view, table, interval);
|
||||
else if (picked === "summary") renderEarthworkSummary(view, table);
|
||||
else if (picked === "haul") renderHaulDistance(view, table);
|
||||
else renderFreeHaul(view, table);
|
||||
};
|
||||
host.replaceChildren(tabs, view);
|
||||
select("summary");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/* =============================================================================
|
||||
* B08_Quantity_UI_Earthwork_Sheets.ts
|
||||
* 토공 보조 탭 셋(PLAN 69-2) — 토공집계표 · 평균운반거리 · 종방향 무대운반거리.
|
||||
*
|
||||
* 칸 · 식 = 거창 실무 워크북 같은 이름 시트 그대로 · 값은 서버가 셈(`…_Engine_Earthwork_Summary`).
|
||||
* 운반 Q · L = 저장된 유토곡선 배분(B07 토적도와 같은 정본) — 없으면 그 사유만 보임.
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
cell,
|
||||
note,
|
||||
tableCard,
|
||||
type EarthworkTable,
|
||||
type HaulList,
|
||||
} from "./B08_Quantity_UI_Earthwork";
|
||||
|
||||
const fmt = (value: number | null | undefined): string =>
|
||||
value == null || !Number.isFinite(value)
|
||||
? "—"
|
||||
: value.toLocaleString("ko-KR", { maximumFractionDigits: 2 });
|
||||
|
||||
function row(cells: HTMLElement[], className = ""): HTMLTableRowElement {
|
||||
const tr = document.createElement("tr");
|
||||
if (className) tr.className = className;
|
||||
tr.append(...cells);
|
||||
return tr;
|
||||
}
|
||||
|
||||
function station(text: string, span?: { row?: number; col?: number }): HTMLElement {
|
||||
const th = cell("th", text, span);
|
||||
th.className = "b08-summary__station";
|
||||
return th;
|
||||
}
|
||||
|
||||
/** 토공집계표 — 구분 · 공종 · 규격 · 단위 · 계 · 비고 · 같은 구분은 한 칸으로 묶음 */
|
||||
export function renderEarthworkSummary(host: HTMLElement, table: EarthworkTable): void {
|
||||
const thead = document.createElement("thead");
|
||||
thead.append(
|
||||
row([station("구분"), ...["공종", "규격", "단위", "계", "비고"].map((t) => cell("th", t))]),
|
||||
);
|
||||
const tbody = document.createElement("tbody");
|
||||
const rows = table.summary;
|
||||
rows.forEach((item, i) => {
|
||||
const cells: HTMLElement[] = [];
|
||||
if (rows[i - 1]?.group !== item.group) {
|
||||
let span = 1;
|
||||
while (rows[i + span]?.group === item.group) span += 1;
|
||||
cells.push(station(item.group, { row: span }));
|
||||
}
|
||||
const value = cell("td", fmt(item.value));
|
||||
value.className = "b08-earthwork__num";
|
||||
cells.push(cell("td", item.kind), cell("td", item.spec), cell("td", item.unit), value);
|
||||
cells.push(cell("td", item.note));
|
||||
tbody.append(row(cells));
|
||||
});
|
||||
const info = note(
|
||||
"토적표 합계 · 평균운반거리 · 종방향 무대운반거리를 실무 토공집계표 식대로 묶음 · " +
|
||||
"리핑암 = 연암 · 발파암 = 경암 · 구조물 쪽 줄은 구조물 수량에서(— = 아직 값 없음)",
|
||||
);
|
||||
host.replaceChildren(tableCard([info], "b08-earthwork__sheet", [thead, tbody]));
|
||||
}
|
||||
|
||||
function haulNote(table: EarthworkTable): HTMLElement[] {
|
||||
return table.haul_saved
|
||||
? []
|
||||
: [note("유토곡선 배분이 저장되지 않았습니다 — 횡단 설계 [확정] 뒤에 채워집니다.")];
|
||||
}
|
||||
|
||||
/** 운반 시트 한 장 — 묶음마다 토량(Q) · 거리(L) · Q×L · 맨 아래 합계 · 아래 평균운반거리 줄 */
|
||||
function haulSheet(
|
||||
host: HTMLElement,
|
||||
table: EarthworkTable,
|
||||
groups: Array<[string, HaulList]>,
|
||||
info: string,
|
||||
): void {
|
||||
const thead = document.createElement("thead");
|
||||
thead.append(
|
||||
row([station("번호", { row: 2 }), ...groups.map(([label]) => cell("th", label, { col: 3 }))]),
|
||||
row(groups.flatMap(() => ["토량(Q)", "거리(L)", "Q×L"].map((t) => cell("th", t)))),
|
||||
);
|
||||
const count = Math.max(0, ...groups.map(([, list]) => list.rows.length));
|
||||
const tbody = document.createElement("tbody");
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const cells = groups.flatMap(([, list]) => {
|
||||
const item = list.rows[i];
|
||||
return item ? [fmt(item.q), fmt(item.l), fmt(item.ql)] : ["", "", ""];
|
||||
});
|
||||
tbody.append(row([station(String(i + 1)), ...cells.map((t) => cell("td", t))]));
|
||||
}
|
||||
const tfoot = document.createElement("tfoot");
|
||||
tfoot.append(
|
||||
row(
|
||||
[
|
||||
station("합계"),
|
||||
...groups.flatMap(([, list]) => [fmt(list.q), "", fmt(list.ql)].map((t) => cell("td", t))),
|
||||
],
|
||||
"b08-earthwork__total",
|
||||
),
|
||||
);
|
||||
const averages = groups.map(([label, list], i) =>
|
||||
note(
|
||||
`${i + 1}) ${label} 평균운반거리: ${fmt(list.ql)} ÷ ${fmt(list.q)} = ${fmt(list.average_m)} M`,
|
||||
),
|
||||
);
|
||||
const card = tableCard([note(info), ...haulNote(table)], "b08-earthwork__haul", [
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
]);
|
||||
card.append(...averages);
|
||||
host.replaceChildren(card);
|
||||
}
|
||||
|
||||
export function renderHaulDistance(host: HTMLElement, table: EarthworkTable): void {
|
||||
haulSheet(
|
||||
host,
|
||||
table,
|
||||
[
|
||||
["덤프트럭", table.haul.dump_truck],
|
||||
["불도우저", table.haul.dozer],
|
||||
],
|
||||
"유토곡선 배분의 덤프 · 도저 띠와 떨어진 구간 운반 · 평균 = ΣQ×L ÷ ΣQ (소수 둘째 자리) · 단위 ㎥ · m",
|
||||
);
|
||||
}
|
||||
|
||||
export function renderFreeHaul(host: HTMLElement, table: EarthworkTable): void {
|
||||
haulSheet(
|
||||
host,
|
||||
table,
|
||||
[["종방향무대", table.haul.free_haul]],
|
||||
"유토곡선 배분의 무대(20 m 안) 띠 · 평균 = ΣQ×L ÷ ΣQ (소수 둘째 자리) · 단위 ㎥ · m",
|
||||
);
|
||||
}
|
||||
@@ -68,11 +68,18 @@
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
/* 3탭 토적표 — 수는 오른쪽 · 합계 줄은 아래에 붙어 굵게 */
|
||||
.b08-earthwork__table td {
|
||||
/* 토공 탭 — 수는 오른쪽 · 합계 줄은 아래에 붙어 굵게 */
|
||||
.b08-earthwork__table td,
|
||||
.b08-earthwork__haul td,
|
||||
.b08-earthwork__sheet .b08-earthwork__num {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 토적표 머리 셋째 줄 — 둘째 줄 아래에 붙음 */
|
||||
.b08-summary__table thead tr + tr + tr th {
|
||||
top: calc(2 * (1lh + 2 * var(--spacing-4, 4px) + 1px));
|
||||
}
|
||||
|
||||
.b08-earthwork__total th,
|
||||
.b08-earthwork__total td {
|
||||
position: sticky;
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
"""B08 토공 탭 (PLAN 69-2 · 69-3) — 토적표 견본 칸 · 면 계열 · 운반 시트 · 토공집계표 식.
|
||||
|
||||
정답지 ① 사용자 견본 두 행(120 · 120+18) ② 거창 실무 워크북 토적표 131행(70) ·
|
||||
③ 같은 워크북 토공집계표 · 평균운반거리 · 종방향무대운반거리 시트 식.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cross_Quantity import derived_cells # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Engine_Earthwork import ( # noqa: E402
|
||||
FACE_KEYS,
|
||||
StationArea,
|
||||
build_rows,
|
||||
build_table,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_Earthwork_Summary import ( # noqa: E402
|
||||
excel_round,
|
||||
haul_lists,
|
||||
summary_rows,
|
||||
)
|
||||
|
||||
# 견본 보정량 36.2 / 41.1 · 55.2 / 48.0 에서 거꾸로 — 토사 0.88 · 연암 1.15
|
||||
SAMPLE_FACTORS = {
|
||||
"soil": {"compacted": 0.88},
|
||||
"ripping_rock": {"compacted": 1.15},
|
||||
"blasting_rock": {"compacted": 1.3},
|
||||
}
|
||||
SAMPLE_FACES = (
|
||||
# grading_fill · grading_cut · tree_fill · tree_cut · grubbing · road
|
||||
(4.4, 1.3, 4.7, 4.6, 1.3, 4.0),
|
||||
(6.9, 0.4, 10.1, 4.6, 4.8, 5.7),
|
||||
)
|
||||
|
||||
|
||||
def _faces(values: tuple[float, ...]) -> dict[str, float | None]:
|
||||
return dict(zip(FACE_KEYS, (*values, None)))
|
||||
|
||||
|
||||
def 견본_두_측점() -> list[StationArea]:
|
||||
return [
|
||||
StationArea(
|
||||
chainage_m=120.0,
|
||||
cut_soil_area_m2=3.28,
|
||||
cut_rock_area_m2=1.61,
|
||||
ditch_soil_area_m2=0.0,
|
||||
ditch_rock_area_m2=0.18,
|
||||
fill_area_m2=1.9,
|
||||
cut_rock_kind="ripping_rock",
|
||||
faces=_faces(SAMPLE_FACES[0]),
|
||||
),
|
||||
StationArea(
|
||||
chainage_m=138.0,
|
||||
cut_soil_area_m2=1.29,
|
||||
cut_rock_area_m2=3.72,
|
||||
ditch_soil_area_m2=0.0,
|
||||
ditch_rock_area_m2=0.0,
|
||||
fill_area_m2=19.13,
|
||||
cut_rock_kind="ripping_rock",
|
||||
faces=_faces(SAMPLE_FACES[1]),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_견본_120_18_행() -> None:
|
||||
first, row = build_rows(견본_두_측점(), SAMPLE_FACTORS)
|
||||
assert row.distance_m == pytest.approx(18)
|
||||
assert round(row.cut_soil_volume_m3, 1) == 41.1
|
||||
assert round(row.cut_soil_adjusted_m3, 1) == 36.2
|
||||
assert round(row.cut_rock_volume_m3, 1) == 48.0
|
||||
assert round(row.cut_rock_adjusted_m3, 1) == 55.2
|
||||
assert round(row.ditch_rock_volume_m3, 1) == 1.6
|
||||
assert round(row.ditch_rock_adjusted_m3, 1) == 1.9
|
||||
assert round(row.adjusted_total_m3, 1) == 91.4 # 절토 보정량만
|
||||
assert round(row.fill_volume_m3, 1) == 189.3
|
||||
assert round(row.diverted_m3, 1) == 91.4
|
||||
assert round(row.balance_m3, 1) == -97.9
|
||||
assert round(row.cumulative_m3, 1) == -97.9
|
||||
areas = [round(row.faces[key]["area_m2"], 1) for key in FACE_KEYS[:6]]
|
||||
assert areas == [101.7, 15.3, 133.2, 82.8, 54.9, 87.3]
|
||||
|
||||
|
||||
def test_첫_측점은_면_거리만() -> None:
|
||||
first = build_rows(견본_두_측점(), SAMPLE_FACTORS)[0]
|
||||
assert [first.faces[key]["length_m"] for key in FACE_KEYS[:6]] == list(SAMPLE_FACES[0])
|
||||
assert all(first.faces[key]["area_m2"] is None for key in FACE_KEYS)
|
||||
|
||||
|
||||
def test_거창_토적표_131행() -> None:
|
||||
"""워크북 130(+10) → 131(70) — 절토 · 측구 · 성토 · 지장목 성토 면적 같은 입력 다시 셈."""
|
||||
stations = [
|
||||
StationArea(
|
||||
chainage_m=0.0,
|
||||
cut_soil_area_m2=6.72,
|
||||
ditch_soil_area_m2=0.18,
|
||||
ditch_rock_area_m2=0.0,
|
||||
fill_area_m2=0.19,
|
||||
faces=_faces((0.1, 4.7, 2.4, 6.3, 0, 0)),
|
||||
),
|
||||
StationArea(
|
||||
chainage_m=10.0,
|
||||
cut_soil_area_m2=3.15,
|
||||
ditch_soil_area_m2=0.18,
|
||||
ditch_rock_area_m2=0.0,
|
||||
fill_area_m2=15.02,
|
||||
faces=_faces((8.0, 3.8, 11.3, 4.5, 0, 0)),
|
||||
),
|
||||
]
|
||||
row = build_rows(stations)[1]
|
||||
assert row.cut_soil_volume_m3 == pytest.approx(49.35) # E131
|
||||
assert row.cut_soil_adjusted_m3 == pytest.approx(44.415) # F131 44.41
|
||||
assert row.ditch_soil_volume_m3 == pytest.approx(1.8) # K131
|
||||
assert row.fill_volume_m3 == pytest.approx(76.05) # R131
|
||||
assert row.faces["tree_removal_fill"]["area_m2"] == pytest.approx(68.5) # AG131
|
||||
# 워크북 면적은 거리 소수 둘째 자리로 셈 — 표기 거리로는 ±0.5 안
|
||||
assert row.faces["grading_fill"]["area_m2"] == pytest.approx(40.7, abs=0.5) # Y131
|
||||
assert row.faces["tree_removal_cut"]["area_m2"] == pytest.approx(54.4, abs=0.5) # AI131
|
||||
|
||||
|
||||
def test_면_거리는_B07_횡단도_수량과_같음() -> None:
|
||||
design = {
|
||||
"design_line": [
|
||||
{"offset_m": o, "elevation_m": z}
|
||||
for o, z in ((-8, 102), (-2, 100), (2, 100), (6, 102.5))
|
||||
],
|
||||
"road_edges": {"left": {"offset_m": 2}, "right": {"offset_m": -2}},
|
||||
"cut_slope_ratio": 0.8,
|
||||
"fill_slope_ratio": 1.5,
|
||||
"roadbed_width_m": 4.0,
|
||||
}
|
||||
area = StationArea.from_design(120.0, design)
|
||||
cells = derived_cells(120.0, design)
|
||||
for key in ("grading_fill", "grading_cut", "tree_removal_fill", "tree_removal_cut"):
|
||||
assert area.faces[key] == cells.get(key)
|
||||
assert area.faces["road_surface"] == 4.0
|
||||
assert area.faces["grubbing"] is None # B07 에 값 없음
|
||||
|
||||
|
||||
def test_엑셀_ROUND() -> None:
|
||||
assert excel_round(2.5) == 3
|
||||
assert excel_round(-97.5) == -98
|
||||
assert excel_round(1.005, 2) == 1.01
|
||||
|
||||
|
||||
HAUL_PLAN = {
|
||||
"blocks": [
|
||||
{
|
||||
"bands": [
|
||||
{"equipment": "free_haul", "volume_m3": 50, "haul_distance_m": 10},
|
||||
{"equipment": "dozer", "volume_m3": 20, "haul_distance_m": 40},
|
||||
{"equipment": "dump_truck", "volume_m3": 68, "haul_distance_m": 35},
|
||||
]
|
||||
},
|
||||
{"bands": [{"equipment": "free_haul", "volume_m3": 32, "haul_distance_m": 13}]},
|
||||
],
|
||||
"transfers": [{"equipment": "dump_truck", "volume_m3": 2, "haul_distance_m": 61}],
|
||||
}
|
||||
|
||||
|
||||
def test_운반_시트() -> None:
|
||||
haul = haul_lists(HAUL_PLAN)
|
||||
free = haul["free_haul"]
|
||||
assert [(r["q"], r["l"]) for r in free["rows"]] == [(50, 10), (32, 13)]
|
||||
assert free["q"] == 82 and free["ql"] == 50 * 10 + 32 * 13
|
||||
assert free["average_m"] == excel_round((500 + 416) / 82, 2)
|
||||
dump = haul["dump_truck"]
|
||||
assert dump["q"] == 70 and dump["ql"] == 68 * 35 + 2 * 61 # 덤프 띠 + 떨어진 구간 운반
|
||||
assert haul["dozer"]["average_m"] == 40
|
||||
assert haul_lists(None)["dump_truck"] == {"rows": [], "q": 0, "ql": 0, "average_m": None}
|
||||
|
||||
|
||||
def test_토공집계표_식() -> None:
|
||||
table = build_table(견본_두_측점(), SAMPLE_FACTORS)
|
||||
t = table["totals"]
|
||||
rows = summary_rows(t, haul_lists(HAUL_PLAN))
|
||||
value = {(r["group"], r["kind"], r["spec"]): r["value"] for r in rows}
|
||||
assert value[("흙깍기", "토사", "기계(굴삭기)")] == excel_round(t["cut_soil_volume_m3"])
|
||||
assert value[("흙깍기", "연암", "굴삭기 + 브레카")] == 48
|
||||
assert value[("흙깍기", "경암", "발파 + 굴삭기")] == 0
|
||||
assert value[("측구 터파기", "연암", "굴삭기 + 브레카")] == 2
|
||||
assert value[("보정량계", "", "")] == 91
|
||||
assert value[("성토", "", "")] == 189
|
||||
assert value[("종방향유용토", "", "백호우")] == 82
|
||||
assert value[("토사운반", "", "BH+DT+BH")] == 70
|
||||
assert value[("토사운반", "", "19 TON B/D")] == 20
|
||||
assert value[("유용토 소계", "", "")] == pytest.approx(t["diverted_m3"] + 82)
|
||||
assert value[("잔토처리", "", "백호우")] == -98
|
||||
assert value[("성토면다짐", "", "")] == excel_round(101.7 * 0.8)
|
||||
assert value[("초류종자살포", "", "씨드스프레이")] == excel_round(101.7 * 0.5 + 15.3)
|
||||
tree = excel_round(133.2 * 0.5 + 82.8, 2)
|
||||
assert value[("지장목제거", "", "")] == excel_round(tree * 0.8)
|
||||
assert value[("표토제거", "", "")] is None
|
||||
assert len(rows) == 33 # 시트 C5:C37
|
||||
@@ -70,15 +70,14 @@ def test_측구_단면_0_18이_체적으로() -> None:
|
||||
assert rows[2].ditch_soil_adjusted_m3 == pytest.approx(1.62)
|
||||
|
||||
|
||||
def test_보정량계는_네_갈래_합() -> None:
|
||||
def test_보정량계는_절토_보정량만() -> None:
|
||||
"""PLAN 69 정함 — 측구터파기 보정량은 칸에만(거창 BOM 15열 17.91 = 16.29 + 1.62 와 다름)."""
|
||||
row = build_rows(거창_앞_네_측점())[2]
|
||||
assert row.adjusted_total_m3 == pytest.approx(
|
||||
row.cut_soil_adjusted_m3
|
||||
+ row.cut_rock_adjusted_m3
|
||||
+ row.ditch_soil_adjusted_m3
|
||||
+ row.ditch_rock_adjusted_m3
|
||||
row.cut_soil_adjusted_m3 + row.cut_rock_adjusted_m3
|
||||
)
|
||||
assert row.adjusted_total_m3 == pytest.approx(16.29 + 1.62) # BOM 15열 17.91
|
||||
assert row.adjusted_total_m3 == pytest.approx(16.29)
|
||||
assert row.ditch_soil_adjusted_m3 == pytest.approx(1.62)
|
||||
|
||||
|
||||
def test_누가토량은_차인토량의_누계() -> None:
|
||||
|
||||
@@ -595,7 +595,7 @@ export const ui_locales_b2 = {
|
||||
"수량 단계 확정에 실패했습니다.",
|
||||
"Failed to confirm the quantity stage.",
|
||||
],
|
||||
B08_Quantity_Tab_Earthwork: ["토적표", "Earthwork Table"],
|
||||
B08_Quantity_Tab_Earthwork: ["토공", "Earthwork"],
|
||||
B08_Quantity_Grid_Loading: ["토적표를 만드는 중입니다…", "Building the earthwork table…"],
|
||||
B08_Quantity_Grid_Empty: [
|
||||
"측점 단면적이 아직 없습니다. 횡단 설계를 먼저 마치세요.",
|
||||
|
||||
Reference in New Issue
Block a user