import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config"; import { getCookie, removeCookie, setCookie } from "@/src/shared/lib/cookie"; type JwtPayload = { sub?: string; // 토큰 subject exp?: number; // 만료 시각(초 단위) iat?: number; // 발급 시각(초 단위) [key: string]: unknown; }; export class TokenManager { private static instance: TokenManager; private refreshTimer: NodeJS.Timeout | null = null; private constructor() {} public static getInstance(): TokenManager { if (!TokenManager.instance) { TokenManager.instance = new TokenManager(); } return TokenManager.instance; } public getToken(key: string): string | undefined { return getCookie(key); } private parseJwt(token: string): JwtPayload { const parts = token.split("."); if (parts.length !== 3) { throw new Error("유효한 JWT 형식이 아닙니다."); } const base64Url = parts[1] ?? ""; // Payload를 가져온다. const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "="); const jsonPayload = decodeURIComponent( atob(padded) .split("") .map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`) .join("") ); return JSON.parse(jsonPayload) as JwtPayload; } public getRemainingMsFromToken(key: string = CLAIM_NAME): number | null { const token = this.getToken(key); if (!token) { return null; } try { const payload = this.parseJwt(token); if (typeof payload.exp !== "number") { return null; } return payload.exp * 1000 - Date.now(); } catch (error) { console.error("JWT 파싱 오류:", error); return null; } } public isExpiredToken(key: string = CLAIM_NAME): boolean { const remainingMs = this.getRemainingMsFromToken(key); return remainingMs === null || remainingMs <= 0; } public setToken(key: string, value: string): void { setCookie(key, value, { secure: window.location.protocol === "https:", sameSite: "lax", }); } public removeToken(key: string): void { removeCookie(key); } public setupRefreshTimer( callback: () => Promise, expirationTime: number, ): void { if (this.refreshTimer) { clearTimeout(this.refreshTimer); } this.refreshTimer = setTimeout(() => { callback().catch((error) => { console.error("Silent refresh failed:", error); this.clearAuthToken(); }); }, expirationTime - 60000); } public clearAuthToken(): void { if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.refreshTimer = null; } this.removeToken(CLAIM_NAME); this.removeToken(AUTH_USER_ID); } } export const tokenManager = TokenManager.getInstance();