- b_project_switch.enterProject — 공용 goToWorkflowStage 에서 부름 · 탭마다 들어온 표식이 다를 때만 - 다른 프로젝트 세션 칸(초안 · 결과 · 옛 키 · 키 마디에 번호가 든 페이지 칸) + 3D 보관함 지움 · 지금 프로젝트 · 계정 취향 그대로 - 다른 프로젝트에 저장 안 한 고침이 있으면 확인창(지우고 열기 / 남기기) · 남기기 = 그 초안만 둠 - showConfirmDialog 취소 단추 글 받는 칸(기본 「취소」) - 시험 test_290_1(진짜 TS · 가짜 저장소) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
123 lines
5.2 KiB
TypeScript
123 lines
5.2 KiB
TypeScript
/* =============================================================================
|
|
* 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<string, StateEntry>).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>) => string[];
|
|
}
|
|
|
|
/** 세션 칸(키 → 값)에서 다른 프로젝트 몫을 가른다 — 저장소를 만지지 않는다. */
|
|
export function planProjectPurge(
|
|
session: Record<string, string>,
|
|
projectId: string,
|
|
): ProjectPurgePlan {
|
|
const prefixes = projectPrefixes();
|
|
const owners = new Map<string, { project: string; draft: boolean }>();
|
|
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<string, string> {
|
|
const out: Record<string, string> = {};
|
|
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<string>,
|
|
): Promise<void> {
|
|
try {
|
|
if (window.sessionStorage.getItem(ENTERED_KEY) === projectId) return;
|
|
const plan = planProjectPurge(sessionEntries(), projectId);
|
|
const keep = new Set<string>();
|
|
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 {
|
|
/* 저장소가 막힌 브라우저 — 캐시를 못 지워도 프로젝트는 연다. */
|
|
}
|
|
}
|