/* 451 헬퍼(440-4) — 노선 초안 둔 채 새로고침: 진짜 상세 가게 · 진짜 측점 열쇠 · 세션(모듈을 다시 세워도 남음). * 초안 시작 hold = 세션 한 벌(열쇠 · 횡단 초안 칸 · 관 · 구조물) · 새로고침 뒤 hold = 그 한 벌(초안으로 옮겨 쓴 세션 아님) · * 버림 = 초안 전 칸 · 초안 얹기 다시(되쓴 관으로) 뒤 초안 전 열쇠. 서버 · 초안 얹기는 가짜. 짝 = `test_451_refresh_hold.py`. */ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); const CATALOG = require("./helper_322_catalog_module.cjs"); const ROOT = path.join(__dirname, "..", ".."); const P = "P451"; const ROUTE = 3; function memoryStorage() { const map = new Map(); return { getItem: (k) => (map.has(k) ? map.get(k) : null), setItem: (k, v) => map.set(k, String(v)), removeItem: (k) => map.delete(k), key: (i) => [...map.keys()][i] ?? null, get length() { return map.size; }, }; } globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage() }; const anyModule = (extra = {}) => new Proxy(extra, { get: (t, key) => (key in t ? t[key] : () => undefined) }); /** 서버 — 부를 때마다 지금 `server` 사본 · `gate` 가 풀릴 때 답함. */ let server = null; let gate = Promise.resolve(); let asked = 0; const keyAt = (sections) => sections.map((s) => `at:${s.chainage_m.toFixed(2)}`); const STUBS = { "design_pipeline_page.ts": { reconcilePagePipeline: async () => 0, pipelineOwnsCross: () => false, pagePipeline: () => null, }, "design_pipeline_cross.ts": { forceCross: () => undefined }, "B06_Section_Section_Drafts.ts": { forgetDraftStations: () => undefined, withDraftShapes: async () => undefined, withDraftStations: async (detail) => { // 초안 얹기 흉내 — 가게 관 자리(세션 pipes)마다 그 측점 관 표시 · 부른 차례를 적음 const pipes = state.readState("pipes", P) ?? []; calls.push(`stations:${pipes.map((p) => p.chainage_m).join(",")}`); for (const s of detail.cross_sections) s.pipeAt = pipes.some((p) => p.chainage_m === s.chainage_m); }, supersedeDraftStations: () => undefined, }, "B05_Profile_Api_PlusStations.ts": { loadSavedPlusStations: async () => undefined, plusStationsOf: () => [], }, "B06_Section_Api_Fetch.ts": anyModule({ fetchSectionDetail: async () => { asked += 1; await gate; return JSON.parse(JSON.stringify(server)); }, }), "B06_Section_Api_Culvert_Options.ts": anyModule({ STRUCTURE_ADJUST_KEYS: [] }), "B06_Section_Swap_Bus.ts": anyModule(), "ui_template_elements.ts": anyModule(), "B06_Section_UI_Page_Persist.ts": anyModule({ readCutSlopeSession: () => ({}) }), "B06_Section_UI_Standard_Panel.ts": anyModule(), "B06_Section_Cross_Design_Session.ts": anyModule({ crossDesignChoices: () => new Map() }), "B06_Section_Section_Adjusts.ts": anyModule({ withPipeOptions: async () => undefined }), "common_util_cross_design.ts": anyModule(), "B06_Section_Cross_Culvert_Wire.ts": anyModule(), "B06_Section_Cross_Ford_Geom.ts": anyModule(), "B06_Section_Cross_Box_Geom.ts": anyModule(), "B06_Section_UI_IrregularStations.ts": anyModule({ PIPE_DISPLAY_NAME: {} }), "B05_Profile_UI_IrregularStations.ts": anyModule({ PIPE_DISPLAY_NAME: {} }), "config_frontend.ts": anyModule({ PIPE_TYPES: [] }), }; let cache = new Map(); const calls = []; const resolve = (base) => [base, `${base}.ts`, path.join(base, "index.ts")].find( (p) => fs.existsSync(p) && fs.statSync(p).isFile(), ) ?? `${base}.ts`; const { loadTs } = makeLoader({ root: ROOT, interop: true, fileStub: (full) => STUBS[path.basename(full)], requireFor: (full) => (request) => { if (CATALOG.isCatalogRequest(request)) return CATALOG; const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" }; for (const [prefix, dir] of Object.entries(alias)) if (request.startsWith(prefix)) return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length)))); return loadTs(resolve(path.join(path.dirname(full), request))); }, }); let store = loadTs(path.join(ROOT, "B06_Section", "B06_Section_Section_Store.ts")); const state = loadTs(path.join(ROOT, "A00_Common", "b_page_state.ts")); let keys = loadTs(path.join(ROOT, "B06_Section", "B06_Section_Station_Keys.ts")); /** 새로고침 흉내 — 모듈 메모리를 비우고 다시 세움(세션 · 서버는 그대로). */ function refresh() { const keep = cache.get(path.join(ROOT, "A00_Common", "b_page_state.ts")); cache = new Map([[path.join(ROOT, "A00_Common", "b_page_state.ts"), keep]]); store = loadTs(path.join(ROOT, "B06_Section", "B06_Section_Section_Store.ts")); keys = loadTs(path.join(ROOT, "B06_Section", "B06_Section_Station_Keys.ts")); } const sec = (c, kind) => ({ chainage_m: c, kind, samples: [], design: { design_elevation_m: 100 }, }); // 저장 상세: 340 · 관 351 · 360 const saved = { longitudinal: { design_profiles: [], stations: [] }, cross_sections: [sec(340, "regular"), sec(351, "irregular"), sec(360, "regular")], }; const at = (c) => ({ key: `at:${c.toFixed(2)}`, chainageM: c }); const savedPipes = [{ id: "p1", chainage_m: 351 }]; const draftPipes = [{ id: "p1", chainage_m: 361 }]; const cell = () => state.readState("crossdesign", P, ROUTE); const keysNow = () => keys.registeredKeys(P, ROUTE, store.peekSectionDetail(P, ROUTE).cross_sections, 20); (async () => { const out = {}; server = saved; state.writeState("section-detail", saved, P, ROUTE); state.writeState("pipes", savedPipes, P); state.writeState("crossdesign", { before: true }, P, ROUTE); // 탭 1 — 열기 · 노선 초안 시작(hold) · 초안이 세션 칸 · 관을 초안 자리로 옮겨 씀 const opened = await store.loadSectionDetail(P, ROUTE); const first = store.holdSectionDetail(P, ROUTE, opened); const kept = state.readState("route-draft-hold", P); out.start = { kept: !!kept && kept.routeId === ROUTE, cells: JSON.stringify(first.cells.find(([n]) => n === "crossdesign")[1]), pipes: first.facilities.pipes.map((p) => p.chainage_m), keys: first.keys.map((k) => k.chainageM), }; state.writeState("route-draft", { nodes: [], rev: 1 }, P); state.writeState("crossdesign", { draft: true }, P, ROUTE); state.writeState("pipes", draftPipes, P); // 새로고침(초안 둔 채) — 새 모듈 · 세션 그대로 · 상세 = 세션(초안 관 자리로 얹힘) refresh(); calls.length = 0; const reopened = await store.loadSectionDetail(P, ROUTE); const hold = store.holdSectionDetail(P, ROUTE, reopened); out.refresh = { cells: JSON.stringify(hold.cells.find(([n]) => n === "crossdesign")[1]), pipes: hold.facilities.pipes.map((p) => p.chainage_m), keys: JSON.stringify(hold.keys) === JSON.stringify(first.keys), pipeShownAt: reopened.cross_sections.filter((s) => s.pipeAt).map((s) => s.chainage_m), }; // 묶음이 초안을 버림 — 가게 관 = hold 관(묶음 몫 흉내) → 되돌림 state.writeState("route-draft", null, P); state.writeState("pipes", hold.facilities.pipes, P); calls.length = 0; const back = await store.restoreSectionDetail( P, ROUTE, hold, saved.cross_sections.map((s) => at(s.chainage_m)), ); out.drop = { same: back && back.same, cells: JSON.stringify(cell()), kept: state.readState("route-draft-hold", P), calls: [...calls], pipeShownAt: back.detail.cross_sections.filter((s) => s.pipeAt).map((s) => s.chainage_m), keys: JSON.stringify(keysNow()) === JSON.stringify(first.keys.map((k) => k.key)), }; // 새 초안(route-draft 없음) = 지금 값을 다시 뜸 · 다른 노선 hold = 안 씀 state.writeState("crossdesign", { later: true }, P, ROUTE); const fresh = store.holdSectionDetail(P, ROUTE, back.detail); state.writeState("route-draft", { nodes: [], rev: 2 }, P); const other = store.holdSectionDetail(P, ROUTE + 1, back.detail); out.next = { cells: JSON.stringify(fresh.cells.find(([n]) => n === "crossdesign")[1]), otherRoute: state.readState("route-draft-hold", P).routeId, }; process.stdout.write(JSON.stringify(out)); })().catch((error) => { process.stderr.write(String(error && error.stack ? error.stack : error)); process.exit(1); });