Files
Aislo/A00_Common/design/design_pipeline_cross.ts
T
eomsangdonandClaude Opus 5.5 7efee22ab4 feat(365-2): 횡단 고침 → 묶음 cross 입력 · 입력 지문 고리(최대 3바퀴)
- 횡단 입력 = 부를 때마다 저장 상세 + 세션 고침(지반 유형 · 측구 · 2단 경사 · 암 경계 · 암 경사 · 측점 열쇠) + 표준 — 상세 객체마다 기억 걷음
- B06 고침(DesignSync 다시 셈 끝 · 암 경계) 뒤 pipelineCross — 노선 초안 동안 B06 고침이 카드(새 노선 상세)에 보임
- 고리 — 열 때 · 페이지 들어올 때 입력(세션 · 가게)을 묶음 입력(넘긴 값 · follow 뒤 값)과 견줌 · 다르면 한 바퀴 · 끝난 뒤 다시 · 3바퀴 넘으면 「계산 불안정」 한 줄
- 시험 test_365_page_pipeline(초안 동안 고침 카드 · 같은 입력 0바퀴 · 세션만 바뀜 1바퀴 · 불안정 3바퀴 알림 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
2026-10-17 12:08:22 +09:00

142 lines
6.8 KiB
TypeScript

/* =============================================================================
* A00_Common/design/design_pipeline_cross.ts
* 계산 묶음 cross 입력 모음(PLAN 346-2 붙임) — 저장 노선 상세의 측점마다 횡단 입력 · 얹힌 시설 제원을
* **측점 열쇠**로 모음. 노선이 바뀌어도 같은 열쇠 측점(격자 번호 · 관 id · 구조물 id)이 그 입력을 받음.
* 열쇠 = 저장 노선을 stations 단계와 같은 함수로 짚은 측점(`buildStations`)과 누가거리로 짝지음 —
* 묶음 측점과 같은 열쇠 규칙(관 = 저장 관 id).
*
* 넘기는 칸 = 서버 노선 저장(`CARRY_INPUT_KEYS`)과 같은 일곱(지반 유형 · 측구 종류 · 측구 선택 · 2단 경사 ·
* 암 경계 · 암 경사 · 소단) + 얹힌 시설 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강). 단면 유형 · 측구
* 방향은 새 지형이 정함(`logic/B05_Profile_Pipeline_Detail.ts` `crossWithUphill`) · 노견 확장 · 소단
* 깊이는 안 넘김. 값 = 상세에 얹힌 설계 위에 세션 고침(측점 열쇠 칸 · 지반 유형 · 측구 · 2단 경사 · 암 경계 ·
* 암 경사) — 노선 초안 동안 B06 고침은 저장 상세에 안 얹혀도 입력에 들어감(365-2). 부를 때마다 모음.
* ========================================================================== */
import { readState } from "../b_page_state";
import type { CrossSection, SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
import {
effectiveStandardCross,
readRockBoundaryDefault,
} from "../../B06_Section/B06_Section_UI_Standard_Panel";
import type { CrossInput, SectionSpecs } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
import type { PipelineStation } from "./logic/design_pipeline_types";
export interface PipelineCrossSource {
input: CrossInput;
/** 측점 열쇠 → 얹힌 시설 제원. */
specs: Map<string, SectionSpecs>;
}
const SPEC_KEYS = [
"structure",
"culvert",
"ford",
"box",
"ford_pavement",
"revetment",
"pavement",
"underdrain",
] as const;
/** 같은 측점으로 보는 누가거리 차(m) — 측점 열쇠 글 자릿수(0.01 m). */
const SAME_M = 0.01;
const finite = (value: unknown): number | null =>
typeof value === "number" && Number.isFinite(value) ? value : null;
/** 측점 하나의 넘기는 입력(cross 단계 `CrossStationInput` 꼴). */
function carried(section: CrossSection): Record<string, unknown> {
const design = (section.design ?? {}) as unknown as Record<string, unknown>;
const berm = design.berm as
{ width_m: number; interval_m: number; slope_deg: number; depth_m?: number } | undefined;
const cutSlope = finite(design.cut_slope_ratio_user);
const drop = Number(section.ford?.overflow_depth_m);
return {
...(typeof design.ground_type === "string" ? { groundType: design.ground_type } : {}),
...(typeof design.ditch_type === "string" ? { ditchType: design.ditch_type } : {}),
ditchChoice: typeof design.ditch_choice === "boolean" ? design.ditch_choice : null,
twoStageSlope: Boolean(design.two_stage_slope),
rockBoundaryOffsetM: finite(design.rock_boundary_offset_m),
cutSlopeRatio: cutSlope !== null && cutSlope > 0 ? cutSlope : null,
berm: berm
? {
widthM: berm.width_m,
intervalM: berm.interval_m,
slopeDeg: berm.slope_deg,
depthM: berm.depth_m,
}
: null,
paved: Boolean(section.pavement?.length) || Boolean(section.ford_pavement),
tilted: Boolean(section.ford_pavement || section.underdrain),
tiltPct: section.ford_pavement?.slope_pct ?? section.underdrain?.slope_pct ?? null,
surfaceDropM: drop > 0 ? drop : 0,
};
}
/** 세션 칸 하나를 측점 열쇠마다 — 옛 열쇠(누가거리 · `at:`)는 읽는 쪽이 옮긴 뒤에 들어옴(빼고 봄). */
function sessionCells(
name: "crossdesign" | "rockb" | "cutslope",
projectId: string,
routeId: number,
): Array<[string, unknown]> {
const raw = readState<Record<string, unknown>>(name, projectId, routeId);
if (!raw || typeof raw !== "object") return [];
return Object.entries(raw).filter(([key]) => key.includes(":") && !key.startsWith("at:"));
}
/** 세션 횡단 고침 → 측점 열쇠마다 넘기는 입력 조각(`carried` 와 같은 칸 이름). */
function sessionCarried(projectId: string, routeId: number): CrossInput["byKey"] {
const out: CrossInput["byKey"] = {};
for (const [key, value] of sessionCells("crossdesign", projectId, routeId)) {
const choice = (value ?? {}) as Record<string, unknown>;
const entry = (out[key] ??= {});
if (typeof choice.ground_type === "string") entry.groundType = choice.ground_type;
if (typeof choice.ditch_type === "string") entry.ditchType = choice.ditch_type;
if ("ditch_choice" in choice)
entry.ditchChoice = typeof choice.ditch_choice === "boolean" ? choice.ditch_choice : null;
if ("two_stage_slope" in choice) entry.twoStageSlope = Boolean(choice.two_stage_slope);
}
for (const [key, value] of sessionCells("rockb", projectId, routeId))
if (finite(value) !== null) (out[key] ??= {}).rockBoundaryOffsetM = value;
for (const [key, value] of sessionCells("cutslope", projectId, routeId)) {
const ratio = finite(value);
if (ratio !== null) (out[key] ??= {}).cutSlopeRatio = ratio > 0 ? ratio : null;
}
return out;
}
/** 저장 상세 + 저장 노선 측점(열쇠) + 세션 고침(`routeId` 칸) → cross 입력 · 시설 제원 — 표준단면 ·
* 상세가 없으면 null. */
export function pipelineCrossSource(
projectId: string,
detail: SectionDetailResponse | null,
savedStations: ReadonlyArray<PipelineStation>,
routeId: number | null = null,
): PipelineCrossSource | null {
const standard = detail && effectiveStandardCross(projectId);
if (!detail || !standard) return null;
const byKey: CrossInput["byKey"] = {};
const specs = new Map<string, SectionSpecs>();
for (const section of detail.cross_sections) {
let best: PipelineStation | null = null;
for (const station of savedStations) {
const gap = Math.abs(station.chainageM - section.chainage_m);
if (gap < SAME_M && (!best || gap < Math.abs(best.chainageM - section.chainage_m)))
best = station;
}
if (!best || byKey[best.key]) continue;
byKey[best.key] = carried(section);
const own: Record<string, unknown> = {};
for (const name of SPEC_KEYS) if (section[name] !== undefined) own[name] = section[name];
specs.set(best.key, own as SectionSpecs);
}
if (routeId !== null)
for (const [key, entry] of Object.entries(sessionCarried(projectId, routeId)))
byKey[key] = { ...byKey[key], ...entry };
return {
// 암 경계 기본값 = 서버 config(화면이 문맥에서 기억) — 입력 없는 측점(초안 새 측점)도 서버처럼(349-0p)
input: { standard, byKey, rockBoundaryDefaultM: readRockBoundaryDefault(projectId) },
specs,
};
}