Files
vexplor/frontend/lib/sessionManager.ts

295 lines
7.2 KiB
TypeScript

/**
* 세션 관리 유틸리티
* 세션 만료 감지, 자동 로그아웃, 세션 갱신 등을 담당
*
* 모바일/데스크톱 환경별 타임아웃 설정:
* - 데스크톱: 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<SessionConfig> = {}, 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<SessionConfig> = {},
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;
}
},
};