/* ============================================================================= * A00_Common/design/logic/design_calc_node.ts * 계산 등록부(`design_calc_registry.ts`)의 서버 Node 입구 하나(PLAN 472-3) — id → 함수 표. * 등록부에서 실행기가 `node:` 인 줄만 여기 있음(나머지는 472-4 동안 파이썬). * 브라우저가 쓰는 같은 TS 를 그대로 부름 — 계산을 두 벌로 짜지 않음. * * 실행: node <묶음> <입력.json> <출력.json> (package.json `build:design-calc`) * node <묶음> --serve 늘 떠 있음 — 표준 입력 한 줄 = 요청 JSON · 표준 출력 한 줄 = 출력 JSON(511-A) * 입력 { run: [id…], inputs: { detail?: 종횡단 상세(API 꼴) · route_points?: 노선 폴리라인 · * context?: { earthwork_conversion · natural_spoil_min_ground_slope · * haul_equipment_limits · 채집석 · 구조물 잔토(B08) } · * excavation?: { pipes: [{ diameter_mm }] · walls: [{ height_m · average_thickness_m · * has_foundation }] } · * cross_design?: { standard: 표준 횡단(config 위에 저장분 덮은 한 벌) · * stations: [{ samples · design_elevation_m · options: 파이썬 `compute_cross_design` 키 · * chainage_m? }] · facilities?: { pipes · structures }(시설 구간 · 511-B B-4) } · * berm?: { structures: 구조물 정본 목록 · chainages: [측점 m] } · * rebase? · carry?(노선 바뀐 [저장] · `design_calc_node_carry.ts`) · * stages?(묶음 단계 · `design_calc_node_stages.ts` · 511-B) · * stations?(측점 · `design_calc_node_sections.ts` · 511-B B-2) } } * 출력 { results: { [id]: 값 }, available: [id…] } * — 차례는 요청 차례가 아니라 기댐 차례(누가토량 앞에 구조물 면적을 상세에 얹음 · * server-calc 묶음과 같은 차례). * — 시험 길 id(`design_calc_node_trial.ts`) · 단계 id(`stage.*`)도 돌지만 `available` 에는 없음. * 끝 코드: 0 성공 / 1 셈 실패 / 2 인자 오류 / 4 모르는 id(--serve 는 그 줄 { error }) * ========================================================================== */ import { readFileSync, writeFileSync } from "node:fs"; import { bermSpansFromStructures, bermSpecAt } from "@util/common_util_berm_spans"; import { computeCrossDesign } from "@util/common_util_cross_design"; import type { CrossDesignOptions, CrossGroundSample } from "@util/common_util_cross_design"; import type { CulvertPipePoint } from "@util/common_util_culvert_sets"; import { pipeTrenchWidthM, wallTrenchAreaM2, wallTrenchWidthM } from "@util/common_util_excavation"; import { computeMassHaul, massHaulPayload } from "@util/common_util_mass_haul"; import type { MassHaulResult } from "@util/common_util_mass_haul"; import { computeHaulPlan, haulPlanPayload } from "@util/common_util_mass_haul_balance"; import type { RoutePoint } from "../../../B05_Profile/B05_Profile_Api_Fetch"; import { buildCorridor } from "../../../B05_Profile/B05_Profile_UI_Corridor_Build"; import { corridorHash, serialize } from "../../../B05_Profile/B05_Profile_UI_Corridor_Envelope"; import type { SectionDetailResponse } from "../../../B06_Section/B06_Section_Api_Fetch"; import { facilityAt, facilityRanges, type FacilityStructure, } from "../../../B06_Section/logic/B06_Section_Cross_Enforce"; import { crossOptions } from "../../../B06_Section/logic/B06_Section_Cross_Save"; import { applyStructureAreaRows, extraWallRows, structureAreaRows, } from "../../../B06_Section/B06_Section_Structure_Layouts"; import { carryRunners, type CarryInputs } from "./design_calc_node_carry"; import { followRunners, type FollowInputs } from "./design_calc_node_follow"; import { sectionsRunners, type SectionsInputs } from "./design_calc_node_sections"; import { STAGE_IDS, stageRunners, type StagesInputs } from "./design_calc_node_stages"; import { TRIAL_IDS, trialRunners, type TrialInputs } from "./design_calc_node_trial"; type HaulOptions = NonNullable[2]>; interface CalcInputs extends FollowInputs, CarryInputs, TrialInputs, StagesInputs, SectionsInputs { detail?: SectionDetailResponse; route_points?: RoutePoint[]; context?: Partial< Pick< HaulOptions, | "collected_stone_deduction_m3" | "collected_stone_by_ground_m3" | "collected_stone_ground_unknown_m3" | "structure_spoil_m3" | "structure_spoil_points" > > & { earthwork_conversion?: Parameters[1]; natural_spoil_min_ground_slope?: number | null; haul_equipment_limits?: Parameters[1]; }; excavation?: { pipes?: Array<{ diameter_mm?: number | null }>; walls?: Array<{ height_m?: number | null; average_thickness_m?: number | null; has_foundation?: boolean; }>; }; berm?: { structures: Parameters[0]; chainages: number[]; }; cross_design?: { standard: CrossDesignOptions["standard"]; stations?: Array<{ samples: CrossGroundSample[]; design_elevation_m: number | null; options: Record; /** 시설 구간을 얹을 누가거리(`facilities` 와 함께 · 511-B B-4) */ chainage_m?: number; }>; /** 관 지점 · 구조물 정본 → 측점마다 포장 · 기울임 · 세월교 하강(`facilityRanges` · C3) */ facilities?: { pipes: CulvertPipePoint[]; structures: FacilityStructure[] }; }; } /** 한 번 돌림 — 같은 id 는 한 번만 셈(memo) · 앞 id 는 `need` 로 · 단계 id 는 약속(기다림). */ async function runDesignCalc(run: readonly string[], inputs: CalcInputs) { const memo = new Map(); const sections = inputs.detail?.cross_sections ?? []; const context = inputs.context ?? {}; let massHaul: MassHaulResult | null = null; const RUNNERS: Record unknown> = { ...followRunners(inputs), // 측점 열쇠 · 따라감 · 꼭짓점 세움(472-4 ①) ...carryRunners(inputs), // 노선 바뀐 [저장] 다시 얹기 — 관 · 구조물 초안 · 횡단 고침(472-6 6-B) ...trialRunners(inputs), // 계산 자리 「서버」 시험 길(511-A) ...stageRunners(inputs), // 묶음 단계 노선 ~ 횡단(511-B B-1) ...sectionsRunners(inputs), // 측점 목록 · 서버 측점 생성(511-B B-2) // 구조물 측점 면적 · 관 길이를 상세에 얹음(자리에서) — 뒤 누가토량이 고친 면적 위에서 쌓임. "structure.layouts": () => { const areas = structureAreaRows(sections); applyStructureAreaRows(sections, areas); return { areas, extra_walls: extraWallRows(sections) }; }, "masshaul.cumulative": () => { need("structure.layouts"); const conversion = context.earthwork_conversion; massHaul = conversion ? computeMassHaul(sections, conversion, context.natural_spoil_min_ground_slope ?? undefined) : null; return massHaul ? massHaulPayload(massHaul) : null; }, // 두 벌 — haul_plan = 거르지 않은 수량 정본(B08) · haul_plan_drawing = 잔진동 거른 그림. "haul.plan": () => { need("masshaul.cumulative"); const result = massHaul; if (!result) return null; const planFor = (drawing: boolean) => computeHaulPlan(result, context.haul_equipment_limits, { collected_stone_deduction_m3: context.collected_stone_deduction_m3 ?? null, collected_stone_by_ground_m3: context.collected_stone_by_ground_m3 ?? null, collected_stone_ground_unknown_m3: context.collected_stone_ground_unknown_m3 ?? null, structure_spoil_m3: context.structure_spoil_m3 ?? null, structure_spoil_points: context.structure_spoil_points ?? null, conversion: context.earthwork_conversion ?? null, drawing, }); const plan = planFor(false); const drawing = planFor(true); return { ...(plan ? { haul_plan: haulPlanPayload(plan) } : {}), ...(drawing ? { haul_plan_drawing: haulPlanPayload(drawing) } : {}), }; }, "excavation.trench": () => ({ pipes: (inputs.excavation?.pipes ?? []).map((pipe) => ({ width_m: pipeTrenchWidthM(pipe.diameter_mm), })), walls: (inputs.excavation?.walls ?? []).map((wall) => ({ width_m: wallTrenchWidthM(wall.average_thickness_m), area_m2: wallTrenchAreaM2( wall.height_m, wall.average_thickness_m, wall.has_foundation !== false, ), })), }), // 소단 구간(C4) — 구조물 정본 → 구간 · 측점마다 제원(겹치면 먼저 놓은 것). "cross.berm_spans": () => { const spans = bermSpansFromStructures(inputs.berm?.structures ?? []); return { spans, at: (inputs.berm?.chainages ?? []).map((c) => bermSpecAt(spans, c)) }; }, // 측점마다 TS 짝(`computeCrossDesign`) — 못 세우면 그 자리 { error }(파이썬은 ValueError 로 되던짐). "cross.design": () => { const batch = inputs.cross_design; const ranges = batch?.facilities ? facilityRanges(batch.facilities.pipes, batch.facilities.structures) : null; return (batch?.stations ?? []).map((station) => { try { const options = { ...crossOptions(station.options, batch!.standard), ...(ranges && station.chainage_m != null ? facilityAt(ranges, station.chainage_m) : {}), }; return computeCrossDesign(station.samples, station.design_elevation_m, options); } catch (error) { return { error: error instanceof Error ? error.message : String(error) }; } }); }, // 편집 전 정본 — 계획선 샘플은 저장분 그대로(corridor 묶음과 같은 값) · 측점 둘 미만이면 null. "corridor.mesh": () => { const detail = inputs.detail; if (!detail) return null; const routePoints = inputs.route_points ?? []; const samples = detail.longitudinal?.design_profiles?.[0]?.samples; const build = buildCorridor(detail.cross_sections, routePoints, samples); return build ? serialize(build, corridorHash(detail, routePoints)) : null; }, }; function need(id: string): unknown { if (!memo.has(id)) memo.set(id, RUNNERS[id]()); return memo.get(id); } const unknown = run.filter((id) => !(id in RUNNERS)); const results: Record = {}; if (!unknown.length) for (const id of run) results[id] = await need(id); const outside = [...TRIAL_IDS, ...STAGE_IDS]; const available = Object.keys(RUNNERS).filter((id) => !outside.includes(id)); return { results, unknown, available }; } type CalcRequest = { run?: string[]; inputs?: CalcInputs }; /** 한 줄 요청 → 한 줄 답 · 실패는 그 줄 { error }(프로세스는 삶). */ async function answerLine(line: string): Promise { try { const request = JSON.parse(line) as CalcRequest; const t0 = performance.now(); const { results, unknown } = await runDesignCalc(request.run ?? [], request.inputs ?? {}); const ms = Math.round((performance.now() - t0) * 100) / 100; return unknown.length ? { error: `모르는 id: ${unknown.join(", ")}` } : { results, ms }; } catch (error) { return { error: error instanceof Error ? error.message : String(error) }; } } /** 늘 떠 있음(511-A) — 한 줄 받아 한 줄 냄 · 받은 차례대로(단계 id 는 기다림). */ function serve(): void { let buffer = ""; let queue: Promise = Promise.resolve(); process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk: string) => { buffer += chunk; for (let at = buffer.indexOf("\n"); at >= 0; at = buffer.indexOf("\n")) { const line = buffer.slice(0, at); buffer = buffer.slice(at + 1); queue = queue.then(async () => { process.stdout.write(`${JSON.stringify(await answerLine(line))}\n`); }); } }); process.stdin.on("end", () => void queue.then(() => process.exit(0))); } const [inputPath, outputPath] = process.argv.slice(2); if (inputPath === "--serve") serve(); else if (!inputPath || !outputPath) { console.error("사용법: node <묶음> <입력.json> <출력.json> | node <묶음> --serve"); process.exit(2); } else { const request = JSON.parse(readFileSync(inputPath, "utf8")) as CalcRequest; void runDesignCalc(request.run ?? [], request.inputs ?? {}).then( ({ results, unknown, available }) => { if (unknown.length) { console.error(`모르는 id: ${unknown.join(", ")}`); process.exit(4); } writeFileSync(outputPath, JSON.stringify({ results, available })); process.exit(0); // 묶음 타이머를 기다리지 않음 }, (error: unknown) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); }, ); }