Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
460 lines
19 KiB
TypeScript
460 lines
19 KiB
TypeScript
/* =============================================================================
|
|
* common_util_culvert_sets.ts
|
|
* 배수관 세트(배관·기슭막이·보호공·세월교·BOX암거·물넘이포장) 제원 — **브라우저 몫**.
|
|
*
|
|
* ⚠ 파이썬 `B06_Section/B06_Section_Engine_Culvert.py` 와 **한 벌**이다(2026-09-06).
|
|
* 한쪽만 고치면 화면과 저장본이 갈린다. 거울 테스트: `tmp/tests/test_b06_culvert_sets_mirror.py`
|
|
*
|
|
* 왜 짝으로 두나(CLAUDE.md 5장 「계산 자리」) — 이 계산은 종횡단 **상세를 읽을 때마다**
|
|
* 서버에서 돈다. 매 요청 도는 자리라 Node 왕복(코리도·구조물 면적 방식)이 비싸다.
|
|
* 그래서 세 갈래 중 ①(파이썬·TS 짝 + 거울 테스트)을 골랐다.
|
|
*
|
|
* 값을 새로 만들지 않는다 — **정본(관 지점 옵션) + 레지스트리 기본값**만 조합한다.
|
|
* 상수 사본을 두면 B05 폼과 갈라지므로 기본값은 레지스트리에서만 꺼낸다.
|
|
* ========================================================================== */
|
|
|
|
/** 관 지점 정본 1건 — `pipe_points.json` 의 한 줄(브라우저는 API 로 같은 것을 받는다). */
|
|
export interface CulvertPipePoint {
|
|
chainage_m: number;
|
|
facility?: string;
|
|
options?: Record<string, unknown> | null;
|
|
}
|
|
|
|
/** 레지스트리 타입별 옵션 기본값 — `type_id → { 옵션키: 기본값 }`. */
|
|
export type CulvertRegistryDefaults = Record<string, Record<string, unknown>>;
|
|
|
|
/** 세트 제원 — 파이썬이 내는 dict 와 같은 모양이라 키를 그대로 쓴다. */
|
|
export type CulvertSetSpec = Record<string, unknown>;
|
|
|
|
/* ── 파이썬 짝과 같은 상수 (근거 주석은 파이썬 쪽에 있다) ───────────────── */
|
|
const CHAINAGE_TOLERANCE_M = 0.02;
|
|
const SECTION_KEYS: Record<string, string> = {
|
|
ford: "ford",
|
|
box: "box",
|
|
ford_pavement: "ford_pavement",
|
|
};
|
|
/** 폭 절반만큼 옆 측점에도 걸치는 종류 — 물넘이포장만. */
|
|
const SPAN_LINKED_TYPES = new Set(["ford_pavement"]);
|
|
|
|
export const MIN_PIPE_COVER_M = 0.5;
|
|
export const APRON_LENGTH_FACTOR = 2.0;
|
|
export const APRON_THICKNESS_M = 0.45;
|
|
export const REVET_FACE_SLOPE = 0.3;
|
|
export const INLET_STRUCTURE_BASIN = "집수정";
|
|
export const FORD_SLAB_THICKNESS_M = 0.3;
|
|
export const FORD_WALL_THICKNESS_M = 0.2;
|
|
export const FORD_DEFAULT_WIDTH_M = 10.0;
|
|
export const FORD_PAVEMENT_DEFAULT_WIDTH_M = 5.0;
|
|
export const BOX_COVER_M = 0.5;
|
|
/** 관 벽 최소 높이 — 횡단도 `B06_Section_UI_Cross_Culvert_Const.ts` 와 같은 값(거울 시험이 셋을 대조).
|
|
* 높이를 안 적은 관 벽은 이 값으로 섬(2026-09-14 브레인 판정 · 옛 등록부 기본 2.5 걷음). */
|
|
export const REVET_FREEBOARD_M = 0.5;
|
|
export const REVET_HEIGHT_STEP_M = 0.1;
|
|
export const REVET_EMBED_DEPTH_M = 0.5;
|
|
|
|
/** 관 벽 순수 높이(근입 포함) 최소값 — 파이썬 `pipe_wall_min_height_m`. */
|
|
export function pipeWallMinHeightM(diameterM: number): number {
|
|
const raw = diameterM + REVET_FREEBOARD_M;
|
|
const steps = Math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9);
|
|
return round(steps * REVET_HEIGHT_STEP_M + REVET_EMBED_DEPTH_M, 3);
|
|
}
|
|
|
|
/** 시설 종류 — 파이썬 `common_util_drainage_pipes` 의 상수와 같은 문자열. */
|
|
const FACILITY_PIPE = "pipe";
|
|
const FACILITY_BOX = "box_culvert";
|
|
const FACILITY_FORD_PAVEMENT = "ford_pavement";
|
|
const FACILITY_FORD_BRIDGE = "ford_bridge";
|
|
const FACILITY_REVET = "revetment";
|
|
|
|
/** 숫자 옵션 하나를 정리한다. 문자열 저장분(관경 "1000")도 받는다 — 파이썬 `_number`. */
|
|
function num(value: unknown, fallback: number | null): number | null {
|
|
if (typeof value === "boolean") return fallback;
|
|
if (typeof value === "number") return Number.isFinite(value) ? value : fallback;
|
|
if (typeof value === "string") {
|
|
const parsed = Number(value.trim());
|
|
return value.trim() !== "" && Number.isFinite(parsed) ? parsed : fallback;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
/** 파이썬 `round(x, n)` 자리에 쓰는 반올림. */
|
|
function round(value: number, digits: number): number {
|
|
return Number(value.toFixed(digits));
|
|
}
|
|
|
|
function text(value: unknown): string | null {
|
|
return value === null || value === undefined || value === "" ? null : String(value);
|
|
}
|
|
|
|
/**
|
|
* 유입("inlet")·유출("outlet") 한쪽의 부속 제원.
|
|
* 구조가 집수정이면 기슭막이·보호공을 만들지 않는다 — 화면은 라벨만 쓴다.
|
|
*/
|
|
function sideSpec(
|
|
options: Record<string, unknown>,
|
|
defaults: Record<string, unknown>,
|
|
side: string,
|
|
diameterM: number,
|
|
): CulvertSetSpec {
|
|
const structure = text(options[`${side}_type`]) ?? text(defaults[`${side}_type`]) ?? "기슭막이";
|
|
const spec: CulvertSetSpec = { role: side, structure };
|
|
if (structure === INLET_STRUCTURE_BASIN) {
|
|
spec.basin_length_m = num(
|
|
options.inlet_basin_length_m,
|
|
num(defaults.inlet_basin_length_m, 2.0),
|
|
);
|
|
spec.basin_before_m = num(
|
|
options.inlet_basin_before_m,
|
|
num(defaults.inlet_basin_before_m, null),
|
|
);
|
|
spec.basin_after_m = num(options.inlet_basin_after_m, num(defaults.inlet_basin_after_m, null));
|
|
return spec;
|
|
}
|
|
|
|
const height = num(
|
|
options[`${side}_revet_height_m`],
|
|
num(defaults[`${side}_revet_height_m`], pipeWallMinHeightM(diameterM)),
|
|
);
|
|
const length = num(
|
|
options[`${side}_revet_length_m`],
|
|
num(defaults[`${side}_revet_length_m`], null),
|
|
);
|
|
const form = text(options[`${side}_revet_form`]) ?? text(defaults[`${side}_revet_form`]);
|
|
const before = num(
|
|
options[`${side}_revet_before_m`],
|
|
num(defaults[`${side}_revet_before_m`], null),
|
|
);
|
|
const after = num(options[`${side}_revet_after_m`], num(defaults[`${side}_revet_after_m`], null));
|
|
spec.revet_form = form;
|
|
spec.revet_height_m = height;
|
|
spec.revet_length_m = length;
|
|
spec.revet_before_m = before;
|
|
spec.revet_after_m = after;
|
|
spec.face_slope = REVET_FACE_SLOPE;
|
|
// 보호공은 기슭막이 바닥의 세굴 방지 구조 — 낙차고(기슭막이 높이)에 종속한다.
|
|
if (height !== null && height > 0) {
|
|
spec.apron_length_m = round(height * APRON_LENGTH_FACTOR, 3);
|
|
spec.apron_thickness_m = APRON_THICKNESS_M;
|
|
}
|
|
return spec;
|
|
}
|
|
|
|
/** 관 1개소의 세트 제원(관 + 유입·유출 기슭막이 + 보호공). */
|
|
function culvertSet(
|
|
options: Record<string, unknown>,
|
|
registry: CulvertRegistryDefaults,
|
|
): CulvertSetSpec {
|
|
const defaults = registry.pipe ?? {};
|
|
const diameterMm = num(options.pipe_diameter_mm, num(defaults.pipe_diameter_mm, 1000.0));
|
|
const diameterM = pipeDiameterM(diameterMm);
|
|
return {
|
|
type: "pipe",
|
|
pipe_kind: text(options.pipe_kind) ?? text(defaults.pipe_kind),
|
|
diameter_m: diameterM,
|
|
min_cover_m: MIN_PIPE_COVER_M,
|
|
// 관 기슭막이 기초 축 — **유입·유출 한 칸으로** 받는다. 한 공사에서 한쪽만 기초유로
|
|
// 하는 일은 드물어 폼을 둘로 늘리지 않았다(2026-09-09). 실무가 갈라 쓰는 것이
|
|
// 확인되면 그때 나눌 것.
|
|
foundation: text(options.revet_foundation) ?? text(defaults.revet_foundation),
|
|
inlet: sideSpec(options, defaults, "inlet", diameterM),
|
|
outlet: sideSpec(options, defaults, "outlet", diameterM),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 독립 기슭막이 한쪽 벽 제원 — 배관 벽과 **같은 옵션 키**(`{role}_revet_*`)를 쓴다.
|
|
* 역할 키가 없으면 B05 폼이 한 벌로 담던 옛 키(`length_m`…)로 폴백한다.
|
|
*/
|
|
function revetSide(values: Record<string, unknown>, role: string): CulvertSetSpec {
|
|
const height = num(values[`${role}_revet_height_m`], num(values.height_m, null));
|
|
const form = text(values[`${role}_revet_form`]) ?? text(values.form);
|
|
const spec: CulvertSetSpec = {
|
|
role,
|
|
structure: "기슭막이",
|
|
revet_form: form,
|
|
revet_height_m: height,
|
|
revet_length_m: num(values[`${role}_revet_length_m`], num(values.length_m, null)),
|
|
revet_before_m: num(values[`${role}_revet_before_m`], num(values.before_m, null)),
|
|
revet_after_m: num(values[`${role}_revet_after_m`], num(values.after_m, null)),
|
|
face_slope: REVET_FACE_SLOPE,
|
|
};
|
|
if (height !== null && height > 0) {
|
|
spec.apron_length_m = round(height * APRON_LENGTH_FACTOR, 3);
|
|
spec.apron_thickness_m = APRON_THICKNESS_M;
|
|
}
|
|
return spec;
|
|
}
|
|
|
|
/** 독립 기슭막이 1개소 — 배관 세트 모양이되 **관을 숨긴다**(hidden_pipe). */
|
|
function revetSet(values: Record<string, unknown>): CulvertSetSpec {
|
|
return {
|
|
type: "pipe",
|
|
hidden_pipe: true,
|
|
side: String(values.side ?? "양쪽"),
|
|
tiers: Math.trunc(num(values.tiers, 1.0) || 1),
|
|
// 기초 축(기초유/기초버림) — 터파기 그림·물량이 같이 쓴다. 비어 있으면 안 그린다.
|
|
foundation: text(values.foundation),
|
|
pipe_kind: null,
|
|
diameter_m: 0.3,
|
|
min_cover_m: 0.0,
|
|
inlet: revetSide(values, "inlet"),
|
|
outlet: revetSide(values, "outlet"),
|
|
};
|
|
}
|
|
|
|
/** 관경(㎜) → 관 지름(m). 파이썬 `_culvert_set`·`_ford_set` 과 같은 반올림. */
|
|
export function pipeDiameterM(diameterMm: number | null | undefined): number {
|
|
return round((num(diameterMm, 1000.0) ?? 1000.0) / 1000.0, 3);
|
|
}
|
|
|
|
/** BOX암거 도로 진행 방향 길이 = 내공 폭 + 측벽 두 장. */
|
|
export function boxSpanM(innerWidthM: number): number {
|
|
return innerWidthM + 2 * FORD_WALL_THICKNESS_M;
|
|
}
|
|
|
|
/** 날개벽이 만드는 바닥판 연장량 = 길이 × cos(벌어짐각). 안 세우면 0. */
|
|
export function wingSlabExtendM(
|
|
installed: boolean,
|
|
lengthM: number | null | undefined,
|
|
angleDeg: number | null | undefined,
|
|
): number {
|
|
if (!installed) return 0;
|
|
const extend = (lengthM ?? 0) * Math.cos(((angleDeg ?? 45) * Math.PI) / 180);
|
|
return round(Math.max(extend, 0.0), 3);
|
|
}
|
|
|
|
/**
|
|
* 날개벽 한쪽 제원 + 그 각도가 만드는 바닥판 연장량.
|
|
* 연장량 = 길이 × cos(각도) — 각도는 관축(계류 방향) 기준 벌어짐각이다.
|
|
*/
|
|
function wingSpec(
|
|
values: Record<string, unknown>,
|
|
defaults: Record<string, unknown>,
|
|
side: string,
|
|
): CulvertSetSpec {
|
|
const prefix = `wing_${side}`;
|
|
const install = values[prefix] ?? defaults[prefix];
|
|
const length = num(values[`${prefix}_length_m`], num(defaults[`${prefix}_length_m`], 0.0));
|
|
const angle = num(values[`${prefix}_angle_deg`], num(defaults[`${prefix}_angle_deg`], 45.0));
|
|
const height = num(values[`${prefix}_height_m`], num(defaults[`${prefix}_height_m`], 0.0));
|
|
const installed = String(install ?? "").trim() !== "없음";
|
|
return {
|
|
installed,
|
|
height_m: height,
|
|
length_m: length,
|
|
angle_deg: angle,
|
|
slab_extend_m: wingSlabExtendM(installed, length, angle ?? 0),
|
|
};
|
|
}
|
|
|
|
/** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 파이썬 `_defaulted`(2026-09-14 브레인 판정 ②). */
|
|
function defaulted(
|
|
values: Record<string, unknown>,
|
|
fields: ReadonlyArray<[string, string, unknown]>,
|
|
): string[] {
|
|
return fields
|
|
.filter(([key, , used]) => used !== null && used !== undefined && text(values[key]) === null)
|
|
.map(([, label, used]) => `${label} ${String(used)}`);
|
|
}
|
|
|
|
/** 날개벽 한쪽의 기본값 칸 — 파이썬 `_wing_defaulted`. */
|
|
function wingDefaulted(
|
|
values: Record<string, unknown>,
|
|
wing: CulvertSetSpec,
|
|
side: string,
|
|
): string[] {
|
|
const name = side === "in" ? "유입" : "유출";
|
|
const prefix = `wing_${side}`;
|
|
const fields: Array<[string, string, unknown]> = [
|
|
[prefix, `${name} 날개벽`, wing.installed ? "있음" : "없음"],
|
|
];
|
|
if (wing.installed) {
|
|
fields.push(
|
|
[`${prefix}_height_m`, `${name} 날개벽 높이`, wing.height_m],
|
|
[`${prefix}_length_m`, `${name} 날개벽 길이`, wing.length_m],
|
|
[`${prefix}_angle_deg`, `${name} 날개벽 각도`, wing.angle_deg],
|
|
);
|
|
}
|
|
return defaulted(values, fields);
|
|
}
|
|
|
|
/** 세월교 1개소의 세트 제원(관 + 양측 측벽 + 바닥판 + 날개벽 연장). */
|
|
function fordSet(
|
|
values: Record<string, unknown>,
|
|
registry: CulvertRegistryDefaults,
|
|
): CulvertSetSpec {
|
|
const defaults = registry.ford_bridge ?? {};
|
|
const diameterMm = num(values.pipe_diameter_mm, num(defaults.pipe_diameter_mm, 1000.0));
|
|
const width = num(values.ford_width_m, num(defaults.ford_width_m, null));
|
|
const count = num(values.pipe_count, num(defaults.pipe_count, null));
|
|
const depth = num(values.ford_height_m, null);
|
|
const kind = text(values.pipe_kind) ?? text(defaults.pipe_kind);
|
|
const span = width && width > 0 ? width : FORD_DEFAULT_WIDTH_M;
|
|
const pipeCount = count ? Math.max(Math.trunc(count), 1) : 1;
|
|
const wingIn = wingSpec(values, defaults, "in");
|
|
const wingOut = wingSpec(values, defaults, "out");
|
|
return {
|
|
type: "ford",
|
|
pipe_kind: kind,
|
|
diameter_m: pipeDiameterM(diameterMm),
|
|
pipe_count: pipeCount,
|
|
span_m: span,
|
|
// 월류 높이 — 구체 위 노면은 이만큼 낮게 앉는다. 없으면 0 = 내리지 않는다.
|
|
overflow_depth_m: depth && depth > 0 ? depth : 0.0,
|
|
slab_thickness_m: FORD_SLAB_THICKNESS_M,
|
|
wall_thickness_m: FORD_WALL_THICKNESS_M,
|
|
min_cover_m: MIN_PIPE_COVER_M,
|
|
wing_in: wingIn,
|
|
wing_out: wingOut,
|
|
defaulted: [
|
|
...defaulted(values, [
|
|
["pipe_kind", "관종", kind],
|
|
["pipe_diameter_mm", "관경", diameterMm],
|
|
["ford_width_m", "월류 폭", span],
|
|
["pipe_count", "배관 수량", pipeCount],
|
|
["ford_height_m", "월류 높이", 0],
|
|
]),
|
|
...wingDefaulted(values, wingIn, "in"),
|
|
...wingDefaulted(values, wingOut, "out"),
|
|
],
|
|
};
|
|
}
|
|
|
|
/** 물넘이포장 1개소 — 구조물이 아니라 **파인 노면**이라 형상이 다르다. */
|
|
function fordPavementSet(
|
|
values: Record<string, unknown>,
|
|
registry: CulvertRegistryDefaults,
|
|
): CulvertSetSpec {
|
|
const defaults = registry.ford_pavement ?? {};
|
|
const width = num(values.ford_width_m, num(defaults.ford_width_m, null));
|
|
const depth = num(values.ford_height_m, null);
|
|
const span = width && width > 0 ? width : FORD_PAVEMENT_DEFAULT_WIDTH_M;
|
|
return {
|
|
type: "ford_pavement",
|
|
span_m: span,
|
|
// 노선 중심에서 잰 깊이. 없으면 화면이 파임을 그리지 않는다(수치를 지어내지 않는다).
|
|
depth_m: depth && depth > 0 ? depth : null,
|
|
slope_pct: num(values.ford_slope_pct, null),
|
|
defaulted: defaulted(values, [["ford_width_m", "월류 폭", span]]),
|
|
};
|
|
}
|
|
|
|
/** BOX암거 1개소의 세트 제원(구체 + 날개벽 연장). */
|
|
function boxSet(
|
|
values: Record<string, unknown>,
|
|
registry: CulvertRegistryDefaults,
|
|
): CulvertSetSpec {
|
|
const defaults = registry.box_culvert ?? {};
|
|
const innerWidth = num(values.body_width_m, num(defaults.body_width_m, 2.0));
|
|
const innerHeight = num(values.body_height_m, num(defaults.body_height_m, 2.0));
|
|
const wall = FORD_WALL_THICKNESS_M;
|
|
const slab = FORD_SLAB_THICKNESS_M;
|
|
const wingIn = wingSpec(values, defaults, "in");
|
|
const wingOut = wingSpec(values, defaults, "out");
|
|
return {
|
|
type: "box",
|
|
inner_width_m: innerWidth || 2.0,
|
|
inner_height_m: innerHeight || 2.0,
|
|
wall_thickness_m: wall,
|
|
slab_thickness_m: slab,
|
|
top_thickness_m: slab,
|
|
cover_m: BOX_COVER_M,
|
|
span_m: boxSpanM(innerWidth || 2.0),
|
|
wing_in: wingIn,
|
|
wing_out: wingOut,
|
|
defaulted: [
|
|
...defaulted(values, [
|
|
["body_width_m", "본체 폭", innerWidth || 2.0],
|
|
["body_height_m", "본체 높이", innerHeight || 2.0],
|
|
]),
|
|
...wingDefaulted(values, wingIn, "in"),
|
|
...wingDefaulted(values, wingOut, "out"),
|
|
],
|
|
};
|
|
}
|
|
|
|
/** 관 지점 목록 → 누가거리(소수 2자리)별 세트 제원. */
|
|
export function buildCulvertSets(
|
|
points: readonly CulvertPipePoint[],
|
|
registry: CulvertRegistryDefaults,
|
|
): Map<number, CulvertSetSpec> {
|
|
const sets = new Map<number, CulvertSetSpec>();
|
|
for (const point of points) {
|
|
const chainage = num(point.chainage_m, null);
|
|
if (chainage === null) continue;
|
|
const values = (point.options ?? {}) as Record<string, unknown>;
|
|
const facility = point.facility || FACILITY_PIPE;
|
|
let spec: CulvertSetSpec;
|
|
if (facility === FACILITY_FORD_BRIDGE) spec = fordSet(values, registry);
|
|
else if (facility === FACILITY_BOX) spec = boxSet(values, registry);
|
|
else if (facility === FACILITY_FORD_PAVEMENT) spec = fordPavementSet(values, registry);
|
|
else if (facility === FACILITY_REVET) spec = revetSet(values);
|
|
else spec = culvertSet(values, registry);
|
|
sets.set(round(chainage, 2), spec);
|
|
}
|
|
return sets;
|
|
}
|
|
|
|
/** 측점 자료에 세트를 얹는다(파이썬 `attach_culvert_sets`). 얹은 개수를 돌려준다. */
|
|
export function attachCulvertSets(
|
|
sections: Array<Record<string, unknown>>,
|
|
sets: ReadonlyMap<number, CulvertSetSpec>,
|
|
): number {
|
|
if (sets.size === 0) return 0;
|
|
let attached = 0;
|
|
// ⚠⚠ **관 자리와 측점 자리는 최대 0.5m 어긋난다 — 그것이 설계다**(2026-09-09 실측).
|
|
// 측점을 만들 때 정수 미터가 같은 격자 측점이 있으면 그리로 스냅한다(횡단 파일명이
|
|
// 정수 미터라 두 측점이 한 파일을 덮어쓰는 것을 막는 가드). 관 440.241 은 측점 440.0 위에 선다.
|
|
// ⇒ 0.02m 로만 보면 그런 관은 어느 측점에도 안 붙어 **그림도 수량도 통째로 빠진다.**
|
|
// ⇒ **가장 가까운 측점 하나**는 거리와 무관하게 그 관의 자리로 본다(짝: 파이썬 `attach_culvert_sets`).
|
|
const owner = new Map<number, number>();
|
|
for (const [pipeChainage] of sets) {
|
|
let nearest: number | null = null;
|
|
for (const section of sections) {
|
|
const value = num(section.chainage_m, null);
|
|
if (value === null) continue;
|
|
if (nearest === null || Math.abs(value - pipeChainage) < Math.abs(nearest - pipeChainage)) {
|
|
nearest = value;
|
|
}
|
|
}
|
|
if (nearest !== null) owner.set(pipeChainage, nearest);
|
|
}
|
|
for (const section of sections) {
|
|
const chainage = num(section.chainage_m, null);
|
|
if (chainage === null) continue;
|
|
for (const [pipeChainage, spec] of sets) {
|
|
// 연동 대상 종류만 폭의 절반까지 옆 측점에 걸친다.
|
|
let reach = CHAINAGE_TOLERANCE_M;
|
|
if (SPAN_LINKED_TYPES.has(String(spec.type))) reach += (num(spec.span_m, 0) ?? 0) / 2;
|
|
if (Math.abs(chainage - pipeChainage) <= reach || owner.get(pipeChainage) === chainage) {
|
|
// ⚠ 스펙에 **그 시설이 놓인 누가거리**를 함께 얹는다(2026-09-08, 짝: 파이썬
|
|
// `attach_culvert_sets`). 세트는 폭의 절반까지 옆 측점에도 붙으므로, 이것이 없으면
|
|
// 소비처가 「소유 측점」을 못 가려 **같은 시설을 여러 측점에서 센다**.
|
|
section[SECTION_KEYS[String(spec.type)] ?? "culvert"] = {
|
|
...spec,
|
|
chainage_m: pipeChainage,
|
|
};
|
|
attached += 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return attached;
|
|
}
|
|
|
|
/** 레지스트리 응답(타입 목록) → 기본값 표. 옵션 정의가 유일한 기본값 출처다. */
|
|
export function registryDefaults(
|
|
types: ReadonlyArray<{
|
|
type_id: string;
|
|
options: ReadonlyArray<{ key: string; default: unknown }>;
|
|
}>,
|
|
): CulvertRegistryDefaults {
|
|
const table: CulvertRegistryDefaults = {};
|
|
for (const type of types) {
|
|
const entry: Record<string, unknown> = {};
|
|
for (const option of type.options) entry[option.key] = option.default;
|
|
table[type.type_id] = entry;
|
|
}
|
|
return table;
|
|
}
|