- A00_Common/design/design_store.ts — useDesignStore(projectId, routeId) get · set · subscribe
(structures · pipes · alignmentDraft · sectionDetail · selection) · 밑은 b_page_state 등록 키
(selection 등록 · alignmentDraft 는 옛 키 감쌈 — 브레인 판정 B)
- A00_Common/design/design_save.ts — saveDesign(ctx, {confirm}) 한 줄기:
관 → 구조물 → 계획고 → 암거 옵션 → 오르막 → 횡단 · B05 [임시저장] · B06 [저장] · [확정]이 같은 차례
- 계획고 초안 가게 노선마다 한 벌(B05 Profile_Edit 주인 · B06 Grade_Edit 는 useProfileEditStore)
- 관 목록 주인 = 가게 pipes(Pipes_Draft 창구)
- 시험: test_design_store_save 추가 · save_channels · save_recompute · save_roundtrip 새 차례로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
149 lines
6.1 KiB
TypeScript
149 lines
6.1 KiB
TypeScript
/* =============================================================================
|
|
* A00_Common/design/design_store.ts
|
|
* B05 · B06 설계 상태 **한 곳**(계획서 24-1) — 두 보기가 같은 값을 같은 창구로 읽고 쓴다.
|
|
*
|
|
* const store = useDesignStore(projectId, routeId);
|
|
* store.get("pipes") · store.set("pipes", next) · store.subscribe("pipes", fn)
|
|
*
|
|
* 밑은 지금 캐시 그대로다 — 값마다 `b_page_state` 등록 키 하나(새 키는 등록부에 먼저).
|
|
* 가게는 값을 따로 들고 있지 않는다: 읽을 때마다 캐시를 읽고, 쓸 때 캐시에 쓴 뒤 듣는 쪽을
|
|
* 부른다. 그래서 가게를 거치지 않고 캐시를 고친 옛 자리가 남아 있어도 읽는 값은 갈리지 않는다
|
|
* (다만 그 자리는 듣는 쪽을 못 부른다 — 쓰는 길은 가게로 옮길 것).
|
|
*
|
|
* ⚠ `alignmentDraft` 만 등록부 밖 옛 키(`b05-profile-alignment-draft:{노선}`)를 감싼다 —
|
|
* B05 되돌리기(`_Profile_History`)가 `b05-` 접두어 키만 사진 찍기 때문이다. 등록부 이관은
|
|
* 24-2c 가 되돌리기와 함께 한다(브레인 판정 9-29).
|
|
* ========================================================================== */
|
|
|
|
import { readByKey, readState, writeByKey, writeState } from "../b_page_state";
|
|
import type { StructureInstance } from "../../B05_Profile/B05_Profile_Api_Structures";
|
|
import type { DetailPipeInput } from "../../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
import type { AlignmentEdits } from "../../B05_Profile/B05_Profile_UI_Profile_Alignment";
|
|
import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
|
|
|
|
/** 한 페이지의 두 보기(계획서 24-1). */
|
|
export type DesignView = "profile" | "cross";
|
|
|
|
/** 고른 자리 — 측점 · 구조물. 둘 다 비면 고른 것이 없다. */
|
|
export interface DesignSelection {
|
|
chainageM: number | null;
|
|
structureId: string | null;
|
|
}
|
|
|
|
/** 가게가 드는 값 — 없으면 null(= 만진 적 없음 · 저장분을 쓸 것). */
|
|
export interface DesignStoreValues {
|
|
/** 미저장 구조물 목록(정본 `structures.json`). */
|
|
structures: StructureInstance[] | null;
|
|
/** 미저장 관 목록(정본 `pipe_points.json`) — 주인은 이 가게 하나다. */
|
|
pipes: DetailPipeInput[] | null;
|
|
/** 미저장 계획고 편집(▲▼ · 곡선반경). 노선이 있어야 한다. */
|
|
alignmentDraft: AlignmentEdits | null;
|
|
/** 종횡단 상세 — 세션 사본. 화면이 함께 쥐는 객체는 `B06_Section_Section_Store`. */
|
|
sectionDetail: SectionDetailResponse | null;
|
|
/** 고른 측점 · 구조물. */
|
|
selection: DesignSelection | null;
|
|
}
|
|
|
|
export type DesignStoreKey = keyof DesignStoreValues;
|
|
|
|
export interface DesignStore {
|
|
get<K extends DesignStoreKey>(key: K): DesignStoreValues[K];
|
|
set<K extends DesignStoreKey>(key: K, value: DesignStoreValues[K]): void;
|
|
/** 가게로 쓸 때마다 부른다. 돌려받은 함수로 끊는다. */
|
|
subscribe<K extends DesignStoreKey>(
|
|
key: K,
|
|
fn: (value: DesignStoreValues[K]) => void,
|
|
): () => void;
|
|
}
|
|
|
|
/** 옛 계획고 초안 키 — 위 머리 주석 ⚠ 참조. */
|
|
export const ALIGNMENT_DRAFT_KEY_PREFIX = "b05-profile-alignment-draft";
|
|
|
|
interface Slot<T> {
|
|
/** 듣는 쪽을 묶는 이름 — 같은 캐시 칸이면 같은 이름. 칸을 못 정하면 null. */
|
|
id: string | null;
|
|
read(): T | null;
|
|
write(value: T | null): void;
|
|
}
|
|
|
|
function slotOf<K extends DesignStoreKey>(
|
|
key: K,
|
|
projectId: string | null,
|
|
routeId: number | null,
|
|
): Slot<DesignStoreValues[K]> {
|
|
type V = DesignStoreValues[K];
|
|
if (key === "alignmentDraft") {
|
|
const raw = routeId === null ? null : `${ALIGNMENT_DRAFT_KEY_PREFIX}:${routeId}`;
|
|
return {
|
|
id: raw,
|
|
read: () => {
|
|
const text = raw ? readByKey(raw) : null;
|
|
if (!text) return null;
|
|
try {
|
|
const parsed = JSON.parse(text) as Partial<AlignmentEdits>;
|
|
return {
|
|
station_offsets: parsed.station_offsets ?? {},
|
|
curve_radii: parsed.curve_radii ?? {},
|
|
} as V;
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
write: (value) => {
|
|
if (raw) writeByKey(raw, value === null ? null : JSON.stringify(value));
|
|
},
|
|
};
|
|
}
|
|
// 등록부 이름 — 가게 이름과 캐시 이름이 다른 것만 적는다.
|
|
const name = (
|
|
{
|
|
structures: "structures",
|
|
pipes: "pipes",
|
|
sectionDetail: "section-detail",
|
|
selection: "selection",
|
|
} as const
|
|
)[key as Exclude<DesignStoreKey, "alignmentDraft">];
|
|
const routeScoped = name === "section-detail";
|
|
if (!projectId || (routeScoped && routeId === null)) {
|
|
return { id: null, read: () => null, write: () => undefined };
|
|
}
|
|
const scopeRoute = routeScoped ? routeId : undefined;
|
|
return {
|
|
id: `${name}:${projectId}${routeScoped ? `:${routeId}` : ""}`,
|
|
read: () => readState<V>(name, projectId, scopeRoute),
|
|
write: (value) => writeState(name, value, projectId, scopeRoute),
|
|
};
|
|
}
|
|
|
|
const listeners = new Map<string, Set<(value: unknown) => void>>();
|
|
|
|
/**
|
|
* 그 프로젝트 · 노선의 가게. 부를 때마다 새로 만들어도 된다 — 값도 듣는 쪽도 가게 밖
|
|
* (캐시 · 모듈 표)에 있어서 같은 칸을 가리키면 같은 가게다.
|
|
*
|
|
* 노선을 모르면 `routeId = null` — 노선 칸(`alignmentDraft` · `sectionDetail`)은 비어 보이고
|
|
* 쓰기는 버려진다. 프로젝트를 모르면 프로젝트 칸이 그렇다.
|
|
*/
|
|
export function useDesignStore(projectId: string | null, routeId: number | null): DesignStore {
|
|
return {
|
|
get(key) {
|
|
return slotOf(key, projectId, routeId).read() as DesignStoreValues[typeof key];
|
|
},
|
|
set(key, value) {
|
|
const slot = slotOf(key, projectId, routeId);
|
|
slot.write(value);
|
|
if (slot.id) listeners.get(`${key}|${slot.id}`)?.forEach((fn) => fn(value));
|
|
},
|
|
subscribe(key, fn) {
|
|
const id = slotOf(key, projectId, routeId).id;
|
|
if (!id) return () => undefined;
|
|
const bucket = `${key}|${id}`;
|
|
const set = listeners.get(bucket) ?? new Set();
|
|
listeners.set(bucket, set);
|
|
const listener = fn as (value: unknown) => void;
|
|
set.add(listener);
|
|
return () => set.delete(listener);
|
|
},
|
|
};
|
|
}
|