/* ============================================================================= * B06_Section_UI_Page_Link_Session.ts * 연동 기슭막이 옵션(연동 해제·종단경사 반영)의 **세션 보관** — 측점 제어기 * (`_UI_Page_Station_Controls.ts`)에서 700줄 제한으로 분리했다(2026-08-25). * 두 플래그는 한 세션 항목에 같이 담아 확정 전 리로드에도 살아남는다. * * 측점별 조작값을 담는 **한 겹 맵**도 여기서 낸다(`createSessionMap`, 2026-09-02) — * 반폭·기슭막이 4축·유입 형식·집수정·추가 벽 수·단별 구간값이 같은 모양(읽기 → 항목 * 검증 → 담기 / 쓰기 → 통째 직렬화)이라 제어기마다 짝을 두던 것을 하나로 모았다. * ========================================================================== */ import type { CrossSection } from "./B06_Section_Api_Fetch"; import type { RevetKey } from "./B06_Section_UI_Cross_Culvert"; import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types"; import { computeCardCulvert, culvertLinkFor, culvertOwnerFor, culvertReach, culvertWallsStandAt, ownerAppliedAdjusts, type ExtraWallControl, type InletStructureControl, type RevetLinkControl, type RevetOffsetControl, } from "./B06_Section_UI_Cross_Culvert_Wire"; import { culvertCardState } from "./B06_Section_UI_Cross_View_Structure"; import type { PipeRevetChoice } from "../A00_Common/design/design_structure_pipe_calc"; /** 세션에 담기는 측점별 조작값 한 겹 맵. */ export interface SessionMap { values: Map; /** 세션값 읽기 — 맵을 비우고 다시 채운다. */ load: () => void; /** 맵을 통째로 세션에 쓴다. */ persist: () => void; } /** * 세션 보관 맵을 만든다. `accept` 가 항목마다 값을 검증·정규화해 돌려주고, * `undefined` 를 내면 그 항목은 버린다(손상값·옛 형식 정리 자리). */ export function createSessionMap( sessionKey: () => string | null, accept: (value: unknown) => V | undefined, ): SessionMap { const values = new Map(); return { values, load(): void { values.clear(); const key = sessionKey(); if (!key) return; try { const raw = window.sessionStorage.getItem(key); if (!raw) return; const parsed = JSON.parse(raw) as Record; Object.entries(parsed).forEach(([entry, value]) => { const taken = accept(value); if (taken !== undefined) values.set(entry, taken); }); } catch { /* 손상된 세션 값은 무시 — 정본·기본값으로 재시작. */ } }, persist(): void { const key = sessionKey(); if (!key) return; try { window.sessionStorage.setItem(key, JSON.stringify(Object.fromEntries(values))); } catch { /* 세션 저장 실패는 무시(용량 초과 등) — 값은 메모리에 유지된다. */ } }, }; } export interface LinkFlagMaps { detached: Map; followGrade: Map; load: () => void; persist: () => void; } export function createLinkFlagSession(sessionKey: () => string | null): LinkFlagMaps { const detached = new Map(); const followGrade = new Map(); return { detached, followGrade, load(): void { detached.clear(); followGrade.clear(); const key = sessionKey(); if (!key) return; try { const raw = window.sessionStorage.getItem(key); if (!raw) return; const parsed = JSON.parse(raw) as { detached?: Record; grade?: Record; }; Object.entries(parsed.detached ?? {}).forEach(([at, value]) => detached.set(at, !!value)); Object.entries(parsed.grade ?? {}).forEach(([at, value]) => followGrade.set(at, !!value)); } catch { /* 손상된 세션 값은 무시 — 정본·기본값으로 재시작. */ } }, persist(): void { const key = sessionKey(); if (!key) return; window.sessionStorage.setItem( key, JSON.stringify({ detached: Object.fromEntries(detached), grade: Object.fromEntries(followGrade), }), ); }, }; } /** * 연동을 푸는 순간(카드 [연동] · 좌측 [연동] 둘 다 · 139-2) — 그 카드가 그리던 벽마다 4축을 이 * 측점 값으로 굳힘: 위치(x · d) = 소유 측점 적용값(물려받던 값) · 높이 · 형태 = 그 카드가 그린 값. * d 가 차서 이 측점 지형으로 자동 자리 · 관 정수 맞춤을 다시 타지 않음(튐 없음). 연동 중에 부름. */ export function freezeLinkedWalls( section: CrossSection, sections: readonly CrossSection[], c: { revetOffset: RevetOffsetControl; inletStructure?: InletStructureControl; extraWalls?: ExtraWallControl; revetLink: RevetLinkControl; }, ): void { const link = section.culvert ? undefined : culvertLinkFor(section, sections, () => null); const owner = link && ownerAppliedAdjusts(link.source, c.revetOffset, c.inletStructure, c.extraWalls); if (!link || !owner) return; const card = computeCardCulvert( section, section.samples, null, c.revetOffset, c.inletStructure, c.extraWalls, link, c.revetLink, ); const drawn = card ? culvertCardState(card).wallSpecs : new Map(); const roles: Array<[RevetKey, WallAdjust]> = [ ["inlet", owner.inlet], ["outlet", owner.outlet], ...owner.extras.map((a, i): [RevetKey, WallAdjust] => [`extra${i}` as RevetKey, a]), ...owner.basinExtras.map((a, i): [RevetKey, WallAdjust] => [`bextra${i}` as RevetKey, a]), ]; for (const [role, applied] of roles) { const wall = drawn.get(role); if (!wall) continue; const round3 = (v: number): number => Math.round(v * 1000) / 1000; c.revetOffset.update(section.chainage_m, role, { x: round3(applied.x), d: applied.d == null ? null : round3(applied.d), h: wall.height, m: wall.form, }); } } /** * 좌측 관 보호공 [연동] · [경사](54-30) — 옛 연동 제어 그대로 읽고 씀. 연동 = 벽이 서는 옆 측점이 * 있을 때만 · 켬 = 그 측점이 모두 연동 · 끄면 모두 따로. 경사 = 보호공 구간이 있을 때만. * 풀 때 굳히기 · 다시 걸 때 지우기는 `setLinked` 가 함(139-2 · 카드 단추와 한 길). */ export function pipeRevetChoiceBridge(revetLink: RevetLinkControl) { const linkCards = (owner: CrossSection, sections: readonly CrossSection[]) => sections.filter((s) => culvertOwnerFor(s, sections) === owner && culvertWallsStandAt(owner, s)); return { read: (owner: CrossSection, sections: readonly CrossSection[]): PipeRevetChoice => { const cards = linkCards(owner, sections); return { linked: cards.length ? cards.every((s) => revetLink.linkedFor(s)) : null, grade: culvertReach(owner) ? revetLink.followGradeFor(owner) : null, }; }, write: ( owner: CrossSection, sections: readonly CrossSection[], kind: "link" | "grade", on: boolean, ): void => { if (kind === "grade") revetLink.setFollowGrade(owner, on); else linkCards(owner, sections).forEach((s) => revetLink.setLinked(s.chainage_m, on)); }, }; }