Files
Aislo/B06_Section/B06_Section_Api_Culvert_Options.ts
T

205 lines
8.4 KiB
TypeScript

/* =============================================================================
* B06_Section_Api_Culvert_Options.ts
* B06 조정창에서 만진 **배수관 세트 구간값**(기슭막이·집수정 길이·전/후)을
* 원천인 `pipe_points.json`에 되돌려 쓴다(2026-08-24 사용자).
*
* 값을 B06 정본(design)에 따로 담지 않는 이유: 같은 숫자가 B05 배수관 카드에도
* 있어서 두 곳이 갈리면 어느 쪽이 참인지 알 수 없다. 원천은 하나여야 한다.
*
* 조작분은 **세션에만** 쌓고 영구저장은 [저장]·[확정]에서만 한다(CLAUDE.md 5장
* 조작·데이터 흐름 정책, 2026-08-29 사용자 확정). 종전 800ms 디바운스 자동 PUT은
* 폐지했다 — 저장을 누르지 않았는데 영구저장소가 바뀌면 [초기화]의 기준선이 흐려진다.
* 세션에 담는 이유는 B06에서 만진 값을 B05 [저장]으로도 내보내야 하기 때문이다.
*
* 관 지점 종류를 바꾸면(318-4) 그 측점에 쌓인 옛 종류 조정값은 뜻을 잃음 — 세션 몫은 바꾸는
* 자리에서 버리고(`forgetStationAdjusts`) 정본 몫은 [저장] 때 `structure_reset` 으로 비움.
* 열쇠 목록 `STRUCTURE_ADJUST_KEYS` = 서버 `B06_Section_Router_Design` 짝(거울 시험 test_318_kind_reset).
* ========================================================================== */
import {
fetchDetailPipePoints,
saveDetailPipePoints,
} from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import type { DetailPipeInput } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import {
readByKey,
readState,
routeStateKeys,
writeByKey,
writeState,
type StateName,
} from "../A00_Common/b_page_state";
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
import { forgetPreviewAdjusts } from "./B06_Section_Section_Drafts";
/** 측점 하나에 얹을 옵션 조각 — 키는 `pipe_points` 옵션 키 그대로.
* 값은 수치가 대부분이나 날개벽 설치("있음"/"없음")처럼 문자열도 있다. */
export type CulvertOptionPatch = Record<string, number | string>;
/** 세션에 쌓인 조각 — 측점(누가거리 2자리 문자열) → 옵션 조각. */
type PendingMap = Record<string, CulvertOptionPatch>;
export interface CulvertOptionWriter {
/** 측점 옵션을 세션에 예약한다. 같은 측점의 앞선 예약과는 합쳐진다. */
queue: (chainageM: number, patch: CulvertOptionPatch) => void;
/** 예약분을 즉시 내보낸다([저장]·[확정]에서만 부른다). */
flush: () => Promise<void>;
}
const keyOf = (chainageM: number): string => chainageM.toFixed(2);
function readPending(sessionKey: string | null): PendingMap {
if (!sessionKey) return {};
try {
const raw = readByKey(sessionKey);
return raw ? (JSON.parse(raw) as PendingMap) : {};
} catch {
return {};
}
}
/** 세션 저장 실패는 `writeByKey` 가 삼킴 — 값은 화면 캐시에 남아 있다. */
function writePending(sessionKey: string | null, pending: PendingMap): void {
if (sessionKey)
writeByKey(sessionKey, Object.keys(pending).length ? JSON.stringify(pending) : null);
}
/**
* 세션에 쌓인 구간값을 `pipe_points.json`에 한 번에 쓴다. 예약분이 없으면 아무것도
* 하지 않는다. 성공하면 세션을 비우고, 실패하면 **그대로 두어** 다음 [저장]에서
* 다시 시도한다 — 조용히 잃으면 화면과 저장분이 갈린다.
* 옛 「관 이동 예약」(culvertmove)은 읽지 않음 — 관 위치는 B05·B06 이 같은 관 목록 스냅샷으로만
* 옮김. 옛 세션 잔값을 저장 때 적용하면 관이 이웃 측점으로 튀어 시설 속성이 넘어감.
*/
export async function flushCulvertOptions(
projectId: string | null,
sessionKey: string | null,
): Promise<void> {
const pending = readPending(sessionKey);
if (!projectId || Object.keys(pending).length === 0) return;
const current = await fetchDetailPipePoints(projectId);
let touched = false;
const points: DetailPipeInput[] = current.pipe_points.map((point) => {
// 좌표는 서버가 다시 계산하므로 싣지 않는다(DetailPipeInput 규약).
const { lonlat: _lonlat, ...input } = point;
const patch = pending[keyOf(point.chainage_m)];
if (!patch) return input;
touched = true;
return { ...input, options: { ...(input.options ?? {}), ...patch } };
});
if (touched) await saveDetailPipePoints(projectId, points);
writePending(sessionKey, {});
}
/** 구조물(관 · 박스 · 세월교) 조정값 열쇠 — 종류를 바꾸면 버리는 몫 · 서버 짝. */
export const STRUCTURE_ADJUST_KEYS = [
"inlet_structure",
"basin_adjust",
"revet_adjust",
"ford_adjust",
"box_adjust",
"extra_wall_counts",
"extra_spans",
"revet_link_detached",
"revet_follow_grade",
] as const;
/** 측점 조정값 세션 — 열쇠가 누가거리(`b` · `:역할` 붙은 것 포함)로 시작함. */
const STATION_ADJUST_STATES: readonly StateName[] = [
"culvertopt",
"revetx",
"basinadjust",
"inletstruct",
"extrawall",
"extraspan",
"revetlink",
"fordadjust",
"boxadjust",
];
let reloadStationAdjusts: (() => void) | null = null;
/** 횡단 보기가 메모리 사본을 세션에서 다시 읽는 창구(한 페이지 하나 · 나중 것이 씀). */
export function setStationAdjustReload(reload: () => void): void {
reloadStationAdjusts = reload;
}
const atChainage = (key: string, chainageM: number): boolean => {
const head = /^b?(-?\d+(?:\.\d+)?)/.exec(key);
return head !== null && Math.abs(Number(head[1]) - chainageM) < PIPE_MATCH_M;
};
function dropAt(map: unknown, chainageM: number): void {
if (!map || typeof map !== "object") return;
const entries = map as Record<string, unknown>;
for (const key of Object.keys(entries)) if (atChainage(key, chainageM)) delete entries[key];
}
/**
* 관 지점 종류를 바꿈(318-4) — 그 측점 옛 조정값(세션 · 미리 뜬 횡단 사본)을 버리고, [저장] 때
* 정본에서도 비울 자리로 적음. 처음 종류로 되돌리면 그 자리는 지움(저장분 그대로).
*/
export function forgetStationAdjusts(
projectId: string | null,
chainageM: number,
was: string,
next: string,
): void {
if (!projectId || was === next) return;
for (const name of STATION_ADJUST_STATES)
for (const key of routeStateKeys(name, projectId)) {
const raw = readByKey(key);
if (raw === null) continue;
try {
const stored = JSON.parse(raw) as Record<string, unknown>;
if (name === "revetlink") {
dropAt(stored.detached, chainageM);
dropAt(stored.grade, chainageM);
} else dropAt(stored, chainageM);
writeByKey(key, JSON.stringify(stored));
} catch {
/* 손상된 세션 값은 그대로 — 읽는 쪽이 버림 */
}
}
const marks = readState<Record<string, string>>("kindreset", projectId) ?? {};
const hit = Object.keys(marks).find((key) => atChainage(key, chainageM));
const first = hit === undefined ? was : marks[hit];
if (hit !== undefined) delete marks[hit];
if (next !== first) marks[chainageM.toFixed(2)] = first;
writeState("kindreset", Object.keys(marks).length ? marks : null, projectId);
forgetPreviewAdjusts(projectId, chainageM, STRUCTURE_ADJUST_KEYS);
reloadStationAdjusts?.();
}
/** [저장] 때 옛 조정값을 정본에서 비울 측점 — 종류를 바꾼 관 자리(누가거리). */
export function kindChangedChainages(projectId: string | null): number[] {
return Object.keys(readState<Record<string, string>>("kindreset", projectId) ?? {}).map(Number);
}
/**
* 저장기 하나를 만든다. `projectId`·`sessionKey`가 없으면 예약만 메모리에 남는다
* (조회 전용 상태). `onError`는 [저장] 시점 실패를 화면에 알리는 쪽이 구현한다.
*/
export function createCulvertOptionWriter(
projectId: () => string | null,
sessionKey: () => string | null,
onError?: (message: string) => void,
): CulvertOptionWriter {
return {
queue(chainageM, patch) {
const key = sessionKey();
const pending = readPending(key);
pending[keyOf(chainageM)] = { ...(pending[keyOf(chainageM)] ?? {}), ...patch };
writePending(key, pending);
},
async flush() {
try {
await flushCulvertOptions(projectId(), sessionKey());
} catch (error) {
onError?.(error instanceof Error ? error.message : "배수관 구간값 저장 실패");
throw error;
}
},
};
}