Files
Aislo/B06_Section/B06_Section_Api_Culvert_Options.ts
T
eomsangdonandClaude Opus 5 b1ba37b004 refactor(b06): 관 목록을 담는 자리를 한 벌로 합침
B05 는 관 목록 전체를 세션 스냅샷으로, B06 은 바뀐 것만 따로 담고 있어 담는 꼴이 둘이었음.
저장 순서를 맞춰 결과는 같게 했으나 자리가 둘인 한 같은 사고가 다시 남.

B06 의 추가·삭제·이동을 모두 B05 가 쓰는 스냅샷(pipes)에 쓰도록 바꾸고, B06 전용 예약
경로(culvertedit 세션·queueAdd·queueRemove·queueMove)를 걷어 냄. 옛 세션에 남아 있던
이동 예약만 저장 때 한 번 흘려보내게 읽기를 남김.

이제 관을 넣고 빼고 옮기는 길이 좌측 폼·목록·종단 우클릭·측점선 끌기 모두 한 함수이고,
담기는 자리도 한 곳임.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
2026-09-12 16:16:19 +09:00

135 lines
5.7 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 [저장]으로도 내보내야 하기 때문이다.
* ========================================================================== */
import {
fetchDetailPipePoints,
saveDetailPipePoints,
} from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import type { DetailPipeInput } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
/** 측점 하나에 얹을 옵션 조각 — 키는 `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 = window.sessionStorage.getItem(sessionKey);
return raw ? (JSON.parse(raw) as PendingMap) : {};
} catch {
return {};
}
}
function writePending(sessionKey: string | null, pending: PendingMap): void {
if (!sessionKey) return;
try {
if (Object.keys(pending).length === 0) window.sessionStorage.removeItem(sessionKey);
else window.sessionStorage.setItem(sessionKey, JSON.stringify(pending));
} catch {
/* 세션 저장 실패는 무시 — 값은 화면 캐시에 남아 있다. */
}
}
/**
* 세션에 쌓인 구간값을 `pipe_points.json`에 한 번에 쓴다. 예약분이 없으면 아무것도
* 하지 않는다. 성공하면 세션을 비우고, 실패하면 **그대로 두어** 다음 [저장]에서
* 다시 시도한다 — 조용히 잃으면 화면과 저장분이 갈린다.
*/
/** 예약된 이동 — 옛 측점 키 → 새 누가거리(m).
* ⚠ **이제 쓰는 곳은 없다**(2026-09-12 일원화: 관 목록은 B05·B06 이 같은 스냅샷을 쓴다).
* 옛 세션에 남아 있던 예약을 저장 때 한 번 흘려보내려고 읽기만 남겨 둔다. */
type PendingMoves = Record<string, number>;
function readMoves(sessionKey: string | null): PendingMoves {
if (!sessionKey) return {};
try {
const raw = window.sessionStorage.getItem(sessionKey);
return raw ? (JSON.parse(raw) as PendingMoves) : {};
} catch {
return {};
}
}
export async function flushCulvertOptions(
projectId: string | null,
sessionKey: string | null,
moveKey: string | null = null,
): Promise<void> {
const pending = readPending(sessionKey);
const moves = readMoves(moveKey);
if (!projectId || (Object.keys(pending).length === 0 && Object.keys(moves).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 key = keyOf(point.chainage_m);
const patch = pending[key];
const movedTo = moves[key];
if (!patch && movedTo === undefined) return input;
touched = true;
const next: DetailPipeInput = patch
? { ...input, options: { ...(input.options ?? {}), ...patch } }
: { ...input };
// 기준 측점 이동 — 관 위치가 바뀌면 서버가 세부 배수유역을 다시 나눠 응답한다.
if (movedTo !== undefined) next.chainage_m = movedTo;
return next;
});
if (touched) await saveDetailPipePoints(projectId, points);
writePending(sessionKey, {});
writePending(moveKey, {});
}
/**
* 저장기 하나를 만든다. `projectId`·`sessionKey`가 없으면 예약만 메모리에 남는다
* (조회 전용 상태). `onError`는 [저장] 시점 실패를 화면에 알리는 쪽이 구현한다.
*/
export function createCulvertOptionWriter(
projectId: () => string | null,
sessionKey: () => string | null,
onError?: (message: string) => void,
/** 이동 예약 세션 키 — 없으면 이동은 메모리에도 남지 않는다. */
moveKey: () => string | null = () => null,
): 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(), moveKey());
} catch (error) {
onError?.(error instanceof Error ? error.message : "배수관 구간값 저장 실패");
throw error;
}
},
};
}