Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_4
This commit is contained in:
@@ -73,6 +73,8 @@ config/spreadsheet_node/
|
||||
config/b07_extent_node/
|
||||
# 횡단도 표 위 CAD 칸 옵션 줄 번들 — `npm run build:b07-note` 산출물(166-1).
|
||||
config/b07_note_node/
|
||||
# 계산 등록부 서버 Node 입구 번들 — `npm run build:design-calc` 산출물(472-3).
|
||||
config/design_calc_node/
|
||||
|
||||
# graphify 위키 산출물 — 창끼리 같은 위키를 찾게 **통째로** git 으로 나름
|
||||
# (2026-09-09 사용자 확정 「출력물 전체를 공유해도 됨」).
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 다시 지어 시험
|
||||
|
||||
남의 페이지가 부르는 파일(고치면 부르는 쪽 시험도):
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) · 관 기준 측점 =
|
||||
|
||||
@@ -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)",
|
||||
},
|
||||
];
|
||||
@@ -44,11 +44,8 @@ import {
|
||||
} from "@util/common_util_mass_haul";
|
||||
import { badgeValuesFrom, type MassHaulBadgeValues } from "@util/common_util_mass_haul_badge";
|
||||
import { resetBalloonOffsets } from "@util/common_util_mass_haul_balance_view";
|
||||
import {
|
||||
createMassHaulChart,
|
||||
createMassHaulLegend,
|
||||
MASS_HAUL_MIN_HEIGHT,
|
||||
} from "@util/common_util_mass_haul_view";
|
||||
import { createMassHaulChart, MASS_HAUL_MIN_HEIGHT } from "@util/common_util_mass_haul_view";
|
||||
import { createMassHaulLegend } from "@util/common_util_mass_haul_legend";
|
||||
import { Y_WINDOW_BAKED_ATTR } from "../B06_Section/B06_Section_UI_Longitudinal";
|
||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
import { createPanelResizer } from "@ui/ui_template_resizer";
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
*
|
||||
* ① **손잡이 → 교각점·반지름**(`dragHandleTo`) — 서버 셈의 **반대 방향**이다. 서버는
|
||||
* 교각점·R 에서 접선점을 내고, 여기서는 끈 접선점에서 교각점·R 을 낸다.
|
||||
* 왕복이 제자리인지는 `tmp/tests/test_route_polyline_handle_drag.py` 가 지킨다.
|
||||
* 왕복이 제자리인지는 `resources/tester/test_route_polyline_handle_drag.py` 가 지킨다.
|
||||
*
|
||||
* ② **노드 → 폴리라인**(`buildEditedPolyline`) — ⚠⚠ **짝**:
|
||||
* `common_util/common_util_route_polyline.py` 의 `build_planned_polyline` 중
|
||||
* **편집 갈래**(`simplify=False` + `curve_flags`/`radii`)와 같은 값을 내야 한다.
|
||||
* 거울 시험: `tmp/tests/test_route_polyline_browser_mirror.py`.
|
||||
* 거울 시험: `resources/tester/test_route_polyline_browser_mirror.py`.
|
||||
*
|
||||
* 왜 브라우저에도 두나(2026-09-07 사용자 지적 ①②) — 곡선을 서버만 그리면 노드를 잡는
|
||||
* 순간 그려 둔 선을 통째로 버려야 해서 **곡선이 전부 사라진 것처럼 보인다**. 값은 남아
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"""계산 등록부 파이썬 실행기(PLAN 472-3 3-B) — 등록부 id 를 받아 서버 Node 입구 하나를 부름.
|
||||
|
||||
등록부 = `A00_Common/design/logic/design_calc_registry.ts` · 입구 = `design_calc_node.ts`
|
||||
(`build:design-calc`). 부름은 `common_util_node_bundle.run_bundle_json` 한 길 · 계산 없음.
|
||||
`NODE_IDS` = 등록부에서 실행기가 `node:` 인 줄(같은지 시험 `test_472_3_calc_runner.py`).
|
||||
입력 꼴 = 입구 머리(`detail` · `route_points` · `context` · `excavation`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BUNDLE = ROOT / "config" / "design_calc_node" / "design_calc_node.js"
|
||||
NPM_SCRIPT = "build:design-calc"
|
||||
NODE_IDS = frozenset(
|
||||
{
|
||||
"structure.layouts",
|
||||
"masshaul.cumulative",
|
||||
"haul.plan",
|
||||
"excavation.trench",
|
||||
"corridor.mesh",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def run_calc(ids: Sequence[str], inputs: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""id 들을 한 번에 돌려 `{id: 값}` · 묶음 실패는 None · Node 실행기 아닌 id 는 ValueError."""
|
||||
unknown = [item for item in ids if item not in NODE_IDS]
|
||||
if unknown:
|
||||
raise ValueError(f"Node 실행기 아닌 id: {', '.join(unknown)}")
|
||||
output = run_bundle_json(BUNDLE, NPM_SCRIPT, {"run": list(ids), "inputs": inputs})
|
||||
return output.get("results") if isinstance(output, dict) else None
|
||||
@@ -51,15 +51,6 @@ export function sideBerm(berm: BermSpec | null, side: string): BermSpec | null {
|
||||
return own === undefined || own === null ? berm : { ...berm, depthM: Math.max(own, 0) };
|
||||
}
|
||||
|
||||
export function bermSpec(
|
||||
widthM = BERM_DEFAULT_WIDTH_M,
|
||||
intervalM = BERM_DEFAULT_INTERVAL_M,
|
||||
slopeDeg = BERM_DEFAULT_SLOPE_DEG,
|
||||
depthM = BERM_DEFAULT_DEPTH_M,
|
||||
): BermSpec {
|
||||
return { widthM, intervalM, slopeDeg, depthM };
|
||||
}
|
||||
|
||||
/**
|
||||
* 짝: `cut_profile_points`. 절토 사면 꼭짓점 `[[거리, 표고], ...]` — 시작에서 바깥으로.
|
||||
*
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* 짝: `B06_Section/B06_Section_Engine_Design.py` (`compute_cross_design`)
|
||||
* 면적 적분은 `common_util_cross_design_areas.ts` ↔ `B06_Section_Engine_Areas.py`.
|
||||
* 회귀 테스트가 두 구현을 같은 입력으로 실제 비교한다:
|
||||
* `tmp/tests/test_b06_cross_design_mirror.py` — 어느 쪽을 고치든 반드시 같이 돌릴 것.
|
||||
* `resources/tester/test_b06_cross_design_mirror.py` — 어느 쪽을 고치든 반드시 같이 돌릴 것.
|
||||
*
|
||||
* ── 왜 같은 계산이 두 벌인가 (2026-09-03 사용자 확정) ────────────────
|
||||
* 사용자가 계획선을 만지는 동안의 계산은 **브라우저 안에서 끝나야 한다**. 조작은 세션
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* ⚠⚠ 파이썬 짝 파일과 **한 벌**이다 — 한쪽만 고치면 두 화면 값이 갈린다 ⚠⚠
|
||||
* 짝: `B06_Section/B06_Section_Engine_Areas.py`
|
||||
* 같은 입력에 같은 값을 내야 한다. 회귀 테스트가 두 구현을 실제로 비교한다:
|
||||
* `tmp/tests/test_b06_cross_design_mirror.py` — 고칠 때 반드시 같이 돌릴 것.
|
||||
* `resources/tester/test_b06_cross_design_mirror.py` — 고칠 때 반드시 같이 돌릴 것.
|
||||
* 왜 두 벌인가: 사용자 조작 중 계산은 브라우저 안에서 끝나야 하고(2026-09-03 사용자
|
||||
* 확정), 저장·확정·도면 산출은 서버가 정본으로 다시 계산하기 때문이다.
|
||||
*
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 배수관 세트(배관·기슭막이·보호공·세월교·BOX암거·물넘이포장) 제원 — **브라우저 몫**.
|
||||
*
|
||||
* ⚠ 파이썬 `B06_Section/B06_Section_Engine_Culvert.py` 와 **한 벌**이다(2026-09-06).
|
||||
* 한쪽만 고치면 화면과 저장본이 갈린다. 거울 테스트: `tmp/tests/test_b06_culvert_sets_mirror.py`
|
||||
* 한쪽만 고치면 화면과 저장본이 갈린다. 거울 테스트: `resources/tester/test_b06_culvert_sets_mirror.py`
|
||||
*
|
||||
* 왜 짝으로 두나(CLAUDE.md 5장 「계산 자리」) — 이 계산은 종횡단 **상세를 읽을 때마다**
|
||||
* 서버에서 돈다. 매 요청 도는 자리라 Node 왕복(코리도·구조물 면적 방식)이 비싸다.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""터파기 단면 — 관·벽 두 갈래. **근거의 급이 다르므로 갈라 둔다.**
|
||||
|
||||
⚠⚠ TS 짝 파일과 **한 벌**이다 — `common_util/common_util_excavation.ts`.
|
||||
거울 시험: `tmp/tests/test_excavation_mirror.py` (고칠 때 같이 돌릴 것).
|
||||
거울 시험: `resources/tester/test_excavation_mirror.py` (고칠 때 같이 돌릴 것).
|
||||
화면(B06 횡단도)이 그리고 서버(B08 수량)가 세는 값이라 두 쪽에 같은 수가 있어야 한다.
|
||||
|
||||
① 관 — **법정 시방서에 표가 있다**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 터파기 단면 — 관·벽 두 갈래. **근거의 급이 다르므로 갈라 둔다.**
|
||||
*
|
||||
* ⚠⚠ 파이썬 짝과 **한 벌**이다 — `common_util/common_util_excavation.py`.
|
||||
* 거울 시험: `tmp/tests/test_excavation_mirror.py`. 어느 쪽을 고치든 같이 돌릴 것.
|
||||
* 거울 시험: `resources/tester/test_excavation_mirror.py`. 어느 쪽을 고치든 같이 돌릴 것.
|
||||
* 화면(B06 횡단도)이 그리고 서버(B08 수량)가 세는 값이라 두 쪽 수가 같아야 한다.
|
||||
*
|
||||
* ① 관 — 법정 시방서에 표가 있다: `KCS 44 40 10 배수관(23.01) §3.2(4) 그림 3.2-1`.
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/* =============================================================================
|
||||
* common_util_mass_haul_callout.ts
|
||||
* 유토곡선 값 표기 · 선택 측점 구간 물량 말풍선(`common_util_mass_haul_view` 에서 나눔).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { MassHaulPoint } from "./common_util_mass_haul";
|
||||
import { L, stationLabel, svgElement, svgText } from "./common_util_svg";
|
||||
|
||||
/** 누가토량 값 표기 — 천 단위 구분 + 소수점 1자리. */
|
||||
export function formatVolume(value: number): string {
|
||||
return value.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CalloutPlacement {
|
||||
anchorX: number;
|
||||
anchorY: number;
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
/** 주어진 가로 구간에서 표시 중인 곡선이 지나는 세로 범위(없으면 null). */
|
||||
curveYAt: (fromPx: number, toPx: number) => { top: number; bottom: number } | null;
|
||||
}
|
||||
|
||||
const CALLOUT_LINE_H = 14;
|
||||
const CALLOUT_PAD_X = 9;
|
||||
const CALLOUT_PAD_Y = 7;
|
||||
/** 말풍선과 곡선 사이 최소 틈(px). */
|
||||
const CALLOUT_CLEARANCE = 6;
|
||||
|
||||
/** SVG는 자동 크기가 없어 폭을 어림해야 한다. 한글은 라틴 글자보다 넓다. */
|
||||
export function textWidth(value: string): number {
|
||||
let width = 0;
|
||||
for (const char of value) width += /[가-힣ㄱ-ㅎㅏ-ㅣ]/.test(char) ? 11 : 6.1;
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택 측점의 구간 물량 말풍선(둥근 사각형)을 **곡선과 겹치지 않는 자리에** 그린다.
|
||||
*
|
||||
* 말풍선이 놓일 가로 구간에서 곡선이 실제로 지나는 세로 범위를 먼저 재고, 그 위/아래
|
||||
* 빈 곳 중 들어갈 수 있는 쪽에 놓는다(2026-08-02 사용자 지시). 오른쪽·왼쪽 자리를 모두
|
||||
* 재서 여유가 큰 쪽을 고르며, 어느 쪽에도 안 들어가면 여유가 더 큰 쪽 끝에 붙인다.
|
||||
*/
|
||||
export function appendVolumeCallout(
|
||||
/** 붙일 자리 — 그래프 칸 밖을 자르는 겹이다(2026-09-04). */
|
||||
svg: SVGSVGElement | SVGGElement,
|
||||
point: MassHaulPoint,
|
||||
stationInterval: number,
|
||||
box: CalloutPlacement,
|
||||
): void {
|
||||
const lines = [
|
||||
`${stationLabel(point.chainage_m, stationInterval)}` +
|
||||
` · ${L("B06_MassHaul_Station_Net")} ${formatVolume(point.net_volume_m3)}` +
|
||||
` · ${L("B06_MassHaul_Station_Cumulative")} ${formatVolume(point.cumulative_volume_m3)}`,
|
||||
`${L("B06_Design_Cut_Soil_Area")} ${formatVolume(point.cut_soil_m3)}` +
|
||||
` · ${L("B06_Design_Cut_Rock_Area")} ${formatVolume(point.cut_rock_m3)}` +
|
||||
` · ${L("B06_MassHaul_CutCompacted")} ${formatVolume(point.cut_compacted_m3)}` +
|
||||
` · ${L("B06_MassHaul_Fill")} ${formatVolume(point.fill_m3)} (㎥)`,
|
||||
];
|
||||
const width = Math.min(
|
||||
Math.max(...lines.map(textWidth)) + CALLOUT_PAD_X * 2,
|
||||
Math.max(box.right - box.left, 80),
|
||||
);
|
||||
const height = lines.length * CALLOUT_LINE_H + CALLOUT_PAD_Y * 2;
|
||||
|
||||
const clampX = (value: number): number =>
|
||||
Math.min(Math.max(value, box.left), Math.max(box.left, box.right - width));
|
||||
// 오른쪽 먼저, 안 되면 왼쪽 — 각 자리마다 위/아래 여유를 재서 가장 넉넉한 조합을 고른다.
|
||||
let best: { x: number; y: number; room: number; fits: boolean } | null = null;
|
||||
for (const candidateX of [clampX(box.anchorX + 14), clampX(box.anchorX - 14 - width)]) {
|
||||
const band = box.curveYAt(candidateX, candidateX + width);
|
||||
const roomAbove = (band ? band.top : box.bottom) - box.top;
|
||||
const roomBelow = box.bottom - (band ? band.bottom : box.top);
|
||||
for (const [room, top] of [
|
||||
[roomAbove, (band ? band.top : box.bottom) - height - CALLOUT_CLEARANCE],
|
||||
[roomBelow, (band ? band.bottom : box.top) + CALLOUT_CLEARANCE],
|
||||
] as Array<[number, number]>) {
|
||||
const fits = room >= height + CALLOUT_CLEARANCE;
|
||||
const better = !best || (fits && !best.fits) || (fits === best.fits && room > best.room);
|
||||
if (better) best = { x: candidateX, y: top, room, fits };
|
||||
}
|
||||
}
|
||||
if (!best) return;
|
||||
const boxY = Math.min(
|
||||
Math.max(best.y, box.top + 2),
|
||||
Math.max(box.top + 2, box.bottom - height - 2),
|
||||
);
|
||||
|
||||
const group = svgElement("g", { class: "b06-masshaul__callout" });
|
||||
// 꼬리는 앵커가 말풍선 가로 범위 안에 있을 때만 — 밖이면 지시선으로 잇는다.
|
||||
const tailX = Math.min(Math.max(box.anchorX, best.x + 12), best.x + width - 12);
|
||||
const below = boxY > box.anchorY;
|
||||
if (box.anchorX >= best.x && box.anchorX <= best.x + width) {
|
||||
const edge = below ? boxY : boxY + height;
|
||||
const tip = below ? edge - 7 : edge + 7;
|
||||
group.append(
|
||||
svgElement("polygon", {
|
||||
points: `${tailX - 6},${edge} ${tailX + 6},${edge} ${tailX},${tip}`,
|
||||
class: "b06-masshaul__callout-tail",
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
group.append(
|
||||
svgElement("line", {
|
||||
x1: box.anchorX,
|
||||
y1: box.anchorY,
|
||||
x2: box.anchorX < best.x ? best.x : best.x + width,
|
||||
y2: boxY + height / 2,
|
||||
class: "b06-masshaul__callout-leader",
|
||||
}),
|
||||
);
|
||||
}
|
||||
group.append(
|
||||
svgElement("rect", { x: best.x, y: boxY, width, height, rx: 8, ry: 8 }),
|
||||
...lines.map((line, index) =>
|
||||
svgText(line, {
|
||||
x: best.x + CALLOUT_PAD_X,
|
||||
y: boxY + CALLOUT_PAD_Y + CALLOUT_LINE_H * (index + 0.8),
|
||||
class: "b06-masshaul__callout-text",
|
||||
}),
|
||||
),
|
||||
);
|
||||
svg.append(group);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/* =============================================================================
|
||||
* common_util_mass_haul_legend.ts
|
||||
* 유토곡선 곡선 이름 · 범례 줄(`common_util_mass_haul_view` 에서 나눔).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { MassHaulBasis, MassHaulSeries } from "./common_util_mass_haul";
|
||||
import type { LocaleKey } from "@ui/ui_template_locale";
|
||||
import { MASS_HAUL_BALANCE_KEY } from "./common_util_mass_haul";
|
||||
import { L } from "./common_util_svg";
|
||||
|
||||
const BASIS_LABEL: Record<MassHaulBasis, LocaleKey> = {
|
||||
cross: "B06_MassHaul_Basis_Cross",
|
||||
longitudinal: "B06_MassHaul_Basis_Long",
|
||||
};
|
||||
|
||||
/** 범례·곡선에 함께 쓰는 이름. 색이 곧 산출 기준이다. */
|
||||
export function massHaulSeriesLabel(series: MassHaulSeries): string {
|
||||
return L(BASIS_LABEL[series.basis]);
|
||||
}
|
||||
|
||||
export function seriesClass(series: MassHaulSeries, base: string): string {
|
||||
return `${base} ${base}--${series.basis}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 곡선 표시/숨김 버튼 줄.
|
||||
*
|
||||
* 그래프 **아래**에 둔다(2026-08-02 사용자 지시). 예전에는 그래프 위에 겹치는 오버레이였는데,
|
||||
* 가로 스크롤 컨테이너 안에 있으면 컨테이너 폭 계산에 끼어들어 종단면도와 X축이 어긋난다.
|
||||
* 지금은 스크롤 컨테이너 **밖** 패널 본문에 붙어 곡선 폭에 전혀 관여하지 않는다.
|
||||
*/
|
||||
export function createMassHaulLegend(
|
||||
series: MassHaulSeries[],
|
||||
visibleKeys: ReadonlySet<string>,
|
||||
onToggle: (key: string) => void,
|
||||
onResetBalloons?: () => void,
|
||||
): HTMLElement {
|
||||
const legend = document.createElement("div");
|
||||
legend.className = "b06-masshaul__legend";
|
||||
legend.setAttribute("role", "group");
|
||||
legend.setAttribute("aria-label", L("B06_MassHaul_Legend_Title"));
|
||||
const addItem = (key: string, label: string, variantClass: string): void => {
|
||||
const shown = visibleKeys.has(key);
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = variantClass;
|
||||
button.classList.toggle("is-off", !shown);
|
||||
button.setAttribute("aria-pressed", String(shown));
|
||||
button.title = `${label} — ${L(shown ? "B06_MassHaul_Legend_Hide" : "B06_MassHaul_Legend_Show")}`;
|
||||
const swatch = document.createElement("span");
|
||||
swatch.className = "b06-masshaul__legend-swatch";
|
||||
swatch.setAttribute("aria-hidden", "true");
|
||||
const text = document.createElement("span");
|
||||
text.textContent = label;
|
||||
button.append(swatch, text);
|
||||
button.addEventListener("click", () => onToggle(key));
|
||||
legend.append(button);
|
||||
};
|
||||
for (const entry of series) {
|
||||
addItem(entry.key, massHaulSeriesLabel(entry), seriesClass(entry, "b06-masshaul__legend-item"));
|
||||
}
|
||||
// 토량 분배는 곡선이 아니라 곡선 위에 얹는 레이어라 스와치 모양을 따로 준다.
|
||||
addItem(
|
||||
MASS_HAUL_BALANCE_KEY,
|
||||
L("B06_MassHaul_Balance_Layer"),
|
||||
"b06-masshaul__legend-item b06-masshaul__legend-item--balance",
|
||||
);
|
||||
// 끌어 옮긴 balloon을 한 번에 자동 배치로 되돌린다(프론트 캐시 + 다음 확정 시 영구저장소).
|
||||
if (onResetBalloons && visibleKeys.has(MASS_HAUL_BALANCE_KEY)) {
|
||||
const reset = document.createElement("button");
|
||||
reset.type = "button";
|
||||
reset.className = "b06-masshaul__legend-item b06-masshaul__legend-item--reset";
|
||||
reset.textContent = L("B06_MassHaul_Balloon_Reset");
|
||||
reset.title = L("B06_MassHaul_Balloon_Reset_Tip");
|
||||
reset.addEventListener("click", onResetBalloons);
|
||||
// [횡단 기준] 왼쪽(367-1)
|
||||
legend.prepend(reset);
|
||||
}
|
||||
return legend;
|
||||
}
|
||||
@@ -13,15 +13,17 @@
|
||||
* Y축은 데이터 범위 기반 자동 스케일이다. 참고 도면의 수직축척이 공구마다 다르므로
|
||||
* (1공구 V=1:10,000 / 3공구 V=1:50,000) 고정 축척을 쓰면 안 된다.
|
||||
* 근거: docs/raw/2026-08-02_유토곡선_3공구_분석.md 3.2절
|
||||
*
|
||||
* 값 표기 · 말풍선 = `common_util_mass_haul_callout` · 곡선 이름 · 범례 = `common_util_mass_haul_legend`.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { MassHaulBasis, MassHaulPoint, MassHaulSeries } from "./common_util_mass_haul";
|
||||
import type { MassHaulPoint, MassHaulSeries } from "./common_util_mass_haul";
|
||||
import type { MassHaulStationSource } from "./common_util_mass_haul_types";
|
||||
import type { HaulPlan } from "./common_util_mass_haul_balance";
|
||||
import type { LocaleKey } from "@ui/ui_template_locale";
|
||||
import { MASS_HAUL_BALANCE_KEY } from "./common_util_mass_haul";
|
||||
import { appendBalanceLayer, haulPlanChips } from "./common_util_mass_haul_balance_view";
|
||||
import { appendBalanceLayer } from "./common_util_mass_haul_balance_view";
|
||||
import { appendVolumeCallout, formatVolume, textWidth } from "./common_util_mass_haul_callout";
|
||||
import { segmentOf } from "./common_util_mass_haul_curve";
|
||||
import { seriesClass } from "./common_util_mass_haul_legend";
|
||||
import { L, stationLabel, svgElement, svgText } from "./common_util_svg";
|
||||
import { viewOf } from "./common_util_view_window";
|
||||
|
||||
@@ -111,8 +113,6 @@ const WINDOW_EASE = 0.2;
|
||||
|
||||
/** 축 눈금이 읽히는 최소 높이. 이보다 낮아지면 그래프가 뭉개진다. */
|
||||
export const MASS_HAUL_MIN_HEIGHT = 90;
|
||||
/** 패널을 건드리지 않았을 때의 유토곡선 높이. */
|
||||
export const MASS_HAUL_HEIGHT = 190;
|
||||
|
||||
/**
|
||||
* 유토곡선 전용 상하 여백. 좌우는 종단면도와 **반드시 같아야**(LONG_PAD) X축이 맞으므로
|
||||
@@ -123,20 +123,6 @@ export const MASS_HAUL_HEIGHT = 190;
|
||||
const MASS_PAD_TOP = 10;
|
||||
const MASS_PAD_BOTTOM = 8;
|
||||
|
||||
const BASIS_LABEL: Record<MassHaulBasis, LocaleKey> = {
|
||||
cross: "B06_MassHaul_Basis_Cross",
|
||||
longitudinal: "B06_MassHaul_Basis_Long",
|
||||
};
|
||||
|
||||
/** 범례·곡선에 함께 쓰는 이름. 색이 곧 산출 기준이다. */
|
||||
export function massHaulSeriesLabel(series: MassHaulSeries): string {
|
||||
return L(BASIS_LABEL[series.basis]);
|
||||
}
|
||||
|
||||
function seriesClass(series: MassHaulSeries, base: string): string {
|
||||
return `${base} ${base}--${series.basis}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 곡선 경로. **`common_util_mass_haul_curve`가 쓰는 것과 같은 기하**로 그려야 한다 — 평형선·장비
|
||||
* 경계현·평균운반거리선의 양 끝이 그 기하와의 교점으로 계산되므로, 그리기와 재기가 어긋나면
|
||||
@@ -173,14 +159,6 @@ function curvePath(
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
/** 누가토량 값 표기 — 천 단위 구분 + 소수점 1자리. */
|
||||
export function formatVolume(value: number): string {
|
||||
return value.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Y축 기본 스케일(㎥). 계획선을 편집할 때마다 범위가 데이터로 다시 잡히면 그래프가
|
||||
* 출렁여 변화를 읽기 어렵다(2026-08-04 사용자 확정) — 기본 ±200㎥으로 고정하고,
|
||||
@@ -295,119 +273,6 @@ export function massHaulPointAt(
|
||||
return bestGap <= 0.5 ? best : null;
|
||||
}
|
||||
|
||||
interface CalloutPlacement {
|
||||
anchorX: number;
|
||||
anchorY: number;
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
/** 주어진 가로 구간에서 표시 중인 곡선이 지나는 세로 범위(없으면 null). */
|
||||
curveYAt: (fromPx: number, toPx: number) => { top: number; bottom: number } | null;
|
||||
}
|
||||
|
||||
const CALLOUT_LINE_H = 14;
|
||||
const CALLOUT_PAD_X = 9;
|
||||
const CALLOUT_PAD_Y = 7;
|
||||
/** 말풍선과 곡선 사이 최소 틈(px). */
|
||||
const CALLOUT_CLEARANCE = 6;
|
||||
|
||||
/** SVG는 자동 크기가 없어 폭을 어림해야 한다. 한글은 라틴 글자보다 넓다. */
|
||||
function textWidth(value: string): number {
|
||||
let width = 0;
|
||||
for (const char of value) width += /[가-힣ㄱ-ㅎㅏ-ㅣ]/.test(char) ? 11 : 6.1;
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택 측점의 구간 물량 말풍선(둥근 사각형)을 **곡선과 겹치지 않는 자리에** 그린다.
|
||||
*
|
||||
* 말풍선이 놓일 가로 구간에서 곡선이 실제로 지나는 세로 범위를 먼저 재고, 그 위/아래
|
||||
* 빈 곳 중 들어갈 수 있는 쪽에 놓는다(2026-08-02 사용자 지시). 오른쪽·왼쪽 자리를 모두
|
||||
* 재서 여유가 큰 쪽을 고르며, 어느 쪽에도 안 들어가면 여유가 더 큰 쪽 끝에 붙인다.
|
||||
*/
|
||||
function appendVolumeCallout(
|
||||
/** 붙일 자리 — 그래프 칸 밖을 자르는 겹이다(2026-09-04). */
|
||||
svg: SVGSVGElement | SVGGElement,
|
||||
point: MassHaulPoint,
|
||||
stationInterval: number,
|
||||
box: CalloutPlacement,
|
||||
): void {
|
||||
const lines = [
|
||||
`${stationLabel(point.chainage_m, stationInterval)}` +
|
||||
` · ${L("B06_MassHaul_Station_Net")} ${formatVolume(point.net_volume_m3)}` +
|
||||
` · ${L("B06_MassHaul_Station_Cumulative")} ${formatVolume(point.cumulative_volume_m3)}`,
|
||||
`${L("B06_Design_Cut_Soil_Area")} ${formatVolume(point.cut_soil_m3)}` +
|
||||
` · ${L("B06_Design_Cut_Rock_Area")} ${formatVolume(point.cut_rock_m3)}` +
|
||||
` · ${L("B06_MassHaul_CutCompacted")} ${formatVolume(point.cut_compacted_m3)}` +
|
||||
` · ${L("B06_MassHaul_Fill")} ${formatVolume(point.fill_m3)} (㎥)`,
|
||||
];
|
||||
const width = Math.min(
|
||||
Math.max(...lines.map(textWidth)) + CALLOUT_PAD_X * 2,
|
||||
Math.max(box.right - box.left, 80),
|
||||
);
|
||||
const height = lines.length * CALLOUT_LINE_H + CALLOUT_PAD_Y * 2;
|
||||
|
||||
const clampX = (value: number): number =>
|
||||
Math.min(Math.max(value, box.left), Math.max(box.left, box.right - width));
|
||||
// 오른쪽 먼저, 안 되면 왼쪽 — 각 자리마다 위/아래 여유를 재서 가장 넉넉한 조합을 고른다.
|
||||
let best: { x: number; y: number; room: number; fits: boolean } | null = null;
|
||||
for (const candidateX of [clampX(box.anchorX + 14), clampX(box.anchorX - 14 - width)]) {
|
||||
const band = box.curveYAt(candidateX, candidateX + width);
|
||||
const roomAbove = (band ? band.top : box.bottom) - box.top;
|
||||
const roomBelow = box.bottom - (band ? band.bottom : box.top);
|
||||
for (const [room, top] of [
|
||||
[roomAbove, (band ? band.top : box.bottom) - height - CALLOUT_CLEARANCE],
|
||||
[roomBelow, (band ? band.bottom : box.top) + CALLOUT_CLEARANCE],
|
||||
] as Array<[number, number]>) {
|
||||
const fits = room >= height + CALLOUT_CLEARANCE;
|
||||
const better = !best || (fits && !best.fits) || (fits === best.fits && room > best.room);
|
||||
if (better) best = { x: candidateX, y: top, room, fits };
|
||||
}
|
||||
}
|
||||
if (!best) return;
|
||||
const boxY = Math.min(
|
||||
Math.max(best.y, box.top + 2),
|
||||
Math.max(box.top + 2, box.bottom - height - 2),
|
||||
);
|
||||
|
||||
const group = svgElement("g", { class: "b06-masshaul__callout" });
|
||||
// 꼬리는 앵커가 말풍선 가로 범위 안에 있을 때만 — 밖이면 지시선으로 잇는다.
|
||||
const tailX = Math.min(Math.max(box.anchorX, best.x + 12), best.x + width - 12);
|
||||
const below = boxY > box.anchorY;
|
||||
if (box.anchorX >= best.x && box.anchorX <= best.x + width) {
|
||||
const edge = below ? boxY : boxY + height;
|
||||
const tip = below ? edge - 7 : edge + 7;
|
||||
group.append(
|
||||
svgElement("polygon", {
|
||||
points: `${tailX - 6},${edge} ${tailX + 6},${edge} ${tailX},${tip}`,
|
||||
class: "b06-masshaul__callout-tail",
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
group.append(
|
||||
svgElement("line", {
|
||||
x1: box.anchorX,
|
||||
y1: box.anchorY,
|
||||
x2: box.anchorX < best.x ? best.x : best.x + width,
|
||||
y2: boxY + height / 2,
|
||||
class: "b06-masshaul__callout-leader",
|
||||
}),
|
||||
);
|
||||
}
|
||||
group.append(
|
||||
svgElement("rect", { x: best.x, y: boxY, width, height, rx: 8, ry: 8 }),
|
||||
...lines.map((line, index) =>
|
||||
svgText(line, {
|
||||
x: best.x + CALLOUT_PAD_X,
|
||||
y: boxY + CALLOUT_PAD_Y + CALLOUT_LINE_H * (index + 0.8),
|
||||
class: "b06-masshaul__callout-text",
|
||||
}),
|
||||
),
|
||||
);
|
||||
svg.append(group);
|
||||
}
|
||||
|
||||
export function createMassHaulChart(
|
||||
series: MassHaulSeries[],
|
||||
visibleKeys: ReadonlySet<string>,
|
||||
@@ -734,133 +599,3 @@ export function createMassHaulChart(
|
||||
);
|
||||
return svg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 곡선 표시/숨김 버튼 줄.
|
||||
*
|
||||
* 그래프 **아래**에 둔다(2026-08-02 사용자 지시). 예전에는 그래프 위에 겹치는 오버레이였는데,
|
||||
* 가로 스크롤 컨테이너 안에 있으면 컨테이너 폭 계산에 끼어들어 종단면도와 X축이 어긋난다.
|
||||
* 지금은 스크롤 컨테이너 **밖** 패널 본문에 붙어 곡선 폭에 전혀 관여하지 않는다.
|
||||
*/
|
||||
export function createMassHaulLegend(
|
||||
series: MassHaulSeries[],
|
||||
visibleKeys: ReadonlySet<string>,
|
||||
onToggle: (key: string) => void,
|
||||
onResetBalloons?: () => void,
|
||||
): HTMLElement {
|
||||
const legend = document.createElement("div");
|
||||
legend.className = "b06-masshaul__legend";
|
||||
legend.setAttribute("role", "group");
|
||||
legend.setAttribute("aria-label", L("B06_MassHaul_Legend_Title"));
|
||||
const addItem = (key: string, label: string, variantClass: string): void => {
|
||||
const shown = visibleKeys.has(key);
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = variantClass;
|
||||
button.classList.toggle("is-off", !shown);
|
||||
button.setAttribute("aria-pressed", String(shown));
|
||||
button.title = `${label} — ${L(shown ? "B06_MassHaul_Legend_Hide" : "B06_MassHaul_Legend_Show")}`;
|
||||
const swatch = document.createElement("span");
|
||||
swatch.className = "b06-masshaul__legend-swatch";
|
||||
swatch.setAttribute("aria-hidden", "true");
|
||||
const text = document.createElement("span");
|
||||
text.textContent = label;
|
||||
button.append(swatch, text);
|
||||
button.addEventListener("click", () => onToggle(key));
|
||||
legend.append(button);
|
||||
};
|
||||
for (const entry of series) {
|
||||
addItem(entry.key, massHaulSeriesLabel(entry), seriesClass(entry, "b06-masshaul__legend-item"));
|
||||
}
|
||||
// 토량 분배는 곡선이 아니라 곡선 위에 얹는 레이어라 스와치 모양을 따로 준다.
|
||||
addItem(
|
||||
MASS_HAUL_BALANCE_KEY,
|
||||
L("B06_MassHaul_Balance_Layer"),
|
||||
"b06-masshaul__legend-item b06-masshaul__legend-item--balance",
|
||||
);
|
||||
// 끌어 옮긴 balloon을 한 번에 자동 배치로 되돌린다(프론트 캐시 + 다음 확정 시 영구저장소).
|
||||
if (onResetBalloons && visibleKeys.has(MASS_HAUL_BALANCE_KEY)) {
|
||||
const reset = document.createElement("button");
|
||||
reset.type = "button";
|
||||
reset.className = "b06-masshaul__legend-item b06-masshaul__legend-item--reset";
|
||||
reset.textContent = L("B06_MassHaul_Balloon_Reset");
|
||||
reset.title = L("B06_MassHaul_Balloon_Reset_Tip");
|
||||
reset.addEventListener("click", onResetBalloons);
|
||||
// [횡단 기준] 왼쪽(367-1)
|
||||
legend.prepend(reset);
|
||||
}
|
||||
return legend;
|
||||
}
|
||||
|
||||
/**
|
||||
* 곡선 아래 총괄 요약 줄. 절토는 자연상태(내역 기준)와 환산 후(곡선 기준)를 함께 적는다.
|
||||
* 곡선이 여러 개라 수치가 어느 곡선의 것인지 끝에 반드시 밝힌다.
|
||||
*/
|
||||
export function createMassHaulSummary(
|
||||
series: MassHaulSeries,
|
||||
haulPlan: HaulPlan | null,
|
||||
): HTMLElement {
|
||||
const result = series.result;
|
||||
const row = document.createElement("div");
|
||||
row.className = "b06-masshaul__summary";
|
||||
const cut = result.cut_natural_m3;
|
||||
const cutTotal = cut.soil + cut.ripping_rock + cut.blasting_rock;
|
||||
/* 요약 줄이 12칩까지 늘어 한눈에 안 읽혔다(2026-08-19 사용자 지적). 판단에 쓰는
|
||||
* 값만 칩으로 남기고, 세부 내역은 그 칩의 툴팁으로 내린다.
|
||||
* 절토(자연) — 토질 3종 내역은 툴팁
|
||||
* 성토(다짐) — 절토 다짐 환산은 툴팁(토량환산계수 f 적용 결과)
|
||||
* 잉여 또는 부족 — 둘은 동시에 성립하지 않는다
|
||||
* 운반·검산 — 토량 분배를 켰을 때만(haulPlanChips)
|
||||
* B05 종단 패널과 B06 횡단 화면이 같은 함수를 쓰므로 양쪽이 함께 바뀐다. */
|
||||
const chips: Array<[string, string, string?]> = [
|
||||
[
|
||||
L("B06_MassHaul_CutNatural"),
|
||||
`${formatVolume(cutTotal)}㎥`,
|
||||
[
|
||||
`${L("B06_Design_Ground_Soil")} ${formatVolume(cut.soil)}㎥`,
|
||||
`${L("B06_Design_Ground_Ripping")} ${formatVolume(cut.ripping_rock)}㎥`,
|
||||
`${L("B06_Design_Ground_Blasting")} ${formatVolume(cut.blasting_rock)}㎥`,
|
||||
].join(" · "),
|
||||
],
|
||||
[
|
||||
L("B06_MassHaul_Fill"),
|
||||
`${formatVolume(result.fill_compacted_m3)}㎥`,
|
||||
`${L("B06_MassHaul_CutCompacted")} ${formatVolume(result.cut_compacted_m3)}㎥`,
|
||||
],
|
||||
];
|
||||
// 잉여와 부족은 동시에 성립하지 않으므로 해당하는 쪽만 보여 준다.
|
||||
chips.push(
|
||||
result.shortage_m3 > 0
|
||||
? [L("B06_MassHaul_Shortage"), `${formatVolume(result.shortage_m3)}㎥`]
|
||||
: [L("B06_MassHaul_Surplus"), `${formatVolume(result.surplus_m3)}㎥`],
|
||||
);
|
||||
// 절·성토 균형 지표 — 판정·조정은 사용자 몫이고 프로그램은 값만 드러낸다(2026-09-03
|
||||
// 사용자 확정). 곡선이 0선을 넘지 않으면 평형선·운반 블록이 아예 생기지 않으므로,
|
||||
// 벌룬이 없을 때 왜 없는지를 이 값 하나로 읽을 수 있어야 한다.
|
||||
const finalCumulative = result.points.length
|
||||
? result.points[result.points.length - 1].cumulative_volume_m3
|
||||
: 0;
|
||||
chips.push([
|
||||
L("B06_MassHaul_FinalCumulative"),
|
||||
`${formatVolume(finalCumulative)}㎥`,
|
||||
L("B06_MassHaul_FinalCumulative_Tip"),
|
||||
]);
|
||||
// 토량 분배를 켜 두면 운반 총량·검산이 뒤에 붙는다(사토·토취는 발생 지점 기준 합계).
|
||||
chips.push(...haulPlanChips(haulPlan));
|
||||
for (const [label, value, tip] of chips) {
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "b06-masshaul__chip";
|
||||
if (tip) chip.title = tip;
|
||||
const name = document.createElement("em");
|
||||
name.textContent = label;
|
||||
const amount = document.createElement("strong");
|
||||
amount.textContent = value;
|
||||
chip.append(name, amount);
|
||||
row.append(chip);
|
||||
}
|
||||
const basis = document.createElement("span");
|
||||
basis.className = seriesClass(series, "b06-masshaul__basis");
|
||||
basis.textContent = massHaulSeriesLabel(series);
|
||||
row.append(basis);
|
||||
return row;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "A00_Common
|
||||
| `berm` | `:166` | 시설 줄 `syncBerm` 만(구조물 파생) | 안 감 — 서버가 `structures.json` 에서 |
|
||||
| `cross-display` | `:177` | 전역 표시 반폭 | 안 감 |
|
||||
| `selection` · `structure-pick` · `pipe-ids` · `pipe-option-queue` · `structure-type-cache` | `:148` · `:138` · `:144` · `:146` · `:150` | 고름 · 관 id 장부 · 옵션 넘김 · 종류 캐시 | 안 감 · `structure-type-cache` 는 저장 뒤 비움 |
|
||||
| `culvertmove` | `:156` | 부르는 곳 없음 | 안 감 |
|
||||
|
||||
## result
|
||||
|
||||
|
||||
+2
-1
@@ -5,13 +5,14 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node ./config/node_modules/vite/bin/vite.js --configLoader runner",
|
||||
"build": "node ./config/node_modules/typescript/bin/tsc --noEmit && node ./config/node_modules/vite/bin/vite.js build --configLoader runner && npm run build:corridor && npm run build:server-calc && npm run build:formula && npm run build:spreadsheet && npm run build:b07-extent && npm run build:b07-note && npm run build:b07-cad",
|
||||
"build": "node ./config/node_modules/typescript/bin/tsc --noEmit && node ./config/node_modules/vite/bin/vite.js build --configLoader runner && npm run build:corridor && npm run build:server-calc && npm run build:formula && npm run build:spreadsheet && npm run build:b07-extent && npm run build:b07-note && npm run build:design-calc && npm run build:b07-cad",
|
||||
"build:corridor": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B05_Profile/B05_Profile_Corridor_Node.ts --outDir ../config/corridor_node",
|
||||
"build:server-calc": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B06_Section/B06_Section_Server_Calc_Node.ts --outDir ../config/server_calc_node",
|
||||
"build:formula": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../common_util/common_util_sheet_recalc_node.ts --outDir ../config/formula_node",
|
||||
"build:spreadsheet": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../common_util/common_util_spreadsheet_node.ts --outDir ../config/spreadsheet_node",
|
||||
"build:b07-extent": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B07_DesignDetail/B07_DesignDetail_Cross_Extent_Node.ts --outDir ../config/b07_extent_node",
|
||||
"build:b07-note": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B07_DesignDetail/B07_DesignDetail_Note_Options_Node.ts --outDir ../config/b07_note_node",
|
||||
"build:design-calc": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../A00_Common/design/logic/design_calc_node.ts --outDir ../config/design_calc_node",
|
||||
"install:b07-cad": "npm --prefix B07_DesignDetail/openwebcad install",
|
||||
"build:b07-cad": "npm run install:b07-cad && npm --prefix B07_DesignDetail/openwebcad run build",
|
||||
"preview": "node ./config/node_modules/vite/bin/vite.js preview --configLoader runner",
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/* 472-3 헬퍼 — TS import 도달 집합(472-0 g472/graph.mjs 꼴 · 읽기만).
|
||||
* node helper_472_3_graph.mjs <저장소 뿌리> <입구.ts> [<입구.ts> …]
|
||||
* 입구마다 도달 파일(뿌리 상대 · `/`)을 JSON 한 벌로 표준출력 — 정적 import · export from · 동적
|
||||
* import() · new URL(…, import.meta.url)(워커) · `import type` · `export type` 은 뺌(지워짐). */
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const [root, ...entries] = process.argv.slice(2);
|
||||
const alias = { "@ui/": "ui_template/", "@config/": "config/", "@util/": "common_util/" };
|
||||
const rel = (p) => path.relative(root, p).split(path.sep).join("/");
|
||||
const exists = (p) => {
|
||||
for (const c of [p, `${p}.ts`, `${p}/index.ts`])
|
||||
if (fs.existsSync(c) && fs.statSync(c).isFile()) return c;
|
||||
return null;
|
||||
};
|
||||
function resolve(from, spec) {
|
||||
let base;
|
||||
if (spec.startsWith(".")) base = path.resolve(path.dirname(from), spec);
|
||||
else {
|
||||
const a = Object.keys(alias).find((k) => spec.startsWith(k));
|
||||
if (!a) return null;
|
||||
base = path.resolve(root, alias[a] + spec.slice(a.length));
|
||||
}
|
||||
return exists(base.replace(/\.js$/, ""));
|
||||
}
|
||||
const re =
|
||||
/(?:import|export)\s+(?:type\s+)?(?:[^;"'`]*?\sfrom\s+)?["']([^"']+)["']|import\(\s*["']([^"']+)["']\s*\)|new URL\(\s*["']([^"']+)["']\s*,\s*import\.meta\.url/g;
|
||||
function deps(file) {
|
||||
if (!/\.(ts|mts)$/.test(file)) return [];
|
||||
const src = fs.readFileSync(file, "utf8");
|
||||
const out = [];
|
||||
for (const m of src.matchAll(re)) {
|
||||
if (/^(?:import|export)\s+type\s/.test(m[0])) continue;
|
||||
const target = resolve(file, m[1] || m[2] || m[3]);
|
||||
if (target) out.push(target);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const result = {};
|
||||
for (const entry of entries) {
|
||||
const seen = new Set();
|
||||
const stack = [path.resolve(root, entry)];
|
||||
while (stack.length) {
|
||||
const file = stack.pop();
|
||||
if (seen.has(file)) continue;
|
||||
seen.add(file);
|
||||
stack.push(...deps(file));
|
||||
}
|
||||
result[entry] = [...seen].map(rel).sort();
|
||||
}
|
||||
process.stdout.write(JSON.stringify(result));
|
||||
@@ -0,0 +1,7 @@
|
||||
/* 472-3 헬퍼 — 등록부와 묶음 단계 기댐을 한 묶음으로(vite ssr) · test_472_3_calc_registry.py 가 JSON 으로 받음. */
|
||||
export { CALC_REGISTRY } from "../../A00_Common/design/logic/design_calc_registry";
|
||||
export {
|
||||
AFTER_STAGES,
|
||||
BROWSER_STAGES,
|
||||
STAGE_DEPS,
|
||||
} from "../../A00_Common/design/logic/design_pipeline";
|
||||
@@ -33,6 +33,8 @@ POPOUT_FILES = [
|
||||
"B05_Profile/B05_Profile_UI_Drainage_Panel.ts",
|
||||
"B05_Profile/B05_Profile_UI_Structures_Marks.ts",
|
||||
"common_util/common_util_mass_haul_view.ts",
|
||||
"common_util/common_util_mass_haul_callout.ts",
|
||||
"common_util/common_util_mass_haul_legend.ts",
|
||||
]
|
||||
# 메인 창 전역 틀 · 시계 — `x.requestAnimationFrame` 처럼 창을 짚은 것은 제외
|
||||
MAIN_WINDOW_CLOCK = re.compile(
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
"""PLAN 472-3 (요구 273) — 계산 등록부 · 서버 Node 입구 · 경계 시험.
|
||||
|
||||
등록부 = `A00_Common/design/logic/design_calc_registry.ts`(순수 자료 · 목록 1.1 표 # 하나 = id 하나)
|
||||
Node 입구 = `A00_Common/design/logic/design_calc_node.ts`(`build:design-calc` · id → 함수 표).
|
||||
|
||||
못박는 것
|
||||
- id · # 하나씩 · 1.1 표 # 전부(R1 – N1 · 43).
|
||||
- id 마다 실행기 있음 — `node:` 는 입구 표에 · `py:` · `browser:` · ts · 짝 이름은 그 파일에 ·
|
||||
거울 시험 파일 있음.
|
||||
- 묶음 `STAGE_DEPS`(`design_pipeline.ts`) = 등록부 입력에서 만든 기댐(같은지만 · 묶음 안 고침).
|
||||
- 서버 쪽(server_canon · server_only) TS 가 브라우저 import 그래프에 없음 · 등록부는 안 끔.
|
||||
- Node 입구 도달 집합 = logic · common_util · 자료만 + 「알려진 끌림」(`KNOWN_PULLS`) — 그 밖은
|
||||
실패 · 안 나타난 알려진 끌림도 실패(목록에서 지움 — 472-2 2-B · 472-4 가 하나씩 걷어 빈 목록).
|
||||
- Node 입구 값 = 기존 묶음 값(고정 자료 `fixtures/golden_472` · server-calc 면적 · 다단 벽 ·
|
||||
누가토량 · 배분 · corridor 3D) · 터파기 = 파이썬 짝.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_472_3_calc_registry.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
FIXTURE = HERE / "fixtures" / "golden_472"
|
||||
REGISTRY = "A00_Common/design/logic/design_calc_registry.ts"
|
||||
NODE_ENTRY = "A00_Common/design/logic/design_calc_node.ts"
|
||||
BROWSER_ENTRY = "A00_Common/main.ts"
|
||||
BUNDLE = ROOT / "config" / "design_calc_node" / "design_calc_node.js"
|
||||
NODE_IDS = {
|
||||
"structure.layouts",
|
||||
"masshaul.cumulative",
|
||||
"haul.plan",
|
||||
"excavation.trench",
|
||||
"corridor.mesh",
|
||||
}
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "vite").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
# 화면 · 세션 끌림(목록 1.3) — 472-2 2-B 가 정리(PIPE_DISPLAY_NAME → common_util · Api_Fetch 세션 ·
|
||||
# 구조물 그림이 고른 것 읽기 · 사진첩).
|
||||
SESSION_PULLS = (
|
||||
"A00_Common/b_page_state.ts",
|
||||
"A00_Common/design/design_pick_match.ts",
|
||||
"A00_Common/design/design_pipe_ids.ts",
|
||||
"A00_Common/design/design_selection.ts",
|
||||
"A00_Common/design/design_store.ts",
|
||||
"B05_Profile/B05_Profile_UI_IrregularStations.ts",
|
||||
"B05_Profile/B05_Profile_UI_Profile_History.ts",
|
||||
"B06_Section/B06_Section_Api_Fetch.ts",
|
||||
"B06_Section/B06_Section_UI_Section_Common.ts",
|
||||
"config/config_frontend.ts",
|
||||
"ui_template/ui_template_elements.ts",
|
||||
"ui_template/ui_template_elements_base.ts",
|
||||
"ui_template/ui_template_elements_chart.ts",
|
||||
"ui_template/ui_template_elements_styles.ts",
|
||||
"ui_template/ui_template_locale.ts",
|
||||
"ui_template/ui_template_locale_a.ts",
|
||||
"ui_template/ui_template_locale_b1.ts",
|
||||
"ui_template/ui_template_locale_b2.ts",
|
||||
"ui_template/ui_template_locale_b3.ts",
|
||||
"ui_template/ui_template_locale_b4.ts",
|
||||
"ui_template/ui_template_locale_common.ts",
|
||||
"ui_template/ui_template_locale_m1.ts",
|
||||
"ui_template/ui_template_locale_m2.ts",
|
||||
)
|
||||
# 기하가 logic 밖(구조물 그림 · 3D 빌더 파일 안) — 472-4 가 logic · common_util 로.
|
||||
GEOMETRY_PULLS = (
|
||||
"B05_Profile/B05_Profile_UI_Corridor_Build.ts",
|
||||
"B05_Profile/B05_Profile_UI_Corridor_Envelope.ts",
|
||||
"B05_Profile/B05_Profile_UI_Corridor_Station.ts",
|
||||
"B05_Profile/B05_Profile_UI_Corridor_Station_Structure.ts",
|
||||
"B05_Profile/B05_Profile_UI_Corridor_Structures.ts",
|
||||
"B05_Profile/B05_Profile_UI_Corridor_Structures_Box.ts",
|
||||
"B05_Profile/B05_Profile_UI_Corridor_Structures_Chute.ts",
|
||||
"B05_Profile/B05_Profile_UI_Corridor_Structures_Path.ts",
|
||||
"B06_Section/B06_Section_Structure_Layouts.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Body_Guard.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Box.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Box_Geom.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Culvert.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Culvert_Tiers.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Culvert_Types.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Design_Surface.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Excavation.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Ford.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Ford_Geom.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Revetment.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Wall.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts",
|
||||
)
|
||||
KNOWN_PULLS = set(SESSION_PULLS) | set(GEOMETRY_PULLS)
|
||||
|
||||
|
||||
def _node(*args: str, timeout: int = 120) -> str:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", *args], cwd=ROOT, capture_output=True, text=True, encoding="utf-8", timeout=timeout
|
||||
)
|
||||
if proc.returncode:
|
||||
raise RuntimeError(proc.stderr)
|
||||
return proc.stdout
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def loaded(tmp_path_factory) -> dict:
|
||||
"""vite ssr 로 등록부 + 묶음 단계 기댐을 묶어 JSON 으로(472-1 꼴)."""
|
||||
work = tmp_path_factory.mktemp("reg")
|
||||
_node(
|
||||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../resources/tester/helper_472_3_registry_entry.ts",
|
||||
"--outDir",
|
||||
str(work),
|
||||
"--logLevel",
|
||||
"error",
|
||||
timeout=300,
|
||||
)
|
||||
bundle = work / "bundle.mjs"
|
||||
shutil.copy(work / "helper_472_3_registry_entry.js", bundle)
|
||||
url = bundle.as_uri()
|
||||
out = _node(
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
f"const m = await import({json.dumps(url)}); console.log(JSON.stringify("
|
||||
"{registry: m.CALC_REGISTRY, deps: m.STAGE_DEPS, "
|
||||
"stages: [...m.BROWSER_STAGES, ...m.AFTER_STAGES]}))",
|
||||
)
|
||||
return json.loads(out)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def reach() -> dict[str, list[str]]:
|
||||
out = _node(
|
||||
str(HERE / "helper_472_3_graph.mjs"), str(ROOT), NODE_ENTRY, REGISTRY, BROWSER_ENTRY
|
||||
)
|
||||
return json.loads(out)
|
||||
|
||||
|
||||
def _run(payload: dict) -> dict | None:
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
|
||||
return run_bundle_json(BUNDLE, "build:design-calc", payload)
|
||||
|
||||
|
||||
def _fixture(name: str):
|
||||
return json.loads((FIXTURE / name).read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _context() -> dict:
|
||||
ctx = _fixture("context.json")
|
||||
keys = ("earthwork_conversion", "haul_equipment_limits", "natural_spoil_min_ground_slope")
|
||||
return {k: ctx[k] for k in keys}
|
||||
|
||||
|
||||
def _has_name(ref: str) -> bool:
|
||||
"""`<경로>#<이름>` — 파일 있음 · 그 이름이 정의됨(py def · ts export)."""
|
||||
path, name = ref.split("#")
|
||||
file = ROOT / path
|
||||
if not file.is_file():
|
||||
return False
|
||||
src = file.read_text(encoding="utf-8")
|
||||
if path.endswith(".py"):
|
||||
return re.search(rf"^\s*(?:async\s+)?def {re.escape(name)}\(", src, re.M) is not None
|
||||
pattern = rf"^export\s+(?:async\s+)?(?:function\*?|const|let|class)\s+{re.escape(name)}\b"
|
||||
return re.search(pattern, src, re.M) is not None
|
||||
|
||||
|
||||
# ── 등록부 자료 ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_id_하나씩_표_전부(loaded) -> None:
|
||||
rows = loaded["registry"]
|
||||
ids = [row["id"] for row in rows]
|
||||
nos = [row["no"] for row in rows]
|
||||
assert len(ids) == len(set(ids))
|
||||
assert len(nos) == len(set(nos))
|
||||
want = {f"{k}{i}" for k, n in (("R", 8), ("G", 4), ("P", 8), ("C", 7)) for i in range(1, n + 1)}
|
||||
want |= {f"F{i}" for i in range(1, 10)} | {"E1", "E2"} | {f"D{i}" for i in range(1, 5)} | {"N1"}
|
||||
assert set(nos) == want
|
||||
|
||||
|
||||
def test_id_마다_실행기_있음(loaded) -> None:
|
||||
bad = []
|
||||
for row in loaded["registry"]:
|
||||
kind, _, ref = row["runner"].partition(":")
|
||||
if row["site"] == "browser_only":
|
||||
ok = kind == "browser" and _has_name(ref)
|
||||
elif kind == "node":
|
||||
ok = ref == row["id"] and ref in NODE_IDS
|
||||
else:
|
||||
ok = kind == "py" and _has_name(ref)
|
||||
ok = ok and ("until" not in row or kind == "py")
|
||||
ok = ok and all(_has_name(r) for r in (row["ts"], row["pair"]) if r)
|
||||
ok = ok and all((HERE / f"{name}.py").is_file() for name in row["mirror"])
|
||||
if not ok:
|
||||
bad.append(row["id"])
|
||||
assert bad == []
|
||||
node_rows = {row["id"] for row in loaded["registry"] if row["runner"].startswith("node:")}
|
||||
assert node_rows == NODE_IDS
|
||||
|
||||
|
||||
def test_묶음_STAGE_DEPS_등록부_입력과_같음(loaded) -> None:
|
||||
"""같은 단계 줄들의 inputs 중 `input.*` 은 그대로 · 다른 단계 id 는 그 단계 이름."""
|
||||
rows = loaded["registry"]
|
||||
stage_of = {row["id"]: row["stage"] for row in rows}
|
||||
derived: dict[str, set[str]] = {name: set() for name in loaded["stages"]}
|
||||
for row in rows:
|
||||
if not row["stage"]:
|
||||
continue
|
||||
for item in row["inputs"]:
|
||||
if item.startswith("input."):
|
||||
derived[row["stage"]].add(item)
|
||||
elif stage_of.get(item) and stage_of[item] != row["stage"]:
|
||||
derived[row["stage"]].add(stage_of[item])
|
||||
assert {k: set(v) for k, v in loaded["deps"].items()} == derived
|
||||
# 앞 id 는 등록부에 있음(이름 틀림 막기)
|
||||
ids = set(stage_of)
|
||||
prefixes = ("input.", "file:", "session:", "context.", "store.")
|
||||
stray = [
|
||||
i for row in rows for i in row["inputs"] if not i.startswith(prefixes) and i not in ids
|
||||
]
|
||||
assert stray == []
|
||||
|
||||
|
||||
# ── import 경계 ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_서버_쪽_코드_브라우저_밖(loaded, reach) -> None:
|
||||
browser = set(reach[BROWSER_ENTRY])
|
||||
server = [
|
||||
row["ts"].split("#")[0]
|
||||
for row in loaded["registry"]
|
||||
if row["site"] in ("server_canon", "server_only") and row["ts"]
|
||||
]
|
||||
assert server # 유토 배분(common_util_mass_haul_balance)
|
||||
assert [path for path in server if path in browser] == []
|
||||
assert NODE_ENTRY not in browser
|
||||
assert reach[REGISTRY] == [REGISTRY] # 순수 자료 — 아무것도 안 끔
|
||||
|
||||
|
||||
def test_Node_입구_도달_경계(reach) -> None:
|
||||
files = set(reach[NODE_ENTRY])
|
||||
own = {
|
||||
f for f in files if "/logic/" in f or f.startswith("common_util/") or f.endswith(".json")
|
||||
}
|
||||
outside = files - own
|
||||
assert sorted(outside - KNOWN_PULLS) == [] # 새 끌림
|
||||
assert sorted(KNOWN_PULLS - outside) == [] # 걷힌 끌림 — KNOWN_PULLS 에서 지울 것
|
||||
|
||||
|
||||
# ── Node 입구 값 ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def golden_run() -> dict:
|
||||
out = _run(
|
||||
{
|
||||
"run": ["haul.plan", "structure.layouts", "masshaul.cumulative"],
|
||||
"inputs": {"detail": _fixture("detail.json"), "context": _context()},
|
||||
}
|
||||
)
|
||||
assert isinstance(out, dict)
|
||||
assert set(out["available"]) == NODE_IDS
|
||||
return out["results"]
|
||||
|
||||
|
||||
def test_server_calc_묶음과_같음(golden_run) -> None:
|
||||
from B06_Section.B06_Section_Server_Calc_Prebuild import _NPM_SCRIPT
|
||||
from B06_Section.B06_Section_Server_Calc_Prebuild import BUNDLE as SC_BUNDLE
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
|
||||
sc = run_bundle_json(
|
||||
SC_BUNDLE, _NPM_SCRIPT, {"detail": _fixture("detail.json"), "context": _context()}
|
||||
)
|
||||
layouts = golden_run["structure.layouts"]
|
||||
assert layouts["areas"] == sc["areas"] and sc["areas"]
|
||||
assert layouts["extra_walls"] == sc["extra_walls"]
|
||||
merged = {**golden_run["masshaul.cumulative"], **golden_run["haul.plan"]}
|
||||
assert merged == sc["mass_haul"]
|
||||
assert merged["final_cumulative_m3"] == -17570.7 # 472-1 화면 참값
|
||||
|
||||
|
||||
def test_corridor_묶음과_같음() -> None:
|
||||
from B05_Profile.B05_Profile_Corridor_Prebuild import BUNDLE as CO_BUNDLE
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
|
||||
detail = _fixture("detail.json")
|
||||
points = [
|
||||
{"x": x, "y": y, "order": i} for i, (x, y) in enumerate(_fixture("plan.json")["planned"])
|
||||
]
|
||||
co = run_bundle_json(CO_BUNDLE, "build:corridor", {"detail": detail, "route_points": points})
|
||||
out = _run({"run": ["corridor.mesh"], "inputs": {"detail": detail, "route_points": points}})
|
||||
assert co and out["results"]["corridor.mesh"] == co
|
||||
|
||||
|
||||
def test_터파기_파이썬_짝과_같음() -> None:
|
||||
from common_util import common_util_excavation as py
|
||||
|
||||
pipes = [{"diameter_mm": d} for d in (300, 800, 1500, 250, None)]
|
||||
walls = [
|
||||
{"height_m": 2.0, "average_thickness_m": 0.5, "has_foundation": True},
|
||||
{"height_m": 1.2, "average_thickness_m": 0.35, "has_foundation": False},
|
||||
{"height_m": None, "average_thickness_m": 0.4, "has_foundation": True},
|
||||
]
|
||||
out = _run(
|
||||
{"run": ["excavation.trench"], "inputs": {"excavation": {"pipes": pipes, "walls": walls}}}
|
||||
)
|
||||
got = out["results"]["excavation.trench"]
|
||||
assert [p["width_m"] for p in got["pipes"]] == [
|
||||
py.pipe_trench_width_m(p["diameter_mm"]) for p in pipes
|
||||
]
|
||||
for row, wall in zip(got["walls"], walls, strict=True):
|
||||
assert row["width_m"] == py.wall_trench_width_m(wall["average_thickness_m"])
|
||||
want = py.wall_trench_area_m2(
|
||||
wall["height_m"], wall["average_thickness_m"], has_foundation=wall["has_foundation"]
|
||||
)
|
||||
assert row["area_m2"] == pytest.approx(want) if want is not None else row["area_m2"] is None
|
||||
|
||||
|
||||
def test_모르는_id_는_실패() -> None:
|
||||
assert _run({"run": ["stations.list"], "inputs": {}}) is None
|
||||
@@ -0,0 +1,70 @@
|
||||
"""PLAN 472-3 3-B (요구 273) — 파이썬 실행기 `common_util_calc_runner` → Node 입구 → 기존 묶음 값.
|
||||
|
||||
못박는 것
|
||||
- `NODE_IDS` = 등록부 `node:` 줄 = 입구 표(`available`).
|
||||
- 고정 자료 `fixtures/golden_472` 로 `run_calc` 값 = [저장] 재계산이 부르는 server-calc 묶음 값
|
||||
(`Server_Calc_Prebuild` BUNDLE · 면적 · 다단 벽 · 누가토량 · 배분 · 끝값 −17,570.7).
|
||||
- Node 실행기 아닌 id(파이썬 · 브라우저 실행기)는 ValueError.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_472_3_calc_runner.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from common_util import common_util_calc_runner as runner
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
FIXTURE = HERE / "fixtures" / "golden_472"
|
||||
REGISTRY = ROOT / "A00_Common" / "design" / "logic" / "design_calc_registry.ts"
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "vite").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def _fixture(name: str):
|
||||
return json.loads((FIXTURE / name).read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _context() -> dict:
|
||||
ctx = _fixture("context.json")
|
||||
keys = ("earthwork_conversion", "haul_equipment_limits", "natural_spoil_min_ground_slope")
|
||||
return {k: ctx[k] for k in keys}
|
||||
|
||||
|
||||
def test_NODE_IDS_등록부_입구와_같음() -> None:
|
||||
src = REGISTRY.read_text(encoding="utf-8")
|
||||
assert set(re.findall(r'runner: "node:([a-z_.]+)"', src)) == runner.NODE_IDS
|
||||
out = runner.run_bundle_json(runner.BUNDLE, runner.NPM_SCRIPT, {"run": [], "inputs": {}})
|
||||
assert set(out["available"]) == runner.NODE_IDS
|
||||
|
||||
|
||||
def test_실행기_값_server_calc_와_같음() -> None:
|
||||
from B06_Section.B06_Section_Server_Calc_Prebuild import _NPM_SCRIPT, BUNDLE
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
|
||||
sc = run_bundle_json(
|
||||
BUNDLE, _NPM_SCRIPT, {"detail": _fixture("detail.json"), "context": _context()}
|
||||
)
|
||||
got = runner.run_calc(
|
||||
["structure.layouts", "masshaul.cumulative", "haul.plan"],
|
||||
{"detail": _fixture("detail.json"), "context": _context()},
|
||||
)
|
||||
assert got["structure.layouts"]["areas"] == sc["areas"] and sc["areas"]
|
||||
assert got["structure.layouts"]["extra_walls"] == sc["extra_walls"]
|
||||
merged = {**got["masshaul.cumulative"], **got["haul.plan"]}
|
||||
assert merged == sc["mass_haul"]
|
||||
assert merged["final_cumulative_m3"] == -17570.7
|
||||
|
||||
|
||||
def test_Node_실행기_아닌_id_는_ValueError() -> None:
|
||||
with pytest.raises(ValueError, match="stations.list"):
|
||||
runner.run_calc(["masshaul.cumulative", "stations.list"], {})
|
||||
Reference in New Issue
Block a user