Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 14 additions & 36 deletions src/apis/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,11 @@ import axios from 'axios';

import useAuthStore from '@/stores/authStore';

import { getNewToken } from './auth';

const client = axios.create({
baseURL: import.meta.env.VITE_API_URL,
headers: { 'Content-Type': 'application/json' },
});

let isRefreshing = false;

const callReissue = async () => {
try {
const response = await getNewToken();
if(response?.status !== 200) throw new Error('error while fetching newToken');
const newToken = response?.data.data.accessToken;
return newToken;
} catch (e) {
return Promise.reject(e);
}
};

let retry = false;

client.interceptors.request.use(
(config) => {
const accessToken = useAuthStore.getState().accessToken;
Expand All @@ -39,36 +22,31 @@ client.interceptors.request.use(
client.interceptors.response.use(
(response) => response,
async (error) => {
const setAccessToken = useAuthStore.getState().setAccessToken;
const logout = useAuthStore.getState().logout;
const isLoggedIn = useAuthStore.getState().isLoggedIn;

const originalRequest = error.config;

if (!originalRequest || originalRequest.url === '/auth/reissue') {
if (!originalRequest || originalRequest.url === '/api/reissue') {
if (isLoggedIn) logout();
return Promise.reject(error);
}

if ((error.response?.status === 401 || error.response?.status === 403) && !retry) {
retry = true;
if (isRefreshing) {
if (isLoggedIn) logout();
} else {
isRefreshing = true;
try {
const newToken = await callReissue();
setAccessToken(newToken);
isRefreshing = false;
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return client(originalRequest);
} catch (e) {
isRefreshing = false;
if (isLoggedIn) logout();
return Promise.reject(e);
}
if (
(error.response?.status === 401 || error.response?.status === 403) &&
!originalRequest._retry
) {
originalRequest._retry = true;

try {
const newToken = await useAuthStore.getState().refreshToken();
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return client(originalRequest);
} catch (e) {
return Promise.reject(e);
}
}

return Promise.reject(error);
},
);
Expand Down
30 changes: 11 additions & 19 deletions src/hooks/useServerSentEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import useAuthStore from '@/stores/authStore';
import useToastStore from '@/stores/toastStore';
import { useNavigate } from 'react-router';
import useNotificationStore from '@/stores/notificationStore';
import { getNewToken } from '@/apis/auth';

interface MessageEventData {
title: string;
Expand All @@ -19,7 +18,6 @@ export const useServerSentEvents = () => {
// const recallCountRef = useRef(1);

const accessToken = useAuthStore((state) => state.accessToken);
const setAccessToken = useAuthStore((state) => state.setAccessToken);
const sourceRef = useRef<EventSourcePolyfill | null>(null);

const setToastActive = useToastStore((state) => state.setToastActive);
Expand All @@ -41,27 +39,17 @@ export const useServerSentEvents = () => {
}
};

// 토큰 재발급 함수
const callReissue = async () => {
try {
const response = await getNewToken();
if (response?.status !== 200) throw new Error('error while fetching newToken');
const newToken = response?.data.data.accessToken;
return setAccessToken(newToken);
} catch (e) {
return Promise.reject(e);
}
};

useEffect(() => {
if (!accessToken) {
console.log('로그인 정보 확인불가');
// console.log('로그인 정보 확인불가');
return;
}

const connectSSE = () => {
const accessToken = useAuthStore.getState().accessToken;

try {
console.log('구독 시작');
// console.log('구독 시작');
sourceRef.current = new EventSourcePolyfill(
`${import.meta.env.VITE_API_URL}/api/notifications/sub`,
{
Expand All @@ -77,11 +65,15 @@ export const useServerSentEvents = () => {
handleOnMessage(event.data);
};

sourceRef.current.onerror = (event) => {
sourceRef.current.onerror = async (event) => {
console.log(event);
const errorEvent = event as unknown as { status?: number };
if (errorEvent.status === 401) {
callReissue();
try {
await useAuthStore.getState().refreshToken();
} catch (error) {
console.log('다른 api에서 리프레시 토큰 호출중입니다.');
}
closeSSE();
reconnect = setTimeout(connectSSE, 5000);
} else {
Expand All @@ -97,7 +89,7 @@ export const useServerSentEvents = () => {
connectSSE();

return () => {
console.log('컴포넌트 언마운트로 인한 구독해제');
// console.log('컴포넌트 언마운트로 인한 구독해제');
closeSSE();
};
}, [accessToken]);
Expand Down
2 changes: 1 addition & 1 deletion src/layouts/PrivateRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useServerSentEvents } from '@/hooks/useServerSentEvents';
import Toast from '@/components/Toast';

export default function PrivateRoute() {
useServerSentEvents();
// useServerSentEvents();
const isLoggedIn = useAuthStore((state) => state.isLoggedIn);
const navigate = useNavigate();

Expand Down
30 changes: 29 additions & 1 deletion src/stores/authStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import useThemeStore from './themeStore';
import { getNewToken } from '@/apis/auth';

interface AuthStore {
isLoggedIn: boolean;
Expand All @@ -12,11 +13,14 @@ interface AuthStore {
setZipCode: (zipCode: string) => void;
setAccessToken: (accessToken: string) => void;
setIsAdmin: () => void;
isRefreshing: boolean;
refreshPromise: Promise<string> | null;
refreshToken: () => Promise<string>;
}

const useAuthStore = create(
persist<AuthStore>(
(set) => ({
(set, get) => ({
isLoggedIn: false,
accessToken: '',
zipCode: '',
Expand All @@ -40,6 +44,30 @@ const useAuthStore = create(
setZipCode: (zipCode) => set({ zipCode: zipCode }),
setAccessToken: (accessToken) => set({ accessToken: accessToken }),
setIsAdmin: () => set({ isAdmin: true }),
isRefreshing: false,
refreshPromise: null,
refreshToken: async () => {
// 이미 재발급 중이면 진행 중인 Promise 반환
if (get().isRefreshing && get().refreshPromise) {
return get().refreshPromise;
}
const refreshPromise = getNewToken()
.then((response) => {
if (response?.status !== 200) throw new Error('Token refresh failed');
const newToken = response?.data.data.accessToken;
set({ accessToken: newToken, isRefreshing: false, refreshPromise: null });
return newToken;
})
.catch((error) => {
set({ isRefreshing: false, refreshPromise: null });
// 재발급 실패 시 로그아웃
if (get().isLoggedIn) get().logout();
throw error;
});

set({ isRefreshing: true, refreshPromise });
return refreshPromise;
},
}),
{
name: 'userInfo',
Expand Down