import {QueryClientProvider} from '@tanstack/react-query'; import {useEffect, type ReactNode} from 'react'; import {Toaster} from 'sonner'; import {getAccessToken, me, setTokens} from '@/api'; import {decodeJwtSubject} from '@/lib/jwt'; import {queryClient} from '@/lib/query-client'; import {toAuthUser, useAuthStore} from '@/stores/auth'; /** 저장된 액세스 토큰으로 세션을 복구한다. */ function useRestoreSession() { const setUser = useAuthStore((s) => s.setUser); const finishRestore = useAuthStore((s) => s.finishRestore); useEffect(() => { let alive = true; // ★ 프래그먼트(#auto=)를 먼저 본다. 메일의 '고쳐서 올리려면' 링크가 그쪽으로 넘긴다 — // 프래그먼트는 서버 로그와 Referer 에 남지 않기 때문이다. 쿼리(?auto=)도 계속 받는다: // 이미 나간 메일이 자정까지 살아 있고, 그걸 깨면 그 링크들이 통째로 죽는다. const hashToken = new URLSearchParams(window.location.hash.replace(/^#/, '')).get('auto'); const autoToken = hashToken ?? new URLSearchParams(window.location.search).get('auto'); const claims = autoToken ? decodeJwtSubject(autoToken) : null; if (autoToken && claims) { setTokens(autoToken); setUser({userId: claims.user_id, id: claims.id, role: claims.role}); // 주소창에 토큰을 남기지 않는다 — 히스토리·스크린샷·어깨너머로 새는 자리다. if (hashToken) { window.history.replaceState(null, '', window.location.pathname + window.location.search); } finishRestore(); return; } if (!getAccessToken()) { finishRestore(); return; } void me() .then((res) => { if (!alive || res.result?.success === false) return; // RemoveNoneResponse 라 신원 필드가 통째로 빠져 올 수 있다. if (!res.user_id || !res.id) return; setUser(toAuthUser(res)); }) .catch(() => { /* 토큰이 죽었으면 비로그인 상태로 계속 간다. */ }) .finally(() => { if (alive) finishRestore(); }); return () => { alive = false; }; }, [setUser, finishRestore]); } function SessionGate({children}: {children: ReactNode}) { useRestoreSession(); return <>{children}; } export function Providers({children}: {children: ReactNode}) { return ( {children} ); }