|
|
|
@@ -199,6 +199,13 @@ export interface HaulPlan {
|
|
|
|
|
collected_stone_deduction_m3: number | null;
|
|
|
|
|
/** 실제로 사토에서 뺀 양(㎥). 사토가 모자라면 받은 값보다 작을 수 있다. */
|
|
|
|
|
collected_stone_deducted_m3: number;
|
|
|
|
|
/**
|
|
|
|
|
* 구조물 터파기가 남긴 잔토(㎥, 양수) — **사토에 더한다**. `null` 은 「아직 안 옴」이고
|
|
|
|
|
* `0` 은 「없음」이다(공제와 같은 태도).
|
|
|
|
|
*/
|
|
|
|
|
structure_spoil_m3: number | null;
|
|
|
|
|
/** 실제로 사토에 더한 양(㎥). 받을 잔량이 없으면 받은 값보다 작을 수 있다. */
|
|
|
|
|
structure_spoil_added_m3: number;
|
|
|
|
|
/** 블록 안에서 옮기는 양(㎥). */
|
|
|
|
|
hauled_m3: number;
|
|
|
|
|
/** 떨어진 구간끼리 장거리로 옮기는 양(㎥). */
|
|
|
|
@@ -334,6 +341,9 @@ function bandOutline(
|
|
|
|
|
* ⚠ 순서가 중요하다 — 캔 돌은 **실어 낼 흙 속에 있던 것**이다. 자연방토(운반비를 안 세는 몫)
|
|
|
|
|
* 에서 먼저 깎으면 **줄어야 할 운반비가 안 줄어든다.**
|
|
|
|
|
* ⚠ 잔량 하나하나(`residuals`)를 줄인다 — 총량만 줄이면 사토 balloon·운반거리가 안 따라간다.
|
|
|
|
|
* ⚠ **토취는 안 건드린다.** 「채집석을 캐 가면 성토에 쓸 흙이 줄어 토취가 는다」는 갈래가
|
|
|
|
|
* 있으나 **근거가 없다** — 근거 없이 금액을 올리지 않는다. 확정되면 이 함수만 고치면 된다.
|
|
|
|
|
* ⚠ **순서** — 이 함수는 `applyStructureSpoil` **뒤에** 돈다. 잔토가 담긴 뒤라야 뺄 대상이 있다.
|
|
|
|
|
*
|
|
|
|
|
* 돌려주는 값은 **실제로 뺀 양(㎥)**. 사토가 모자라면 받은 값보다 작다.
|
|
|
|
|
*/
|
|
|
|
@@ -367,10 +377,150 @@ function applyCollectedStoneDeduction(residuals: HaulResidual[], deduction: numb
|
|
|
|
|
return deduction - Math.max(left, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 구조물 잔토 — **사토에 한 번만 더한다**(2026-09-09 네 창 합의).
|
|
|
|
|
*
|
|
|
|
|
* 구조물 잔토는 사토에 한 번만 더한다.
|
|
|
|
|
* B08 은 소요량(structure_spoil_m3, ㎥ 양수)을 내기만 하고,
|
|
|
|
|
* 더하는 자리는 유토곡선의 사토뿐이다.
|
|
|
|
|
*
|
|
|
|
|
* ⚠ **잔량 하나하나를 늘린다** — 총량만 늘리면 사토는 늘고 **운반이 안 는다**(공제 때와 같은 자리).
|
|
|
|
|
* ⚠ **나누는 법** — B08 이 지금은 **총량 하나**만 준다. 어느 측점에서 나온 잔토인지 모르므로
|
|
|
|
|
* **남은 사토 잔량의 크기에 비례**해 나눈다. 한 곳에 몰면 운반거리가 틀리기 때문이고,
|
|
|
|
|
* 측점별 값이 오면 그때 그 자리에 얹을 것(그때는 이 함수만 고치면 된다).
|
|
|
|
|
* ⚠ **자연방토(`natural_m3`)는 안 늘린다** — 구조물 잔토는 실어 내는 흙이다.
|
|
|
|
|
* ⚠ **지반유형 안분(ea/rr/br)도 안 건드린다** — 어느 지반에서 나온 흙인지 모른다.
|
|
|
|
|
* 합계(`volume_m3`)와 갈래 합이 어긋나는 것은 그 사실을 드러내는 표시다.
|
|
|
|
|
*
|
|
|
|
|
* 돌려주는 값은 **실제로 더한 양(㎥)**. 받을 사토 잔량이 하나도 없으면 0 이다.
|
|
|
|
|
*/
|
|
|
|
|
/**
|
|
|
|
|
* 터파기 토질 문자열 → 잔량의 지반 갈래 칸. 모르면 `null`(그 몫은 「지반 모름」으로 남는다).
|
|
|
|
|
* 받는 말은 B08 의 품셈 9-13 3구분과 우리 내부 이름 둘 다 받는다.
|
|
|
|
|
*/
|
|
|
|
|
function groundBucket(ground: string | null | undefined): "ea_m3" | "rr_m3" | "br_m3" | null {
|
|
|
|
|
const text = (ground ?? "").trim();
|
|
|
|
|
if (!text) return null;
|
|
|
|
|
if (text === "토사" || text === "soil" || text === "ea") return "ea_m3";
|
|
|
|
|
if (text === "암절취" || text === "리핑암" || text === "ripping_rock" || text === "rr") {
|
|
|
|
|
return "rr_m3";
|
|
|
|
|
}
|
|
|
|
|
if (text === "발파암" || text === "blasting_rock" || text === "br") return "br_m3";
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function newSpoilResidual(fromM: number, toM: number, volumeM3: number): HaulResidual {
|
|
|
|
|
// 잔토를 담을 사토 잔량이 없을 때 **새로 세운다**(2026-09-09 확정 ㉰).
|
|
|
|
|
// ⚠ 지반유형 안분·자연방토는 0 이다 — 어느 지반에서 나온 흙인지 모르고, **실어 내는** 흙이다.
|
|
|
|
|
return {
|
|
|
|
|
index: 0,
|
|
|
|
|
kind: "spoil",
|
|
|
|
|
from_m: fromM,
|
|
|
|
|
to_m: toM,
|
|
|
|
|
volume_m3: volumeM3,
|
|
|
|
|
level_from_m3: 0,
|
|
|
|
|
level_to_m3: 0,
|
|
|
|
|
ea_m3: 0,
|
|
|
|
|
rr_m3: 0,
|
|
|
|
|
br_m3: 0,
|
|
|
|
|
natural_m3: 0,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 구조물 잔토 — **사토에 한 번만 더한다**(2026-09-09 네 창 합의).
|
|
|
|
|
*
|
|
|
|
|
* 구조물 잔토는 사토에 한 번만 더한다.
|
|
|
|
|
* B08 은 소요량(structure_spoil_m3, ㎥ 양수)을 내기만 하고,
|
|
|
|
|
* 더하는 자리는 유토곡선의 사토뿐이다.
|
|
|
|
|
*
|
|
|
|
|
* ⚠ **잔량 하나하나를 늘린다** — 총량만 늘리면 사토는 늘고 **운반이 안 는다**.
|
|
|
|
|
* ⚠ **담을 사토가 없으면 사토를 새로 세운다**(확정 ㉰). 파낸 흙은 어디로든 가므로
|
|
|
|
|
* **물량이 사라지면 안 된다**. 토취를 줄이는 길(㉯)은 **「그 잔토를 성토재로 쓸 수 있다」**는
|
|
|
|
|
* 근거가 있어야 하는데 구조물 터파기 흙은 암이 섞일 수 있고 우리가 그 판정을 안 한다 —
|
|
|
|
|
* 근거 없이 금액을 내리지 않고 **내보내는 쪽(안전측)** 으로 둔다.
|
|
|
|
|
* ⇒ 나중에 「성토재로 쓴다」가 확정되면 **이 함수 한 곳만** 바꾸면 된다.
|
|
|
|
|
* ⚠ 자연방토·지반유형 안분은 안 건드린다 — 실어 내는 흙이고, 어느 지반에서 나온지 모른다.
|
|
|
|
|
*/
|
|
|
|
|
function applyStructureSpoil(
|
|
|
|
|
residuals: HaulResidual[],
|
|
|
|
|
amount: number | null,
|
|
|
|
|
points: Array<{
|
|
|
|
|
chainage_m: number;
|
|
|
|
|
spoil_m3: number;
|
|
|
|
|
/** B08 이 보내는 이름은 `ground_type`(soil·ripping_rock·blasting_rock). 옛 이름·라벨도 받는다. */
|
|
|
|
|
ground_type?: string | null;
|
|
|
|
|
ground_label?: string | null;
|
|
|
|
|
ground?: string | null;
|
|
|
|
|
}> | null,
|
|
|
|
|
): number {
|
|
|
|
|
const spoilsOf = (): HaulResidual[] => residuals.filter((residual) => residual.kind === "spoil");
|
|
|
|
|
|
|
|
|
|
// ① 측점별 값이 오면 **그 자리**에 얹는다 — 구조물이 실제로 선 자리라 운반거리가 맞다.
|
|
|
|
|
if (points && points.length) {
|
|
|
|
|
let added = 0;
|
|
|
|
|
for (const point of points) {
|
|
|
|
|
const value = Number(point?.spoil_m3);
|
|
|
|
|
const chainage = Number(point?.chainage_m);
|
|
|
|
|
if (!Number.isFinite(value) || value <= 0 || !Number.isFinite(chainage)) continue;
|
|
|
|
|
const spoils = spoilsOf();
|
|
|
|
|
const covering = spoils.find(
|
|
|
|
|
(residual) => chainage >= residual.from_m - 1e-9 && chainage <= residual.to_m + 1e-9,
|
|
|
|
|
);
|
|
|
|
|
// 터파기 토질이 함께 오면 **그 갈래로 담는다** — B08 이 이미 판정한 값을 이어받는 것이라
|
|
|
|
|
// 새 근거를 만드는 것이 아니다. 안 오면 갈래 없이 담겨 「지반 모름」으로 남는다.
|
|
|
|
|
const bucket = groundBucket(point?.ground_type ?? point?.ground ?? point?.ground_label);
|
|
|
|
|
const target = covering ?? newSpoilResidual(chainage, chainage, 0);
|
|
|
|
|
if (!covering) residuals.push(target);
|
|
|
|
|
target.volume_m3 += value;
|
|
|
|
|
if (bucket) target[bucket] += value;
|
|
|
|
|
added += value;
|
|
|
|
|
}
|
|
|
|
|
return added;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ② 총량만 오면 남은 사토 잔량 크기에 **비례**해 나눈다(어느 자리인지 모를 때의 차선).
|
|
|
|
|
if (amount === null || !Number.isFinite(amount) || amount <= 0) return 0;
|
|
|
|
|
const spoils = spoilsOf();
|
|
|
|
|
const total = spoils.reduce((sum, residual) => sum + residual.volume_m3, 0);
|
|
|
|
|
if (!spoils.length || total <= EPSILON) {
|
|
|
|
|
// 담을 사토가 없다 — 있는 잔량이 덮는 구간 전체에 사토를 하나 세운다. 자리를 모르므로
|
|
|
|
|
// 구간을 넓게 잡고, **물량은 보존**한다.
|
|
|
|
|
const froms = residuals.map((residual) => residual.from_m);
|
|
|
|
|
const tos = residuals.map((residual) => residual.to_m);
|
|
|
|
|
const fromM = froms.length ? Math.min(...froms) : 0;
|
|
|
|
|
const toM = tos.length ? Math.max(...tos) : 0;
|
|
|
|
|
residuals.push(newSpoilResidual(fromM, toM, amount));
|
|
|
|
|
return amount;
|
|
|
|
|
}
|
|
|
|
|
let added = 0;
|
|
|
|
|
spoils.forEach((residual, index) => {
|
|
|
|
|
const share =
|
|
|
|
|
index === spoils.length - 1 ? amount - added : amount * (residual.volume_m3 / total);
|
|
|
|
|
residual.volume_m3 += share;
|
|
|
|
|
added += share;
|
|
|
|
|
});
|
|
|
|
|
return added;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function computeHaulPlan(
|
|
|
|
|
result: MassHaulResult,
|
|
|
|
|
limits: HaulEquipmentLimit[] | undefined,
|
|
|
|
|
options?: { collected_stone_deduction_m3?: number | null },
|
|
|
|
|
options?: {
|
|
|
|
|
collected_stone_deduction_m3?: number | null;
|
|
|
|
|
structure_spoil_m3?: number | null;
|
|
|
|
|
/** 측점별 구조물 잔토 — 오면 **이쪽이 이긴다**(그 자리 잔량에 얹어 운반거리를 맞춘다). */
|
|
|
|
|
structure_spoil_points?: Array<{
|
|
|
|
|
chainage_m: number;
|
|
|
|
|
spoil_m3: number;
|
|
|
|
|
/** 그 터파기의 토질 — B08 이 `design.ground_type` 으로 이미 판정한 값(품셈 9-13 3구분).
|
|
|
|
|
* **새 근거를 만드는 것이 아니라 이어받는 것**이다. 섞여서 못 고른 구조물은 `null` 로
|
|
|
|
|
* 오고, 그 몫은 「지반 모름」(`ground_unknown_m3`)으로 남는다. */
|
|
|
|
|
ground_type?: string | null;
|
|
|
|
|
ground_label?: string | null;
|
|
|
|
|
ground?: string | null;
|
|
|
|
|
}> | null;
|
|
|
|
|
},
|
|
|
|
|
): HaulPlan | null {
|
|
|
|
|
const points = result.points;
|
|
|
|
|
if (points.length < 2) return null;
|
|
|
|
@@ -549,6 +699,17 @@ export function computeHaulPlan(
|
|
|
|
|
residual.index = index + 1;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ⚠ **순서가 뜻을 가른다 — 「잔토를 더하고 → 공제를 뺀다」**(2026-09-09 확정).
|
|
|
|
|
// 유토곡선의 사토는 「현장에 남는 흙 총량」이고 **출처를 안 가린다**. 공제는 그 총량에서
|
|
|
|
|
// 빼는 것이라 **잔토가 담긴 뒤라야 뺄 대상이 있다.** 반대 순서로 두면 사토가 0 인
|
|
|
|
|
// 노선(전 구간 토취)에서 공제가 **영영 안 걸린다** — 두 창 저장분에서 실제로 그랬다.
|
|
|
|
|
const structureSpoilInput = options?.structure_spoil_m3 ?? null;
|
|
|
|
|
const structureSpoilPoints = options?.structure_spoil_points ?? null;
|
|
|
|
|
const structureSpoilAdded = applyStructureSpoil(
|
|
|
|
|
settled,
|
|
|
|
|
structureSpoilInput,
|
|
|
|
|
structureSpoilPoints,
|
|
|
|
|
);
|
|
|
|
|
const deductionInput = options?.collected_stone_deduction_m3 ?? null;
|
|
|
|
|
const deducted = applyCollectedStoneDeduction(settled, deductionInput);
|
|
|
|
|
const remaining = settled.filter((residual) => residual.volume_m3 > EPSILON);
|
|
|
|
@@ -577,6 +738,8 @@ export function computeHaulPlan(
|
|
|
|
|
natural_spoil_m3: naturalSpoil,
|
|
|
|
|
collected_stone_deduction_m3: deductionInput,
|
|
|
|
|
collected_stone_deducted_m3: deducted,
|
|
|
|
|
structure_spoil_m3: structureSpoilInput,
|
|
|
|
|
structure_spoil_added_m3: structureSpoilAdded,
|
|
|
|
|
hauled_m3: blocks.reduce((sum, block) => sum + block.volume_m3, 0),
|
|
|
|
|
transferred_m3: transfers.reduce((sum, entry) => sum + entry.volume_m3, 0),
|
|
|
|
|
fill_total_m3: fillTotal,
|
|
|
|
|