o2o-site-AEO/solution/frontend/src/app/provider.tsx
Mina Choi 1dc4f5fbf5 merge: origin/main(에이전트 여러 동작 · 사진 도구 · 카톡 승인 알림) 을 feature/template-catalog 에 병합
충돌은 이 브랜치의 주석 정리 커밋(11d30bb)과 main 의 기능 변경이 겹친 백엔드 9개 파일 —
main 쪽을 그대로 썼다(기능이 main 에만 있다). AGENTS.md 는 양쪽 링크를 모두 두고,
DEVLOG 는 요약본 위에 main 의 09-28 에이전트 항목 셋을 붙였다.

site eslint 통과 · vitest 123 passed · frontend tsc·eslint 통과 · 백엔드 py_compile 통과
(백엔드 pytest 는 이 기계에 테스트 DB 계정이 없어 실행하지 못함)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:51:53 +09:00

70 lines
2.6 KiB
TypeScript

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 (
<QueryClientProvider client={queryClient}>
<SessionGate>{children}</SessionGate>
<Toaster richColors position="top-center" expand visibleToasts={9} />
</QueryClientProvider>
);
}