/* ============================================================================= * A00_Common/b_project_switch.ts * 다른 프로젝트에 들어갈 때 **그 밖 프로젝트의 캐시**를 지운다(계획서 290-1 · 요구 183-1). * * 지우는 것 — 이 탭 세션의 다른 프로젝트 칸(초안 · 결과 · 고름 · 옛 키 · 페이지별 칸)과 * 3D 지형 보관함(IndexedDB). 남기는 것 — 지금 프로젝트 것 · 계정 취향(pref, localStorage). * 다른 프로젝트에 저장 안 한 고침(초안)이 있으면 확인창 — 「남기기」면 그 초안만 둔다. * 같은 프로젝트로 다시 들어오면(새로고침 포함) 아무것도 안 지운다(탭마다 들어온 표식). * ========================================================================== */ import { showConfirmDialog } from "@ui/ui_template_elements"; import { purgeOtherProjects } from "./b_asset_cache"; import { STATE_REGISTRY, type StateEntry } from "./b_page_state"; /** 이 탭이 마지막으로 들어온 프로젝트. */ const ENTERED_KEY = "aislo:entered-project"; const MARK_P = "\u0001"; const MARK_R = "\u0002"; /** 프로젝트를 가르는 키 머리 — 등록표 키(`aislo:{통}:{이름}:`)와 옛 키 머리. 초안 여부를 함께. */ function projectPrefixes(): Array<{ prefix: string; draft: boolean }> { const out: Array<{ prefix: string; draft: boolean }> = []; Object.entries(STATE_REGISTRY as Record).forEach(([name, entry]) => { if (entry.bucket === "pref" || entry.scope === "global") return; const draft = entry.bucket === "draft"; const suffix = entry.version && entry.version > 1 ? `-v${entry.version}` : ""; out.push({ prefix: `aislo:${entry.bucket}:${name}${suffix}:`, draft }); const legacy = entry.legacy?.(MARK_P, MARK_R); const at = legacy?.indexOf(MARK_P) ?? -1; if (legacy && at > 0) out.push({ prefix: legacy.slice(0, at), draft }); }); return out; } const filled = (value: string | null): boolean => value !== null && value !== "" && value !== "{}" && value !== "[]"; export interface ProjectPurgePlan { /** 저장 안 한 고침이 남은 다른 프로젝트. */ draftProjects: string[]; /** 지울 키 — `keep` 프로젝트의 초안은 뺀다. */ doomed: (keep: ReadonlySet) => string[]; } /** 세션 칸(키 → 값)에서 다른 프로젝트 몫을 가른다 — 저장소를 만지지 않는다. */ export function planProjectPurge( session: Record, projectId: string, ): ProjectPurgePlan { const prefixes = projectPrefixes(); const owners = new Map(); Object.keys(session).forEach((key) => { const hit = prefixes.find((item) => key.startsWith(item.prefix)); if (!hit) return; const project = key.slice(hit.prefix.length).split(":")[0]; if (project) owners.set(key, { project, draft: hit.draft }); }); const others = new Set( [...owners.values()].map((owner) => owner.project).filter((id) => id !== projectId), ); const draftProjects = [ ...new Set( [...owners.entries()] .filter(([key, owner]) => owner.draft && others.has(owner.project) && filled(session[key])) .map(([, owner]) => owner.project), ), ]; return { draftProjects, // 등록표 밖 칸도 키 마디에 다른 프로젝트 번호가 있으면 그 프로젝트 몫으로 본다. doomed: (keep) => Object.keys(session).filter((key) => { const owner = owners.get(key); if (owner) return others.has(owner.project) && !(owner.draft && keep.has(owner.project)); return key.split(":").some((part) => others.has(part) && !keep.has(part)); }), }; } function sessionEntries(): Record { const out: Record = {}; try { for (let i = 0; i < window.sessionStorage.length; i += 1) { const key = window.sessionStorage.key(i); const value = key === null ? null : window.sessionStorage.getItem(key); if (key !== null && value !== null) out[key] = value; } } catch { /* 저장소가 막힌 브라우저 — 지울 것이 없다. */ } return out; } /** * 프로젝트에 들어감 — 들어온 표식이 다르면 그 밖 프로젝트 캐시를 지운다. 실패해도 이동은 막지 않는다. * `nameOf` 는 확인창에 적을 프로젝트 이름. */ export async function enterProject( projectId: string, nameOf: (projectId: string) => Promise, ): Promise { try { if (window.sessionStorage.getItem(ENTERED_KEY) === projectId) return; const plan = planProjectPurge(sessionEntries(), projectId); const keep = new Set(); for (const other of plan.draftProjects) { const name = await nameOf(other).catch(() => other); const drop = await showConfirmDialog( `「${name}」에 저장 안 한 고침이 있음 — 지우고 열기 / 남기기`, "지우고 열기", "남기기", ); if (!drop) keep.add(other); } plan.doomed(keep).forEach((key) => window.sessionStorage.removeItem(key)); window.sessionStorage.setItem(ENTERED_KEY, projectId); await purgeOtherProjects(projectId); } catch { /* 저장소가 막힌 브라우저 — 캐시를 못 지워도 프로젝트는 연다. */ } }