/* 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);