Files
sam-react-prod/src/lib/auth/logout.ts
kent 5d0e453a68 refactor(WEB): 레이아웃 및 설정 관리 개선
- AuthenticatedLayout: FCM 통합 및 레이아웃 개선
- logout: 로그아웃 시 FCM 토큰 정리 로직 추가
- AccountInfoManagement: 계정 정보 관리 UI 개선
- not-found 페이지 스타일 개선
- 환경변수 예시 파일 업데이트
2025-12-30 17:43:59 +09:00

242 lines
6.7 KiB
TypeScript

/**
* 완전한 로그아웃 처리
*
* 로그아웃 시 수행하는 작업:
* 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<boolean> {
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<boolean> {
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();
}