fix(388-1 · 388-3 보안): 로그아웃 · 계정 바뀜 지움 · 세션 넘침 · 탭 프로젝트 고정
- 로그아웃 = 떠남 확인 먼저 → 서버 로그아웃 → 세션 전부 · 현재 프로젝트 · 3D 지형 지움 → 홈 새로고침 - 계정 바뀜(저장한 주인 ≠ 지금 사용자) = 같은 지움 뒤 새로고침 · 열쇠에 계정 안 섞음 - 세션 한도 넘침 = 결과 칸 비우고 한 번 다시 · 그래도 넘치면 「저장 공간 부족」 안내 - B05 · B06 이 연 프로젝트 고정 — 다른 탭이 바꿔도 이 탭 초안 칸 그대로 - 새 test_384_s_logout_wipe Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
This commit is contained in:
+10
-3
@@ -10,7 +10,7 @@
|
||||
import { ui_locales, currentLanguageIndex, setLanguage } from "@ui/ui_template_locale";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { THEME_STORAGE_KEY, LANG_STORAGE_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { navigateTo, isAuthenticated } from "./router";
|
||||
import { navigateTo, isAuthenticated, setLeaveGuard, takeLeaveGuard } from "./router";
|
||||
import { readTouchMode, setTouchMode, type TouchMode } from "./device_context";
|
||||
|
||||
const BRAND_ICON_URL = new URL("../resources/app_branding/prog_icon.jpg", import.meta.url).href;
|
||||
@@ -49,14 +49,21 @@ function onApp_Lang_Toggle_Click(): void {
|
||||
window.dispatchEvent(new HashChangeEvent("hashchange"));
|
||||
}
|
||||
|
||||
/** 로그아웃(388-1) — 떠남 확인 먼저 → 서버 로그아웃 → 이 브라우저 자료 지움 → 홈으로 새로고침. */
|
||||
async function onApp_Logout_Click(): Promise<void> {
|
||||
const guard = takeLeaveGuard();
|
||||
if (guard && !(await guard())) return; // [취소] — 머묾
|
||||
setLeaveGuard(null);
|
||||
const { logout } = await import("../A06_Login/A06_Login_Api_Fetch");
|
||||
try {
|
||||
await logout();
|
||||
} catch {
|
||||
// 세션이 이미 만료된 경우 등은 무시하고 홈으로 이동
|
||||
// 세션이 이미 만료된 경우 등은 무시하고 지움 · 홈으로 이동
|
||||
}
|
||||
navigateTo(ROUTES.A01_HOME);
|
||||
const { wipeAccountCache } = await import("./b_project_switch");
|
||||
await wipeAccountCache();
|
||||
history.replaceState(null, "", `#/${ROUTES.A01_HOME}`); // hashchange 없이 — 바로 새로고침
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
* 않는다** — ②·④는 두 화면이 같은 키를 쓴다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
|
||||
/** 값이 속한 통. `pref`·`draft`·`result` 셋만 저장소를 쓴다(즉시 반영은 표에 없다). */
|
||||
export type StateBucket = "pref" | "draft" | "result";
|
||||
|
||||
@@ -438,13 +440,35 @@ function writeRaw(key: string, value: string | null): void {
|
||||
try {
|
||||
const store = storageFor(key);
|
||||
if (value === null) store.removeItem(key);
|
||||
else store.setItem(key, value);
|
||||
else setOrMakeRoom(store, key, value);
|
||||
} catch {
|
||||
/* 무시 — 값은 화면 메모리에 남는다. */
|
||||
}
|
||||
if (key.startsWith("aislo:pref:")) pushUiPrefs();
|
||||
}
|
||||
|
||||
/** 세션 한도 넘침(388-3) — 결과 칸(다시 셀 수 있음)을 비우고 한 번 다시 · 그래도 넘치면 [저장] 안내. */
|
||||
function setOrMakeRoom(store: Storage, key: string, value: string): void {
|
||||
try {
|
||||
store.setItem(key, value);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (store !== window.sessionStorage || (error as DOMException)?.name !== "QuotaExceededError")
|
||||
throw error;
|
||||
}
|
||||
Object.keys(store)
|
||||
.filter((stale) => stale.startsWith("aislo:result:") && stale !== key)
|
||||
.forEach((stale) => writeRaw(stale, null));
|
||||
try {
|
||||
store.setItem(key, value);
|
||||
} catch (error) {
|
||||
void import("@ui/ui_template_elements").then(({ showToast }) =>
|
||||
showToast(t("Session_Storage_Full"), "warning", 8000),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** 옛 키에 남은 값을 새 키로 한 번 옮긴다(옮기고 나면 옛 키는 지운다). */
|
||||
function migrate(
|
||||
name: StateName,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* 같은 프로젝트로 다시 들어오면(새로고침 포함) 아무것도 안 지운다(탭마다 들어온 표식).
|
||||
* ========================================================================== */
|
||||
|
||||
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { showConfirmDialog } from "@ui/ui_template_elements";
|
||||
import { purgeOtherProjects } from "./b_asset_cache";
|
||||
import { STATE_REGISTRY, type StateEntry } from "./b_page_state";
|
||||
@@ -120,3 +121,34 @@ export async function enterProject(
|
||||
/* 저장소가 막힌 브라우저 — 캐시를 못 지워도 프로젝트는 연다. */
|
||||
}
|
||||
}
|
||||
|
||||
/** 이 브라우저에 자료를 남긴 계정(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 {
|
||||
/* 저장소가 막힌 브라우저 — 지울 것이 없다. */
|
||||
}
|
||||
await purgeOtherProjects(""); // 빈 id 보다 큰 열쇠 = 모든 프로젝트
|
||||
}
|
||||
|
||||
/** 로그인 확인 때 — 앞서 자료를 남긴 계정과 다르면 지우고 true(부른 쪽이 새로고침). */
|
||||
export async function wipeIfOwnerChanged(userId: string): Promise<boolean> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import type { DesignView } from "./design_dock";
|
||||
import { saveSharedDesign, type DesignPageShared } from "./design_shared";
|
||||
import { guardDesignLeave } from "./design_leave";
|
||||
import { usePipeIds } from "./design_pipe_ids";
|
||||
import { pinDraftProject } from "./design_store";
|
||||
import { setRouteShiftOpener } from "../../B06_Section/B06_Section_UI_Cross_Center";
|
||||
import { reconcilePagePipeline } from "./design_pipeline_page";
|
||||
import "./design_page.css";
|
||||
@@ -109,6 +110,7 @@ export function renderDesignPage(root: HTMLElement): MountedPage {
|
||||
page.className = "design-page";
|
||||
root.replaceChildren(page);
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
|
||||
pinDraftProject(projectId); // 연 프로젝트 고정(388-3) — 다른 탭이 바꿔도 이 탭 초안 칸 그대로
|
||||
usePipeIds(projectId || null); // 관 세션 id 장부 = 이 프로젝트(323-5 · 카드 · 알약 · 종단 · 3D 같은 id)
|
||||
|
||||
// 한 벌 틀 — 좌측 자리 · 가운데(위 칸 + 아래 종단도). 진행 단계 틀은 단계 상태가 오면 감싼다.
|
||||
|
||||
@@ -76,10 +76,17 @@ export interface DesignStore {
|
||||
/** 옛 계획고 초안 키 — 위 머리 주석 참조(남은 옛 값을 쓸어 낼 때만 쓴다). */
|
||||
export const ALIGNMENT_DRAFT_KEY_PREFIX = "b05-profile-alignment-draft";
|
||||
|
||||
/** 계획고 초안 칸의 프로젝트 — 부르는 쪽이 모르면 지금 연 프로젝트, 그것도 없으면 `-`
|
||||
/** 이 탭 페이지가 연 프로젝트(388-3) — 다른 탭이 현재 프로젝트를 바꿔도 이 탭 초안이 안 섞임. */
|
||||
let openedProjectId: string | null = null;
|
||||
export function pinDraftProject(projectId: string | null): void {
|
||||
openedProjectId = projectId || null;
|
||||
}
|
||||
|
||||
/** 계획고 초안 칸의 프로젝트 — 부르는 쪽이 모르면 이 탭이 연 프로젝트, 그것도 없으면 `-`
|
||||
* (노선 번호는 프로젝트를 건너 겹치지 않는다). */
|
||||
export function draftProjectId(projectId: string | null = null): string {
|
||||
if (projectId) return projectId;
|
||||
if (openedProjectId) return openedProjectId;
|
||||
try {
|
||||
return window.localStorage.getItem(CURRENT_PROJECT_ID_KEY) || "-";
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user