- 정의 「상세 내용」 = 폭만 · depth_m 표시 끔(옛 구간 값 = 기본값으로 읽음 · 새 기본 1 m)
- 짝: BermSpec 쪽마다 깊이 · side_berm ↔ sideBerm · 설계 결과 berm_depth_{side}_m 되싣기(파이썬 ↔ TS)
- 저장: patch berm_depth_{side}_m(≥ 0) · 재계산 살림 목록 짝(RD ↔ CR) · 세션 bermdepth
- 서버 [저장] 다시 셈 = 구조물 정본 소단 구간(load_berm_spans) + 측점 깊이 · 구간 지우면 소단 없음
- 고름: 소단 수평 단 = 높이만(▲ 0 에서 멈춤 · ▼ 사면 끝 앞 멈춤 · ◀▶ 막힘) · 사면 = 가로 · 노면 = 종단
- 고름을 조각 키(갈래 + 쪽 + 차례)로 기억 — 옮겨 다시 그려도 같은 조각
- 새 시험 test_194_1_berm_depth + helper · test_178_2 · test_54_26 고침
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
135 lines
5.4 KiB
TypeScript
135 lines
5.4 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_Cross_Design_Session.ts
|
|
* 횡단 카드 버튼(지반유형·단면유형·측구·포장·2단 비탈) 선택의 **세션 보관소**.
|
|
*
|
|
* 왜 생겼나(2026-09-06 사용자 확정) — 예전에는 버튼을 누를 때마다
|
|
* `POST …/sections/{route}/cross-design` 이 나가 서버가 계산하고 **바로 저장**했다.
|
|
* 조작은 캐시에 쌓이고 [저장]·[확정]에서만 정본으로 나가야 한다는 규칙에 어긋난다.
|
|
* 계산은 브라우저가 하고(`common_util_cross_design.ts`), 선택값은 여기 담긴다.
|
|
*
|
|
* 담는 것은 **사용자가 고른 것만**이다 — 계산 결과(면적·설계선)는 담지 않는다.
|
|
* ========================================================================== */
|
|
|
|
import { readState, writeState } from "../A00_Common/b_page_state";
|
|
|
|
/** 카드에서 고를 수 있는 값 한 벌 — 서버 `CrossDesignRequest` 와 같은 이름을 쓴다. */
|
|
export interface CrossDesignChoice {
|
|
ground_type: string;
|
|
section_mode: string;
|
|
ditch_side?: string | null;
|
|
ditch_type?: string | null;
|
|
paved?: boolean;
|
|
two_stage_slope?: boolean;
|
|
/** 측구를 둘지 **사용자가 정한 선택**(`null` = 자동 판정). 결과(`ditch_enabled`)가 아니다.
|
|
* ⚠ 이 칸이 없으면 측구 토글이 눌려도 캐시에 안 담겨 **화면에서 안 돈다**(2026-09-09). */
|
|
ditch_choice?: boolean | null;
|
|
}
|
|
|
|
type ChoiceMap = Record<string, CrossDesignChoice>;
|
|
|
|
/** 측점 키 — 암 경계선 저장소와 같은 규칙(0.01m 단위). */
|
|
const keyOf = (chainageM: number): string => chainageM.toFixed(2);
|
|
|
|
function readAll(projectId: string, routeId: number): ChoiceMap {
|
|
return readState<ChoiceMap>("crossdesign", projectId, routeId) ?? {};
|
|
}
|
|
|
|
/** 이 측점의 선택값(없으면 null). */
|
|
export function readCrossDesignChoice(
|
|
projectId: string,
|
|
routeId: number,
|
|
chainageM: number,
|
|
): CrossDesignChoice | null {
|
|
return readAll(projectId, routeId)[keyOf(chainageM)] ?? null;
|
|
}
|
|
|
|
/** 선택값을 세션에 담는다. 같은 측점의 앞선 값은 덮어쓴다. */
|
|
export function writeCrossDesignChoice(
|
|
projectId: string,
|
|
routeId: number,
|
|
chainageM: number,
|
|
choice: CrossDesignChoice,
|
|
): void {
|
|
const all = readAll(projectId, routeId);
|
|
all[keyOf(chainageM)] = choice;
|
|
writeState("crossdesign", all, projectId, routeId);
|
|
}
|
|
|
|
/** [저장]·[확정]이 patch 로 실을 목록 — 누가거리(숫자) → 선택값. */
|
|
export function crossDesignChoices(
|
|
projectId: string | null,
|
|
routeId: number | null,
|
|
): Map<number, CrossDesignChoice> {
|
|
const choices = new Map<number, CrossDesignChoice>();
|
|
if (!projectId || routeId === null) return choices;
|
|
for (const [key, value] of Object.entries(readAll(projectId, routeId))) {
|
|
const chainage = Number(key);
|
|
if (Number.isFinite(chainage) && value) choices.set(chainage, value);
|
|
}
|
|
return choices;
|
|
}
|
|
|
|
/* ── 설계선 쉬프트 = 쪽마다 노견 확장(m · ≥ 0 · 139-6) — 카드 버튼 선택과 따로 담음(버튼 선택이
|
|
* 통째로 덮어써도 안 지워지게). [저장]·[확정] 이 patch 로 싣고, 브라우저 재계산이 입력으로 씀. */
|
|
export type ShoulderSide = "left" | "right";
|
|
export type ShoulderExtends = Map<number, Partial<Record<ShoulderSide, number>>>;
|
|
|
|
type SideState = "shoulderext" | "bermdepth";
|
|
|
|
/** 누가거리(숫자) → 쪽마다 값(세션에 있는 쪽만). */
|
|
function readSides(name: SideState, projectId: string | null, routeId: number | null) {
|
|
const out: ShoulderExtends = new Map();
|
|
if (!projectId || routeId === null) return out;
|
|
const raw = readState<Record<string, Partial<Record<ShoulderSide, number>>>>(
|
|
name,
|
|
projectId,
|
|
routeId,
|
|
);
|
|
for (const [key, value] of Object.entries(raw ?? {})) {
|
|
const chainage = Number(key);
|
|
if (Number.isFinite(chainage) && value) out.set(chainage, value);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function writeSide(
|
|
name: SideState,
|
|
projectId: string,
|
|
routeId: number,
|
|
chainageM: number,
|
|
side: ShoulderSide,
|
|
valueM: number,
|
|
): void {
|
|
const all =
|
|
readState<Record<string, Partial<Record<ShoulderSide, number>>>>(name, projectId, routeId) ??
|
|
{};
|
|
all[keyOf(chainageM)] = { ...(all[keyOf(chainageM)] ?? {}), [side]: Math.max(valueM, 0) };
|
|
writeState(name, all, projectId, routeId);
|
|
}
|
|
|
|
/** 누가거리(숫자) → 쪽마다 노견 확장(세션에 있는 쪽만). */
|
|
export const readShoulderExtends = (projectId: string | null, routeId: number | null) =>
|
|
readSides("shoulderext", projectId, routeId);
|
|
|
|
/** 한 측점 한 쪽 노견 확장을 담는다(0 = 되돌림 · 저장 때 정본의 옛 값을 지움). */
|
|
export const writeShoulderExtend = (
|
|
projectId: string,
|
|
routeId: number,
|
|
chainageM: number,
|
|
side: ShoulderSide,
|
|
valueM: number,
|
|
) => writeSide("shoulderext", projectId, routeId, chainageM, side, valueM);
|
|
|
|
/* ── 소단 쪽마다 옮긴 깊이(노면 끝 아래 m · ≥ 0 · 194-1 · 횡단도 ▲▼) — 노견 확장과 같은 꼴.
|
|
* [저장]·[확정] 이 patch 로 싣고(서버가 다시 셈), 브라우저 재계산이 입력으로 씀. */
|
|
export const readBermDepths = (projectId: string | null, routeId: number | null) =>
|
|
readSides("bermdepth", projectId, routeId);
|
|
|
|
export const writeBermDepth = (
|
|
projectId: string,
|
|
routeId: number,
|
|
chainageM: number,
|
|
side: ShoulderSide,
|
|
depthM: number,
|
|
) => writeSide("bermdepth", projectId, routeId, chainageM, side, depthM);
|