fix(B06): 구조물 몫(공제·잔토)이 실제로 흐르게 — 부르는 자리 둘이 인자를 안 넘기고 있었음
통로는 났는데 `_mass_haul_context()` 를 **인자 없이** 불러 값이 늘 `None` 이었다 (`Router_HaulPlan.py:64` · `Server_Calc_Prebuild.py:170`). 오늘 세 번째로 나온 「받는 곳은 있는데 넣는 곳이 없던」 자리다. - `haul_inputs_for()` 로 B08 의 `project_haul_inputs` 를 받아 두 자리 모두에 넘긴다. ⚠ 여기서 다시 세지 않는다 — 두 값 다 B08 전개에서 나온다(CLAUDE.md 5장). - 측점별 잔토(`structure_spoil_points`)를 받으면 **그 자리 잔량에 얹는다** — 총량 비례로 흩으면 운반거리가 틀린다. 총량만 오면 종전대로 비례 분배(차선). - 저장 payload 에 받은 값·실제로 먹은 값 넷을 남긴다 — 「값이 흐르는지」를 저장분에서 바로 가릴 수 있어야 한다. 실측(route 184): 공제 0(구입이라 채집 없음) · 구조물 잔토 12.875㎥ 가 **실제로 전달됨**. ⚠ 그 노선은 사토가 0(전 구간 토취)이라 더할 잔량이 없어 `added=0` — 아래 물음 참조. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,11 @@ from uuid import UUID
|
||||
from fastapi import APIRouter, Body
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from B06_Section.B06_Section_Server_Calc_Prebuild import BUNDLE, _mass_haul_context
|
||||
from B06_Section.B06_Section_Server_Calc_Prebuild import (
|
||||
BUNDLE,
|
||||
_mass_haul_context,
|
||||
haul_inputs_for,
|
||||
)
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -56,12 +60,22 @@ async def compute_haul_plan(
|
||||
status_code=400,
|
||||
content={"status": "error", "message": "측점 수가 너무 많습니다."},
|
||||
)
|
||||
# 구조물 몫(공제·잔토)을 **넘겨야** 사토가 줄고 는다 — 인자 없이 부르면 늘 `None` 이라
|
||||
# 통로만 있고 값이 안 흐른다(2026-09-09 실측으로 드러난 자리).
|
||||
haul_inputs = await haul_inputs_for(project_id)
|
||||
try:
|
||||
output = await asyncio.to_thread(
|
||||
run_bundle_json,
|
||||
BUNDLE,
|
||||
_NPM_SCRIPT,
|
||||
{"haul_plan_for": result, "context": _mass_haul_context()},
|
||||
{
|
||||
"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"),
|
||||
),
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("유토 배분 계산 실패: project_id=%s route_id=%s", project_id, route_id)
|
||||
|
||||
@@ -63,9 +63,26 @@ _AREA_KEYS = (
|
||||
)
|
||||
|
||||
|
||||
async def haul_inputs_for(project_id: Any) -> dict[str, Any]:
|
||||
"""B08 이 낸 **구조물 몫**(채집석 공제·구조물 잔토)을 받아 온다.
|
||||
|
||||
⚠ **여기서 다시 세지 않는다** — 두 값 다 B08 전개에서 나오는 것이라 이쪽이 세면
|
||||
같은 계산이 두 벌이 된다(CLAUDE.md 5장). 못 읽으면 빈 값으로 두고 **0 으로 눅이지 않는다**.
|
||||
"""
|
||||
try:
|
||||
from B08_Quantity.B08_Quantity_Router_Material import project_haul_inputs
|
||||
|
||||
data = await project_haul_inputs(project_id)
|
||||
return data if isinstance(data, dict) else {}
|
||||
except Exception:
|
||||
logger.exception("구조물 몫(공제·잔토) 조회 실패 — 값 없이 진행: project_id=%s", project_id)
|
||||
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]:
|
||||
"""유토곡선 계산에 필요한 값 — 화면이 `sections/context`로 받는 것과 같은 상수다.
|
||||
|
||||
@@ -93,6 +110,8 @@ def _mass_haul_context(
|
||||
# B08 은 소요량(structure_spoil_m3, ㎥ 양수)을 내기만 하고,
|
||||
# 더하는 자리는 유토곡선의 사토뿐이다.
|
||||
"structure_spoil_m3": structure_spoil_m3,
|
||||
# 측점별 잔토 — 오면 이쪽이 이긴다(구조물이 선 자리 잔량에 얹어 운반거리를 맞춘다).
|
||||
"structure_spoil_points": structure_spoil_points,
|
||||
}
|
||||
|
||||
|
||||
@@ -134,10 +153,13 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int:
|
||||
# 상세 만들기(파일 읽기 위주)와 DB 두 건은 서로 기다릴 이유가 없다 — 같이 보낸다.
|
||||
# 원격 DB 라 순차로 내면 왕복이 그대로 더해진다(질의 하나 약 12ms, 2026-09-06 실측).
|
||||
pool = get_db_pool()
|
||||
response, stored_path, longitudinal_row = await asyncio.gather(
|
||||
# 구조물 몫(채집석 공제·구조물 잔토)도 함께 받아 온다 — **B08 이 낸 값**이고, 안 넘기면
|
||||
# 통로만 있고 값이 안 흐른다(2026-09-09 실측: 공제가 늘 `None` 이라 사토가 안 줄었다).
|
||||
response, stored_path, longitudinal_row, haul_inputs = await asyncio.gather(
|
||||
get_section_detail(project_uuid, route_id),
|
||||
run_with_connection(get_project_storage_relative_path, project_uuid),
|
||||
run_with_connection(get_longitudinal_section, project_uuid, route_id),
|
||||
haul_inputs_for(project_uuid),
|
||||
)
|
||||
marks.append(("종횡단 상세+DB 조회(병렬)", time.perf_counter()))
|
||||
payload = getattr(response, "model_dump", None)
|
||||
@@ -167,7 +189,14 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int:
|
||||
run_bundle_json,
|
||||
BUNDLE,
|
||||
_NPM_SCRIPT,
|
||||
{"detail": detail, "context": _mass_haul_context()},
|
||||
{
|
||||
"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"),
|
||||
),
|
||||
},
|
||||
)
|
||||
marks.append(("Node 번들(면적·유토곡선)", time.perf_counter()))
|
||||
if not isinstance(output, dict):
|
||||
|
||||
@@ -391,14 +391,44 @@ function applyCollectedStoneDeduction(residuals: HaulResidual[], deduction: numb
|
||||
*
|
||||
* 돌려주는 값은 **실제로 더한 양(㎥)**. 받을 사토 잔량이 하나도 없으면 0 이다.
|
||||
*/
|
||||
function applyStructureSpoil(residuals: HaulResidual[], amount: number | null): number {
|
||||
if (amount === null || !Number.isFinite(amount) || amount <= 0) return 0;
|
||||
function applyStructureSpoil(
|
||||
residuals: HaulResidual[],
|
||||
amount: number | null,
|
||||
points: Array<{ chainage_m: number; spoil_m3: number }> | null,
|
||||
): number {
|
||||
const spoils = residuals.filter((residual) => residual.kind === "spoil");
|
||||
if (!spoils.length) return 0;
|
||||
|
||||
// ① 측점별 값이 오면 **그 자리 잔량**에 얹는다 — 구조물이 실제로 선 자리라 운반거리가 맞다.
|
||||
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 covering = spoils.find(
|
||||
(residual) => chainage >= residual.from_m - 1e-9 && chainage <= residual.to_m + 1e-9,
|
||||
);
|
||||
const target =
|
||||
covering ??
|
||||
spoils.reduce((best, residual) => {
|
||||
const middle = (residual.from_m + residual.to_m) / 2;
|
||||
const bestMiddle = (best.from_m + best.to_m) / 2;
|
||||
return Math.abs(chainage - middle) < Math.abs(chainage - bestMiddle) ? residual : best;
|
||||
}, spoils[0]);
|
||||
target.volume_m3 += value;
|
||||
added += value;
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
// ② 총량만 오면 **남은 사토 잔량 크기에 비례**해 나눈다(어느 자리인지 모를 때의 차선).
|
||||
if (amount === null || !Number.isFinite(amount) || amount <= 0) return 0;
|
||||
const total = spoils.reduce((sum, residual) => sum + residual.volume_m3, 0);
|
||||
if (!spoils.length || total <= EPSILON) return 0;
|
||||
if (total <= EPSILON) return 0;
|
||||
let added = 0;
|
||||
spoils.forEach((residual, index) => {
|
||||
// 마지막 잔량은 나머지를 그대로 받아 반올림 오차가 새지 않게 한다.
|
||||
const share =
|
||||
index === spoils.length - 1 ? amount - added : amount * (residual.volume_m3 / total);
|
||||
residual.volume_m3 += share;
|
||||
@@ -413,6 +443,8 @@ export function computeHaulPlan(
|
||||
options?: {
|
||||
collected_stone_deduction_m3?: number | null;
|
||||
structure_spoil_m3?: number | null;
|
||||
/** 측점별 구조물 잔토 — 오면 **이쪽이 이긴다**(그 자리 잔량에 얹어 운반거리를 맞춘다). */
|
||||
structure_spoil_points?: Array<{ chainage_m: number; spoil_m3: number }> | null;
|
||||
},
|
||||
): HaulPlan | null {
|
||||
const points = result.points;
|
||||
@@ -595,7 +627,12 @@ export function computeHaulPlan(
|
||||
const deductionInput = options?.collected_stone_deduction_m3 ?? null;
|
||||
const deducted = applyCollectedStoneDeduction(settled, deductionInput);
|
||||
const structureSpoilInput = options?.structure_spoil_m3 ?? null;
|
||||
const structureSpoilAdded = applyStructureSpoil(settled, structureSpoilInput);
|
||||
const structureSpoilPoints = options?.structure_spoil_points ?? null;
|
||||
const structureSpoilAdded = applyStructureSpoil(
|
||||
settled,
|
||||
structureSpoilInput,
|
||||
structureSpoilPoints,
|
||||
);
|
||||
const remaining = settled.filter((residual) => residual.volume_m3 > EPSILON);
|
||||
remaining.forEach((residual, index) => {
|
||||
residual.index = index + 1;
|
||||
|
||||
@@ -179,6 +179,13 @@ export function haulPlanPayload(plan: HaulPlan): Record<string, unknown> {
|
||||
hauled_m3: round(plan.hauled_m3),
|
||||
transferred_m3: round(plan.transferred_m3),
|
||||
fill_total_m3: round(plan.fill_total_m3),
|
||||
// 구조물 몫 — **받은 값**과 **실제로 먹은 값**을 함께 남긴다. 「통로만 있고 값이 안 흐른다」를
|
||||
// 저장분에서 바로 가릴 수 있어야 한다(2026-09-09 그 사고가 세 번 났다).
|
||||
// `null` 은 「아직 안 옴」, `0` 은 「없음」이다 — 눅이지 않는다.
|
||||
collected_stone_deduction_m3: plan.collected_stone_deduction_m3,
|
||||
collected_stone_deducted_m3: round(plan.collected_stone_deducted_m3),
|
||||
structure_spoil_m3: plan.structure_spoil_m3,
|
||||
structure_spoil_added_m3: round(plan.structure_spoil_added_m3),
|
||||
// 떨어진 구간끼리의 장거리 운반 — B08 내역서가 별도 운반 항목으로 세운다.
|
||||
transfers: plan.transfers.map((transfer) => ({
|
||||
index: transfer.index,
|
||||
|
||||
Reference in New Issue
Block a user