Ramble-FE / utils / tokenUtils.ts
tokenUtils.ts
Raw
import { AuthTokens } from "@/types";
import { jwtDecode } from "jwt-decode";

/**
 * JWT 토큰의 페이로드 인터페이스
 */
interface JwtPayload {
    exp?: number;
    iat?: number;
    [key: string]: any;
}

/**
 * 토큰 관련 유틸리티 함수들
 */
export class TokenUtils {
    private static readonly BEARER_PREFIX = "Bearer ";

    /**
     * Response 헤더에서 인증 토큰들을 추출
     * @param response Response 헤더
     * @returns AuthTokens 객체
     * @throws Error 인증 헤더가 없거나 유효하지 않은 경우
     */
    static extractTokensFromHeaders(response: Headers): AuthTokens {
        const authHeader = response.get("authorization");
        const cookieHeader = response.get("set-cookie") || "";

        if (!authHeader) {
            throw new Error("인증 헤더가 없습니다");
        }

        if (!authHeader.startsWith(TokenUtils.BEARER_PREFIX)) {
            throw new Error("유효하지 않은 인증 헤더입니다");
        }

        return {
            accessToken: authHeader.substring(TokenUtils.BEARER_PREFIX.length),
            cookieHeader, 
        };
    }

    /**
     * Bearer 토큰 형식인지 검증
     * @param token 검증할 토큰
     * @returns 유효한 Bearer 토큰 여부
     */
    static isBearerToken(token: string): boolean {
        return token.startsWith(TokenUtils.BEARER_PREFIX);
    }

    /**
     * Bearer 접두사를 추가합니다.
     * @param token Access Token
     * @returns Bearer 형식의 토큰
     */
    static formatBearerToken(token: string): string {
        return token.startsWith(TokenUtils.BEARER_PREFIX) 
            ? token 
            : `${TokenUtils.BEARER_PREFIX}${token}`;
    }

    /**
     * JWT 토큰이 만료되었는지 확인 (1분 안전 마진 포함)
     * @param token JWT 토큰 (Bearer 접두사 포함 가능)
     * @returns 토큰이 만료되었으면 true, 아니면 false
     */
    static isTokenExpired(token: string): boolean {
        try {
            const cleanToken = token.startsWith(TokenUtils.BEARER_PREFIX) 
                ? token.substring(TokenUtils.BEARER_PREFIX.length)
                : token;

            const payload = jwtDecode<JwtPayload>(cleanToken);
            
            if (!payload.exp) {
                return false;
            }

            const currentTime = Math.floor(Date.now() / 1000);
            const bufferTime = 60; // 1분 안전 마진
            return payload.exp < (currentTime + bufferTime);

        } catch {
            return true;
        }
    }
}