fix: 토큰 갱신/로그인 유지(keepLogin) 정상화

세션이 짧게 유지되고 '로그인 유지'가 동작하지 않던 문제 수정.

원인1(프론트): 401 자동 refresh 가 '/auth/' 포함 URL을 모두 제외해,
부트스트랩 /auth/me 가 access 만료(15분)로 401 이어도 refresh 를 시도하지 않아
새로고침/재방문 시 로그아웃됨.
 - useApi 인터셉터: refresh-재시도 제외 대상을 /auth/refresh·/auth/login 으로만 한정
   → /auth/me 등은 access 만료 시 refresh 후 재시도되어 세션 복원

원인2(백엔드): keepLogin 값이 무시되어 쿠키 영속 여부에 반영되지 않음.
 - RefreshSession 에 persistent 플래그 추가(마이그레이션, default true)
 - 로그인 시 keepLogin 저장, refresh 회전 시에도 그대로 유지
 - setAuthCookies: persistent=true 면 maxAge(영속), false 면 세션 쿠키
 - 소셜 로그인은 체크박스가 없어 영속(true) 기본

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-23 10:56:25 +09:00
parent 0cdd2bb98e
commit b8756d34ab
5 changed files with 85 additions and 19 deletions
+8 -3
View File
@@ -79,8 +79,13 @@ api.interceptors.request.use(
(error: unknown) => Promise.reject(error),
)
// 인증 만료 응답으로 판단할지 — /auth/* 자체 호출은 재시도 대상에서 제외
const isAuthEndpoint = (url?: string): boolean => (url ?? '').includes('/auth/')
// 401 refresh-재시도에서 제외할 호출 — refresh/login 자체만 제외한다.
// (refresh 무한 루프·잘못된 자격증명 재시도 방지. 단 /auth/me 등은 access 만료 시
// refresh 후 재시도되어야 새로고침/재진입에도 세션이 복원된다)
const isRefreshExcluded = (url?: string): boolean => {
const u = url ?? ''
return u.includes('/auth/refresh') || u.includes('/auth/login')
}
// refresh 단일 실행(single-flight) — 동시 401 이 각자 refresh 를 호출하면 회전형 토큰이
// 재사용으로 오탐되어 세션이 폐기될 수 있다. 진행 중 refresh 가 있으면 그 Promise 를 공유한다.
@@ -120,7 +125,7 @@ api.interceptors.response.use(
// access 토큰 만료(401) → refresh 1회 시도 후 원요청 재시도
// (동시 401 은 single-flight 로 묶어 refresh 토큰 회전이 한 번만 일어나게 한다)
if (status === 401 && config && !config._retry && !isAuthEndpoint(config.url)) {
if (status === 401 && config && !config._retry && !isRefreshExcluded(config.url)) {
config._retry = true
try {
await refreshOnce()