diff --git a/B06_Section/B06_Section_Router_HaulPlan.py b/B06_Section/B06_Section_Router_HaulPlan.py index 2fc2dc7c..73454d7f 100644 --- a/B06_Section/B06_Section_Router_HaulPlan.py +++ b/B06_Section/B06_Section_Router_HaulPlan.py @@ -70,11 +70,7 @@ async def compute_haul_plan( _NPM_SCRIPT, { "haul_plan_for": result, - "context": _mass_haul_context( - haul_inputs.get("collected_stone_deduction_m3"), - haul_inputs.get("structure_spoil_m3"), - haul_inputs.get("structure_spoil_points"), - ), + "context": _mass_haul_context(haul_inputs), }, ) except Exception: diff --git a/B06_Section/B06_Section_Server_Calc_Node.ts b/B06_Section/B06_Section_Server_Calc_Node.ts index 480f6126..99099d7c 100644 --- a/B06_Section/B06_Section_Server_Calc_Node.ts +++ b/B06_Section/B06_Section_Server_Calc_Node.ts @@ -39,6 +39,10 @@ interface ServerCalcInput { haul_equipment_limits?: Parameters[1]; /** 채집석 공제(㎥, 양수) — B08 이 낸다. `null`/없음은 「아직 안 옴」이다. */ collected_stone_deduction_m3?: number | null; + /** 갈래별 채집석(㎥) — 벽 입적(자연 축)이라 곡선 쪽이 ×C 해 다짐 축에 맞춘다. */ + collected_stone_by_ground_m3?: Record | null; + /** 갈래를 못 가른 채집석(㎥) — 계수가 없어 환산하지 않는다. */ + collected_stone_ground_unknown_m3?: number | null; /** 구조물 터파기 잔토(㎥, 양수) — B08 이 낸다. 사토에 **더한다**. */ structure_spoil_m3?: number | null; /** 측점별 잔토 — 오면 **그 자리**에 얹는다(운반거리가 맞다). */ @@ -67,6 +71,8 @@ if (input.haul_plan_for) { // 그리기 코드가 손대지 않고 그대로 받는다. 전부 숫자·문자열이라 JSON 으로 오간다. const plan = computeHaulPlan(input.haul_plan_for, input.context?.haul_equipment_limits, { collected_stone_deduction_m3: input.context?.collected_stone_deduction_m3 ?? null, + collected_stone_by_ground_m3: input.context?.collected_stone_by_ground_m3 ?? null, + collected_stone_ground_unknown_m3: input.context?.collected_stone_ground_unknown_m3 ?? null, structure_spoil_m3: input.context?.structure_spoil_m3 ?? null, structure_spoil_points: input.context?.structure_spoil_points ?? null, // 잔토는 자연상태로 오고 곡선은 다짐상태다 — 담기 전에 ×C 하는 데 쓴다. @@ -95,6 +101,8 @@ const result = conversion const plan = result ? computeHaulPlan(result, input.context?.haul_equipment_limits, { collected_stone_deduction_m3: input.context?.collected_stone_deduction_m3 ?? null, + collected_stone_by_ground_m3: input.context?.collected_stone_by_ground_m3 ?? null, + collected_stone_ground_unknown_m3: input.context?.collected_stone_ground_unknown_m3 ?? null, structure_spoil_m3: input.context?.structure_spoil_m3 ?? null, structure_spoil_points: input.context?.structure_spoil_points ?? null, conversion: conversion ?? null, diff --git a/B06_Section/B06_Section_Server_Calc_Prebuild.py b/B06_Section/B06_Section_Server_Calc_Prebuild.py index 001dbaf3..cb3a260c 100644 --- a/B06_Section/B06_Section_Server_Calc_Prebuild.py +++ b/B06_Section/B06_Section_Server_Calc_Prebuild.py @@ -79,11 +79,7 @@ async def haul_inputs_for(project_id: Any) -> dict[str, Any]: return {} -def _mass_haul_context( - collected_stone_deduction_m3: float | None = None, - structure_spoil_m3: float | None = None, - structure_spoil_points: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: +def _mass_haul_context(haul_inputs: dict[str, Any] | None = None) -> dict[str, Any]: """유토곡선 계산에 필요한 값 — 화면이 `sections/context`로 받는 것과 같은 상수다. ⚠ 채집석 공제(`collected_stone_deduction_m3`)만 상수가 아니라 **B08 이 내는 값**이다. @@ -95,6 +91,7 @@ def _mass_haul_context( 빼는 자리는 유토곡선의 사토뿐이다 — 실어 내는 몫(spoil_m3 − natural_spoil_m3)에서 먼저 빼고 모자라면 자연방토에서 뺀다. """ + inputs = haul_inputs or {} return { "earthwork_conversion": EARTHWORK_CONVERSION_FACTORS, "natural_spoil_min_ground_slope": NATURAL_SPOIL_MIN_GROUND_SLOPE, @@ -104,14 +101,18 @@ def _mass_haul_context( ], # ⚠ B08 이 아직 이 값을 내지 않는다(2026-09-09) — 그때까지 `None`(아직 안 옴)이다. # 값을 내기 시작하면 여기에 실어 주기만 하면 통로가 이어진다. - "collected_stone_deduction_m3": collected_stone_deduction_m3, + "collected_stone_deduction_m3": inputs.get("collected_stone_deduction_m3"), + # 갈래별 채집석(2026-09-09 세 창 확정) — **벽 입적**이라 자연 축으로 보고 곡선 쪽에서 + # ×C 해 다짐 축에 맞춰 뺀다. 갈래를 못 가른 몫은 계수가 없어 **환산하지 않는다**. + "collected_stone_by_ground_m3": inputs.get("collected_stone_by_ground_m3") or {}, + "collected_stone_ground_unknown_m3": inputs.get("collected_stone_ground_unknown_m3"), # 구조물 터파기 잔토(㎥, 양수) — **사토에 더한다**(공제는 빼고 이것은 더한다). # 구조물 잔토는 사토에 한 번만 더한다. # B08 은 소요량(structure_spoil_m3, ㎥ 양수)을 내기만 하고, # 더하는 자리는 유토곡선의 사토뿐이다. - "structure_spoil_m3": structure_spoil_m3, + "structure_spoil_m3": inputs.get("structure_spoil_m3"), # 측점별 잔토 — 오면 이쪽이 이긴다(구조물이 선 자리 잔량에 얹어 운반거리를 맞춘다). - "structure_spoil_points": structure_spoil_points, + "structure_spoil_points": inputs.get("structure_spoil_points"), } @@ -191,11 +192,7 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int: _NPM_SCRIPT, { "detail": detail, - "context": _mass_haul_context( - haul_inputs.get("collected_stone_deduction_m3"), - haul_inputs.get("structure_spoil_m3"), - haul_inputs.get("structure_spoil_points"), - ), + "context": _mass_haul_context(haul_inputs), }, ) marks.append(("Node 번들(면적·유토곡선)", time.perf_counter())) diff --git a/common_util/common_util_mass_haul_balance.ts b/common_util/common_util_mass_haul_balance.ts index 2717c2dc..07842292 100644 --- a/common_util/common_util_mass_haul_balance.ts +++ b/common_util/common_util_mass_haul_balance.ts @@ -348,10 +348,22 @@ function bandOutline( * * 돌려주는 값은 **실제로 뺀 양(㎥)**. 사토가 모자라면 받은 값보다 작다. */ -function applyCollectedStoneDeduction(residuals: HaulResidual[], deduction: number | null): number { - if (deduction === null || !Number.isFinite(deduction) || deduction <= 0) return 0; +function applyCollectedStoneDeduction( + residuals: HaulResidual[], + deduction: number | null, + byGround?: Record | null, + groundUnknownM3?: number | null, + conversion?: EarthworkConversion | null, +): number { + // ⚠ **축을 맞춘 뒤 뺀다**(2026-09-09 세 창 확정) — 채집석은 **벽 입적(제자리 부피)**이라 + // 자연 축이고 이 곡선은 다짐 축이다. 갈래별 값이 오면 **갈래마다 ×C** 해서 뺀다. + // 갈래를 못 가른 몫은 **계수가 없어 환산하지 않고** 그대로 뺀다(근거 없이 안 눅인다). + // 갈래가 아예 안 오면 종전처럼 총량을 그대로 뺀다 — 값이 안 오는 것과 0 은 다르다. + const converted = compactedStoneDeduction(byGround, groundUnknownM3, conversion); + const amountToTake = converted === null ? deduction : converted; + if (amountToTake === null || !Number.isFinite(amountToTake) || amountToTake <= 0) return 0; const spoils = residuals.filter((residual) => residual.kind === "spoil"); - let left = deduction; + let left = amountToTake; const take = (residual: HaulResidual, amount: number): void => { if (amount <= 0) return; const before = residual.volume_m3; @@ -375,7 +387,7 @@ function applyCollectedStoneDeduction(residuals: HaulResidual[], deduction: numb residual.natural_m3 -= amount; take(residual, amount); } - return deduction - Math.max(left, 0); + return amountToTake - Math.max(left, 0); } /** @@ -417,6 +429,33 @@ const GROUND_OF_BUCKET = { br_m3: "blasting_rock", } as const; +/** 환산계수 이름 ↔ 채집석이 오는 갈래 이름. B08 이 `soil`·`ripping_rock`·`blasting_rock` 로 보낸다. */ +const STONE_GROUND_KEYS = ["soil", "ripping_rock", "blasting_rock"] as const; + +/** + * 채집석 공제를 **다짐 축**으로 옮긴다 — 갈래마다 ×C, 갈래를 모르는 몫은 그대로. + * 갈래가 하나도 안 오면 `null`(= 「갈래를 안 보냈음」)이라 부르는 쪽이 총량으로 되돌아간다. + */ +function compactedStoneDeduction( + byGround: Record | null | undefined, + groundUnknownM3: number | null | undefined, + conversion: EarthworkConversion | null | undefined, +): number | null { + if (!byGround) return null; + const entries = STONE_GROUND_KEYS.map((key) => [key, Number(byGround[key] ?? 0)] as const).filter( + ([, value]) => Number.isFinite(value) && value > 0, + ); + const unknown = Number(groundUnknownM3 ?? 0); + const hasUnknown = Number.isFinite(unknown) && unknown > 0; + if (!entries.length && !hasUnknown) return null; + let total = hasUnknown ? unknown : 0; + for (const [ground, value] of entries) { + const factor = conversion?.[ground]?.compacted; + total += typeof factor === "number" && factor > 0 ? value * factor : value; + } + return total; +} + /** * **자연상태 → 다짐상태**(× C). 구조물 잔토를 곡선에 얹기 전에 한 번만 거친다. * @@ -545,6 +584,10 @@ export function computeHaulPlan( limits: HaulEquipmentLimit[] | undefined, options?: { collected_stone_deduction_m3?: number | null; + /** 갈래별 채집석(㎥) — **벽 입적(자연 축)**이라 여기서 ×C 해 다짐 축에 맞춘다. */ + collected_stone_by_ground_m3?: Record | null; + /** 갈래를 못 가른 채집석(㎥) — 계수가 없어 **환산하지 않고** 그대로 뺀다. */ + collected_stone_ground_unknown_m3?: number | null; structure_spoil_m3?: number | null; /** 측점별 구조물 잔토 — 오면 **이쪽이 이긴다**(그 자리 잔량에 얹어 운반거리를 맞춘다). */ structure_spoil_points?: Array<{ @@ -752,7 +795,13 @@ export function computeHaulPlan( options?.conversion ?? null, ); const deductionInput = options?.collected_stone_deduction_m3 ?? null; - const deducted = applyCollectedStoneDeduction(settled, deductionInput); + const deducted = applyCollectedStoneDeduction( + settled, + deductionInput, + options?.collected_stone_by_ground_m3 ?? null, + options?.collected_stone_ground_unknown_m3 ?? null, + options?.conversion ?? null, + ); const remaining = settled.filter((residual) => residual.volume_m3 > EPSILON); remaining.forEach((residual, index) => { residual.index = index + 1;