Merge remote-tracking branch 'origin/main_laptop_1' into main_desktop_1
# Conflicts: # B08_Quantity/B08_Quantity_Router_Earthwork.py
This commit is contained in:
@@ -69,6 +69,8 @@ if (input.haul_plan_for) {
|
|||||||
collected_stone_deduction_m3: input.context?.collected_stone_deduction_m3 ?? null,
|
collected_stone_deduction_m3: input.context?.collected_stone_deduction_m3 ?? null,
|
||||||
structure_spoil_m3: input.context?.structure_spoil_m3 ?? null,
|
structure_spoil_m3: input.context?.structure_spoil_m3 ?? null,
|
||||||
structure_spoil_points: input.context?.structure_spoil_points ?? null,
|
structure_spoil_points: input.context?.structure_spoil_points ?? null,
|
||||||
|
// 잔토는 자연상태로 오고 곡선은 다짐상태다 — 담기 전에 ×C 하는 데 쓴다.
|
||||||
|
conversion: input.context?.earthwork_conversion ?? null,
|
||||||
});
|
});
|
||||||
writeFileSync(outputPath, JSON.stringify({ haul_plan: plan ?? null }));
|
writeFileSync(outputPath, JSON.stringify({ haul_plan: plan ?? null }));
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
@@ -95,6 +97,7 @@ const plan = result
|
|||||||
collected_stone_deduction_m3: input.context?.collected_stone_deduction_m3 ?? null,
|
collected_stone_deduction_m3: input.context?.collected_stone_deduction_m3 ?? null,
|
||||||
structure_spoil_m3: input.context?.structure_spoil_m3 ?? null,
|
structure_spoil_m3: input.context?.structure_spoil_m3 ?? null,
|
||||||
structure_spoil_points: input.context?.structure_spoil_points ?? null,
|
structure_spoil_points: input.context?.structure_spoil_points ?? null,
|
||||||
|
conversion: conversion ?? null,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
const massHaul = result
|
const massHaul = result
|
||||||
|
|||||||
@@ -225,13 +225,23 @@ def _haul_rows(source: SummaryInput) -> list[SummaryRow]:
|
|||||||
ground = str(item.get("ground") or "")
|
ground = str(item.get("ground") or "")
|
||||||
distance = item.get("average_distance_m")
|
distance = item.get("average_distance_m")
|
||||||
note = f"평균운반거리 {float(distance):.2f} m" if isinstance(distance, (int, float)) else ""
|
note = f"평균운반거리 {float(distance):.2f} m" if isinstance(distance, (int, float)) else ""
|
||||||
|
# ⚠ **자연상태로 싣는다** — 「운반거리 산정은 다짐상태, 내역서 수량은 자연상태」
|
||||||
|
# (config 5-4-3 인용). 유토곡선은 다짐으로 쌓으므로 여기서 ÷C 된 값을 받는다.
|
||||||
|
# 환산은 `HaulSummary` 한 곳에서만 하고, 여기서는 **고르기만** 한다(두 번 환산 금지).
|
||||||
|
compacted = float(item.get("volume_m3") or 0.0)
|
||||||
|
natural = item.get("natural_m3")
|
||||||
|
amount = float(natural) if isinstance(natural, (int, float)) else compacted
|
||||||
|
if isinstance(natural, (int, float)):
|
||||||
|
note = (note + f" · 자연상태 환산(다짐 {compacted:,.2f}㎥ ÷ C)").strip(" ·")
|
||||||
|
else:
|
||||||
|
note = (note + " · ⚠ 지반 갈래를 몰라 다짐상태 그대로임").strip(" ·")
|
||||||
if key == "free_haul":
|
if key == "free_haul":
|
||||||
note = (note + " · 내역 제외(품에 포함)").strip(" ·")
|
note = (note + " · 내역 제외(품에 포함)").strip(" ·")
|
||||||
rows.append(
|
rows.append(
|
||||||
SummaryRow(
|
SummaryRow(
|
||||||
group=label,
|
group=label,
|
||||||
item=ground,
|
item=ground,
|
||||||
amount=float(item.get("volume_m3") or 0.0),
|
amount=amount,
|
||||||
note=note,
|
note=note,
|
||||||
in_bill=key != "free_haul",
|
in_bill=key != "free_haul",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,14 @@
|
|||||||
인력운반은 `10-6` 「소운반 20 m **초과분**」이다. 그래서 `in_bill=False` 로 표시해 넘기고
|
인력운반은 `10-6` 「소운반 20 m **초과분**」이다. 그래서 `in_bill=False` 로 표시해 넘기고
|
||||||
값은 검산(`무대+도자+덤프 = 총 운반토량`)에 쓴다.
|
값은 검산(`무대+도자+덤프 = 총 운반토량`)에 쓴다.
|
||||||
|
|
||||||
|
⚠⚠ 상태(狀態)가 두 개다 — **거리는 다짐, 수량은 자연** (2026-09-09)
|
||||||
|
「운반거리의 산정 시에 모든 수량은 다짐상태로 환산하여 계산하고, 내역서에 적용하는
|
||||||
|
수량은 자연상태로 한다」(설계실무 요령 — `config_system_design` 5-4-3 인용문).
|
||||||
|
유토곡선은 다짐상태로 쌓으므로 **가중평균 거리는 그대로 두고**, 내역에 오르는 수량만
|
||||||
|
`natural_m3`(÷C)로 낸다. **환산은 이 파일 한 곳에서 한 번만** 한다 — 받는 쪽(집계표·인계
|
||||||
|
줄)은 고르기만 한다. `L`(팽창률)은 쓰지 않는다: 품셈 10-11·10-12 가 `f = 1/L` 을 식 안에서
|
||||||
|
스스로 곱하므로 우리는 **자연상태 물량만 정확히 넘기면 된다**.
|
||||||
|
|
||||||
입력은 `HaulPlan` 이다 (이미 있는 값 — 다시 세지 않는다)
|
입력은 `HaulPlan` 이다 (이미 있는 값 — 다시 세지 않는다)
|
||||||
띠(`bands`)마다 `equipment` · `haul_distance_m` · 지반유형별 물량(`ea_m3`·`rr_m3`·`br_m3`)이
|
띠(`bands`)마다 `equipment` · `haul_distance_m` · 지반유형별 물량(`ea_m3`·`rr_m3`·`br_m3`)이
|
||||||
들어 있다. 떨어진 구간끼리 옮기는 `transfers` 도 같은 모양이라 함께 센다.
|
들어 있다. 떨어진 구간끼리 옮기는 `transfers` 도 같은 모양이라 함께 센다.
|
||||||
@@ -27,8 +35,47 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Iterable
|
from typing import Any, Iterable
|
||||||
|
|
||||||
|
from config.config_system_design import EARTHWORK_CONVERSION_FACTORS
|
||||||
|
|
||||||
# 지반유형 키 ↔ 표기. `HaulPlan` 이 절토 구간 구성비로 안분해 둔 세 갈래다.
|
# 지반유형 키 ↔ 표기. `HaulPlan` 이 절토 구간 구성비로 안분해 둔 세 갈래다.
|
||||||
GROUND_LABELS = {"ea_m3": "토사", "rr_m3": "리핑암", "br_m3": "발파암"}
|
GROUND_LABELS = {"ea_m3": "토사", "rr_m3": "리핑암", "br_m3": "발파암"}
|
||||||
|
# 표기 ↔ 환산계수 이름(`EARTHWORK_CONVERSION_FACTORS` 의 키).
|
||||||
|
GROUND_KIND_OF = {"토사": "soil", "리핑암": "ripping_rock", "발파암": "blasting_rock"}
|
||||||
|
|
||||||
|
|
||||||
|
def _factor_of(ground: str) -> float | None:
|
||||||
|
"""그 갈래의 다짐 환산계수 `C`. 모르면 `None`(받는 쪽이 환산했는지 되짚는 데 쓴다)."""
|
||||||
|
kind = GROUND_KIND_OF.get(ground)
|
||||||
|
entry = EARTHWORK_CONVERSION_FACTORS.get(kind) if kind else None
|
||||||
|
return float(entry["compacted"]) if entry else None
|
||||||
|
|
||||||
|
|
||||||
|
def natural_m3(compacted_volume_m3: float, ground: str) -> float | None:
|
||||||
|
"""**다짐상태 → 자연상태**(÷ C). 내역서에 오르는 수량은 자연상태다.
|
||||||
|
|
||||||
|
근거 — `config_system_design` 5-4-3 에 이미 적혀 있던 문장이다.
|
||||||
|
|
||||||
|
「운반거리의 산정 시에 모든 수량은 다짐상태로 환산하여 계산하고,
|
||||||
|
내역서에 적용하는 수량은 자연상태로 한다.」
|
||||||
|
(2021년도 국도건설공사 설계실무 요령 / 표준품셈 계열)
|
||||||
|
|
||||||
|
⚠ **나누기다.** `C = 다짐 ÷ 자연` 이므로 되돌리려면 나눠야 한다. 곱하면 토사가
|
||||||
|
1.111배가 아니라 0.9배가 되어 **방향이 뒤집힌다**(거울 시험이 이 방향을 잠근다).
|
||||||
|
⚠ **`L`(팽창률 1.3·1.35·1.625)을 쓰지 않는다.** 우리 곡선은 `×C` 로 쌓았으니
|
||||||
|
되돌리는 것도 `C` 다. 품셈 10-11·10-12 는 `f = 1/L` 을 **식 안에서 스스로** 곱하므로
|
||||||
|
우리가 `L` 을 또 들면 두 번 환산이 된다.
|
||||||
|
⚠ **환산은 내보내는 이 자리에서 한 번만.** 곡선 안쪽(띠·이동·잔량)은 다짐상태 그대로 둔다
|
||||||
|
— 성토 배분은 다짐으로 세는 것이 맞다.
|
||||||
|
⚠ 갈래를 모르면 `None` 이다 — 토사 계수로 눅이면 근거 없이 금액이 움직인다.
|
||||||
|
"""
|
||||||
|
kind = GROUND_KIND_OF.get(ground)
|
||||||
|
entry = EARTHWORK_CONVERSION_FACTORS.get(kind) if kind else None
|
||||||
|
if not entry:
|
||||||
|
return None
|
||||||
|
factor = float(entry["compacted"])
|
||||||
|
return compacted_volume_m3 / factor if factor > 0 else None
|
||||||
|
|
||||||
|
|
||||||
# 무대 — 품에 포함이라 내역 줄이 되지 않는다.
|
# 무대 — 품에 포함이라 내역 줄이 되지 않는다.
|
||||||
FREE_HAUL_KEY = "free_haul"
|
FREE_HAUL_KEY = "free_haul"
|
||||||
|
|
||||||
@@ -152,7 +199,13 @@ def build_table(plan: dict[str, Any] | None) -> dict[str, Any]:
|
|||||||
{
|
{
|
||||||
"equipment": row.equipment,
|
"equipment": row.equipment,
|
||||||
"ground": row.ground,
|
"ground": row.ground,
|
||||||
|
# ⚠ 이 칸은 **다짐상태**다 — 운반거리를 낸 그 상태 그대로(검산도 이 값으로 한다).
|
||||||
"volume_m3": row.volume_m3,
|
"volume_m3": row.volume_m3,
|
||||||
|
"volume_basis": "compacted",
|
||||||
|
# 내역서에 오르는 수량 = **자연상태**(÷C). 갈래를 모르면 `None`.
|
||||||
|
"natural_m3": natural_m3(row.volume_m3, row.ground),
|
||||||
|
"natural_volume_basis": "natural",
|
||||||
|
"conversion_c": _factor_of(row.ground),
|
||||||
"average_distance_m": row.average_distance_m,
|
"average_distance_m": row.average_distance_m,
|
||||||
"work_m3m": row.work_m3m,
|
"work_m3m": row.work_m3m,
|
||||||
"legs": row.legs,
|
"legs": row.legs,
|
||||||
@@ -178,12 +231,19 @@ def build_table(plan: dict[str, Any] | None) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def summary_input_rows(table: dict[str, Any]) -> list[dict[str, Any]]:
|
def summary_input_rows(table: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
"""토공집계표가 받는 모양으로 줄인다 — 집계표는 근거 줄을 안 쓴다."""
|
"""토공집계표가 받는 모양으로 줄인다 — 집계표는 근거 줄을 안 쓴다.
|
||||||
|
|
||||||
|
⚠ **두 상태를 함께 넘긴다** — 집계표·내역은 `natural_m3`(자연상태)를 쓰고,
|
||||||
|
`volume_m3`(다짐상태)는 검산·되짚기용이다. 받는 쪽이 또 환산하지 않게 칸 이름으로 가른다.
|
||||||
|
"""
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"equipment": row["equipment"],
|
"equipment": row["equipment"],
|
||||||
"ground": row["ground"],
|
"ground": row["ground"],
|
||||||
"volume_m3": row["volume_m3"],
|
"volume_m3": row["volume_m3"],
|
||||||
|
"volume_basis": row.get("volume_basis") or "compacted",
|
||||||
|
"natural_m3": row.get("natural_m3"),
|
||||||
|
"conversion_c": row.get("conversion_c"),
|
||||||
"average_distance_m": row["average_distance_m"],
|
"average_distance_m": row["average_distance_m"],
|
||||||
}
|
}
|
||||||
for row in table.get("rows") or []
|
for row in table.get("rows") or []
|
||||||
@@ -201,7 +261,11 @@ class HaulCheck:
|
|||||||
|
|
||||||
|
|
||||||
def check_against_plan(table: dict[str, Any], plan: dict[str, Any] | None) -> HaulCheck:
|
def check_against_plan(table: dict[str, Any], plan: dict[str, Any] | None) -> HaulCheck:
|
||||||
"""`무대 + 도자 + 덤프` 합이 `HaulPlan` 의 총 운반량과 맞는가."""
|
"""`무대 + 도자 + 덤프` 합이 `HaulPlan` 의 총 운반량과 맞는가.
|
||||||
|
|
||||||
|
⚠ **다짐상태끼리 비교한다** — 계획(`HaulPlan`)이 다짐이라 자연상태로 환산한 값을 대면
|
||||||
|
늘 어긋난다. 검산은 환산 전 값(`volume_m3`)으로 하는 것이 맞다.
|
||||||
|
"""
|
||||||
hauled = sum(float(row.get("volume_m3") or 0.0) for row in table.get("rows") or [])
|
hauled = sum(float(row.get("volume_m3") or 0.0) for row in table.get("rows") or [])
|
||||||
plan = plan or {}
|
plan = plan or {}
|
||||||
planned = float(plan.get("hauled_m3") or 0.0) + float(plan.get("transferred_m3") or 0.0)
|
planned = float(plan.get("hauled_m3") or 0.0) + float(plan.get("transferred_m3") or 0.0)
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ from common_util.common_util_project_settings import (
|
|||||||
)
|
)
|
||||||
from common_util.common_util_storage import resolve_stored_project_path
|
from common_util.common_util_storage import resolve_stored_project_path
|
||||||
from config.config_db import run_with_connection
|
from config.config_db import run_with_connection
|
||||||
|
from config.config_system_design import EARTHWORK_CONVERSION_FACTORS
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter(prefix="/api/projects", tags=["B08 Quantity"])
|
router = APIRouter(prefix="/api/projects", tags=["B08 Quantity"])
|
||||||
@@ -154,6 +155,14 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
|
|||||||
return JSONResponse(content=table)
|
return JSONResponse(content=table)
|
||||||
|
|
||||||
|
|
||||||
|
#: 갈래 칸 ↔ 다짐 환산계수 `C`. 정의처는 `config_system_design` 한 곳뿐이다.
|
||||||
|
_COMPACTED_FACTOR = {
|
||||||
|
"ea_m3": float(EARTHWORK_CONVERSION_FACTORS["soil"]["compacted"]),
|
||||||
|
"rr_m3": float(EARTHWORK_CONVERSION_FACTORS["ripping_rock"]["compacted"]),
|
||||||
|
"br_m3": float(EARTHWORK_CONVERSION_FACTORS["blasting_rock"]["compacted"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _spoil_of(plan: dict[str, Any] | None, settings: dict[str, Any]) -> dict[str, Any]:
|
def _spoil_of(plan: dict[str, Any] | None, settings: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""사토 — 실어 낼 물량과 거리. 유토곡선 결과에서 **다시 세지 않고 그대로** 가져온다.
|
"""사토 — 실어 낼 물량과 거리. 유토곡선 결과에서 **다시 세지 않고 그대로** 가져온다.
|
||||||
|
|
||||||
@@ -175,7 +184,6 @@ def _spoil_of(plan: dict[str, Any] | None, settings: dict[str, Any]) -> dict[str
|
|||||||
# 토사로 눅이면 덤프 단가가 임의로 정해진다.
|
# 토사로 눅이면 덤프 단가가 임의로 정해진다.
|
||||||
grounds: dict[str, float] = {}
|
grounds: dict[str, float] = {}
|
||||||
unknown = 0.0
|
unknown = 0.0
|
||||||
natural_basis = True
|
|
||||||
# 잔량마다 **사토장까지 거리**가 실려 올 수 있다(2026-09-09 사용자 확정 — 사토장은 이미
|
# 잔량마다 **사토장까지 거리**가 실려 올 수 있다(2026-09-09 사용자 확정 — 사토장은 이미
|
||||||
# 있는 측점 위에만 놓이므로 「발생점 → 사토장 측점」 누가거리로 그냥 나온다).
|
# 있는 측점 위에만 놓이므로 「발생점 → 사토장 측점」 누가거리로 그냥 나온다).
|
||||||
# 갈래별 **가중평균**을 낸다 — 실무 내역이 (운반수단 × 지반)별 평균 하나를 올린다.
|
# 갈래별 **가중평균**을 낸다 — 실무 내역이 (운반수단 × 지반)별 평균 하나를 올린다.
|
||||||
@@ -185,13 +193,10 @@ def _spoil_of(plan: dict[str, Any] | None, settings: dict[str, Any]) -> dict[str
|
|||||||
if str(residual.get("kind") or "") != "spoil":
|
if str(residual.get("kind") or "") != "spoil":
|
||||||
continue
|
continue
|
||||||
leg_distance = residual.get("spoil_haul_distance_m")
|
leg_distance = residual.get("spoil_haul_distance_m")
|
||||||
# 되돌린 값이 오면 그것이 이긴다 — 우리가 다시 환산하지 않는다(계수가 저쪽에 있다).
|
# ⚠ 잔량은 **다짐상태**로만 읽는다 — 되돌리는 자리는 아래 한 곳뿐이다.
|
||||||
returned = residual.get("natural_m3_by_ground")
|
# 두 곳에서 되돌리면 ÷C 가 두 번 걸린다.
|
||||||
source_map = returned if isinstance(returned, dict) else residual
|
|
||||||
if not isinstance(returned, dict):
|
|
||||||
natural_basis = False
|
|
||||||
for key in ("ea_m3", "rr_m3", "br_m3"):
|
for key in ("ea_m3", "rr_m3", "br_m3"):
|
||||||
value = float(source_map.get(key) or source_map.get(key[:2]) or 0.0)
|
value = float(residual.get(key) or 0.0)
|
||||||
if value > 0:
|
if value > 0:
|
||||||
grounds[key] = grounds.get(key, 0.0) + value
|
grounds[key] = grounds.get(key, 0.0) + value
|
||||||
if isinstance(leg_distance, (int, float)) and float(leg_distance) > 0:
|
if isinstance(leg_distance, (int, float)) and float(leg_distance) > 0:
|
||||||
@@ -207,24 +212,32 @@ def _spoil_of(plan: dict[str, Any] | None, settings: dict[str, Any]) -> dict[str
|
|||||||
deducted = source.get("collected_stone_deducted_m3")
|
deducted = source.get("collected_stone_deducted_m3")
|
||||||
if deducted:
|
if deducted:
|
||||||
note_parts.append(f"채집석 {float(deducted):,.2f}㎥ 빠진 뒤")
|
note_parts.append(f"채집석 {float(deducted):,.2f}㎥ 빠진 뒤")
|
||||||
if grounds and not natural_basis:
|
# ⚠ **상태를 값으로 낸다**(2026-09-09) — 잔량은 유토곡선이 쌓은 **다짐상태**이고,
|
||||||
note_parts.append(
|
# 내역서에 오르는 수량은 **자연상태**다(`config_system_design` 5-4-3 「운반거리 산정 시
|
||||||
"⚠ 다짐상태 물량 — 품셈 운반 밑수는 자연상태라 유토곡선이 되돌린 값이 오면 그것을 씀"
|
# 모든 수량은 다짐상태로 환산해 계산하고, **내역서에 적용하는 수량은 자연상태로 한다**」).
|
||||||
)
|
# 여기서 ÷C 한 값을 함께 내 받는 쪽이 **또 환산하지 않게** 한다.
|
||||||
|
# ⚠ 갈래를 못 붙인 몫은 계수가 없어 **환산하지 않는다** — 토사 계수로 눅이면 근거 없이
|
||||||
|
# 금액이 움직인다. 그 사실을 사유로 낸다.
|
||||||
|
natural_by_ground = {
|
||||||
|
key: round(value / _COMPACTED_FACTOR[key], 3)
|
||||||
|
for key, value in grounds.items()
|
||||||
|
if key in _COMPACTED_FACTOR
|
||||||
|
}
|
||||||
if unknown > 0:
|
if unknown > 0:
|
||||||
note_parts.append(f"⚠ 갈래를 못 붙인 {unknown:,.2f}㎥ 는 상태도 못 되돌림")
|
note_parts.append(f"⚠ 갈래를 못 붙인 {unknown:,.2f}㎥ 는 상태도 못 되돌림")
|
||||||
return {
|
return {
|
||||||
"volume_m3": round(volume, 3),
|
"volume_m3": round(volume, 3),
|
||||||
|
"volume_basis": "compacted",
|
||||||
"distance_m": settings.get("spoil_site_distance_m"),
|
"distance_m": settings.get("spoil_site_distance_m"),
|
||||||
"note": " · ".join(note_parts),
|
"note": " · ".join(note_parts),
|
||||||
"by_ground_m3": {key: round(value, 3) for key, value in grounds.items()},
|
"by_ground_m3": {key: round(value, 3) for key, value in grounds.items()},
|
||||||
|
"natural_m3_by_ground": natural_by_ground,
|
||||||
|
"natural_volume_basis": "natural",
|
||||||
"ground_unknown_m3": round(unknown, 3),
|
"ground_unknown_m3": round(unknown, 3),
|
||||||
# 갈래별 가중평균 거리 — 사토장이 놓였을 때만 찬다. 비면 설정 거리로 떨어진다.
|
# 갈래별 가중평균 거리 — 사토장이 놓였을 때만 찬다. 비면 설정 거리로 떨어진다.
|
||||||
"distance_by_ground_m": {
|
"distance_by_ground_m": {
|
||||||
key: round(work[key] / metered[key], 2) for key in work if metered.get(key)
|
key: round(work[key] / metered[key], 2) for key in work if metered.get(key)
|
||||||
},
|
},
|
||||||
# 어느 상태의 물량인가 — 받는 쪽이 단가와 맞는지 스스로 볼 수 있게 값으로 적는다.
|
|
||||||
"volume_basis": "natural" if natural_basis else "compacted",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import type { MassHaulPoint, MassHaulResult } from "./common_util_mass_haul";
|
import type { MassHaulPoint, MassHaulResult } from "./common_util_mass_haul";
|
||||||
|
import type { EarthworkConversion } from "./common_util_mass_haul_types";
|
||||||
import {
|
import {
|
||||||
crossFrom,
|
crossFrom,
|
||||||
EPSILON,
|
EPSILON,
|
||||||
@@ -409,6 +410,34 @@ function groundBucket(ground: string | null | undefined): "ea_m3" | "rr_m3" | "b
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 갈래 칸 ↔ 환산계수 이름. `EARTHWORK_CONVERSION_FACTORS` 의 키다. */
|
||||||
|
const GROUND_OF_BUCKET = {
|
||||||
|
ea_m3: "soil",
|
||||||
|
rr_m3: "ripping_rock",
|
||||||
|
br_m3: "blasting_rock",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **자연상태 → 다짐상태**(× C). 구조물 잔토를 곡선에 얹기 전에 한 번만 거친다.
|
||||||
|
*
|
||||||
|
* ⚠ 왜 필요한가 — 유토곡선은 **다짐상태**로 쌓는다(`common_util_mass_haul` 이 절토량에
|
||||||
|
* ×C 를 곱해 둔다). B08 이 보내는 구조물 잔토는 **자연상태**(터파기 제자리 기하 부피)라
|
||||||
|
* 그대로 더하면 **상태가 다른 두 부피를 섞는 것**이 된다.
|
||||||
|
* ⚠ **되돌리는 곳은 내보내는 자리 한 곳뿐**(B08 `HaulSummary`·사토 줄에서 ÷C).
|
||||||
|
* 여기서 ×C, 거기서 ÷C — **왕복 한 번씩이다. 다른 데서 또 환산하지 말 것.**
|
||||||
|
* ⚠ 계수를 모르면(지반 갈래를 못 붙였으면) **환산하지 않고 그대로 담는다** — 토사 계수로
|
||||||
|
* 눅이면 근거 없이 금액이 움직인다. 그 몫은 `ground_unknown_m3` 로 드러난다.
|
||||||
|
*/
|
||||||
|
function toCompacted(
|
||||||
|
naturalM3: number,
|
||||||
|
bucket: keyof typeof GROUND_OF_BUCKET | null,
|
||||||
|
conversion: EarthworkConversion | null | undefined,
|
||||||
|
): number {
|
||||||
|
if (!bucket || !conversion) return naturalM3;
|
||||||
|
const factor = conversion[GROUND_OF_BUCKET[bucket]]?.compacted;
|
||||||
|
return typeof factor === "number" && factor > 0 ? naturalM3 * factor : naturalM3;
|
||||||
|
}
|
||||||
|
|
||||||
function newSpoilResidual(fromM: number, toM: number, volumeM3: number): HaulResidual {
|
function newSpoilResidual(fromM: number, toM: number, volumeM3: number): HaulResidual {
|
||||||
// 잔토를 담을 사토 잔량이 없을 때 **새로 세운다**(2026-09-09 확정 ㉰).
|
// 잔토를 담을 사토 잔량이 없을 때 **새로 세운다**(2026-09-09 확정 ㉰).
|
||||||
// ⚠ 지반유형 안분·자연방토는 0 이다 — 어느 지반에서 나온 흙인지 모르고, **실어 내는** 흙이다.
|
// ⚠ 지반유형 안분·자연방토는 0 이다 — 어느 지반에서 나온 흙인지 모르고, **실어 내는** 흙이다.
|
||||||
@@ -440,7 +469,11 @@ function newSpoilResidual(fromM: number, toM: number, volumeM3: number): HaulRes
|
|||||||
* 근거가 있어야 하는데 구조물 터파기 흙은 암이 섞일 수 있고 우리가 그 판정을 안 한다 —
|
* 근거가 있어야 하는데 구조물 터파기 흙은 암이 섞일 수 있고 우리가 그 판정을 안 한다 —
|
||||||
* 근거 없이 금액을 내리지 않고 **내보내는 쪽(안전측)** 으로 둔다.
|
* 근거 없이 금액을 내리지 않고 **내보내는 쪽(안전측)** 으로 둔다.
|
||||||
* ⇒ 나중에 「성토재로 쓴다」가 확정되면 **이 함수 한 곳만** 바꾸면 된다.
|
* ⇒ 나중에 「성토재로 쓴다」가 확정되면 **이 함수 한 곳만** 바꾸면 된다.
|
||||||
* ⚠ 자연방토·지반유형 안분은 안 건드린다 — 실어 내는 흙이고, 어느 지반에서 나온지 모른다.
|
* ⚠ 자연방토는 안 건드린다 — 실어 내는 흙이다.
|
||||||
|
* ⚠⚠ **상태가 다르다** — B08 이 보내는 잔토는 **자연상태**(`volume_basis: "natural"`,
|
||||||
|
* 터파기 제자리 기하 부피)이고 이 곡선은 **다짐상태**다. 그래서 담기 전에 `toCompacted`
|
||||||
|
* 로 **×C** 한다. 되돌리는 것은 **내보내는 자리 한 곳**(B08 운반·사토 줄에서 ÷C)뿐이다.
|
||||||
|
* 갈래를 못 붙인 몫은 계수가 없어 **환산하지 않고** 그대로 담긴다(근거 없이 안 눅인다).
|
||||||
*/
|
*/
|
||||||
function applyStructureSpoil(
|
function applyStructureSpoil(
|
||||||
residuals: HaulResidual[],
|
residuals: HaulResidual[],
|
||||||
@@ -453,6 +486,7 @@ function applyStructureSpoil(
|
|||||||
ground_label?: string | null;
|
ground_label?: string | null;
|
||||||
ground?: string | null;
|
ground?: string | null;
|
||||||
}> | null,
|
}> | null,
|
||||||
|
conversion?: EarthworkConversion | null,
|
||||||
): number {
|
): number {
|
||||||
const spoilsOf = (): HaulResidual[] => residuals.filter((residual) => residual.kind === "spoil");
|
const spoilsOf = (): HaulResidual[] => residuals.filter((residual) => residual.kind === "spoil");
|
||||||
|
|
||||||
@@ -472,9 +506,12 @@ function applyStructureSpoil(
|
|||||||
const bucket = groundBucket(point?.ground_type ?? point?.ground ?? point?.ground_label);
|
const bucket = groundBucket(point?.ground_type ?? point?.ground ?? point?.ground_label);
|
||||||
const target = covering ?? newSpoilResidual(chainage, chainage, 0);
|
const target = covering ?? newSpoilResidual(chainage, chainage, 0);
|
||||||
if (!covering) residuals.push(target);
|
if (!covering) residuals.push(target);
|
||||||
target.volume_m3 += value;
|
// ⚠ **자연상태로 와서 다짐상태 곡선에 얹힌다** — 갈래를 아는 몫만 ×C(2026-09-09).
|
||||||
if (bucket) target[bucket] += value;
|
// 갈래를 모르면 그대로 담기고 `ground_unknown_m3` 로 「상태도 못 되돌림」이 드러난다.
|
||||||
added += value;
|
const placed = toCompacted(value, bucket, conversion);
|
||||||
|
target.volume_m3 += placed;
|
||||||
|
if (bucket) target[bucket] += placed;
|
||||||
|
added += placed;
|
||||||
}
|
}
|
||||||
return added;
|
return added;
|
||||||
}
|
}
|
||||||
@@ -520,6 +557,9 @@ export function computeHaulPlan(
|
|||||||
ground_label?: string | null;
|
ground_label?: string | null;
|
||||||
ground?: string | null;
|
ground?: string | null;
|
||||||
}> | null;
|
}> | null;
|
||||||
|
/** 토량환산계수 — 구조물 잔토(자연상태)를 이 곡선의 **다짐상태**로 옮길 때만 쓴다.
|
||||||
|
* 안 오면 환산 없이 담긴다(값을 지어내지 않는다). */
|
||||||
|
conversion?: EarthworkConversion | null;
|
||||||
},
|
},
|
||||||
): HaulPlan | null {
|
): HaulPlan | null {
|
||||||
const points = result.points;
|
const points = result.points;
|
||||||
@@ -709,6 +749,7 @@ export function computeHaulPlan(
|
|||||||
settled,
|
settled,
|
||||||
structureSpoilInput,
|
structureSpoilInput,
|
||||||
structureSpoilPoints,
|
structureSpoilPoints,
|
||||||
|
options?.conversion ?? null,
|
||||||
);
|
);
|
||||||
const deductionInput = options?.collected_stone_deduction_m3 ?? null;
|
const deductionInput = options?.collected_stone_deduction_m3 ?? null;
|
||||||
const deducted = applyCollectedStoneDeduction(settled, deductionInput);
|
const deducted = applyCollectedStoneDeduction(settled, deductionInput);
|
||||||
|
|||||||
Reference in New Issue
Block a user