충돌은 이 브랜치의 주석 정리 커밋(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>
70 lines
2.6 KiB
TypeScript
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>
|
|
);
|
|
}
|