Skip to content

Commit 1dbeb90

Browse files
authored
[fix] 로그아웃 시 auth/me api 자동호출로 401 에러 (#136)
* [fix] 로그아웃 시 auth/me 자동호출로 401 에러 해결 * [fix] 로그인 새로고침 시 오류 로직 수정 * [fix] 리다이렉트 setTimeout 추가
1 parent bd767b0 commit 1dbeb90

File tree

5 files changed

+105
-25
lines changed

5 files changed

+105
-25
lines changed

src/domains/login/components/ClientInitHook.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,29 @@
22

33
import { useFetchInterceptor } from '@/shared/hook/useFetchInterceptor';
44
import { useIdleLogout } from '../hook/useIdleLogout';
5-
import { useEffect } from 'react';
5+
import { useEffect, useRef } from 'react';
66
import { useAuthStore } from '@/domains/shared/store/auth';
77

88
function ClientInitHook() {
9-
const checkAuth = useAuthStore((state) => state.checkAuth);
9+
const { isAuthChecked } = useAuthStore();
10+
const checkAuthRef = useRef(useAuthStore.getState().checkAuth);
1011

1112
useIdleLogout();
13+
1214
useFetchInterceptor();
1315

1416
useEffect(() => {
15-
checkAuth();
16-
}, [checkAuth]);
17+
// ref를 최신 함수로 업데이트
18+
checkAuthRef.current = useAuthStore.getState().checkAuth;
19+
});
20+
21+
useEffect(() => {
22+
if (!isAuthChecked) {
23+
checkAuthRef.current();
24+
}
25+
}, [isAuthChecked]);
26+
1727
return null;
1828
}
29+
1930
export default ClientInitHook;

src/domains/login/hook/useLoginRedirect.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const useLoginRedirect = () => {
3333

3434
if (user && preLoginPath === '/login') {
3535
router.replace('/');
36-
removeCookie('preLoginPath');
36+
setTimeout(() => removeCookie('preLoginPath'), 500);
3737
return;
3838
}
3939

@@ -42,14 +42,14 @@ export const useLoginRedirect = () => {
4242
} else if (pathname.startsWith('/login/user/success')) {
4343
toastSuccess(`${user.nickname}님 \n 로그인 성공 🎉`);
4444
router.replace(preLoginPath);
45-
removeCookie('preLoginPath');
45+
setTimeout(() => removeCookie('preLoginPath'), 500);
4646
}
4747
}, [pathname, user, loading, router, toastSuccess]);
4848

4949
const handleCloseWelcomeModal = () => {
5050
setWelcomeModalOpen(false);
5151
const preLoginPath = getCookie('preLoginPath') || '/';
52-
removeCookie('preLoginPath');
52+
setTimeout(() => removeCookie('preLoginPath'), 500);
5353
router.replace(preLoginPath);
5454
};
5555

src/domains/shared/store/auth.ts

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// auth.ts
12
import { getApi } from '@/app/api/config/appConfig';
23
import { create } from 'zustand';
34

