Skip to content

Commit c9e60f3

Browse files
authored
✨feat:토큰리프레시 테스트 1차 (#580)
1 parent 06bab5c commit c9e60f3

File tree

3 files changed

+125
-50
lines changed

3 files changed

+125
-50
lines changed

src/api/apis/index.tsx

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { updateLoginInfo } from '@/store/queries/update-login-info/updateLoginInfo';
2+
import { useLoginStore } from '@/store/stores/login/loginStore';
13
import axios from 'axios';
24

35
const axiosInstance = axios.create({
@@ -8,6 +10,27 @@ const axiosInstance = axios.create({
810
}
911
});
1012

13+
// 토큰 갱신 중임을 나타내는 변수
14+
let isRefreshing = false;
15+
16+
// refreshSubscribers의 콜백 함수 타입 정의
17+
type RefreshSubscriber = (token: string) => void;
18+
19+
// 토큰 갱신 대기중인 요청들을 저장하는 배열
20+
let refreshSubscribers: RefreshSubscriber[] = [];
21+
22+
// 토큰 갱신 후 대기중인 요청들을 처리하는 함수
23+
const onRefreshed = (newToken: string): void => {
24+
refreshSubscribers.forEach((callback) => callback(newToken));
25+
refreshSubscribers = [];
26+
};
27+
28+
// 토큰 갱신을 기다리는 함수
29+
const addRefreshSubscriber = (callback: RefreshSubscriber): void => {
30+
refreshSubscribers.push(callback);
31+
};
32+
33+
// 요청 인터셉터
1134
axiosInstance.interceptors.request.use(
1235
(config) => {
1336
const token = localStorage.getItem('token');
@@ -27,7 +50,63 @@ axiosInstance.interceptors.response.use(
2750
const res = response.data;
2851
return res;
2952
},
30-
(error) => {
53+
async (error) => {
54+
const originalRequest = error.config;
55+
56+
// 토큰이 만료되었고, 재시도하지 않은 요청인 경우
57+
if (error.response?.status === 401 && !originalRequest._retry) {
58+
originalRequest._retry = true;
59+
60+
if (!isRefreshing) {
61+
isRefreshing = true;
62+
63+
try {
64+
// 토큰 갱신 요청
65+
const oldToken = localStorage.getItem('token');
66+
67+
console.log('oldToken:', oldToken);
68+
69+
const refreshResponse = await axios.post<{ data: string }>(
70+
`${import.meta.env.VITE_APP_BASE_URL}/api/auth/token/refresh`,
71+
{ access_token: oldToken?.replace('Bearer ', '') },
72+
{ withCredentials: true }
73+
);
74+
75+
const newToken = `${refreshResponse.data.data}`;
76+
localStorage.setItem('token', newToken);
77+
78+
console.log('newToken:', newToken);
79+
80+
updateLoginInfo(newToken);
81+
82+
isRefreshing = false;
83+
onRefreshed(newToken);
84+
85+
console.log('newToken으로 갱신완료');
86+
87+
// 원래 요청 재시도
88+
originalRequest.headers.Authorization = newToken;
89+
return axiosInstance(originalRequest);
90+
} catch (refreshError) {
91+
console.log('newToken으로 갱신실패');
92+
93+
isRefreshing = false;
94+
localStorage.removeItem('token');
95+
useLoginStore.getState().clearLoginInfo();
96+
window.location.href = '/login';
97+
return Promise.reject(refreshError);
98+
}
99+
} else {
100+
// 토큰 갱신 중인 경우, 새로운 토큰을 받을 때까지 대기
101+
return new Promise((resolve) => {
102+
addRefreshSubscriber((newToken: string) => {
103+
originalRequest.headers.Authorization = newToken;
104+
resolve(axiosInstance(originalRequest));
105+
});
106+
});
107+
}
108+
}
109+
31110
return Promise.reject(error);
32111
}
33112
);

src/pages/success-page/index.tsx

Lines changed: 21 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,53 @@
1-
import axiosInstance from '@/api/apis';
2-
import { useLoginStore } from '@/store/stores/login/loginStore';
31
import { useEffect } from 'react';
42
import { useNavigate } from 'react-router';
3+
import axiosInstance from '@/api/apis';
4+
import { useLoginStore } from '@/store/stores/login/loginStore';
5+
import { updateLoginInfo } from '@/store/queries/update-login-info/updateLoginInfo';
56

67
const SuccessPage = () => {
78
const token = localStorage.getItem('token');
8-
99
const loginType = useLoginStore((state) => state.loginType);
10-
const setLoginInfo = useLoginStore((state) => state.setLoginInfo);
11-
const clearLoginInfo = useLoginStore((state) => state.clearLoginInfo);
12-
1310
const navigate = useNavigate();
1411

15-
//단기토큰 받는거로 바뀌면, 해당 단기토큰으로 ACCESS토큰 받아오는 useEffect하나 더 필요 (쿠키에서 꺼내서 보내는거로)
16-
//로그인 로직에서는 local에 저장하는 것 삭제하고, 여기서 ACCESS받아서 local에 저장하도록 해야함.
17-
18-
///////////////////////////////
19-
//토큰을 해석해서 zustand에 저장
20-
///////////////////////////////
12+
// 토큰 처리 및 로그인 정보 저장
2113
useEffect(() => {
22-
const getLoginInfo = () => {
23-
try {
24-
if (!token) {
25-
clearLoginInfo();
26-
return;
27-
}
28-
29-
// Bearer 제거
30-
const actualToken = token.replace('Bearer ', '');
31-
32-
// JWT 디코딩 (payload 부분)
33-
const payload = JSON.parse(atob(actualToken.split('.')[1]));
34-
35-
const USER_ID = payload.userId;
36-
const ROLE_ID = payload.roleId;
37-
const ROLE = payload.role;
38-
39-
if (ROLE === 'ROLE_CENTER' || ROLE === 'ROLE_VOLUNTEER') {
40-
setLoginInfo(USER_ID, ROLE_ID, ROLE);
41-
}
42-
} catch (error) {
43-
console.error('토큰 디코딩 실패:', error);
44-
}
45-
};
46-
47-
getLoginInfo();
48-
}, [token, setLoginInfo]);
14+
if (token) {
15+
updateLoginInfo(token);
16+
}
17+
}, [token]);
4918

50-
////////////////////////////
51-
//상세정보입력 여부 가져와서 리디렉션 위치 판별
52-
////////////////////////////
19+
// 상세정보 입력 여부 확인 및 리다이렉션
5320
useEffect(() => {
5421
const checkBasicInfo = async () => {
5522
try {
5623
const response = await axiosInstance.get('api/user/check/basic-info');
5724
const hasBasicInfo = response.data;
5825

59-
console.log('hasBasicInfo', hasBasicInfo);
60-
6126
if (hasBasicInfo) {
6227
navigate('/main');
6328
} else {
64-
if (loginType === 'ROLE_CENTER') {
65-
navigate('/center-detail');
66-
} else if (loginType === 'ROLE_VOLUNTEER') {
67-
navigate('/volunteer-detail');
29+
switch (loginType) {
30+
case 'ROLE_CENTER':
31+
navigate('/center-detail');
32+
break;
33+
case 'ROLE_VOLUNTEER':
34+
navigate('/volunteer-detail');
35+
break;
36+
default:
37+
break;
6838
}
6939
}
7040
} catch (error) {
7141
console.error('상세정보 확인 실패:', error);
42+
// 에러 발생 시 로그인 페이지로 리다이렉션 추가
43+
navigate('/login');
7244
}
7345
};
7446

7547
if (loginType) {
7648
checkBasicInfo();
7749
}
78-
}, [loginType]);
50+
}, [loginType, navigate]);
7951

8052
return <></>;
8153
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useLoginStore } from '@/store/stores/login/loginStore';
2+
3+
export const updateLoginInfo = (token: string) => {
4+
try {
5+
if (!token) {
6+
useLoginStore.getState().clearLoginInfo();
7+
return;
8+
}
9+
10+
const actualToken = token.replace('Bearer ', '');
11+
const payload = JSON.parse(atob(actualToken.split('.')[1]));
12+
13+
const USER_ID = payload.userId;
14+
const ROLE_ID = payload.roleId;
15+
const ROLE = payload.role;
16+
17+
if (ROLE === 'ROLE_CENTER' || ROLE === 'ROLE_VOLUNTEER') {
18+
useLoginStore.getState().setLoginInfo(USER_ID, ROLE_ID, ROLE);
19+
}
20+
} catch (error) {
21+
console.error('토큰 디코딩 실패:', error);
22+
return null;
23+
}
24+
};

0 commit comments

Comments
 (0)