Files
Aislo/B06_Section/B06_Section_Server_Calc_Node.ts
T
eomsangdonandClaude Opus 5 20597071bd fix(b06): 유토 배분 수량은 거르지 않은 계획 · 잔진동 거르기는 그림에만(브레인 ①)
- computeHaulPlan 기본 = 거르지 않음, drawing: true 일 때만 진폭 2% 거르기
- 서버 정본 두 벌 — haul_plan(수량·B08) · haul_plan_drawing(토적도) · 화면 선반입은 그림용
- B07 토적도는 그림용 먼저, 옛 저장분은 haul_plan
- 936be972 실측: 운반 0 → 245.81㎥(7블록 15띠) · 토취 11,100.86 그대로 · 내역 154,857,626 → 155,207,971

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-14 23:13:48 +09:00

136 lines
7.3 KiB
TypeScript

/* =============================================================================
* B06_Section_Server_Calc_Node.ts
* 브라우저에서만 돌던 횡단 계산을 **서버가 한 번 돌리는** 진입점 — 구조물 폐회로
* 절·성토 면적 + 그것을 쌓아 만든 유토곡선.
*
* 왜 있나(2026-09-06, CLAUDE.md 5장 「계산 자리」) — 이 두 값은 지금까지 브라우저에서만
* 나왔다. 사용자가 B06 을 한 번도 안 열면 값이 없고, 저장·확정 뒤 서버가 다시 계산하면
* 구조물을 모르는 표준값으로 되돌아갔다. 코리도(`B05_Profile_Corridor_Node.ts`)와 같은
* 방식으로 **브라우저가 쓰는 코드를 서버가 그대로 실행**한다 — 계산을 두 벌로 짜지 않는다.
*
* 순서가 중요하다: 면적 보정을 **먼저** 얹고 그 위에서 유토곡선을 쌓는다. 화면도 같은
* 순서다(카드를 그리며 면적을 고친 뒤 유토곡선을 낸다).
*
* 실행: node <번들> <입력.json> <출력.json>
* 입력 { detail: 종횡단 상세(API와 같은 꼴), context: { earthwork_conversion,
* natural_spoil_min_ground_slope, haul_equipment_limits } }
* 출력 { areas: [{ chainage_m, cut_area_m2, … }], mass_haul: {…} | null,
* extra_walls: [{ chainage_m, extra_walls: [선 다단 벽…] }] }
* — areas 는 **구조물 트림이 있는 측점만**. 나머지는 표준 계산값이 이미 맞다.
* — extra_walls 는 관 주인 측점마다(빈 목록 포함) · B08 이 다단 기슭막이 줄을 세움(④).
* 끝 코드: 0 성공 / 2 인자 오류
* ========================================================================== */
import { readFileSync, writeFileSync } from "node:fs";
import { computeMassHaul, massHaulPayload } from "@util/common_util_mass_haul";
import { computeHaulPlan, haulPlanPayload } from "@util/common_util_mass_haul_balance";
import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
import {
applyStructureAreaRows,
extraWallRows,
structureAreaRows,
} from "./B06_Section_Structure_Layouts";
interface ServerCalcInput {
detail?: SectionDetailResponse;
/**
* 유토 **배분만** 낼 때 쓰는 입력 — 브라우저가 누가토량(`computeMassHaul`)까지 내고
* 그 결과를 보내면 여기서 배분·운반거리만 얹어 돌려준다(2026-09-06).
* 배분 코드를 브라우저 번들에서 빼기 위한 길이라, 이 갈래는 `detail` 을 안 받는다.
*/
haul_plan_for?: Parameters<typeof computeHaulPlan>[0];
context?: {
earthwork_conversion?: Parameters<typeof computeMassHaul>[1];
natural_spoil_min_ground_slope?: number | null;
haul_equipment_limits?: Parameters<typeof computeHaulPlan>[1];
/** 채집석 공제(㎥, 양수) — B08 이 낸다. `null`/없음은 「아직 안 옴」이다. */
collected_stone_deduction_m3?: number | null;
/** 갈래별 채집석(㎥) — 벽 입적(자연 축)이라 곡선 쪽이 ×C 해 다짐 축에 맞춘다. */
collected_stone_by_ground_m3?: Record<string, number> | null;
/** 갈래를 못 가른 채집석(㎥) — 계수가 없어 환산하지 않는다. */
collected_stone_ground_unknown_m3?: number | null;
/** 구조물 터파기 잔토(㎥, 양수) — B08 이 낸다. 사토에 **더한다**. */
structure_spoil_m3?: number | null;
/** 측점별 잔토 — 오면 **그 자리**에 얹는다(운반거리가 맞다). */
structure_spoil_points?: Array<{
chainage_m: number;
spoil_m3: number;
/** 그 터파기의 토질 — B08 이 이미 판정한 값(품셈 9-13). `null` 이면 「지반 모름」. */
ground_type?: string | null;
ground_label?: string | null;
ground?: string | null;
}> | null;
};
}
const [inputPath, outputPath] = process.argv.slice(2);
if (!inputPath || !outputPath) {
console.error("사용법: node <번들> <입력.json> <출력.json>");
process.exit(2);
}
const input = JSON.parse(readFileSync(inputPath, "utf8")) as ServerCalcInput;
// 배분만 내는 갈래 — 화면이 편집을 멈추면 조용히 물어보는 자리(유토곡선 배경 선반입).
if (input.haul_plan_for) {
// **화면이 쓰는 꼴 그대로** 내보낸다(직렬화 형태 `haulPlanPayload` 가 아니다) — 그래야
// 그리기 코드가 손대지 않고 그대로 받는다. 전부 숫자·문자열이라 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 하는 데 쓴다.
conversion: input.context?.earthwork_conversion ?? null,
// 화면이 그리는 계획 — 잔진동을 거른다(수량은 저장 정본 `haul_plan` 이 따로 낸다).
drawing: true,
});
writeFileSync(outputPath, JSON.stringify({ haul_plan: plan ?? null }));
process.exit(0);
}
const sections: CrossSection[] = input.detail?.cross_sections ?? [];
const areas = structureAreaRows(sections);
// 보정값을 **자리에서** 얹는다 — 유토곡선이 고쳐진 면적 위에서 쌓이게 한다.
applyStructureAreaRows(sections, areas);
// 유토곡선 — balloon 위치는 사용자 화면값이라 서버가 만들지 않는다(파이썬이 보존).
const conversion = input.context?.earthwork_conversion;
const result = conversion
? computeMassHaul(
sections,
conversion,
input.context?.natural_spoil_min_ground_slope ?? undefined,
)
: null;
// 배분은 **서버만** 만든다 — 그래야 그 코드가 브라우저 번들에서 빠진다(2026-09-06).
// 두 벌을 남긴다 — `haul_plan` 은 **거르지 않은** 수량 정본(B08), `haul_plan_drawing` 은
// 잔진동을 거른 그림(B07 토적도). 거르기가 수량에 닿으면 운반이 사라진다(2026-09-14 브레인 ①).
const planFor = (drawing: boolean) =>
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,
drawing,
})
: null;
const plan = planFor(false);
const drawingPlan = planFor(true);
const massHaul = result
? massHaulPayload(result, {
...(plan ? { haul_plan: haulPlanPayload(plan) } : {}),
...(drawingPlan ? { haul_plan_drawing: haulPlanPayload(drawingPlan) } : {}),
})
: null;
// 선 다단 벽 목록(④) — 관 연장처럼 기하가 세운 결과를 정본에 남겨 B08 이 줄을 세움.
const extraWalls = extraWallRows(sections);
writeFileSync(outputPath, JSON.stringify({ areas, mass_haul: massHaul, extra_walls: extraWalls }));