@@ -13,43 +14,47 @@ export interface User {
1314
interface AuthState {
1415
user: User | null;
1516
isLoggedIn: boolean;
17+
isAuthChecked: boolean;
18+
1619
setUser: (user: User) => void;
1720
logout: () => Promise<void>;
1821
loginWithProvider: (provider: User['provider']) => void;
19-
2022
updateUser: () => Promise<User | null>;
2123
checkAuth: () => Promise<User | null>;
2224
}
2325

24-
export const useAuthStore = create<AuthState>()((set) => ({
26+
export const useAuthStore = create<AuthState>()((set, get) => ({
2527
user: null,
2628
isLoggedIn: false,
29+
isAuthChecked: false,
2730

2831
loginWithProvider: (provider) => {
2932
window.location.href = `${getApi}/oauth2/authorization/${provider}`;
3033
},
3134

3235
setUser: (user) => {
3336
const updatedUser = { ...user, abv_degree: user.abv_degree ?? 5.0 };
34-
set({ user: updatedUser, isLoggedIn: true });
37+
set({
38+
user: updatedUser,
39+
isLoggedIn: true,
40+
isAuthChecked: true,
41+
});
3542
},
3643

37-
// 로그아웃
3844
logout: async () => {
3945
try {
4046
await fetch(`${getApi}/user/auth/logout`, {
4147
method: 'POST',
4248
credentials: 'include',
4349
});
44-
set({ user: null, isLoggedIn: false });
50+
51+
set({ user: null, isLoggedIn: false, isAuthChecked: true });
4552
} catch (err) {
4653
console.error('로그아웃 실패', err);
47-
} finally {
48-
set({ user: null, isLoggedIn: false });
54+
set({ user: null, isLoggedIn: false, isAuthChecked: true });
4955
}
5056
},
5157

52-
// idle + refresh 시 호출
5358
updateUser: async () => {
5459
try {
5560
const res = await fetch(`${getApi}/user/auth/refresh`, {
@@ -58,15 +63,22 @@ export const useAuthStore = create<AuthState>()((set) => ({
5863
headers: { 'Content-Type': 'application/json' },
5964
});
6065

61-
if (!res.ok) throw new Error('토큰 갱신 실패');
66+
// 200 응답 기대
67+
if (!res.ok) {
68+
set({ user: null, isLoggedIn: false });
69+
return null;
70+
}
6271

6372
const data = await res.json();
6473
const userInfo = data?.data?.user;
6574

6675
if (userInfo) {
67-
set({ user: userInfo, isLoggedIn: true });
68-
return userInfo;
76+
const updatedUser = { ...userInfo, abv_degree: userInfo.abv_degree ?? 5.0 };
77+
set({ user: updatedUser, isLoggedIn: true });
78+
return updatedUser;
6979
}
80+
81+
set({ user: null, isLoggedIn: false });
7082
return null;
7183
} catch (err) {
7284
console.error('updateUser 실패', err);
@@ -75,24 +87,41 @@ export const useAuthStore = create<AuthState>()((set) => ({
7587
}
7688
},
7789

78-
// 시작 시 로그인 상태 확인
7990
checkAuth: async () => {
91+
const { isAuthChecked } = get();
92+
93+
// 이미 체크했으면 현재 user 반환
94+
if (isAuthChecked) {
95+
return get().user;
96+
}
97+
8098
try {
8199
const res = await fetch(`${getApi}/user/auth/me`, {
82100
method: 'GET',
83101
credentials: 'include',
84102
});
85-
if (!res.ok) throw new Error('인증 실패');
103+
104+
// 항상 200 응답 기대
105+
if (!res.ok) {
106+
set({ user: null, isLoggedIn: false, isAuthChecked: true });
107+
return null;
108+
}
86109

87110
const data = await res.json();
88-
const userInfo = data?.data?.user;
111+
const userInfo = data?.data?.user || null;
112+
89113
if (userInfo) {
90-
set({ user: userInfo, isLoggedIn: true });
91-
return userInfo;
114+
const updatedUser = { ...userInfo, abv_degree: userInfo.abv_degree ?? 5.0 };
115+
set({ user: updatedUser, isLoggedIn: true, isAuthChecked: true });
116+
return updatedUser;
92117
}
118+
119+
// user가 null이어도 정상 응답
120+
set({ user: null, isLoggedIn: false, isAuthChecked: true });
93121
return null;
94-
} catch {
95-
set({ user: null, isLoggedIn: false });
122+
} catch (err) {
123+
console.error('checkAuth 실패', err);
124+
set({ user: null, isLoggedIn: false, isAuthChecked: true });
96125
return null;
97126
}
98127
},

src/middleware.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// middleware.ts
2+
import { NextResponse, NextRequest } from 'next/server';
3+
4+
export function middleware(request: NextRequest) {
5+
const authCookie = request.cookies.get('accessToken')?.value;
6+
const { pathname } = request.nextUrl;
7+
8+
const PUBLIC_PATHS = ['/', '/login', '/community', '/recipe', '/recommend'];
9+
10+
const isPublicPath = PUBLIC_PATHS.some((path) => pathname.startsWith(path));
11+
12+
// 로그인한 사용자가 로그인 페이지 접근 → 홈으로
13+
if (authCookie && pathname === '/login') {
14+
return NextResponse.redirect(new URL('/', request.url));
15+
}
16+
17+
// 공개 경로가 아니면 로그인 필요
18+
if (!authCookie && !isPublicPath) {
19+
return NextResponse.redirect(new URL('/login', request.url));
20+
}
21+
22+
return NextResponse.next();
23+
}
24+
25+
export const config = {
26+
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)'],
27+
};

src/shared/hook/useFetchInterceptor.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,34 @@ export const useFetchInterceptor = () => {
1616
const response = await originalFetch(input, { ...init, credentials: 'include' });
1717

1818
if (response.status === 401) {
19+
// URL 문자열 추출
20+
const url =
21+
typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
22+
23+
// refresh API 자체가 401이면 무한루프 방지
24+
if (url.includes('/user/auth/refresh')) {
25+
return response;
26+
}
27+
1928
try {
2029
const refreshRes = await originalFetch(`${getApi}/user/auth/refresh`, {
2130
method: 'POST',
2231
credentials: 'include',
2332
});
2433

2534
if (refreshRes.ok) {
35+
// 토큰 갱신 성공 시 원래 요청 재시도
2636
return originalFetch(input, { ...init, credentials: 'include' });
2737
} else {
38+
// refresh 실패 → 로그인 페이지로
2839
toastInfo('로그인 인증 만료로 다시 로그인해주세요.');
2940
router.push('/login');
41+
return response;
3042
}
3143
} catch {
3244
toastInfo('로그인 인증 만료로 다시 로그인해주세요.');
3345
router.push('/login');
46+
return response;
3447
}
3548
}
3649
return response;

0 commit comments

Comments
 (0)