/** * JWT를 decoding하면 만료시간을 읽을 수 있다. * JWT는 3부분으로 구성 : Header.Payload.Signature * Payload 부분에 만료시간(exp)이 있다. */ "use client"; import { useEffect, useRef, useState } from "react"; import { authService } from "@/src/entities/auth"; import { CLAIM_NAME } from "@/src/shared/config"; import { getCookie } from "@/src/shared/lib"; interface JwtPayload { sub?: string; // 토큰 subject. 일반적으로 사용자 식별자나 주체 정보 exp?: number; // 만료 시각(Unix timestamp, 초 단위) iat?: number; // 토큰 발급 시각(Unix timestamp, 초 단위) [key: string]: unknown; // 그 외 JWT payload에 포함될 수 있는 추가 클레임 } interface JwtState { token: string; // 쿠키에서 읽어온 JWT 원본 문자열 payload: JwtPayload | null; // JWT를 파싱한 payload 객체 exp: number | null; // payload에서 추출한 exp 값 expDateLocal: string | null; // exp를 로컬 시간 문자열로 변환한 값 remainingMs: number | null; // 현재 시각 기준 남은 시간(ms) error: string | null; // 쿠키 없음, 파싱 실패 등 오류 메시지 } const initialJwtState: JwtState = { token: "", payload: null, exp: null, expDateLocal: null, remainingMs: null, error: null, }; function 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; } function formatRemainingMs(remainingMs: number | null): string { if (remainingMs === null) return "-"; if (remainingMs <= 0) return "만료됨"; const totalSeconds = Math.floor(remainingMs / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; return `${hours}시간 ${minutes}분 ${seconds}초`; } function readJwtState(): JwtState { const token = getCookie(CLAIM_NAME) ?? ""; if (!token) { return { token: "", payload: null, exp: null, expDateLocal: null, remainingMs: null, error: `${CLAIM_NAME} 쿠키가 없습니다.`, }; } try { const payload = parseJwt(token); if (typeof payload.exp !== "number") { return { token, payload, exp: null, expDateLocal: null, remainingMs: null, error: "JWT payload에 exp 값이 없습니다.", }; } const expMs = payload.exp * 1000; return { token, payload, exp: payload.exp, expDateLocal: new Date(expMs).toLocaleString(), remainingMs: Math.max(expMs - Date.now(), 0), error: null, }; } catch (error) { return { token, payload: null, exp: null, expDateLocal: null, remainingMs: null, error: error instanceof Error ? error.message : "JWT 파싱 중 오류가 발생했습니다.", }; } } export default function Page() { const [jwtState, setJwtState] = useState(initialJwtState); const isRefreshingRef = useRef(false); const handleSilentRefresh = async () => { try { await authService.silentRefresh(); } catch (error) { console.error("Manual silent refresh failed:", error); } finally { setJwtState(readJwtState()); } }; useEffect(() => { const syncJwtState = async () => { const nextState = readJwtState(); if ( nextState.remainingMs !== null && nextState.remainingMs > 0 && nextState.remainingMs < 5 * 60 * 1000 && !isRefreshingRef.current ) { isRefreshingRef.current = true; try { await authService.silentRefresh(); } catch (error) { console.error("JWT test silent refresh failed:", error); } } setJwtState(readJwtState()); if ( nextState.remainingMs === null || nextState.remainingMs >= 5 * 60 * 1000 || nextState.remainingMs <= 0 ) { isRefreshingRef.current = false; } }; void syncJwtState(); const timer = window.setInterval(() => { void syncJwtState(); }, 1000); return () => window.clearInterval(timer); }, []); return (

JWT Exp Test

쿠키명
{CLAIM_NAME}
exp
{jwtState.exp ?? "-"}
만료시각
{jwtState.expDateLocal ?? "-"}
남은시간
{formatRemainingMs(jwtState.remainingMs)}
상태
{jwtState.error ?? "정상"}
payload
          {jwtState.payload ? JSON.stringify(jwtState.payload, null, 2) : "-"}
        
); }