Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_4

This commit is contained in:
2026-10-19 08:55:00 +09:00
27 changed files with 1584 additions and 324 deletions
+4 -8
View File
@@ -152,8 +152,6 @@ export const STATE_REGISTRY = {
kindreset: { bucket: "draft", scope: "project" },
/** 배수관·암거 조정창에서 예약한 옵션 값. */
culvertopt: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:culvertopt:${p}:${r}` },
/** 배수관 이동(측점 옮김) 예약. */
culvertmove: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:culvertmove:${p}:${r}` },
/** 암 경계선 오프셋(측점별). */
rockb: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:rockb:${p}:${r}` },
/** 측점별 암 절토 경사비(1:n 의 n) — 카드에서 넣은 사용자 값(2026-09-07).
@@ -166,12 +164,10 @@ export const STATE_REGISTRY = {
berm: { bucket: "draft", scope: "route" },
/** 표준 횡단면 설정 패널의 편집값.
*
* ⚠ **[저장]·[확정] 뒤에도 지우지 않는다**(2026-09-07 확인). 다른 초안과 달리 이 값은
* 브라우저가 가진 **유일한** 사용자 표준단면이다 — `sections/context` 가 주는
* `standard_cross_section` 은 저장분이 아니라 **config 기본값**이고(`B06_Section_Router.py:132`),
* 저장분(`stored_standard_cross_section`)은 서버 안에서만 쓰인다. 그래서 비우면 브라우저
* 횡단 계산이 그 순간 config 기본값으로 되돌아간다. [초기화]에서만 버린다
* (`clearStandardCrossSession`). */
* 칸이 비었으면 `sections/context` 의 저장분(`stored_standard_cross_section`)으로 채운다
* (`B06_Section_Api_Fetch` seedStandardCross) — 같은 응답의 `standard_cross_section` 은
* config 기본값. 고침으로 안 본다 — [폐기]에도 안 지움(`design_leave` NOT_EDITS).
* [초기화] · 추천 노선 [적용]에서 버린다. */
"std-cross": { bucket: "draft", scope: "project", legacy: (p) => `b06:std-cross:${p}` },
/** 표시 반폭 — 사용자가 고른 값이라 초안이되, 바꾸면 횡단 재생성(③)을 함께 부른다. */
"cross-display": {
+1 -1
View File
@@ -19,7 +19,7 @@ B05 · B06 한 페이지 뼈대 — 주소 둘(`#/b05-profile` · `#/b06-section
서버 Node 묶음:
- 입구 없음 · 끌려감 = `design_pick_match` · `design_pipe_ids` · `design_selection`(corridor · server-calc · b07-extent) · `design_store` · `design_structure_{box,ford,ford_pavement,guard,pipe,road,spec}`(b07-extent · b07-note) · 상위 `A00_Common/b_page_state`
- 입구 `logic/design_calc_node.ts`(`build:design-calc` · 계산 등록부 `logic/design_calc_registry.ts` 의 `node:` 줄 · 시험 `test_472_3_calc_registry.py`) · 끌려감 = `design_pick_match` · `design_pipe_ids` · `design_selection`(corridor · server-calc · b07-extent) · `design_store` · `design_structure_{box,ford,ford_pavement,guard,pipe,road,spec}`(b07-extent · b07-note) · 상위 `A00_Common/b_page_state`
- 끌려가는 파일 모듈 맨 위에 DOM · window · sessionStorage 금지 · 고치면 해당 build 다시 지어 시험
남의 페이지가 부르는 파일(고치면 부르는 쪽 시험도):
+1 -6
View File
@@ -45,12 +45,7 @@ import "./design_page.css";
export type { DesignPageShared } from "./design_shared";
/** 보기 ↔ 주소. */
export const DESIGN_VIEW_ROUTES: Record<DesignView, RoutePath> = {
profile: ROUTES.B05_PROFILE,
cross: ROUTES.B06_SECTION,
};
/** 주소 → 보기. */
export function designViewOf(route: RoutePath): DesignView | null {
if (route === ROUTES.B05_PROFILE) return "profile";
if (route === ROUTES.B06_SECTION) return "cross";
+1 -4
View File
@@ -11,7 +11,7 @@
* (다만 그 자리는 듣는 쪽을 못 부른다 — 쓰는 길은 가게로 옮길 것).
*
* `alignmentDraft` 는 등록부 `alignment-draft`(노선 칸)다 — 옛 키(`b05-profile-alignment-draft:{노선}`)
* 값은 처음 읽을 때 옮겨 받는다. B05 되돌리기(`_Profile_History`)도 이 칸을 사진 찍는다(24-2c).
* 값은 안 받는다(387-1). B05 되돌리기(`_Profile_History`)도 이 칸을 사진 찍는다(24-2c).
* 부르는 쪽이 프로젝트를 모르면(`null`) 지금 연 프로젝트 칸을 쓴다.
*
* 계산 묶음 칸(344-4 · 모양 = `design_pipeline_types.ts`) — `routeDraft` 는 초안(세션 즉시) ·
@@ -73,9 +73,6 @@ export interface DesignStore {
): () => void;
}
/** 옛 계획고 초안 키 — 위 머리 주석 참조(남은 옛 값을 쓸어 낼 때만 쓴다). */
export const ALIGNMENT_DRAFT_KEY_PREFIX = "b05-profile-alignment-draft";
/** 이 탭 페이지가 연 프로젝트(388-3) — 다른 탭이 현재 프로젝트를 바꿔도 이 탭 초안이 안 섞임. */
let openedProjectId: string | null = null;
export function pinDraftProject(projectId: string | null): void {
+2 -11
View File
@@ -1,8 +1,7 @@
/* =============================================================================
* A00_Common/design/design_structure_status.ts
* 구조물 현황 한 벌(322-3) — 저장분 둘(관 `pipe_points.json` · 구조물 `structures.json`) + 초안 둘(세션)
* → 한 목록(초안이 있으면 초안 · 없으면 저장분 · 종류마다 따로) · 측점 계획 한 벌(`planStations`) →
* 종단 측점 목록(B05 종단 · 3D 막대 · B06 종단 띠가 같은 꼴).
* 구조물 현황 한 벌(322-3) — 측점 계획 한 벌(`planStations`) → 종단 측점 목록(B05 종단 · 3D 막대 ·
* B06 종단 띠가 같은 꼴) · 관 · 구조물 · 추가 측점 초안 바뀜 신호.
*
* 구조물 id = 구조물은 structure_id · 관은 세션 id(`design_pipe_ids` · 323-5 · 파일 모양 그대로 · 저장 안 함).
* ========================================================================== */
@@ -11,14 +10,6 @@ import type { IrregularStation } from "../../B05_Profile/B05_Profile_UI_Irregula
import type { PlannedStation } from "../../common_util/common_util_structure_station";
import { useDesignStore, type DesignStoreValues } from "./design_store";
/** 현황 — 종류마다 초안이 앞섬(초안 null = 만진 적 없음 · 빈 목록 = 다 지움). */
export function structureStatus<P, S>(
saved: readonly [ReadonlyArray<P>, ReadonlyArray<S>],
drafts: readonly [ReadonlyArray<P> | null, ReadonlyArray<S> | null],
): { pipes: P[]; structures: S[] } {
return { pipes: [...(drafts[0] ?? saved[0])], structures: [...(drafts[1] ?? saved[1])] };
}
/**
* 측점 계획 → 종단 측점(측점번호 · 잔여거리 = 간격 기준) — 기준 측점 id = 첫 주인 id(관 = `pipe-…` ·
* 구조물 = structure_id) · 시작 · 끝 = `{id}:{역할}`(옮겨도 계획고 편집이 따라감 · 272) · 관 기준 측점 =
+156
View File
@@ -0,0 +1,156 @@
/* =============================================================================
* A00_Common/design/logic/design_calc_node.ts
* 계산 등록부(`design_calc_registry.ts`)의 서버 Node 입구 하나(PLAN 472-3) — id → 함수 표.
* 등록부에서 실행기가 `node:<id>` 인 줄만 여기 있음(나머지는 472-4 동안 파이썬).
* 브라우저가 쓰는 같은 TS 를 그대로 부름 — 계산을 두 벌로 짜지 않음.
*
* 실행: node <묶음> <입력.json> <출력.json> (package.json `build:design-calc`)
* 입력 { 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 }] } } }
* 출력 { results: { [id]: 값 }, available: [id…] }
* — 차례는 요청 차례가 아니라 기댐 차례(누가토량 앞에 구조물 면적을 상세에 얹음 ·
* server-calc 묶음과 같은 차례).
* 끝 코드: 0 성공 / 2 인자 오류 / 4 모르는 id
* ========================================================================== */
import { readFileSync, writeFileSync } from "node:fs";
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 {
applyStructureAreaRows,
extraWallRows,
structureAreaRows,
} from "../../../B06_Section/B06_Section_Structure_Layouts";
type HaulOptions = NonNullable<Parameters<typeof computeHaulPlan>[2]>;
interface CalcInputs {
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<typeof computeMassHaul>[1];
natural_spoil_min_ground_slope?: number | null;
haul_equipment_limits?: Parameters<typeof computeHaulPlan>[1];
};
excavation?: {
pipes?: Array<{ diameter_mm?: number | null }>;
walls?: Array<{
height_m?: number | null;
average_thickness_m?: number | null;
has_foundation?: boolean;
}>;
};
}
/** 한 번 돌림 — 같은 id 는 한 번만 셈(memo) · 앞 id 는 `need` 로. */
function runDesignCalc(run: readonly string[], inputs: CalcInputs) {
const memo = new Map<string, unknown>();
const sections = inputs.detail?.cross_sections ?? [];
const context = inputs.context ?? {};
let massHaul: MassHaulResult | null = null;
const RUNNERS: Record<string, () => unknown> = {
// 구조물 측점 면적 · 관 길이를 상세에 얹음(자리에서) — 뒤 누가토량이 고친 면적 위에서 쌓임.
"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,
),
})),
}),
// 편집 전 정본 — 계획선 샘플은 저장분 그대로(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<string, unknown> = {};
if (!unknown.length) for (const id of run) results[id] = need(id);
return { results, unknown, available: Object.keys(RUNNERS) };
}
const [inputPath, outputPath] = process.argv.slice(2);
if (!inputPath || !outputPath) {
console.error("사용법: node <묶음> <입력.json> <출력.json>");
process.exit(2);
}
const request = JSON.parse(readFileSync(inputPath, "utf8")) as {
run?: string[];
inputs?: CalcInputs;
};
const { results, unknown, available } = runDesignCalc(request.run ?? [], request.inputs ?? {});
if (unknown.length) {
console.error(`모르는 id: ${unknown.join(", ")}`);
process.exit(4);
}
writeFileSync(outputPath, JSON.stringify({ results, available }));
@@ -0,0 +1,687 @@
/* =============================================================================
* A00_Common/design/logic/design_calc_registry.ts
* B05 · B06 계산 등록부(PLAN 472-3 · 요구 273) — 계산 한 갈래 = 줄 하나(목록 문서
* `docs/raw/조사/2026-10-19_472_구조다시세움_목록.md` 1.1 표 # 하나).
*
* 순수 자료 — 함수 import 없음(타입만). 실행기는 글 이름이라 브라우저에 실려도 서버 쪽 코드가
* 안 딸려감. 서버 Node 입구 = `design_calc_node.ts`(id → 함수 표 · `build:design-calc`).
*
* 칸:
* id — 갈래 이름(`<묶음>.<값>`)
* no — 목록 1.1 표 #
* inputs — 바뀌면 다시 셈 하는 값: `input.<묶음 입력>`(가게 칸) · 앞 id · `file:<정본>` ·
* `session:<열쇠>` · `context.<서버 문맥>`
* outputs — 낳는 값 · 가게 칸(`store.<칸>`) · 서버 정본 자리(`file:` · `db:`)
* site — browser_now(브라우저 즉시 + 서버 정본 같은 셈) · server_canon(서버 정본만) ·
* server_only(무거운 GIS · 래스터 · 추천 엔진) · browser_only(입력 도구 · 표시)
* stage — 브라우저 묶음 단계(`design_pipeline.ts`) · 없으면 null
* ts — TS 셈 자리 `<경로>#<함수>` · 없으면 null
* runner — 실행기: `node:<id>`(서버 Node 입구) · `py:<경로>#<함수>`(지금 파이썬) ·
* `browser:<경로>#<함수>`(browser_only 만)
* until — "472-4" = 파이썬 실행기는 472-4 짝 모으기 동안만(그 뒤 node:)
* pair — 파이썬 짝 `<경로>#<함수>` · 짝 아니면 null
* mirror — 거울 · 단위 시험(`resources/tester` 파일 이름 · .py 뺌)
*
* 묶음 단계의 기댐(`STAGE_DEPS`) = 같은 단계 줄들의 inputs 중 `input.*` · 다른 단계 id 의 단계
* (같은지 시험 `test_472_3_calc_registry.py`).
* ========================================================================== */
import type { StageName } from "./design_pipeline_types";
export type CalcSite = "browser_now" | "server_canon" | "server_only" | "browser_only";
export interface CalcEntry {
id: string;
no: string;
label: string;
inputs: string[];
outputs: string[];
site: CalcSite;
stage: StageName | null;
ts: string | null;
runner: string;
until?: "472-4";
pair: string | null;
mirror: string[];
note?: string;
}
const B05 = "B05_Profile/";
const B05L = "B05_Profile/logic/";
const B06 = "B06_Section/";
const B06L = "B06_Section/logic/";
const U = "common_util/";
export const CALC_REGISTRY: readonly CalcEntry[] = [
/* ── 노선 ─────────────────────────────────────────────────────────────── */
{
id: "route.polyline",
no: "R1",
label: "평면 폴리라인 · 곡선(BC · EC · MC)",
inputs: ["input.route"],
outputs: ["노선 선", "곡선표", "file:B05_Profile/route/*"],
site: "browser_now",
stage: "route",
ts: `${B05L}B05_Profile_UI_RouteEdit_Curve.ts#buildEditedPolyline`,
runner: `py:${U}common_util_route_polyline.py#build_planned_polyline`,
until: "472-4",
pair: `${U}common_util_route_polyline.py#build_planned_polyline`,
mirror: ["test_route_polyline_browser_mirror"],
note: "파이썬 짝은 추천 엔진 · B04 배수 · 노선 확인이 부름 — 짝 남음",
},
{
id: "route.handle",
no: "R2",
label: "손잡이 → IP · R",
inputs: ["input.route"],
outputs: ["편집 입력"],
site: "browser_only",
stage: null,
ts: `${B05L}B05_Profile_UI_RouteEdit_Curve.ts#dragHandleTo`,
runner: `browser:${B05L}B05_Profile_UI_RouteEdit_Curve.ts#dragHandleTo`,
pair: null,
mirror: ["test_route_polyline_handle_drag"],
},
{
id: "route.node_edits",
no: "R3",
label: "노드 편집 규칙(곡선 한도 · 고정점)",
inputs: ["input.route"],
outputs: ["고친 노드"],
site: "browser_now",
stage: "route",
ts: `${B05L}B05_Profile_UI_RouteEdit_Edits.ts#applyCurveLimits`,
runner: `py:${B05}B05_Profile_Route_NodeEdits.py#node_edits`,
until: "472-4",
pair: `${B05}B05_Profile_Route_NodeEdits.py#node_edits`,
mirror: ["test_430_route_edit_rules"],
},
{
id: "route.station_fit",
no: "R4",
label: "스냅 맞춤(MC → 일반측점)",
inputs: ["input.route"],
outputs: ["IP 자리"],
site: "browser_only",
stage: null,
ts: `${B05L}B05_Profile_RouteEdit_StationFit.ts#fitCurveStations`,
runner: `browser:${B05L}B05_Profile_RouteEdit_StationFit.ts#fitCurveStations`,
pair: null,
mirror: ["test_366_3_station_fit"],
note: "짝 아님(머리 명시) — 서버는 저장 IP 그대로",
},
/* ── 측점 ─────────────────────────────────────────────────────────────── */
{
id: "stations.frame",
no: "R5",
label: "측점 틀(법선 · 방위 · 스냅 측점)",
inputs: ["route.polyline"],
outputs: ["측점 좌표 · 방위"],
site: "browser_now",
stage: "stations",
ts: `${B05L}B05_Profile_UI_RouteEdit_Frame.ts#stationFrameAt`,
runner: `py:${B05}B05_Profile_Engine_Sections_Core.py#generate_sections`,
until: "472-4",
pair: `${B05}B05_Profile_Engine_Sections_Core.py#generate_sections`,
mirror: ["test_b05_routeedit_frame_mirror", "test_224_2_station_snap"],
},
{
id: "stations.list",
no: "R6",
label: "측점 목록(일반 · 관 · 구조물 · 곡선 · 시종점)",
inputs: [
"route.polyline",
"input.pipes",
"input.structures",
"input.plusStations",
"stations.frame",
"stations.key",
"facility.stations",
],
outputs: ["측점 · 열쇠", "store.stations", "db:cross_sections"],
site: "browser_now",
stage: "stations",
ts: `${B05L}B05_Profile_Sections_Stations.ts#buildStations`,
runner: `py:${B05}B05_Profile_Engine_Sections_Core.py#generate_sections`,
until: "472-4",
pair: `${B05}B05_Profile_Engine_Sections_Core.py#generate_sections`,
mirror: ["test_344_3_pipeline_stations_mirror", "test_407_curve_station"],
note: "파이썬 짝은 B03 체인 · B07 IP 표 · 추천도 부름",
},
{
id: "stations.key",
no: "R7",
label: "측점 열쇠(grid:k · pipe: · structure:)",
inputs: ["stations.frame"],
outputs: ["측점 열쇠"],
site: "browser_now",
stage: "stations",
ts: `${U}common_util_station_key.ts#stationKey`,
runner: `py:${U}common_util_station_key.py#station_key`,
until: "472-4",
pair: `${U}common_util_station_key.py#station_key`,
mirror: ["test_344_3_station_key_mirror"],
},
{
id: "stations.swap",
no: "R8",
label: "측점 갈아끼움(새 측점에 횡단 옮김)",
inputs: ["stations.list", "store.stations"],
outputs: ["초안 상세"],
site: "browser_only",
stage: null,
ts: `${B06L}B06_Section_Station_Swap.ts#swapCrossSections`,
runner: `browser:${B06L}B06_Section_Station_Swap.ts#swapCrossSections`,
pair: null,
mirror: ["test_346_2_station_swap", "test_272_swap_keeps_canon"],
note: "가게 stations 구독 · 서버는 처음부터 셈 — R6 이 서버와 한 벌이 되면 대부분 걷힘",
},
/* ── 지반 ─────────────────────────────────────────────────────────────── */
{
id: "ground.sample",
no: "G1",
label: "지반 표본",
inputs: ["stations.list", "file:dtm_grid"],
outputs: ["측점 표본"],
site: "browser_now",
stage: "ground",
ts: `${U}common_util_surface_sampler.ts#heightAt`,
runner: `py:${U}common_util_surface_sampler.py#build_surface_sampler`,
until: "472-4",
pair: `${U}common_util_surface_sampler.py#build_surface_sampler`,
mirror: ["test_344_1_surface_sampler_mirror"],
note: "X2 — 브라우저 격자 .bin · 서버 npz(값 협의)",
},
{
id: "ground.extend",
no: "G2",
label: "미교차 넓힘 표본",
inputs: ["ground.sample", "cross.design"],
outputs: ["넓힌 표본"],
site: "browser_now",
stage: "cross",
ts: `${B05L}B05_Profile_Sections_Ground.ts#extendUnclosed`,
runner: `py:${B06}B06_Section_Engine_SampleExtend.py#extend_unclosed`,
until: "472-4",
pair: `${B06}B06_Section_Engine_SampleExtend.py#extend_unclosed`,
mirror: ["test_344_1_sections_ground_mirror"],
},
{
id: "ground.widen",
no: "G3",
label: "넓힘(표본 폭 넘음)",
inputs: ["stations.list", "file:dtm_grid"],
outputs: ["표본 폭", "db:cross_sections"],
site: "server_only",
stage: null,
ts: null,
runner: `py:${B06}B06_Section_Router_Widen.py#widen_route_sections`,
pair: null,
mirror: [],
note: "저장 밖에서 DB 씀(N10) — 저장 시점 472-6 협의",
},
{
id: "ground.uphill",
no: "G4",
label: "상단측(단면 유형 기본 재료)",
inputs: ["ground.sample"],
outputs: ["상단측"],
site: "browser_now",
stage: "cross",
ts: `${B05L}B05_Profile_Pipeline_Detail.ts#uphillSide`,
runner: `py:${B05}B05_Profile_Engine_Sections_Core.py#generate_sections`,
until: "472-4",
pair: `${B05}B05_Profile_Engine_Sections_Core.py#generate_sections`,
mirror: ["test_346_2_pipeline_cross_page"],
},
/* ── 종단 ─────────────────────────────────────────────────────────────── */
{
id: "profile.base",
no: "P1",
label: "첫 계획선(지반 따라감 base PVI)",
inputs: ["ground.sample"],
outputs: ["base PVI"],
site: "browser_now",
stage: "planline",
ts: `${B05L}B05_Profile_Grade_Ground.ts#groundFollowingBasePvi`,
runner: `py:${B05}B05_Profile_Engine_Grade_Profile.py#rebuild_alignment_profile`,
until: "472-4",
pair: `${B05}B05_Profile_Engine_Grade_Profile.py#rebuild_alignment_profile`,
mirror: ["test_344_2_route_pairs", "test_432_1_curve_law"],
note: "파이썬 짝은 추천 엔진(RouteInitial_Profile)도 씀 — 짝 남음",
},
{
id: "profile.alignment",
no: "P2",
label: "계획선 짓기(편집 → 꼭짓점 · 종단곡선)",
inputs: ["profile.base", "input.alignment"],
outputs: ["측점 계획고", "store.profile", "file:profile_alignment"],
site: "browser_now",
stage: "profile",
ts: `${B05L}B05_Profile_UI_Profile_Alignment.ts#buildAlignment`,
runner: `py:${B05}B05_Profile_Engine_Grade_Alignment.py#evaluate`,
until: "472-4",
pair: `${B05}B05_Profile_Engine_Grade_Alignment.py#evaluate`,
mirror: [
"test_426_profile_straight",
"test_432_1_curve_law",
"test_213_1_hidden_station_pvi",
"test_438_1_initial_straighten",
],
},
{
id: "profile.curve_law",
no: "P3",
label: "종단곡선 법칙(L · R = L/Δ · 큰 R 직선)",
inputs: ["profile.alignment"],
outputs: ["L · R"],
site: "browser_now",
stage: "profile",
ts: `${B05L}B05_Profile_Profile_CurveLaw.ts#flatBends`,
runner: `py:${B05}B05_Profile_Engine_Grade_CurveLaw.py#flat_bends`,
until: "472-4",
pair: `${B05}B05_Profile_Engine_Grade_CurveLaw.py#flat_bends`,
mirror: ["test_432_1_curve_law"],
},
{
id: "profile.straight",
no: "P4",
label: "직선 판정",
inputs: ["profile.alignment"],
outputs: ["직선 꼭짓점"],
site: "browser_now",
stage: "profile",
ts: `${B05L}B05_Profile_Profile_Straight.ts#isStraightBend`,
runner: `py:${B05}B05_Profile_Engine_Grade_Alignment.py#is_straight_bend`,
until: "472-4",
pair: `${B05}B05_Profile_Engine_Grade_Alignment.py#is_straight_bend`,
mirror: ["test_426_profile_straight", "test_271_straighten_pick"],
},
{
id: "profile.carry",
no: "P5",
label: "노선 바뀜 계획고 옮김",
inputs: ["route.polyline"],
outputs: ["옮긴 편집(input.alignment 고침)"],
site: "browser_now",
stage: "follow",
ts: `${B05L}B05_Profile_Grade_Carry.ts#carryEdits`,
runner: `py:${B05}B05_Profile_Engine_Grade_Carry.py#carry_edits`,
until: "472-4",
pair: `${B05}B05_Profile_Engine_Grade_Carry.py#carry_edits`,
mirror: ["test_400_profile_carry", "test_349_0v_route_save_span"],
},
{
id: "profile.seat",
no: "P6",
label: "옮긴 시설 꼭짓점 세움",
inputs: ["route.polyline"],
outputs: ["꼭짓점(input.alignment 고침)"],
site: "browser_now",
stage: "follow",
ts: `${B05L}B05_Profile_Grade_Seat.ts#seatBornNodes`,
runner: `py:${B05}B05_Profile_Router_Confirm.py#_seat_born_nodes`,
until: "472-4",
pair: `${B05}B05_Profile_Router_Confirm.py#_seat_born_nodes`,
mirror: ["test_427_1_follow_seat", "test_459_route_seat_mirror"],
note: "Route_Save `_carry_profile` 도 같은 일",
},
{
id: "profile.tools",
no: "P7",
label: "직선화 · 자유지정 · 쉬프트 · 시종점",
inputs: ["profile.alignment"],
outputs: ["편집 입력"],
site: "browser_only",
stage: null,
ts: `${B05L}B05_Profile_UI_Profile_Straighten.ts#straightenBetween`,
runner: `browser:${B05L}B05_Profile_UI_Profile_Straighten.ts#straightenBetween`,
pair: null,
mirror: [],
note: "편집값만 저장 → profile.alignment · 자유지정 design_profile_free · 쉬프트 Pivot · 시종점 EndOffsets",
},
{
id: "profile.warnings",
no: "P8",
label: "종단 경고(최대 기울기 · 법정 L · 최소 R · 최소 피복)",
inputs: ["profile.alignment", "context.page_settings"],
outputs: ["경고 칩", "file:profile_alignment.warnings"],
site: "browser_now",
stage: null,
ts: `${B05L}B05_Profile_Profile_CurveLaw.ts#verticalCurveShortfalls`,
runner: `py:${B05}B05_Profile_Engine_Grade.py#resolve_grade_options`,
until: "472-4",
pair: `${B05}B05_Profile_Engine_Grade.py#resolve_grade_options`,
mirror: ["test_442_2_grade_limit_mirror"],
note: "X5 — 기준표 두 벌(화면 PROFILE_CRITERIA · 서버 FOREST_ROAD_PROFILE_CRITERIA)",
},
/* ── 횡단 ─────────────────────────────────────────────────────────────── */
{
id: "cross.design",
no: "C1",
label: "횡단 설계선(노면 끝 · 미교차)",
inputs: ["profile.alignment", "ground.sample", "input.cross"],
outputs: ["설계선", "db:cross_sections.design"],
site: "browser_now",
stage: "cross",
ts: `${U}common_util_cross_design.ts#computeCrossDesign`,
runner: `py:${B06}B06_Section_Engine_Design.py#compute_cross_design`,
until: "472-4",
pair: `${B06}B06_Section_Engine_Design.py#compute_cross_design`,
mirror: ["test_b06_cross_design_mirror", "test_178_2_berm_fill_mirror"],
note: "파이썬 짝은 B07 · 추천 · Route_Save · B05 확정도 부름 — 짝 남음",
},
{
id: "cross.areas",
no: "C2",
label: "단면적(절 · 성 · 측구 · 층따기)",
inputs: ["cross.design"],
outputs: ["측점 면적", "db:cross_sections.design"],
site: "browser_now",
stage: "cross",
ts: `${U}common_util_cross_design_areas.ts#splitCutAreas`,
runner: `py:${B06}B06_Section_Engine_Areas.py#_split_cut_areas`,
until: "472-4",
pair: `${B06}B06_Section_Engine_Areas.py#_split_cut_areas`,
mirror: ["test_b06_cross_design_mirror"],
note: "X3 — 표준 측점 = 파이썬 · 구조물 측점 = Node(structure.layouts)",
},
{
id: "cross.enforce",
no: "C3",
label: "확정 보정(포장 구간 · 세월교 하강 · 기울임)",
inputs: ["cross.design", "input.structures"],
outputs: ["보정 설계"],
site: "browser_now",
stage: "cross",
ts: `${B06L}B06_Section_Cross_Enforce.ts#facilityRanges`,
runner: `py:${B06}B06_Section_Router_Design.py#enforce_design_lines`,
until: "472-4",
pair: `${B06}B06_Section_Router_Design.py#enforce_design_lines`,
mirror: ["test_349_0n_cross_enforce_mirror"],
},
{
id: "cross.berm_spans",
no: "C4",
label: "소단 구간(구조물 → 구간)",
inputs: ["input.structures"],
outputs: ["소단 구간"],
site: "browser_now",
stage: null,
ts: `${B06}B06_Section_UI_Page_Persist.ts#bermSpansFromStructures`,
runner: `py:${B06}B06_Section_Engine_Structures_Wall.py#load_berm_spans`,
until: "472-4",
pair: `${B06}B06_Section_Engine_Structures_Wall.py#load_berm_spans`,
mirror: ["test_272_berm_source"],
note: "TS 가 화면 파일 안 — common_util 로(472-4 ②)",
},
{
id: "cross.berm_slope",
no: "C5",
label: "소단 사면 점",
inputs: ["cross.design"],
outputs: ["사면 꺾임"],
site: "browser_now",
stage: "cross",
ts: `${U}common_util_cross_berm.ts#cutProfilePoints`,
runner: `py:${U}common_util_cross_berm.py#cut_profile_points`,
until: "472-4",
pair: `${U}common_util_cross_berm.py#cut_profile_points`,
mirror: ["test_178_2_berm_fill_mirror"],
},
{
id: "cross.underdrain",
no: "C6",
label: "맹암거 얹기 · 기울임",
inputs: ["cross.design", "input.structures"],
outputs: ["underdrain 칸"],
site: "browser_now",
stage: "cross",
ts: `${U}common_util_structure_underdrain.ts#attachUnderdrainSpecs`,
runner: `py:${B06}B06_Section_Engine_Tilt.py#attach_underdrains`,
until: "472-4",
pair: `${B06}B06_Section_Engine_Tilt.py#attach_underdrains`,
mirror: [],
note: "거울 시험 없음 — 옮기기 전 먼저",
},
{
id: "cross.fit",
no: "C7",
label: "반폭 맞춤 · 교차점 손잡이 · 성토 5 m 경고",
inputs: ["cross.design"],
outputs: ["표시 반폭 · 경고"],
site: "browser_only",
stage: null,
ts: `${B06L}B06_Section_UI_Cross_Fit.ts#toeOffsets`,
runner: `browser:${B06L}B06_Section_UI_Cross_Fit.ts#toeOffsets`,
pair: null,
mirror: [],
},
/* ── 시설 ─────────────────────────────────────────────────────────────── */
{
id: "facility.follow",
no: "F1",
label: "시설 따라감(노선 바뀜 → 관 · 구조물 자리)",
inputs: ["route.polyline"],
outputs: ["새 좌표 · 누가거리(input.pipes · input.structures 고침)"],
site: "browser_now",
stage: "follow",
ts: `${U}common_util_route_follow.ts#followStructures`,
runner: `py:${U}common_util_route_follow.py#follow_structures`,
until: "472-4",
pair: `${U}common_util_route_follow.py#follow_structures`,
mirror: ["test_410_follow_xy", "test_344_2_route_pairs", "test_459_route_seat_mirror"],
},
{
id: "facility.plus_follow",
no: "F2",
label: "추가 측점 따라감",
inputs: ["route.polyline"],
outputs: ["추가 측점(input.plusStations 고침)"],
site: "browser_now",
stage: "follow",
ts: `${B05}B05_Profile_Api_PlusStations.ts#followPlusDraft`,
runner: `py:${B05}B05_Profile_Plus_Stations.py#follow_plus_stations`,
until: "472-4",
pair: `${B05}B05_Profile_Plus_Stations.py#follow_plus_stations`,
mirror: ["test_459_route_seat_mirror"],
note: "TS 가 Api 파일 안 — logic 으로(472-4 ①)",
},
{
id: "facility.stations",
no: "F3",
label: "시설 측점 계획",
inputs: ["route.polyline", "input.pipes", "input.structures"],
outputs: ["시설 측점"],
site: "browser_now",
stage: "stations",
ts: `${U}common_util_structure_station.ts#planStations`,
runner: `py:${U}common_util_structure_station.py#plan_stations`,
until: "472-4",
pair: `${U}common_util_structure_station.py#plan_stations`,
mirror: ["test_322_3_plan_mirror", "test_234_1_structure_station"],
note: "파이썬 짝은 B07 · 배수가 넓게 부름",
},
{
id: "facility.attach",
no: "F4",
label: "형상 얹기(관 세트 · 벽 · 포장 · 맹암거 그림 자료)",
inputs: ["input.pipes", "input.structures", "cross.design"],
outputs: ["그림 자료"],
site: "browser_now",
stage: "cross",
ts: `${U}common_util_structure_attach.ts#attachShapes`,
runner: `py:${B06}B06_Section_Engine_Attach.py#attach_shapes`,
until: "472-4",
pair: `${B06}B06_Section_Engine_Attach.py#attach_shapes`,
mirror: ["test_323_4_attach_mirror"],
},
{
id: "facility.culvert_sets",
no: "F5",
label: "관 세트(관경 · 날개벽 · 집수정)",
inputs: ["input.pipes"],
outputs: ["관 세트"],
site: "browser_now",
stage: null,
ts: `${U}common_util_culvert_sets.ts#pipeWallHeightM`,
runner: `py:${B06}B06_Section_Engine_Culvert.py#pipe_wall_height_m`,
pair: `${B06}B06_Section_Engine_Culvert.py#pipe_wall_height_m`,
mirror: ["test_b06_culvert_sets_mirror", "test_b06_pipe_wall_height_mirror"],
note: "짝 유지 — 파이썬 소비자 B04 · B07 · B08(472 밖)",
},
{
id: "facility.walls",
no: "F6",
label: "벽 · 포장",
inputs: ["input.structures"],
outputs: ["벽 · 포장"],
site: "browser_now",
stage: null,
ts: `${U}common_util_structure_walls.ts#wallSpecsFrom`,
runner: `py:${B06}B06_Section_Engine_Structures_Wall.py#wall_specs_from`,
pair: `${B06}B06_Section_Engine_Structures_Wall.py#wall_specs_from`,
mirror: ["test_b06_structure_walls_mirror", "test_b06_structure_pavement_mirror"],
note: "짝 유지 — 포장 짝 common_util_structure_pavement(.py · .ts) · B07 소비",
},
{
id: "structure.layouts",
no: "F7",
label: "관 길이 · 구조물 폐회로 면적 · 다단 벽",
inputs: ["cross.design", "cross.areas", "facility.attach"],
outputs: ["관 길이 · 구조물 면적 · 선 다단 벽", "db:cross_sections.design"],
site: "browser_now",
stage: null,
ts: `${B06}B06_Section_Structure_Layouts.ts#structureAreaRows`,
runner: "node:structure.layouts",
pair: null,
mirror: ["test_b06_pipe_length", "test_299_ford_span_pipe"],
},
{
id: "excavation.trench",
no: "F8",
label: "터파기(관 폭 · 벽 단면)",
inputs: ["facility.culvert_sets", "facility.walls"],
outputs: ["터파기 폭 · 단면적"],
site: "browser_now",
stage: null,
ts: `${U}common_util_excavation.ts#wallTrenchAreaM2`,
runner: "node:excavation.trench",
pair: `${U}common_util_excavation.py#wall_trench_area_m2`,
mirror: ["test_excavation_mirror"],
note: "파이썬 짝 운영 부름 0 — 걷음 후보(472-7)",
},
{
id: "facility.ford",
no: "F9",
label: "세월교 높이 · 깊이",
inputs: ["input.structures"],
outputs: ["높이"],
site: "browser_now",
stage: null,
ts: "A00_Common/design/design_structure_ford_calc.ts#fordHeightM",
runner: "py:B04_PreProcess/B04_PreProcess_Router_Basins.py#ford_height_m",
pair: "B04_PreProcess/B04_PreProcess_Router_Basins.py#ford_height_m",
mirror: ["test_65_ford_defaults", "test_73_11_b08_hydro_box_ford"],
note: "짝 유지(B04 · B08)",
},
/* ── 토량 ─────────────────────────────────────────────────────────────── */
{
id: "masshaul.cumulative",
no: "E1",
label: "누가토량",
inputs: ["cross.areas", "structure.layouts", "context.earthwork_conversion"],
outputs: ["누가토량 · 배지", "file:longitudinal.mass_haul"],
site: "browser_now",
stage: null,
ts: `${U}common_util_mass_haul.ts#computeMassHaul`,
runner: "node:masshaul.cumulative",
pair: null,
mirror: [],
note: "브라우저는 그릴 때 셈(B05_Profile_UI_Profile_MassHaul) · 같은 코드",
},
{
id: "haul.plan",
no: "E2",
label: "유토 배분 · 운반거리",
inputs: ["masshaul.cumulative", "context.haul_equipment_limits"],
outputs: ["배분", "file:longitudinal.mass_haul.haul_plan"],
site: "server_canon",
stage: null,
ts: `${U}common_util_mass_haul_balance.ts#computeHaulPlan`,
runner: "node:haul.plan",
pair: null,
mirror: [],
note: "브라우저 번들 밖(/haul-plan 요청) · 브라우저 실행 여부 협의",
},
/* ── 배수 · 3D · 추천 ────────────────────────────────────────────────── */
{
id: "drainage.basins",
no: "D1",
label: "배수유역 세부 나눔",
inputs: ["route.polyline", "input.pipes"],
outputs: ["유역 · 관경 제안"],
site: "server_only",
stage: "drainage",
ts: null,
runner: `py:${U}common_util_drainage_detail.py#build_detail`,
pair: null,
mirror: [],
note: "TS 는 일정만(B05_Profile/logic/B05_Profile_Drainage_Live.ts)",
},
{
id: "drainage.suggest",
no: "D2",
label: "관 자리 제안",
inputs: ["route.polyline", "drainage.basins"],
outputs: ["제안"],
site: "server_only",
stage: null,
ts: null,
runner: `py:${U}common_util_drainage_detail.py#suggest_pipe_positions`,
pair: null,
mirror: [],
note: "TS 는 모으기만(B05_Profile/logic/B05_Profile_Drainage_Suggest.ts)",
},
{
id: "corridor.mesh",
no: "D3",
label: "3D 길 모형",
inputs: ["profile.alignment", "cross.design"],
outputs: ["3D", "file:corridor"],
site: "browser_now",
stage: "corridor",
ts: `${B05}B05_Profile_UI_Corridor_Build.ts#buildCorridor`,
runner: "node:corridor.mesh",
pair: null,
mirror: [],
},
{
id: "footprint.plan",
no: "D4",
label: "평면 발자국(절성토선)",
inputs: ["cross.design", "ground.sample"],
outputs: ["발자국", "store.footprint"],
site: "browser_only",
stage: "footprint",
ts: `${B05L}B05_Profile_PlanFootprint.ts#footprintRecord`,
runner: `browser:${B05L}B05_Profile_Footprint_Stage.ts#footprintStage`,
pair: "B07_DesignDetail/B07_DesignDetail_Engine_Cad_PlanSlope.py#route_frame",
mirror: ["test_346_2_plan_footprint_mirror"],
note: "서버 정본 없음(즉시 후보) · X4 B07 파이썬이 따로 셈(472 밖)",
},
{
id: "route.recommend",
no: "N1",
label: "노선 추천",
inputs: ["file:dtm_grid", "file:streams"],
outputs: ["추천 노선"],
site: "server_only",
stage: null,
ts: null,
runner: `py:${B05}B05_Profile_Engine_RouteInitial.py#generate_initial_route`,
pair: null,
mirror: ["test_419_1_recommend_engine"],
note: "[적용] 정본은 route.polyline · stations.list 길 · TS 는 보기만(B05_Profile_UI_RouteEdit_Initial_Model.ts)",
},
];