From 0be32b8c50ae60eb22a04a59c5ad392de189bafd Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 19 Oct 2026 09:06:54 +0900 Subject: [PATCH 1/2] =?UTF-8?q?refactor(472-4=20=E2=91=A1=20=EB=82=98?= =?UTF-8?q?=EB=88=94):=20B06=5FSection=5FRouter=5FDesign=20699=EC=A4=84=20?= =?UTF-8?q?=E2=80=94=20=ED=8F=AC=EC=9E=A5=20=C2=B7=20=EC=84=B8=EC=9B=94?= =?UTF-8?q?=EA=B5=90=20=EA=B5=AC=EA=B0=84=20=EC=9D=BD=EA=B8=B0=20=C2=B7=20?= =?UTF-8?q?=ED=8C=90=EC=A0=95=EC=9D=84=20=5FRouter=5FDesign=5FRanges=20?= =?UTF-8?q?=EB=A1=9C(=EB=8F=99=EC=9E=91=20=EA=B0=99=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pavement_suggestions · pavement_ranges · ford_pavement_ranges · ford_surface_drops · ford_drop_at · paved_at 옮김 · Router_Design 이 다시 내보냄(부르는 곳 그대로) - 시험 test_349_0n · test_349_0p — 정본 읽기(load_culvert_sets · load_structures) 갈아 끼움 자리를 나눈 모듈로(까닭: 읽는 함수가 옮겨 감) - Router_Design 699 → 605줄 Co-Authored-By: Claude Opus 5.5 --- B06_Section/B06_Section_Router_Design.py | 116 ++---------------- .../B06_Section_Router_Design_Ranges.py | 113 +++++++++++++++++ .../test_349_0n_cross_enforce_mirror.py | 4 +- .../tester/test_349_0p_fractional_station.py | 4 +- 4 files changed, 130 insertions(+), 107 deletions(-) create mode 100644 B06_Section/B06_Section_Router_Design_Ranges.py diff --git a/B06_Section/B06_Section_Router_Design.py b/B06_Section/B06_Section_Router_Design.py index c06197696..29231cc3e 100644 --- a/B06_Section/B06_Section_Router_Design.py +++ b/B06_Section/B06_Section_Router_Design.py @@ -1,18 +1,13 @@ """B06 라우터의 횡단 설계 파일 읽기와 프리뷰 계산. -짝(349-0n): `pavement_ranges` · `ford_surface_drops` · `paved_at` · `ford_drop_at` ↔ -`B06_Section/logic/B06_Section_Cross_Enforce.ts` · 거울 시험 `test_349_0n_cross_enforce_mirror.py`. +포장 · 세월교 구간 읽기 · 판정(짝 349-0n) = `B06_Section_Router_Design_Ranges.py` · 다시 내보냄. """ import json -import logging from pathlib import Path from typing import Any from B05_Profile.B05_Profile_Engine_Sections import find_cross_file -from B05_Profile.B05_Profile_Structures_Repository import load_structures -from B05_Profile.B05_Profile_Structures_Schema import structure_type_map -from B06_Section.B06_Section_Engine_Culvert import load_culvert_sets from B06_Section.B06_Section_Engine_Design import ( ROUTE_CARRY_KEYS, compute_cross_design, @@ -21,6 +16,16 @@ from B06_Section.B06_Section_Engine_Design import ( stored_tilt, ) from B06_Section.B06_Section_Engine_Tilt import tilt_args, tilt_ranges + +# 포장 · 세월교 구간(나눈 파일) — 이 모듈 이름으로 다시 내보냄(부르는 곳 · 시험 갈아 끼움 그대로). +from B06_Section.B06_Section_Router_Design_Ranges import ( # noqa: F401 + ford_drop_at, + ford_pavement_ranges, + ford_surface_drops, + paved_at, + pavement_ranges, + pavement_suggestions, +) from common_util.common_util_cross_berm import ( BERM_DEFAULT_DEPTH_M, BERM_DEFAULT_INTERVAL_M, @@ -29,15 +34,8 @@ from common_util.common_util_cross_berm import ( BermSpec, ) from common_util.common_util_route_profile import design_elevation_from_longitudinal -from common_util.common_util_structure_station import EDGE_TOLERANCE_M from config.config_system import STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M -logger = logging.getLogger(__name__) - -# 세월교 노면 하강이 걸리는 측점 판정 허용 오차(m) — 세트 부착(`attach_culvert_sets`)과 -# 같은 기준이라 "구체가 그려진 측점"과 "노면이 내려간 측점"이 어긋나지 않는다. -_FORD_DROP_TOLERANCE_M = 0.02 - PREVIEW_DESIGN_FIELDS = ( "ground_type", "roadbed_width_m", @@ -52,98 +50,6 @@ PREVIEW_DESIGN_FIELDS = ( ) -def pavement_suggestions(longitudinal: dict[str, Any]) -> dict[float, bool]: - mapping: dict[float, bool] = {} - stations = longitudinal.get("stations") - for station in stations if isinstance(stations, list) else []: - suggested = station.get("pavement_suggested") - if isinstance(suggested, bool): - mapping[round(float(station.get("chainage_m", 0.0)), 3)] = suggested - return mapping - - -def pavement_ranges(project_root: Path) -> list[tuple[float, float]]: - """포장으로 볼 누가거리 구간 — 구조물 정본 G군 + 물넘이포장(포장 필수). - - 포장 지정은 **사용자가 구간으로 준다**(2026-08-28 사용자 확정). 종단경사 자동 판정은 - 더 이상 포장을 켜지 않는다 — 경고 표기(`pavement_suggested`)만 남는다. - 물넘이포장은 콘크리트 등으로 노면을 만드는 시설이라 범위 안이 항상 포장이다. - 읽기 실패는 비치명 — 빈 목록이면 사용자 지정이 없는 것과 같다. - """ - ranges: list[tuple[float, float]] = [] - try: - types = structure_type_map() - for structure in load_structures(str(project_root))[1]: - definition = types.get(structure.type_id) - if definition is None or definition.group != "G": - continue - anchor = structure.chainage_m - start = structure.start_m if structure.start_m is not None else anchor - end = structure.end_m if structure.end_m is not None else anchor - if start is None or end is None: - continue - ranges.append((min(float(start), float(end)), max(float(start), float(end)))) - except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다 - logger.exception("B06 포장 구간을 읽지 못했습니다 (사용자 지정 없음으로 본다)") - return ranges + ford_pavement_ranges(project_root) - - -def ford_pavement_ranges(project_root: Path | None) -> list[tuple[float, float]]: - """물넘이포장 범위(기준 ± 월류 폭/2) — 포장 필수 · 측구 없음(286-2). 판정은 `paved_at`.""" - if project_root is None: - return [] - sets = load_culvert_sets(project_root).items() - halves = [ - (at, float(s.get("span_m") or 0.0) / 2.0) - for at, s in sets - if s.get("type") == "ford_pavement" - ] - return [(at - half, at + half) for at, half in halves] - - -def ford_surface_drops(project_root: Path | None) -> list[tuple[float, float]]: - """세월교가 앉은 측점의 노면 하강량 — [(누가거리, 월류 높이 m)]. - - 구체 위 노면은 월류 높이만큼 낮게 앉는다(2026-08-30 사용자 확정). 계획고를 그만큼 - 내려 잡아야 측벽·바닥판이 맞고 절·성토 면적도 따라온다. 포장 구간과 같은 방식으로 - 관 지점 정본에서 읽는다 — 읽기 실패는 비치명(하강 없음으로 본다). - """ - if project_root is None: - return [] - drops: list[tuple[float, float]] = [] - try: - for chainage, spec in load_culvert_sets(project_root).items(): - if spec.get("type") != "ford": - continue - depth = float(spec.get("overflow_depth_m") or 0.0) - if depth > 0: - drops.append((chainage, depth)) - except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다 - logger.exception("B06 세월교 월류 높이를 읽지 못했습니다 (노면 하강 없음으로 본다)") - return drops - - -def ford_drop_at(chainage_m: float, drops: list[tuple[float, float]]) -> float: - """그 측점의 노면 하강량(m). 구체가 앉은 측점만 — 붙는 기준은 세트 부착과 같다.""" - for at, depth in drops: - if abs(chainage_m - at) <= _FORD_DROP_TOLERANCE_M: - return depth - return 0.0 - - -# 범위 끝 허용오차(m) — 같은 자리 거리 한 곳(323-7) -_PAVED_EDGE_TOLERANCE_M = EDGE_TOLERANCE_M - - -def paved_at(chainage_m: float, ranges: list[tuple[float, float]]) -> bool: - """이 측점을 포장으로 볼 것인가 — 노면공 · 물넘이 범위 안만 포장, 밖은 비포장(저장값 무시). - - 브라우저 짝 = `B06_Section_Cross_Refresh.ts` `pavedBySpecs`(같은 규칙 · 138-1). - """ - tol = _PAVED_EDGE_TOLERANCE_M - return any(start - tol <= chainage_m <= end + tol for start, end in ranges) - - # 사용자 조작값 — 다시 계산해도 그대로 승계한다. **이 목록이 서버의 유일한 한 벌이다.** # # 브라우저 쪽 짝은 `B06_Section_Cross_Refresh.ts` 의 `PRESERVED_KEYS` 다(+ `status`· diff --git a/B06_Section/B06_Section_Router_Design_Ranges.py b/B06_Section/B06_Section_Router_Design_Ranges.py new file mode 100644 index 000000000..5dd62deef --- /dev/null +++ b/B06_Section/B06_Section_Router_Design_Ranges.py @@ -0,0 +1,113 @@ +"""B06 포장 · 세월교 구간 읽기와 판정 — `B06_Section_Router_Design.py` 에서 나눔(472-4 · 동작 같음). + +짝(349-0n): `pavement_ranges` · `ford_surface_drops` · `paved_at` · `ford_drop_at` ↔ +`B06_Section/logic/B06_Section_Cross_Enforce.ts` · 거울 시험 `test_349_0n_cross_enforce_mirror.py`. +부르는 쪽은 `B06_Section_Router_Design` 이름으로 씀(다시 내보냄 · 시험이 그 이름을 갈아 끼움). +""" + +import logging +from pathlib import Path +from typing import Any + +from B05_Profile.B05_Profile_Structures_Repository import load_structures +from B05_Profile.B05_Profile_Structures_Schema import structure_type_map +from B06_Section.B06_Section_Engine_Culvert import load_culvert_sets +from common_util.common_util_structure_station import EDGE_TOLERANCE_M + +logger = logging.getLogger(__name__) + +# 세월교 노면 하강이 걸리는 측점 판정 허용 오차(m) — 세트 부착(`attach_culvert_sets`)과 +# 같은 기준이라 "구체가 그려진 측점"과 "노면이 내려간 측점"이 어긋나지 않는다. +_FORD_DROP_TOLERANCE_M = 0.02 + + +def pavement_suggestions(longitudinal: dict[str, Any]) -> dict[float, bool]: + mapping: dict[float, bool] = {} + stations = longitudinal.get("stations") + for station in stations if isinstance(stations, list) else []: + suggested = station.get("pavement_suggested") + if isinstance(suggested, bool): + mapping[round(float(station.get("chainage_m", 0.0)), 3)] = suggested + return mapping + + +def pavement_ranges(project_root: Path) -> list[tuple[float, float]]: + """포장으로 볼 누가거리 구간 — 구조물 정본 G군 + 물넘이포장(포장 필수). + + 포장 지정은 **사용자가 구간으로 준다**(2026-08-28 사용자 확정). 종단경사 자동 판정은 + 더 이상 포장을 켜지 않는다 — 경고 표기(`pavement_suggested`)만 남는다. + 물넘이포장은 콘크리트 등으로 노면을 만드는 시설이라 범위 안이 항상 포장이다. + 읽기 실패는 비치명 — 빈 목록이면 사용자 지정이 없는 것과 같다. + """ + ranges: list[tuple[float, float]] = [] + try: + types = structure_type_map() + for structure in load_structures(str(project_root))[1]: + definition = types.get(structure.type_id) + if definition is None or definition.group != "G": + continue + anchor = structure.chainage_m + start = structure.start_m if structure.start_m is not None else anchor + end = structure.end_m if structure.end_m is not None else anchor + if start is None or end is None: + continue + ranges.append((min(float(start), float(end)), max(float(start), float(end)))) + except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다 + logger.exception("B06 포장 구간을 읽지 못했습니다 (사용자 지정 없음으로 본다)") + return ranges + ford_pavement_ranges(project_root) + + +def ford_pavement_ranges(project_root: Path | None) -> list[tuple[float, float]]: + """물넘이포장 범위(기준 ± 월류 폭/2) — 포장 필수 · 측구 없음(286-2). 판정은 `paved_at`.""" + if project_root is None: + return [] + sets = load_culvert_sets(project_root).items() + halves = [ + (at, float(s.get("span_m") or 0.0) / 2.0) + for at, s in sets + if s.get("type") == "ford_pavement" + ] + return [(at - half, at + half) for at, half in halves] + + +def ford_surface_drops(project_root: Path | None) -> list[tuple[float, float]]: + """세월교가 앉은 측점의 노면 하강량 — [(누가거리, 월류 높이 m)]. + + 구체 위 노면은 월류 높이만큼 낮게 앉는다(2026-08-30 사용자 확정). 계획고를 그만큼 + 내려 잡아야 측벽·바닥판이 맞고 절·성토 면적도 따라온다. 포장 구간과 같은 방식으로 + 관 지점 정본에서 읽는다 — 읽기 실패는 비치명(하강 없음으로 본다). + """ + if project_root is None: + return [] + drops: list[tuple[float, float]] = [] + try: + for chainage, spec in load_culvert_sets(project_root).items(): + if spec.get("type") != "ford": + continue + depth = float(spec.get("overflow_depth_m") or 0.0) + if depth > 0: + drops.append((chainage, depth)) + except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다 + logger.exception("B06 세월교 월류 높이를 읽지 못했습니다 (노면 하강 없음으로 본다)") + return drops + + +def ford_drop_at(chainage_m: float, drops: list[tuple[float, float]]) -> float: + """그 측점의 노면 하강량(m). 구체가 앉은 측점만 — 붙는 기준은 세트 부착과 같다.""" + for at, depth in drops: + if abs(chainage_m - at) <= _FORD_DROP_TOLERANCE_M: + return depth + return 0.0 + + +# 범위 끝 허용오차(m) — 같은 자리 거리 한 곳(323-7) +_PAVED_EDGE_TOLERANCE_M = EDGE_TOLERANCE_M + + +def paved_at(chainage_m: float, ranges: list[tuple[float, float]]) -> bool: + """이 측점을 포장으로 볼 것인가 — 노면공 · 물넘이 범위 안만 포장, 밖은 비포장(저장값 무시). + + 브라우저 짝 = `B06_Section_Cross_Refresh.ts` `pavedBySpecs`(같은 규칙 · 138-1). + """ + tol = _PAVED_EDGE_TOLERANCE_M + return any(start - tol <= chainage_m <= end + tol for start, end in ranges) diff --git a/resources/tester/test_349_0n_cross_enforce_mirror.py b/resources/tester/test_349_0n_cross_enforce_mirror.py index f1508b96a..5834a690a 100644 --- a/resources/tester/test_349_0n_cross_enforce_mirror.py +++ b/resources/tester/test_349_0n_cross_enforce_mirror.py @@ -26,6 +26,7 @@ if str(ROOT) not in sys.path: import B05_Profile.B05_Profile_Structures_Repository as structures_repo # noqa: E402 import B06_Section.B06_Section_Engine_Tilt as tilt_mod # noqa: E402 import B06_Section.B06_Section_Router_Design as design_mod # noqa: E402 +import B06_Section.B06_Section_Router_Design_Ranges as ranges_mod # noqa: E402 from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402 from B06_Section.B06_Section_Engine_Culvert import culvert_sets_from # noqa: E402 from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402 @@ -157,7 +158,8 @@ def server(r) -> list[dict]: """서버 [확정] 차례 그대로 — 저장분(보정 전) → 포장 구간 → 세월교 하강.""" patch = pytest.MonkeyPatch() sets = culvert_sets_from(parse_pipe_points(PIPES)) - for module in (design_mod, tilt_mod): + # 구간 읽기는 472-4 에서 `_Router_Design_Ranges` 로 나눔 — 정본 읽기를 그 모듈에서 갈아 끼움. + for module in (ranges_mod, tilt_mod): patch.setattr(module, "load_culvert_sets", lambda _root: sets) patch.setattr(module, "load_structures", lambda _root: (0, STRUCTURES)) patch.setattr(structures_repo, "load_structures", lambda _root: (0, STRUCTURES)) diff --git a/resources/tester/test_349_0p_fractional_station.py b/resources/tester/test_349_0p_fractional_station.py index 156bee2c9..3e2edb595 100644 --- a/resources/tester/test_349_0p_fractional_station.py +++ b/resources/tester/test_349_0p_fractional_station.py @@ -27,6 +27,7 @@ from test_349_0n_cross_enforce_mirror import OFFSETS, _stations, _z # noqa: E40 import B05_Profile.B05_Profile_Structures_Repository as structures_repo # noqa: E402 import B06_Section.B06_Section_Engine_Tilt as tilt_mod # noqa: E402 import B06_Section.B06_Section_Router_Design as design_mod # noqa: E402 +import B06_Section.B06_Section_Router_Design_Ranges as ranges_mod # noqa: E402 from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402 from B06_Section.B06_Section_Engine_Culvert import culvert_sets_from # noqa: E402 from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402 @@ -109,7 +110,8 @@ def server(r) -> list[dict]: """서버 [확정] — 저장분(입력 없는 측점은 config 암 경계) → 포장 구간 → 세월교 하강.""" patch = pytest.MonkeyPatch() sets = culvert_sets_from(parse_pipe_points(PIPES)) - for module in (design_mod, tilt_mod): + # 구간 읽기는 472-4 에서 `_Router_Design_Ranges` 로 나눔 — 정본 읽기를 그 모듈에서 갈아 끼움. + for module in (ranges_mod, tilt_mod): patch.setattr(module, "load_culvert_sets", lambda _root: sets) patch.setattr(module, "load_structures", lambda _root: (0, STRUCTURES)) patch.setattr(structures_repo, "load_structures", lambda _root: (0, STRUCTURES)) From 4752cd223fcf3ea9e86c4f8295ddcd78641af7ad Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 19 Oct 2026 09:08:37 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(472-4=20=E2=91=A1=20Node=20=EC=9E=85?= =?UTF-8?q?=EA=B5=AC):=20cross.design=20=EC=8B=A4=ED=96=89=EA=B8=B0=20?= =?UTF-8?q?=E2=80=94=20TS=20computeCrossDesign=20=EC=9D=84=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90=20=EB=AC=B6=EC=9D=8C=EC=9C=BC=EB=A1=9C(=ED=8C=8C?= =?UTF-8?q?=EC=9D=B4=EC=8D=AC=20=ED=82=A4=20=EB=B0=9B=EC=9D=8C=20=C2=B7=20?= =?UTF-8?q?=EB=AA=BB=20=EC=84=B8=EC=9A=B4=20=EC=9E=90=EB=A6=AC=20error)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 아직 부르는 곳 없음 — 다음 커밋에서 [저장] · [확정] 재계산이 씀 - 고정 자료 견줌: [저장] 59 회 · 노선 체인 179 회 — 면적 · 계획고 · 노면 끝 0 차 · 설계선 점 13 · 34 곳 1e-4 m(반올림 동점) Co-Authored-By: Claude Opus 5.5 --- A00_Common/design/logic/design_calc_node.ts | 76 ++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/A00_Common/design/logic/design_calc_node.ts b/A00_Common/design/logic/design_calc_node.ts index 1708bcc3f..130e0546d 100644 --- a/A00_Common/design/logic/design_calc_node.ts +++ b/A00_Common/design/logic/design_calc_node.ts @@ -9,7 +9,9 @@ * context?: { earthwork_conversion · natural_spoil_min_ground_slope · * haul_equipment_limits · 채집석 · 구조물 잔토(B08) } · * excavation?: { pipes: [{ diameter_mm }] · walls: [{ height_m · average_thickness_m · - * has_foundation }] } } } + * has_foundation }] } · + * cross_design?: { standard: 표준 횡단(config 위에 저장분 덮은 한 벌) · + * stations: [{ samples · design_elevation_m · options: 파이썬 `compute_cross_design` 키 }] } } } * 출력 { results: { [id]: 값 }, available: [id…] } * — 차례는 요청 차례가 아니라 기댐 차례(누가토량 앞에 구조물 면적을 상세에 얹음 · * server-calc 묶음과 같은 차례). @@ -17,6 +19,9 @@ * ========================================================================== */ import { readFileSync, writeFileSync } from "node:fs"; +import type { BermSpec } from "@util/common_util_cross_berm"; +import { computeCrossDesign } from "@util/common_util_cross_design"; +import type { CrossDesignOptions, CrossGroundSample } from "@util/common_util_cross_design"; 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"; @@ -58,6 +63,63 @@ interface CalcInputs { has_foundation?: boolean; }>; }; + cross_design?: { + standard: CrossDesignOptions["standard"]; + stations?: Array<{ + samples: CrossGroundSample[]; + design_elevation_m: number | null; + options: Record; + }>; + }; +} + +/** 파이썬 `compute_cross_design` 키 → TS 짝 키(거울 시험 test_b06_cross_design_mirror 표와 같음). */ +const CROSS_OPTION_KEYS: Record = { + ground_type: "groundType", + section_mode: "sectionMode", + ditch_side: "ditchSide", + ditch_type: "ditchType", + paved: "paved", + rock_boundary_offset_m: "rockBoundaryOffsetM", + two_stage_slope: "twoStageSlope", + cut_slope_ratio: "cutSlopeRatio", + ditch_enabled: "ditchEnabled", + ditch_choice: "ditchChoice", + surface_drop_m: "surfaceDropM", + plan_radius_m: "planRadiusM", + curve_outer_side: "curveOuterSide", + curve_widening_m: "curveWideningM", + shoulder_extend_left_m: "shoulderExtendLeftM", + shoulder_extend_right_m: "shoulderExtendRightM", + tilted: "tilted", + tilt_pct: "tiltPct", +}; + +/** 파이썬 BermSpec(NamedTuple → dict) → TS BermSpec. */ +function bermOf(value: unknown): BermSpec | null { + if (!value || typeof value !== "object") return null; + const berm = value as Record; + return { + widthM: berm.width_m as number, + intervalM: berm.interval_m as number, + slopeDeg: berm.slope_deg as number, + depthM: berm.depth_m as number, + depthLeftM: berm.depth_left_m ?? null, + depthRightM: berm.depth_right_m ?? null, + }; +} + +function crossOptions( + raw: Record, + standard: CrossDesignOptions["standard"], +): CrossDesignOptions { + const options: Record = { standard }; + for (const [key, value] of Object.entries(raw)) { + if (key === "berm") options.berm = bermOf(value); + else if (key in CROSS_OPTION_KEYS) options[CROSS_OPTION_KEYS[key]] = value; + else throw new Error(`모르는 횡단 설계 키: ${key}`); + } + return options as unknown as CrossDesignOptions; } /** 한 번 돌림 — 같은 id 는 한 번만 셈(memo) · 앞 id 는 `need` 로. */ @@ -117,6 +179,18 @@ function runDesignCalc(run: readonly string[], inputs: CalcInputs) { ), })), }), + // 측점마다 TS 짝(`computeCrossDesign`) — 못 세우면 그 자리 { error }(파이썬은 ValueError 로 되던짐). + "cross.design": () => { + const batch = inputs.cross_design; + return (batch?.stations ?? []).map((station) => { + try { + const options = crossOptions(station.options, batch!.standard); + return computeCrossDesign(station.samples, station.design_elevation_m, options); + } catch (error) { + return { error: error instanceof Error ? error.message : String(error) }; + } + }); + }, // 편집 전 정본 — 계획선 샘플은 저장분 그대로(corridor 묶음과 같은 값) · 측점 둘 미만이면 null. "corridor.mesh": () => { const detail = inputs.detail;