/** * 세션 관리 유틸리티 * 세션 만료 감지, 자동 로그아웃, 세션 갱신 등을 담당 * * 모바일/데스크톱 환경별 타임아웃 설정: * - 데스크톱: 30분 비활성 시 만료, 5분 전 경고 * - 모바일: 24시간 비활성 시 만료, 1시간 전 경고 (WebView localStorage 초기화 이슈 대응) */ interface SessionConfig { checkInterval: number; // 세션 체크 간격 (ms) warningTime: number; // 만료 경고 시간 (ms) maxInactiveTime: number; // 최대 비활성 시간 (ms) } interface SessionWarningCallbacks { onWarning?: (remainingTime: number) => void; onExpiry?: () => void; onActivity?: () => void; } export class SessionManager { private config: SessionConfig; private callbacks: SessionWarningCallbacks; private checkTimer: NodeJS.Timeout | null = null; private warningTimer: NodeJS.Timeout | null = null; private lastActivity: number = Date.now(); private isWarningShown: boolean = false; constructor(config: Partial = {}, callbacks: SessionWarningCallbacks = {}) { this.config = { checkInterval: 60000, // 1분마다 체크 warningTime: 300000, // 5분 전 경고 maxInactiveTime: 1800000, // 30분 비활성 시 만료 ...config, }; this.callbacks = callbacks; this.setupActivityListeners(); } /** * 세션 모니터링 시작 */ start() { this.stop(); // 기존 타이머 정리 this.checkTimer = setInterval(() => { this.checkSession(); }, this.config.checkInterval); } /** * 세션 모니터링 중지 */ stop() { if (this.checkTimer) { clearInterval(this.checkTimer); this.checkTimer = null; } if (this.warningTimer) { clearTimeout(this.warningTimer); this.warningTimer = null; } this.removeActivityListeners(); } /** * 사용자 활동 기록 */ recordActivity() { this.lastActivity = Date.now(); this.isWarningShown = false; // 경고 타이머가 있다면 취소 if (this.warningTimer) { clearTimeout(this.warningTimer); this.warningTimer = null; } this.callbacks.onActivity?.(); } /** * 세션 상태 확인 */ private checkSession() { const now = Date.now(); const timeSinceLastActivity = now - this.lastActivity; const timeUntilExpiry = this.config.maxInactiveTime - timeSinceLastActivity; // 세션 만료 체크 if (timeSinceLastActivity >= this.config.maxInactiveTime) { this.handleSessionExpiry(); return; } // 경고 시간 체크 if (timeUntilExpiry <= this.config.warningTime && !this.isWarningShown) { this.showSessionWarning(timeUntilExpiry); } } /** * 세션 만료 경고 표시 */ private showSessionWarning(remainingTime: number) { this.isWarningShown = true; this.callbacks.onWarning?.(remainingTime); // 남은 시간 후 자동 만료 this.warningTimer = setTimeout(() => { this.handleSessionExpiry(); }, remainingTime); } /** * 세션 만료 처리 */ private handleSessionExpiry() { this.stop(); this.callbacks.onExpiry?.(); } /** * 활동 리스너 설정 */ private setupActivityListeners() { // 사용자 활동 이벤트들 const activityEvents = ["mousedown", "mousemove", "keypress", "scroll", "touchstart", "click"]; // 각 이벤트에 대해 리스너 등록 activityEvents.forEach((event) => { document.addEventListener(event, this.handleActivity, true); }); // 페이지 가시성 변경 이벤트 document.addEventListener("visibilitychange", this.handleVisibilityChange); } /** * 활동 리스너 제거 */ private removeActivityListeners() { const activityEvents = ["mousedown", "mousemove", "keypress", "scroll", "touchstart", "click"]; activityEvents.forEach((event) => { document.removeEventListener(event, this.handleActivity, true); }); document.removeEventListener("visibilitychange", this.handleVisibilityChange); } /** * 활동 이벤트 핸들러 */ private handleActivity = () => { this.recordActivity(); }; /** * 페이지 가시성 변경 핸들러 */ private handleVisibilityChange = () => { if (!document.hidden) { this.recordActivity(); } }; /** * 현재 세션 상태 정보 반환 */ getSessionInfo() { const now = Date.now(); const timeSinceLastActivity = now - this.lastActivity; const timeUntilExpiry = this.config.maxInactiveTime - timeSinceLastActivity; return { lastActivity: this.lastActivity, timeSinceLastActivity, timeUntilExpiry: Math.max(0, timeUntilExpiry), isActive: timeSinceLastActivity < this.config.maxInactiveTime, isWarningShown: this.isWarningShown, }; } } /** * 시간을 분:초 형식으로 포맷 */ export function formatTime(milliseconds: number): string { const seconds = Math.floor(milliseconds / 1000); const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`; } /** * 전역 세션 매니저 인스턴스 */ let globalSessionManager: SessionManager | null = null; /** * 전역 세션 매니저 초기화 */ export function initSessionManager( config: Partial = {}, callbacks: SessionWarningCallbacks = {}, ): SessionManager { if (globalSessionManager) { globalSessionManager.stop(); } globalSessionManager = new SessionManager(config, callbacks); return globalSessionManager; } /** * 전역 세션 매니저 가져오기 */ export function getSessionManager(): SessionManager | null { return globalSessionManager; } /** * 세션 매니저 정리 */ export function cleanupSessionManager() { if (globalSessionManager) { globalSessionManager.stop(); globalSessionManager = null; } } export default SessionManager; /** * 토큰 동기화 유틸리티 */ export const tokenSync = { // 토큰 상태 확인 checkToken: () => { const token = localStorage.getItem("authToken"); return !!token; }, // 토큰 강제 동기화 (다른 탭에서 설정된 토큰을 현재 탭에 복사) forceSync: () => { const token = localStorage.getItem("authToken"); if (token) { // sessionStorage에도 복사 sessionStorage.setItem("authToken", token); return true; } return false; }, // 토큰 복원 시도 (sessionStorage에서 복원) restoreFromSession: () => { const sessionToken = sessionStorage.getItem("authToken"); if (sessionToken) { localStorage.setItem("authToken", sessionToken); return true; } return false; }, // 토큰 유효성 검증 validateToken: (token: string) => { if (!token) return false; try { // JWT 토큰 구조 확인 (header.payload.signature) const parts = token.split("."); if (parts.length !== 3) return false; // payload 디코딩 시도 const payload = JSON.parse(atob(parts[1])); const now = Math.floor(Date.now() / 1000); // 만료 시간 확인 if (payload.exp && payload.exp < now) { return false; } return true; } catch (error) { return false; } }, };