- 상세 가게 forgetSectionMemory — 페이지 dispose 에서 메모리 · 받는 중 약속만 비움(세션 정본 그대로) · 다시 열기 = 새로고침 길 - 묶음 열 때 관 · 구조물 · 추가 측점 초안이 있으면 측점을 냄 — 새로고침 · 다시 열기 = 초안 측점(옛: 새로고침 뒤 관 초안 측점 빠짐) - 버린 노선 초안 차례 끝 · 조용히 연 차례 끝 = 가게 측점 · 종단 = 지금 상세 측점 · 저장 기준(갈아끼움 없음) - 화면 선만 바뀜(비정규 측점 도착) = 상세가 그 선 대비 낡았으면 cross 한 번 더(recrossIfStale) - 시험 test_272_detail_memory · test_272_drop_store_stations Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
206 lines
7.8 KiB
JavaScript
206 lines
7.8 KiB
JavaScript
/* 466(요구 272) 헬퍼 — 진짜 상세 가게 · 가게 잇기(vite ssr 묶음 · helper_272_detail_memory_entry.ts).
|
|
* node helper_272_detail_memory.mjs <묶음.mjs>
|
|
* ① 관 초안 갈아끼움 → [폐기] → 페이지 떠남(forgetSectionMemory) → 다시 읽기 = 세션 정본(새로고침 길)
|
|
* ② 묶음 출력 → 가게 — 조용한 차례 · 버린 차례 끝(shown) · 열고 맞춤 끝(opened) · 메아리
|
|
* 표준출력 JSON 한 벌. 짝 = test_272_detail_memory.py · test_272_drop_store_stations.py. */
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
const storage = () => {
|
|
const m = new Map();
|
|
return {
|
|
getItem: (k) => (m.has(k) ? m.get(k) : null),
|
|
setItem: (k, v) => m.set(k, String(v)),
|
|
removeItem: (k) => m.delete(k),
|
|
key: (i) => [...m.keys()][i] ?? null,
|
|
get length() {
|
|
return m.size;
|
|
},
|
|
clear: () => m.clear(),
|
|
};
|
|
};
|
|
globalThis.window = globalThis;
|
|
globalThis.sessionStorage = storage();
|
|
globalThis.localStorage = storage();
|
|
const any = new Proxy(function () {}, {
|
|
get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any),
|
|
apply: () => any,
|
|
construct: () => any,
|
|
});
|
|
globalThis.document = any;
|
|
globalThis.requestAnimationFrame = (run) => setTimeout(run, 0);
|
|
let fetches = 0; // 상세 GET 만 셈
|
|
globalThis.fetch = (url) => {
|
|
if (/\/sections\/\d+\/detail/.test(String(url))) fetches += 1;
|
|
return Promise.reject(new Error("서버 없음"));
|
|
};
|
|
|
|
const m = await import(pathToFileURL(process.argv[2]).href);
|
|
const P = "p272";
|
|
const R = 7;
|
|
localStorage.setItem("frd_current_project_id", P);
|
|
|
|
const section = (chainage_m, kind) => ({
|
|
station_id: `station_${String(Math.round(chainage_m * 1000)).padStart(12, "0")}`,
|
|
chainage_m,
|
|
label: "",
|
|
kind,
|
|
center_z: 100,
|
|
azimuth_deg: 90,
|
|
center_x: chainage_m,
|
|
center_y: 0,
|
|
frame: { left_xy: [0, 1] },
|
|
samples: [{ offset_m: 0, elevation_m: 100, valid: true }],
|
|
design: { design_elevation_m: 101 },
|
|
});
|
|
const saved = {
|
|
longitudinal: {
|
|
length_m: 60,
|
|
stations: [],
|
|
profile_alignment: { policy: { station_interval_m: 20 } },
|
|
},
|
|
cross_sections: [
|
|
section(0, "bp"),
|
|
section(20, "regular"),
|
|
section(40, "regular"),
|
|
section(60, "ep"),
|
|
],
|
|
};
|
|
const station = (key, chainageM, kind, x = chainageM) => ({
|
|
key,
|
|
chainageM,
|
|
x,
|
|
y: 0,
|
|
azimuthDeg: 90,
|
|
left: [0, 1],
|
|
kind,
|
|
id: kind === "pipe" ? key.slice(5) : null,
|
|
ground: { centerZ: 99, left: [{ offsetM: 0.5, z: 99.1 }], right: [{ offsetM: 0.5, z: 98.9 }] },
|
|
planRadiusM: null,
|
|
curveOuterSide: null,
|
|
curveWideningM: null,
|
|
});
|
|
const chainages = (detail) => detail?.cross_sections.map((s) => s.chainage_m) ?? null;
|
|
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
const shop = m.useDesignStore(P, null);
|
|
const out = {};
|
|
|
|
// ── ① 상세 메모리 ─────────────────────────────────────────────────────────
|
|
m.writeState("section-detail", saved, P, R);
|
|
const first = await m.loadSectionDetail(P, R); // 세션 한 벌 → 캐시(새로고침 길)
|
|
const keys = m.registeredKeys(P, R, first.cross_sections, m.stationIntervalOf(first));
|
|
const saveList = () => first.cross_sections.map((s, i) => station(keys[i], s.chainage_m, "grid"));
|
|
// 관 초안(30 m) — 묶음 측점이 늘어 갈아끼움(메모리만)
|
|
shop.set("pipes", [{ id: "p1", chainage_m: 30, facility: "pipe" }]);
|
|
const drafted = [...saveList()];
|
|
drafted.splice(2, 0, station("pipe:p1", 30, "pipe"));
|
|
const swap = await m.swapSectionStations(P, R, drafted);
|
|
const during = await m.loadSectionDetail(P, R);
|
|
out.memory = {
|
|
keys,
|
|
swapped: swap !== null,
|
|
during: chainages(during),
|
|
session: chainages(m.readState("section-detail", P, R)),
|
|
};
|
|
// [폐기] = 초안 칸 지움 — 메모리는 그대로(페이지가 떠나며 비움)
|
|
m.discardDesignEdits(P);
|
|
out.memory.afterDiscard = chainages(await m.loadSectionDetail(P, R));
|
|
out.memory.pipesAfterDiscard = shop.get("pipes");
|
|
m.forgetSectionMemory(P);
|
|
out.memory.peekAfterForget = m.peekSectionDetail(P, R);
|
|
const fetchesBefore = fetches;
|
|
const again = await m.loadSectionDetail(P, R);
|
|
out.memory.reopened = chainages(again);
|
|
out.memory.reopenedSame = again === during;
|
|
out.memory.canonAsked = fetches - fetchesBefore; // 새로고침과 같은 뒤 판 확인(433-16)
|
|
// 다른 프로젝트 · 다른 노선 메모리는 그대로
|
|
m.writeState("section-detail", saved, "other", R);
|
|
await m.loadSectionDetail("other", R);
|
|
m.forgetSectionMemory(P);
|
|
out.memory.otherKept = m.peekSectionDetail("other", R) !== null;
|
|
|
|
// ── ② 가게 잇기 ───────────────────────────────────────────────────────────
|
|
const fakePipe = () => {
|
|
const fns = [];
|
|
const outputs = {};
|
|
const inputs = {};
|
|
return {
|
|
generation: 0,
|
|
outputs,
|
|
inputs,
|
|
subscribe(fn) {
|
|
fns.push(fn);
|
|
return () => undefined;
|
|
},
|
|
output: (name) => outputs[name] ?? null,
|
|
input: (name) => inputs[name] ?? null,
|
|
emit(event) {
|
|
fns.forEach((fn) => fn(event));
|
|
},
|
|
};
|
|
};
|
|
await m.loadSectionDetail(P, R); // 다시 연 페이지 — 세션 정본
|
|
const pipe = fakePipe();
|
|
let quiet = -1;
|
|
const publisher = m.publishPagePipeline(pipe, shop, () => quiet);
|
|
const sets = [];
|
|
const stopSets = shop.subscribe("stations", (value) =>
|
|
sets.push(
|
|
value && chainages({ cross_sections: value.map((s) => ({ chainage_m: s.chainageM })) }),
|
|
),
|
|
);
|
|
const ground = (gen, list) => {
|
|
pipe.outputs.ground = { stations: list, longitudinal: { chainageM: [], z: [] } };
|
|
pipe.emit({ kind: "stage", stage: "ground", generation: gen, value: pipe.outputs.ground });
|
|
};
|
|
const profile = { tag: "saved-profile" };
|
|
// 열 때(조용한 차례) — 묶음 측점 ≠ 상세 측점(저장 상세에 없는 덧 측점 50) · 가게에 안 냄
|
|
const pipeline = [...saveList()];
|
|
pipeline.splice(3, 0, station("grid:x50", 50, "grid"));
|
|
pipe.generation = 1;
|
|
quiet = 1;
|
|
shop.set("stations", drafted); // 지난 페이지 초안 측점이 가게에 남음
|
|
sets.length = 0;
|
|
ground(1, pipeline);
|
|
out.store = { quietSets: sets.length };
|
|
pipe.outputs.profile = profile;
|
|
await publisher.opened(P, 1, R);
|
|
await tick();
|
|
out.store.opened = chainages(m.peekSectionDetail(P, R));
|
|
out.store.openedStore = shop.get("stations").map((s) => s.chainageM);
|
|
out.store.openedProfile = shop.get("profile")?.tag ?? null;
|
|
out.store.openedSwap = await m.swapSectionStations(P, R, shop.get("stations"));
|
|
// 메아리(같은 묶음 목록 · 조용하지 않은 차례) — 가게에 안 냄(349-0q)
|
|
pipe.generation = 2;
|
|
sets.length = 0;
|
|
ground(2, pipeline);
|
|
out.store.echoSets = sets.length;
|
|
// 새 목록(관 옮김) = 냄 · 그 뒤 opened 는 안 냄(낸 차례가 더 새로움)
|
|
pipe.generation = 3;
|
|
sets.length = 0;
|
|
ground(3, drafted);
|
|
out.store.newSets = sets.length;
|
|
await publisher.opened(P, 1, R);
|
|
out.store.openedAfterPublish = shop.get("stations").map((s) => s.chainageM);
|
|
// 버린 차례 끝 — 묶음 측점(초안 목록)과 상세가 달라도 가게 = 상세 측점 · 갈아끼움 없음(349-0o)
|
|
pipe.generation = 4;
|
|
quiet = 4;
|
|
ground(4, drafted);
|
|
await publisher.shown(P, R);
|
|
out.store.dropped = shop.get("stations").map((s) => s.chainageM);
|
|
out.store.droppedKeys = shop.get("stations").map((s) => s.key);
|
|
out.store.droppedSwap = await m.swapSectionStations(P, R, shop.get("stations"));
|
|
out.store.droppedDetail = chainages(m.peekSectionDetail(P, R));
|
|
// 노선 초안이 다시 섰거나 뒤 차례가 뜸 = 안 냄 · 상세 없음(떠남) = 안 냄
|
|
pipe.inputs.route = { rev: 1 };
|
|
shop.set("stations", drafted);
|
|
await publisher.shown(P, R);
|
|
out.store.routeSkip = shop.get("stations").length;
|
|
pipe.inputs.route = null;
|
|
m.forgetSectionMemory(P);
|
|
await publisher.shown(P, R);
|
|
out.store.goneSkip = shop.get("stations").length;
|
|
stopSets();
|
|
await tick();
|
|
process.stdout.write(JSON.stringify(out));
|
|
process.exit(0);
|