지움
- flushDesignStore(design_store · 부름 0) — 시험 344-4 의 flush 대조 한 줄도
- b_page_state 옛 세션 열쇠 옮김(legacy · migrate) · b_project_switch 옛 키 머리
— 옛 열쇠는 세션 저장소(탭 닫으면 사라짐) · 지금 코드가 안 씀 · storage · DB 에 없음
· 시험 290-1 · 401 · 384-1 의 옛 키 줄 뺌
- 시험만 부르던 TS — planFootprint(346-2 거울은 footprintRecord + slopePieces 로 그대로)
· shiftSegment · shiftMovingPoints(쉬프트 단추는 09-02 지움 · min_cover_guard 쉬프트 시험 뺌)
· dropStationsNear(test_b05_station_merge.mjs 지움) · createIrregularStationsSection(부름 0)
· generalStations(234-1 은 기본 맞춤 값으로 같은 것 잼) · sameSummaryItem
- test_b05_b06_save_roundtrip + helper_b05_save_payload 둘 — 프로젝트 936be972 storage 없어 늘 skip
· 같은 저장 왕복은 test_472_6_design_save(창구 원자성) · test_449_save_pipes(관 PUT)
· test_238_1_save_keeps_structure_stations(구조물 측점)가 잼
남김
- migrateLegacyStations — 부르는 곳 있음(design_view_profile_sections) · storage 에 비정규 측점 남음
- common_util_excavation 셋 — 지금은 운영이 부름(design_calc_node)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
297 lines
13 KiB
TypeScript
297 lines
13 KiB
TypeScript
/* =============================================================================
|
|
* A00_Common/b_project_switch.ts
|
|
* 프로젝트마다 따로 동작(401) — 탭마다 지금 프로젝트 · 떠남 물음 · 떠날 때 · 들 때 비움.
|
|
*
|
|
* 지금 프로젝트 = 탭 세션 칸 먼저 · 없으면 마지막으로 연 프로젝트(localStorage · 새 탭이 이어받음).
|
|
* 들 때(`enterProject`) — 다른 프로젝트 세션 칸 · 3D 보관함을 지움(290-1) · 저장 안 한 고침이 있으면 확인창.
|
|
* 떠날 때(`leaveProject`) — 그 프로젝트 세션 칸 · 준비 표식 · 3D 보관함 몫 · 탭 프로젝트 표식 · 취향은 남김.
|
|
* 다른 프로젝트 화면을 그리려 하면 새로 엶(`reloadIfOtherProject` · 모듈 메모리 통째 비움).
|
|
* 떠남 물음(`guardProjectLeave`) — 화면에 붙은 가드를 모아 [취소 · 폐기 · 저장] 한 번.
|
|
* 화면 모듈은 부를 때만 들임 · 라우터는 스스로 이어 줌(`linkRouter`) — 설계 가게 · 인쇄 묶음이 이 파일을 부름.
|
|
* ========================================================================== */
|
|
|
|
import { CURRENT_PROJECT_ID_KEY } from "../config/config_frontend";
|
|
import { clearSessionKeys, STATE_REGISTRY, type StateEntry } from "./b_page_state";
|
|
|
|
/** 이 탭이 마지막으로 들어온 프로젝트. */
|
|
const ENTERED_KEY = "aislo:entered-project";
|
|
|
|
/** 이 탭의 지금 프로젝트(401) — 탭 세션 칸 먼저 · 없으면 마지막으로 연 프로젝트(새 탭) · 없으면 "". */
|
|
export function currentProjectId(): string {
|
|
try {
|
|
return (
|
|
window.sessionStorage.getItem(ENTERED_KEY) ||
|
|
window.localStorage.getItem(CURRENT_PROJECT_ID_KEY) ||
|
|
""
|
|
);
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/** 지금 프로젝트로 적음 — 이 탭 + 새 탭이 이어받을 마지막 값. 다른 프로젝트 캐시 비움은 `enterProject`. */
|
|
export function setCurrentProject(projectId: string): void {
|
|
try {
|
|
window.sessionStorage.setItem(ENTERED_KEY, projectId);
|
|
window.localStorage.setItem(CURRENT_PROJECT_ID_KEY, projectId);
|
|
} catch {
|
|
/* 저장소가 막힌 브라우저 — 기억 못 함. */
|
|
}
|
|
}
|
|
|
|
/** 지워진 · 권한 없는 프로젝트를 잊음 — 이 탭 표식 · 마지막 값이 그 프로젝트일 때만. */
|
|
export function forgetProject(projectId: string): void {
|
|
try {
|
|
if (window.sessionStorage.getItem(ENTERED_KEY) === projectId)
|
|
window.sessionStorage.removeItem(ENTERED_KEY);
|
|
if (window.localStorage.getItem(CURRENT_PROJECT_ID_KEY) === projectId)
|
|
window.localStorage.removeItem(CURRENT_PROJECT_ID_KEY);
|
|
} catch {
|
|
/* 저장소가 막힌 브라우저 — 잊을 것이 없다. */
|
|
}
|
|
}
|
|
|
|
/** 이 화면(모듈 메모리)이 담은 프로젝트 — 프로젝트 화면을 그릴 때마다 견줌. */
|
|
let heldProject = "";
|
|
|
|
/** 다른 프로젝트 화면을 그리려 하면 새로 엶(401 · 모듈 메모리 통째 비움) · true = 새로 여는 중. */
|
|
export function reloadIfOtherProject(): boolean {
|
|
const projectId = currentProjectId();
|
|
if (heldProject && projectId && projectId !== heldProject) {
|
|
window.location.reload();
|
|
return true;
|
|
}
|
|
if (projectId) heldProject = projectId;
|
|
return false;
|
|
}
|
|
|
|
/** 프로젝트를 가르는 키 머리 — 등록표 키(`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 });
|
|
});
|
|
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 { showConfirmDialog } = await import("@ui/ui_template_elements");
|
|
const drop = await showConfirmDialog(
|
|
`「${name}」에 저장 안 한 고침이 있음 — 지우고 열기 / 남기기`,
|
|
"지우고 열기",
|
|
"남기기",
|
|
);
|
|
if (!drop) keep.add(other);
|
|
}
|
|
plan.doomed(keep).forEach((key) => window.sessionStorage.removeItem(key));
|
|
setCurrentProject(projectId);
|
|
const { purgeOtherProjects } = await import("./b_asset_cache");
|
|
await purgeOtherProjects(projectId);
|
|
} catch {
|
|
/* 저장소가 막힌 브라우저 — 캐시를 못 지워도 프로젝트는 연다. */
|
|
}
|
|
}
|
|
|
|
/** 이 브라우저에 자료를 남긴 계정(388-1) — 열쇠에는 안 섞고 바뀜 판정에만. */
|
|
const OWNER_KEY = "aislo:owner";
|
|
|
|
/**
|
|
* 계정이 떠남 · 바뀜(388-1) — 이 탭 세션 전부(초안 · 결과 · 들어온 표식) · 현재 프로젝트 ·
|
|
* 3D 지형 보관함을 지운다. 계정 취향(localStorage pref)은 남긴다. 부른 쪽이 새로고침해 모듈 메모리도 버림.
|
|
*/
|
|
export async function wipeAccountCache(): Promise<void> {
|
|
try {
|
|
window.sessionStorage.clear();
|
|
window.localStorage.removeItem(CURRENT_PROJECT_ID_KEY);
|
|
} catch {
|
|
/* 저장소가 막힌 브라우저 — 지울 것이 없다. */
|
|
}
|
|
const { purgeOtherProjects } = await import("./b_asset_cache");
|
|
await purgeOtherProjects(""); // 빈 id 보다 큰 열쇠 = 모든 프로젝트
|
|
}
|
|
|
|
/** 로그인 확인 때 — 앞서 자료를 남긴 계정과 다르면 지우고 true(부른 쪽이 새로고침). */
|
|
export async function wipeIfOwnerChanged(user: string | number): Promise<boolean> {
|
|
const userId = String(user); // 서버 id 가 수로 옴 — 저장값(글)과 같은 꼴로 견줌
|
|
let previous: string | null = null;
|
|
try {
|
|
previous = window.localStorage.getItem(OWNER_KEY);
|
|
window.localStorage.setItem(OWNER_KEY, userId);
|
|
} catch {
|
|
return false;
|
|
}
|
|
if (previous === userId) return false;
|
|
await wipeAccountCache();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 프로젝트를 떠남(401) — 이 탭 세션의 그 프로젝트 칸 · 준비 표식 · 3D 보관함 몫을 지움.
|
|
* 탭 프로젝트 표식(뒤로 가기 · 재진입) · 취향 · 입력 설정 · CAD 복구본(localStorage)은 남김.
|
|
*/
|
|
export async function leaveProject(projectId: string): Promise<void> {
|
|
if (!projectId) return;
|
|
// 프로젝트 칸 열쇠는 모두 프로젝트 번호(uuid)를 품음 — 등록표 · 옛 키 · 페이지 칸을 한 줄로.
|
|
// 등록표 창구로 지움 — 지움 알림을 받은 설계 가게가 미뤄 둔 쓰기(1 s)를 거둠(떠난 뒤 되살아남 막음).
|
|
// ponytail: 아직 한 번도 안 쓴 칸의 미룬 쓰기는 못 거둠 — 생기면 가게에 프로젝트 단위 거둠을 더함.
|
|
clearSessionKeys((key) => key.includes(projectId));
|
|
const { purgeProject } = await import("./b_asset_cache");
|
|
await purgeProject(projectId);
|
|
}
|
|
|
|
/** 페이지가 알리는 떠남 셋(401) — 고침 있나 · 저장(false · 던짐 = 머묾) · 폐기. */
|
|
export interface ProjectLeaveGuard {
|
|
dirty: () => boolean;
|
|
save: () => Promise<unknown>;
|
|
discard?: () => void | Promise<void>;
|
|
/** 물음 글 — 없으면 공용 */
|
|
message?: string;
|
|
/** 같은 페이지로 보는 주소(B05 ↔ B06) — 그리로 가면 안 묻고 다시 걺 */
|
|
samePage?: readonly string[];
|
|
}
|
|
|
|
/** 라우터의 떠남 물음 자리 — 라우터가 들 때 이어 줌(라우터 → 이 파일 한 방향 · 페이지 표를 안 끌게). */
|
|
interface RouterLink {
|
|
currentRoute: () => string;
|
|
setLeaveGuard: (guard: (() => Promise<boolean>) | null) => void;
|
|
}
|
|
let routerLink: RouterLink | null = null;
|
|
export function linkRouter(link: RouterLink): void {
|
|
routerLink = link;
|
|
}
|
|
|
|
const guards: Array<{ root: HTMLElement; guard: ProjectLeaveGuard }> = [];
|
|
let unloadHooked = false;
|
|
|
|
/** 화면에 붙은 가드만 — 빠진 페이지 것은 버림. */
|
|
function liveGuards(): ProjectLeaveGuard[] {
|
|
for (let index = guards.length - 1; index >= 0; index -= 1) {
|
|
if (!guards[index].root.isConnected) guards.splice(index, 1);
|
|
}
|
|
return guards.map((entry) => entry.guard);
|
|
}
|
|
|
|
/** 저장 안 한 고침이 있나 — 화면에 붙은 가드 모두. */
|
|
export const projectLeaveDirty = (): boolean => liveGuards().some((guard) => guard.dirty());
|
|
|
|
/** 떠나도 되나 — 고친 가드를 모아 [취소 · 폐기 · 저장] 한 번 · true = 넘어감. */
|
|
export async function askProjectLeave(): Promise<boolean> {
|
|
const live = liveGuards();
|
|
if (!live.length) return true;
|
|
const route = routerLink?.currentRoute() ?? "";
|
|
if (live.some((guard) => guard.samePage?.includes(route))) {
|
|
// 라우터가 「넘어감」 뒤 물음을 내림 — 같은 페이지 안 이동이면 한 틱 뒤 다시 걺.
|
|
window.setTimeout(() => liveGuards().length && routerLink?.setLeaveGuard(askProjectLeave), 0);
|
|
return true;
|
|
}
|
|
const dirty = live.filter((guard) => guard.dirty());
|
|
if (!dirty.length) return true;
|
|
const [{ leaveOrStay }, { t }] = await Promise.all([
|
|
import("@ui/ui_template_leave_dialog"),
|
|
import("@ui/ui_template_locale"),
|
|
]);
|
|
const message = (dirty.length === 1 && dirty[0].message) || t("Common_Leave_Unsaved");
|
|
const got = await leaveOrStay(message, {
|
|
save: async () => {
|
|
for (const guard of dirty) {
|
|
if (guard.dirty() && (await guard.save()) === false) throw new Error("저장 못 함");
|
|
}
|
|
},
|
|
stillDirty: () => dirty.some((guard) => guard.dirty()),
|
|
discard: async () => {
|
|
for (const guard of dirty) await guard.discard?.();
|
|
},
|
|
});
|
|
return got !== "stay";
|
|
}
|
|
|
|
/** 페이지를 세울 때 한 번 — `root` 가 화면에서 빠지면 스스로 풂 · 탭 닫기도 물음. */
|
|
export function guardProjectLeave(root: HTMLElement, guard: ProjectLeaveGuard): void {
|
|
liveGuards(); // 빠진 옛 페이지를 붙들지 않음
|
|
guards.push({ root, guard });
|
|
if (!unloadHooked) {
|
|
unloadHooked = true;
|
|
window.addEventListener("beforeunload", (event) => {
|
|
if (!projectLeaveDirty()) return;
|
|
event.preventDefault();
|
|
event.returnValue = "";
|
|
});
|
|
}
|
|
routerLink?.setLeaveGuard(askProjectLeave);
|
|
}
|