Skip to content

Commit 43f0ea4

Browse files
committed
Merge branch 'qa'
2 parents 4c630ac + 3555182 commit 43f0ea4

File tree

4 files changed

+69
-98
lines changed

4 files changed

+69
-98
lines changed

src/api/banner.api.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { useMutation, useQuery } from '@tanstack/react-query';
22
import { apiClient } from './client';
3-
const authState = JSON.parse(localStorage.getItem('authState'));
4-
const accessToken = authState?.accessToken;
53

64
// [관리자] 모든 배너 조회
75
const fetchAllBanners = async () => {

src/api/client.js

Lines changed: 50 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,70 @@
11
import axios from 'axios';
22

3-
// 기존 authState를 가져오는 함수 정의
4-
const getAuthState = () => {
5-
const authState = localStorage.getItem('authState');
6-
return authState ? JSON.parse(authState) : null;
7-
};
8-
93
const baseUrl = process.env.REACT_APP_API_URL;
104

115
export const apiClient = axios.create({
126
baseURL: baseUrl,
13-
headers: {
14-
Authorization: `Bearer ${getAuthState()?.accessToken}`,
15-
},
7+
withCredentials: true,
168
});
179

18-
apiClient.interceptors.request.use(
19-
config => {
20-
const authState = getAuthState(); // authState 객체를 가져옵니다.
21-
const accessToken = authState?.accessToken;
22-
if (accessToken) {
23-
config.headers.Authorization = `Bearer ${accessToken}`;
24-
} else {
25-
delete config.headers.Authorization;
26-
}
27-
return config;
28-
},
29-
error => {
30-
return Promise.reject(error);
31-
},
32-
);
10+
// 동시에 여러 요청이 401 터질 때 refresh 중복 호출 방지
11+
let isRefreshing = false;
12+
let refreshQueue = [];
13+
14+
const runQueue = error => {
15+
refreshQueue.forEach(({ resolve, reject }) => {
16+
if (error) reject(error);
17+
else resolve();
18+
});
19+
refreshQueue = [];
20+
};
3321

3422
apiClient.interceptors.response.use(
35-
response => {
36-
return response;
37-
},
23+
response => response,
3824
async error => {
39-
if (error.response && error.response.status === 401) {
40-
// 리프레시 토큰 가져오기
41-
const authState = getAuthState(); // authState 객체를 가져옵니다.
42-
const refreshToken = authState?.refreshToken;
25+
if (!error?.response) return Promise.reject(error);
4326

44-
if (!refreshToken) {
45-
// 리프레시 토큰이 없으면 로그아웃 처리
46-
return Promise.reject(error);
47-
}
27+
const originalRequest = error.config;
4828

49-
// 현재 요청이 리프레시 요청인지 확인
50-
if (error.config.url.includes('/auth/refresh')) {
51-
return Promise.reject(error);
52-
}
29+
// 401만 처리
30+
if (error.response.status !== 401) {
31+
return Promise.reject(error);
32+
}
5333

54-
try {
55-
// 리프레시 토큰으로 액세스 토큰 갱신
56-
const response = await axios.post(`${baseUrl}/auth/refresh`, {
57-
refreshToken: refreshToken, // 올바른 키 사용
58-
});
59-
const accessToken = response.data.data.accessToken;
34+
// refresh 자체가 401이면 종료 (로그인 만료)
35+
if (originalRequest?.url?.includes('/auth/refresh')) {
36+
return Promise.reject(error);
37+
}
6038

61-
// authState 업데이트
62-
const updatedAuthState = {
63-
...authState,
64-
accessToken: accessToken,
65-
};
39+
// 무한 루프 방지
40+
if (originalRequest._retry) {
41+
return Promise.reject(error);
42+
}
43+
originalRequest._retry = true;
6644

67-
localStorage.setItem('authState', JSON.stringify(updatedAuthState));
68-
// 새로운 토큰으로 원래 요청 재시도
69-
error.config.headers['Authorization'] = `Bearer ${accessToken}`;
70-
return axios(error.config); // 원래 요청 재시도
71-
} catch (refreshError) {
72-
// 토큰 갱신 실패 시 로그아웃 처리
73-
return Promise.reject(error);
74-
}
45+
// 이미 refresh 중이면 큐에서 대기 후 재시도
46+
if (isRefreshing) {
47+
return new Promise((resolve, reject) => {
48+
refreshQueue.push({
49+
resolve: () => resolve(apiClient(originalRequest)),
50+
reject,
51+
});
52+
});
7553
}
7654

77-
return Promise.reject(error);
55+
isRefreshing = true;
56+
57+
try {
58+
// efreshToken 쿠키를 서버가 읽는 구조라면 바디 필요 없음
59+
await apiClient.post('/auth/refresh');
60+
61+
runQueue(null);
62+
return apiClient(originalRequest);
63+
} catch (refreshError) {
64+
runQueue(refreshError);
65+
return Promise.reject(refreshError);
66+
} finally {
67+
isRefreshing = false;
68+
}
7869
},
7970
);

src/hooks/authState.js

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,21 @@
11
import { atom } from 'recoil';
22

3-
const localStorageEffect = key => ({ setSelf, onSet }) => {
4-
const savedValue = localStorage.getItem(key);
5-
if (savedValue != null) {
6-
setSelf(JSON.parse(savedValue));
7-
}
3+
const localStorageEffect =
4+
key =>
5+
({ setSelf, onSet }) => {
6+
const savedValue = localStorage.getItem(key);
7+
if (savedValue != null) setSelf(JSON.parse(savedValue));
88

9-
onSet(newValue => {
10-
if (newValue) {
11-
localStorage.setItem(key, JSON.stringify(newValue));
12-
} else {
13-
localStorage.removeItem(key);
14-
}
15-
});
16-
};
9+
onSet(newValue => {
10+
if (newValue) localStorage.setItem(key, JSON.stringify(newValue));
11+
else localStorage.removeItem(key);
12+
});
13+
};
1714

1815
export const authState = atom({
1916
key: 'authState',
2017
default: {
2118
isAuthenticated: false,
22-
accessToken: null,
23-
refreshToken: null,
2419
},
2520
effects: [localStorageEffect('authState')],
2621
});

src/hooks/useAuth.js

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,24 @@ import { apiClient } from '../api/client';
55

66
const useAuth = () => {
77
const [auth, setAuth] = useRecoilState(authState);
8-
const isTokenValid = token => token !== undefined && token !== null;
98

109
const loggedIn = auth.isAuthenticated;
1110

1211
const login = useCallback(
1312
async ({ id, password }) => {
1413
try {
15-
const response = await apiClient.post(`/auth/login`, {
14+
const response = await apiClient.post('/auth/login', {
1615
username: id,
17-
password: password,
16+
password,
1817
});
1918

2019
const data = response?.data?.data;
21-
const accessToken = data?.accessToken;
22-
const refreshToken = data?.refreshToken;
23-
const tokenType = data?.tokenType;
2420
const isPasswordChangeRequired = Boolean(
2521
data?.isPasswordChangeRequired,
2622
);
2723

28-
if (!isTokenValid(accessToken) || !isTokenValid(refreshToken)) {
29-
throw new Error('유효하지 않는 토큰');
30-
}
31-
32-
setAuth({
33-
isAuthenticated: true,
34-
accessToken: accessToken,
35-
refreshToken: refreshToken,
36-
tokenType: tokenType ?? 'bearer',
37-
});
24+
// 쿠키는 서버가 Set-Cookie로 심어줌. 프론트는 상태만 갱신.
25+
setAuth({ isAuthenticated: true });
3826

3927
return {
4028
isPasswordChangeRequired,
@@ -50,12 +38,11 @@ const useAuth = () => {
5038
[setAuth],
5139
);
5240

53-
const logout = useCallback(() => {
54-
setAuth({
55-
isAuthenticated: false,
56-
accessToken: null,
57-
refreshToken: null,
58-
});
41+
const logout = useCallback(async () => {
42+
// 가능하면 서버에 logout API가 있으면 호출해서 쿠키 만료시키는 게 정석
43+
// await apiClient.post('/auth/logout');
44+
45+
setAuth({ isAuthenticated: false });
5946
}, [setAuth]);
6047

6148
return { ...auth, loggedIn, login, logout };

0 commit comments

Comments
 (0)