/** * 완전한 로그아웃 처리 * * 로그아웃 시 수행하는 작업: * 1. Zustand 스토어 초기화 (메모리 캐시) * 2. sessionStorage 캐시 삭제 (page_config_*, mes-*) * 3. localStorage 사용자 데이터 삭제 (mes-currentUser, mes-users) * 4. FCM 토큰 해제 (Capacitor 네이티브 앱) * 5. 서버 로그아웃 API 호출 (HttpOnly 쿠키 삭제) * * @see claudedocs/security/[PLAN-2025-12-12] tenant-data-isolation-implementation.md */ import { useMasterDataStore } from '@/stores/masterDataStore'; import { useItemStore } from '@/stores/itemStore'; // FCM은 Capacitor 환경에서만 사용 (동적 import로 웹 빌드 에러 방지) // ===== 캐시 삭제 대상 Prefix ===== const SESSION_STORAGE_PREFIXES = [ 'page_config_', // masterDataStore 페이지 구성 캐시 'mes-', // TenantAwareCache 테넌트 캐시 ]; const LOCAL_STORAGE_KEYS = [ 'mes-currentUser', // 현재 사용자 정보 'mes-users', // 사용자 목록 ]; const LOCAL_STORAGE_PREFIXES = [ 'mes-', // 테넌트 캐시 (mes-{tenantId}-*) ]; // ===== 캐시 정리 함수 ===== /** * sessionStorage에서 우리 앱 캐시만 삭제 * - page_config_* (페이지 구성) * - mes-* (테넌트 캐시) */ export function clearSessionStorageCache(): void { if (typeof window === 'undefined') return; const keysToRemove: string[] = []; Object.keys(sessionStorage).forEach((key) => { if (SESSION_STORAGE_PREFIXES.some((prefix) => key.startsWith(prefix))) { keysToRemove.push(key); } }); keysToRemove.forEach((key) => { sessionStorage.removeItem(key); console.log(`[Logout] Cleared sessionStorage: ${key}`); }); if (keysToRemove.length > 0) { console.log(`[Logout] Cleared ${keysToRemove.length} sessionStorage items`); } } /** * localStorage에서 사용자 데이터 및 테넌트 캐시 삭제 */ export function clearLocalStorageCache(): void { if (typeof window === 'undefined') return; // 특정 키 삭제 LOCAL_STORAGE_KEYS.forEach((key) => { if (localStorage.getItem(key)) { localStorage.removeItem(key); console.log(`[Logout] Cleared localStorage: ${key}`); } }); // Prefix 기반 삭제 const keysToRemove: string[] = []; Object.keys(localStorage).forEach((key) => { if (LOCAL_STORAGE_PREFIXES.some((prefix) => key.startsWith(prefix))) { keysToRemove.push(key); } }); keysToRemove.forEach((key) => { localStorage.removeItem(key); console.log(`[Logout] Cleared localStorage: ${key}`); }); if (keysToRemove.length > 0) { console.log(`[Logout] Cleared ${keysToRemove.length} localStorage items`); } } /** * Zustand 스토어 초기화 */ export function resetZustandStores(): void { try { // masterDataStore 초기화 const masterDataStore = useMasterDataStore.getState(); masterDataStore.reset(); console.log('[Logout] Reset masterDataStore'); // itemStore 초기화 const itemStore = useItemStore.getState(); itemStore.reset(); console.log('[Logout] Reset itemStore'); } catch (error) { console.error('[Logout] Failed to reset Zustand stores:', error); } } /** * 서버 로그아웃 API 호출 * - HttpOnly 쿠키 삭제 (access_token, refresh_token) */ export async function callLogoutAPI(): Promise { try { const response = await fetch('/api/auth/logout', { method: 'POST', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { console.warn('[Logout] Server logout failed:', response.status); return false; } console.log('[Logout] Server logout successful'); return true; } catch (error) { console.error('[Logout] Server logout error:', error); return false; } } // ===== 통합 로그아웃 함수 ===== /** * 완전한 로그아웃 수행 * * 실행 순서: * 1. Zustand 스토어 초기화 (즉시 UI 반영) * 2. sessionStorage 캐시 삭제 * 3. localStorage 사용자 데이터 삭제 * 4. FCM 토큰 해제 (Capacitor 네이티브 앱) * 5. 서버 로그아웃 API 호출 * * @param options.skipServerLogout - 서버 로그아웃 생략 여부 (기본: false) * @param options.redirectTo - 로그아웃 후 리다이렉트 경로 (기본: null) * @returns 로그아웃 성공 여부 */ export async function performFullLogout(options?: { skipServerLogout?: boolean; redirectTo?: string | null; }): Promise { const { skipServerLogout = false, redirectTo = null } = options || {}; console.log('[Logout] Starting full logout...'); try { // 1. Zustand 스토어 초기화 (즉시 UI 반영) resetZustandStores(); // 2. sessionStorage 캐시 삭제 clearSessionStorageCache(); // 3. localStorage 사용자 데이터 삭제 clearLocalStorageCache(); // 4. FCM 토큰 해제 (Capacitor 네이티브 앱에서만 실행) // 동적 import로 @capacitor/core 빌드 에러 방지 try { const fcm = await import('@/lib/capacitor/fcm'); if (fcm.isCapacitorNative()) { await fcm.unregisterFCMToken(); console.log('[Logout] FCM token unregistered'); } } catch { // Capacitor 모듈이 없는 환경 (웹) - 무시 console.log('[Logout] Skipping FCM (not in native app)'); } // 5. 서버 로그아웃 API 호출 (HttpOnly 쿠키 삭제) if (!skipServerLogout) { await callLogoutAPI(); } console.log('[Logout] Full logout completed successfully'); // 6. 리다이렉트 (선택적) if (redirectTo && typeof window !== 'undefined') { window.location.href = redirectTo; } return true; } catch (error) { console.error('[Logout] Full logout failed:', error); return false; } } // ===== 유틸리티 함수 ===== /** * 현재 캐시 상태 확인 (디버깅용) */ export function debugCacheStatus(): void { if (typeof window === 'undefined') { console.log('[Debug] Server environment - no cache'); return; } console.group('[Debug] Cache Status'); // sessionStorage const sessionKeys = Object.keys(sessionStorage).filter((key) => SESSION_STORAGE_PREFIXES.some((prefix) => key.startsWith(prefix)) ); console.log('sessionStorage:', sessionKeys); // localStorage const localKeys = Object.keys(localStorage).filter( (key) => LOCAL_STORAGE_KEYS.includes(key) || LOCAL_STORAGE_PREFIXES.some((prefix) => key.startsWith(prefix)) ); console.log('localStorage:', localKeys); // Zustand const masterDataState = useMasterDataStore.getState(); console.log('masterDataStore.pageConfigs:', Object.keys(masterDataState.pageConfigs)); const itemState = useItemStore.getState(); console.log('itemStore.items:', itemState.items.length); console.groupEnd(); }