'use client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { useState, useEffect } from 'react'; import { Toaster } from 'react-hot-toast'; import { useAuthStore } from '@/store/authStore'; import axios from 'axios'; import { ThemeProvider } from '@/components/theme/ThemeProvider'; // πŸ› οΈ JWT 토큰 만료 μ—¬λΆ€ 체크 ν•¨μˆ˜ (라이브러리 없이 κ΅¬ν˜„) function isTokenExpired(token: string) { try { const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const jsonPayload = decodeURIComponent( atob(base64) .split('') .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) .join('') ); const { exp } = JSON.parse(jsonPayload); // ν˜„μž¬ μ‹œκ°„(초)이 만료 μ‹œκ°„(exp)보닀 ν¬κ±°λ‚˜ κ°™μœΌλ©΄ 만료됨 // μ•ˆμ „ λ§ˆμ§„ 60초 μΆ”κ°€ (만료 1λΆ„ 전이면 미리 κ°±μ‹ ) return Date.now() / 1000 >= exp - 60; } catch { return true; // νŒŒμ‹± μ‹€νŒ¨ μ‹œ 만료된 κ²ƒμœΌλ‘œ κ°„μ£Ό } } // πŸ›‘οΈ μ•± μ΄ˆκΈ°ν™” μ‹œ 토큰 검증 μ»΄ν¬λ„ŒνŠΈ function AuthInitializer() { const { accessToken, refreshToken, login, logout } = useAuthStore(); useEffect(() => { const initializeAuth = async () => { // 토큰이 μ—†μœΌλ©΄ 검사할 ν•„μš” μ—†μŒ if (!accessToken || !refreshToken) return; // 토큰이 λ§Œλ£Œλ˜μ—ˆλŠ”μ§€ 확인 if (isTokenExpired(accessToken)) { // console.log('πŸ”„ AccessToken expired on init, refreshing...'); try { const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080'; // κ°±μ‹  μš”μ²­ (http 인터셉터 κ±°μΉ˜μ§€ μ•Šκ³  직접 axios μ‚¬μš©) const { data } = await axios.post( `${BASE_URL}/api/auth/reissue`, { accessToken, refreshToken }, { headers: { 'Content-Type': 'application/json' }, withCredentials: true } ); if (data.code === 'SUCCESS' && data.data) { // κ°±μ‹  성곡: μŠ€ν† μ–΄ μ—…λ°μ΄νŠΈ login(data.data.accessToken, data.data.refreshToken); // console.log('βœ… Token refreshed successfully on init'); } else { throw new Error('Token refresh response invalid'); } } catch { // console.error('❌ Failed to refresh token on init:', error); // κ°±μ‹  μ‹€νŒ¨ μ‹œ κΉ”λ”ν•˜κ²Œ λ‘œκ·Έμ•„μ›ƒ μ²˜λ¦¬ν•˜μ—¬ κΌ¬μž„ λ°©μ§€ logout(); // ν•„μš” μ‹œ 둜그인 νŽ˜μ΄μ§€λ‘œ 이동 (선택 사항) // window.location.href = '/login'; } } }; initializeAuth(); }, [accessToken, refreshToken, login, logout]); return null; // UIλ₯Ό λ Œλ”λ§ν•˜μ§€ μ•ŠμŒ } export default function Providers({ children }: { children: React.ReactNode }) { const [queryClient] = useState( () => new QueryClient({ defaultOptions: { queries: { // μœˆλ„μš° 포컀슀 μ‹œ μžλ™ 리페치 λ°©μ§€ (λΆˆν•„μš”ν•œ μš”μ²­ μ€„μž„) refetchOnWindowFocus: false, retry: 1, staleTime: 1000 * 60 * 5, // 5λΆ„κ°„ 데이터 캐싱 }, }, }) ); return ( {/* πŸ‘ˆ μ•± μ‹€ν–‰ μ‹œ 토큰 μžλ™ 검사 */} {children} {process.env.NODE_ENV === 'development' && ( )} ); }