/* 401 프로젝트마다 따로 동작 — 진짜 TS(b_page_state · b_project_switch · b_asset_cache)를 가짜 저장소 · * 가짜 IndexedDB 위에서 돌려 JSON 으로 냄. 탭 둘 = 세션 둘 · localStorage 하나 · 모듈 한 벌씩. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const { makeIdb } = require("./helper_343_3_asset_cache.cjs"); const ROOT = path.join(__dirname, "..", ".."); function fakeStorage() { const map = new Map(); return { get length() { return map.size; }, key: (i) => [...map.keys()][i] ?? null, getItem: (k) => (map.has(k) ? map.get(k) : null), setItem: (k, v) => map.set(k, String(v)), removeItem: (k) => map.delete(k), dump: () => Object.fromEntries(map), }; } const shared = { localStorage: fakeStorage(), idb: makeIdb() }; const router = { route: "/b07-design-detail", guard: null, sets: 0 }; let choice = "save"; const asked = []; /** 탭 하나 = 창 하나 · 모듈 한 벌(모듈 메모리 따로). */ function openTab() { const win = new EventTarget(); win.sessionStorage = fakeStorage(); win.localStorage = shared.localStorage; win.indexedDB = shared.idb.indexedDB; win.setTimeout = setTimeout; win.reloads = 0; win.location = { reload: () => (win.reloads += 1) }; const cache = new Map(); const load = (rel) => { if (cache.has(rel)) return cache.get(rel); const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, rel), "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const exports = {}; cache.set(rel, exports); new Function("exports", "require", "window", js)(exports, need, win); return exports; }; const need = (id) => { if (id.includes("config_frontend")) return { CURRENT_PROJECT_ID_KEY: "frd_current_project_id", API_BASE_URL: "http://x/api" }; if (id.endsWith("b_page_state")) return load("A00_Common/b_page_state.ts"); if (id.endsWith("b_asset_cache")) return load("A00_Common/b_asset_cache.ts"); if (id.endsWith("ui_template_locale")) return { t: (key) => key }; if (id.endsWith("ui_template_leave_dialog")) return { leaveOrStay: async (message, hooks) => { asked.push(message); if (choice === "cancel") return "stay"; if (choice === "discard") return (await hooks.discard(), "discarded"); try { await hooks.save(); } catch { return "stay"; } return hooks.stillDirty() ? "stay" : "saved"; }, }; if (id.endsWith("ui_template_elements")) return { showConfirmDialog: async () => true }; return {}; // B04 · B05 받기(보관함이 부르지 않음) }; const sw = load("A00_Common/b_project_switch.ts"); sw.linkRouter({ currentRoute: () => router.route, setLeaveGuard: (fn) => ((router.guard = fn), (router.sets += 1)), }); return { win, sw, ps: load("A00_Common/b_page_state.ts") }; } const X = "11111111-aaaa-4aaa-8aaa-000000000001"; const Y = "22222222-bbbb-4bbb-8bbb-000000000002"; /** 브라우저 BeforeUnloadEvent 처럼 returnValue 를 쓸 수 있게. */ class BeforeUnload extends Event { returnValue = true; } const tick = () => new Promise((resolve) => setTimeout(resolve, 5)); const keys = (win) => Object.keys(win.sessionStorage.dump()).sort(); (async () => { const out = {}; // 1. 탭 둘 — B 가 Y 로 바꿔도 A 는 X · 새 탭은 마지막 값(Y) shared.localStorage.setItem("frd_current_project_id", X); const A = openTab(); out.newTabStartsWithLast = A.sw.currentProjectId(); await A.sw.enterProject(X, async (id) => id); const B = openTab(); await B.sw.enterProject(Y, async (id) => id); out.tabs = { a: A.sw.currentProjectId(), b: B.sw.currentProjectId(), fresh: openTab().sw.currentProjectId(), last: shared.localStorage.getItem("frd_current_project_id"), }; // 2. 다른 프로젝트 화면을 그리려 하면 새로 엶 — 같은 프로젝트는 그대로 out.reload = { first: A.sw.reloadIfOtherProject(), same: A.sw.reloadIfOtherProject() }; A.sw.setCurrentProject(Y); out.reload.other = A.sw.reloadIfOtherProject(); out.reload.count = A.win.reloads; A.sw.setCurrentProject(X); // 3. 떠남 — 그 프로젝트 칸 · 준비 표식 · 보관함 몫만 · 밖 칸 · 취향 · CAD 복구본 그대로 const seedX = [ `aislo:draft:structures:${X}`, `aislo:draft:crossw:${X}:12`, `aislo:result:section-detail:${X}:12`, `aislo:draft:d03-plan:${X}`, `aislo:draft:d04-assignees:${X}`, `aislo:draft:e05-report:${X}`, ]; const keep = [ `aislo:draft:structures:${Y}`, "a06:otp-pending", "aislo.adminCompany", "aislo.provenance.tint", "m01_master_draft", "m02-hopyo-check:3", "aislo:entered-project", ]; seedX.forEach((k) => A.win.sessionStorage.setItem(k, '{"v":1}')); keep.slice(0, -1).forEach((k) => A.win.sessionStorage.setItem(k, "1")); A.win.sessionStorage.setItem("frd_preloaded_signature", `${X}|sig`); const localKeep = [`aislo:pref:routeedit-view:${X}`, `OPEN_WEB_CAD__RECOVERY__b07:${X}:1`]; localKeep.forEach((k) => shared.localStorage.setItem(k, "1")); for (const [pid, url] of [ [X, "u1"], [X, "u2"], [Y, "u1"], ]) { shared.idb.data.set(`${pid}|${url}`, { key: `${pid}|${url}`, projectId: pid, url }); } const removed = []; const stop = A.ps.onStateRemoved((key) => removed.push(key)); // 설계 가게가 미룬 쓰기를 거두는 알림 await A.sw.leaveProject(X); stop(); out.leave = { notified: removed.length, keys: keys(A.win), expected: [...keep].sort(), entered: A.win.sessionStorage.getItem("aislo:entered-project"), local: localKeep.map((k) => shared.localStorage.getItem(k)), idb: [...shared.idb.data.keys()].sort(), }; // 다른 탭(B) 세션은 안 건드림 B.win.sessionStorage.setItem(`aislo:draft:structures:${X}`, "1"); await A.sw.leaveProject(X); out.leave.otherTabKept = B.win.sessionStorage.getItem(`aislo:draft:structures:${X}`); // 4. 떠남 물음 — 고친 가드만 모아 한 번 · 저장 실패 = 머묾 · 폐기 · 같은 페이지 · 빠진 페이지 const root1 = { isConnected: true }; const root2 = { isConnected: true }; const state = { d1: false, d2: false, saves: [], discards: [], saveOk: true }; A.sw.guardProjectLeave(root1, { dirty: () => state.d1, save: async () => (state.saves.push(1), state.saveOk && (state.d1 = false), state.saveOk), discard: () => state.discards.push(1), }); A.sw.guardProjectLeave(root2, { dirty: () => state.d2, save: async () => (state.saves.push(2), (state.d2 = false), true), message: "둘째 글", }); await tick(); const ask = router.guard; const unloadBlocked = () => { const event = new BeforeUnload("beforeunload", { cancelable: true }); A.win.dispatchEvent(event); return event.defaultPrevented; }; out.guard = { armed: ask === A.sw.askProjectLeave, cleanLeave: await ask() }; out.guard.cleanUnload = unloadBlocked(); out.guard.asked0 = asked.length; state.d2 = true; out.guard.dirtyUnload = unloadBlocked(); choice = "cancel"; out.guard.cancel = await ask(); choice = "save"; out.guard.save = await ask(); out.guard.messages = [...asked]; state.d1 = true; state.saveOk = false; out.guard.saveFail = await ask(); choice = "discard"; out.guard.discard = await ask(); out.guard.saves = state.saves; out.guard.discards = state.discards.length; router.route = "/b06-section"; A.sw.guardProjectLeave( { isConnected: true }, { dirty: () => true, save: async () => true, samePage: ["/b05-profile", "/b06-section"] }, ); await tick(); const sets = router.sets; const askedBefore = asked.length; out.guard.samePage = await ask(); await tick(); out.guard.reArmed = router.sets === sets + 1; out.guard.samePageAsked = asked.length - askedBefore; process.stdout.write(JSON.stringify(out)); })().catch((error) => { process.stderr.write(String(error && error.stack ? error.stack : error)); process.exit(1); });