diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7ed9c92..2ae7ef3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import React from 'react'; import './globals.scss'; +import './lib/axios'; import { AuthProvider, diff --git a/src/app/lib/axios.ts b/src/app/lib/axios.ts new file mode 100644 index 0000000..0746e6c --- /dev/null +++ b/src/app/lib/axios.ts @@ -0,0 +1,71 @@ +'use client'; + +import axios, { type AxiosError, type AxiosResponse } from 'axios'; + +import { postTokenRefresh } from '@/features/auth'; +import { load, save } from '@/shared/storage'; +import { type FailureDTO } from '@/shared/types'; + +let isRefreshing = false; +let refreshSubscribers: Array<(token: string) => void> = []; + +const subscribeTokenRefresh = (callback: (token: string) => void) => { + refreshSubscribers.push(callback); +}; + +const onRefreshed = (token: string) => { + refreshSubscribers.forEach(callback => { + callback(token); + }); + refreshSubscribers = []; +}; + +axios.interceptors.response.use( + (response: AxiosResponse) => response, + async (error: AxiosError) => { + if (error.response?.status !== 401 || error.response?.data.code !== 'C002') + return await Promise.reject(error); + + const refreshToken = load({ type: 'local', key: 'refreshToken' }); + if (refreshToken == null) { + window.location.href = '/'; + return await Promise.reject(error); + } + + if (isRefreshing) { + return await new Promise(resolve => { + subscribeTokenRefresh((token: string) => { + const { config } = error; + if (config?.headers != null) { + config.headers.Authorization = `Bearer ${token}`; + resolve(axios(config)); + } + }); + }); + } + + isRefreshing = true; + try { + const { + data: { accessToken, refreshToken: newRefreshToken, expiresIn }, + } = await postTokenRefresh(refreshToken); + + axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`; + save({ type: 'local', key: 'refreshToken', value: newRefreshToken }); + save({ type: 'local', key: 'expiresIn', value: `${expiresIn}` }); + + const { config } = error; + onRefreshed(accessToken); + if (config != null) { + config.headers.Authorization = `Bearer ${accessToken}`; + return await axios(config); + } + } catch (refreshError) { + return await Promise.reject(error); + } finally { + isRefreshing = false; + } + + return await Promise.reject(error); + }, +); diff --git a/src/app/lib/providers/InitializationProvider.tsx b/src/app/lib/providers/InitializationProvider.tsx index 6f3d806..0acae5e 100644 --- a/src/app/lib/providers/InitializationProvider.tsx +++ b/src/app/lib/providers/InitializationProvider.tsx @@ -18,7 +18,6 @@ export function InitialzationProvider({ const { data: userData } = useUserData(auth?.accessToken != null); useEffect(() => { - console.log(userData); if (userData != null) { setAuthUserData(userData); if (userData.initialized) { diff --git a/src/app/pages/main-page.tsx b/src/app/pages/main-page.tsx index 38b2880..1764dce 100644 --- a/src/app/pages/main-page.tsx +++ b/src/app/pages/main-page.tsx @@ -81,6 +81,19 @@ const styles = { display: none; } `, + mateRecommendationIsEmpty: styled.div` + color: #000; + font-family: 'Noto Sans KR'; + font-size: 1.5rem; + font-style: normal; + font-weight: 500; + line-height: normal; + + display: flex; + width: 100%; + justify-content: center; + padding-block: 2rem; + `, }; export function MainPage() { @@ -144,30 +157,45 @@ export function MainPage() { {auth?.user?.name}님의 추천 메이트 - - - {recommendationMates?.data?.map( - ({ memberId, score, nickname, location, profileImageUrl }) => ( - - - - ), - )} - - + {recommendationMates?.data != null && + recommendationMates.data.length > 0 ? ( + <> + + + {recommendationMates.data.map( + ({ + memberId, + score, + nickname, + location, + profileImageUrl, + }) => ( + + + + ), + )} + + + + ) : ( + +

추천되는 메이트가 없습니다.

+
+ )}
diff --git a/src/app/pages/mobile/mobile-main-page.tsx b/src/app/pages/mobile/mobile-main-page.tsx index b53457a..dd49959 100644 --- a/src/app/pages/mobile/mobile-main-page.tsx +++ b/src/app/pages/mobile/mobile-main-page.tsx @@ -88,6 +88,19 @@ const styles = { overflow-x: auto; flex-wrap: wrap; `, + mateRecommendationIsEmpty: styled.div` + color: #000; + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; + + display: flex; + width: 100%; + justify-content: center; + padding-block: 2rem; + `, }; export function MobileMainPage() { @@ -136,20 +149,27 @@ export function MobileMainPage() {

{auth?.user?.name}님의 추천 메이트

- - {recommendationMates?.data?.map( - ({ memberId, score, nickname, location, profileImageUrl }) => ( - - - - ), - )} - + {recommendationMates?.data != null && + recommendationMates.data.length > 0 ? ( + + {recommendationMates.data.map( + ({ memberId, score, nickname, location, profileImageUrl }) => ( + + + + ), + )} + + ) : ( + +

추천되는 메이트가 없습니다.

+
+ )} ); diff --git a/src/app/pages/mobile/mobile-shared-posts-page.tsx b/src/app/pages/mobile/mobile-shared-posts-page.tsx index 0a7905f..a03139e 100644 --- a/src/app/pages/mobile/mobile-shared-posts-page.tsx +++ b/src/app/pages/mobile/mobile-shared-posts-page.tsx @@ -22,8 +22,6 @@ import { useDormitorySharedPosts, usePaging, useSharedPosts, - type GetDormitorySharedPostsDTO, - type GetSharedPostsDTO, } from '@/features/shared'; const styles = { @@ -135,6 +133,16 @@ const styles = { justify-content: flex-start; } `, + noRecommendation: styled.div` + font-family: 'Noto Sans KR'; + font-size: 0.85rem; + font-style: normal; + font-weight: 500; + + display: flex; + width: 100%; + justify-content: center; + `, }; export function MobileSharedPostsPage() { @@ -143,9 +151,6 @@ export function MobileSharedPostsPage() { const auth = useAuthValue(); const [selected, setSelected] = useState('hasRoom'); const [totalPageCount, setTotalPageCount] = useState(0); - const [prevSharedPosts, setPrevSharedPosts] = useState< - GetSharedPostsDTO | GetDormitorySharedPostsDTO | null - >(null); const { filter, derivedFilter, reset: resetFilter } = useSharedPostsFilter(); @@ -197,10 +202,8 @@ export function MobileSharedPostsPage() { useEffect(() => { if (selected === 'hasRoom' && sharedPosts != null) { setTotalPageCount(sharedPosts.data.totalPages); - setPrevSharedPosts(null); } else if (selected === 'dormitory' && dormitorySharedPosts != null) { setTotalPageCount(dormitorySharedPosts.data.totalPages); - setPrevSharedPosts(null); } }, [selected, dormitorySharedPosts, sharedPosts]); @@ -224,27 +227,23 @@ export function MobileSharedPostsPage() { {selected === 'hasRoom' || selected === 'dormitory' ? ( <> - {prevSharedPosts != null - ? prevSharedPosts.data.content.map(post => ( - { - router.push(`/shared/${post.id}`); - }} - /> - )) - : posts?.data.content.map(post => ( - { - router.push( - `/shared/${selected === 'hasRoom' ? 'room' : 'dormitory'}/${post.id}`, - ); - }} - /> - ))} + {posts?.data != null && posts.data.content.length > 0 ? ( + posts?.data.content.map(post => ( + { + router.push( + `/shared/${selected === 'hasRoom' ? 'room' : 'dormitory'}/${post.id}`, + ); + }} + /> + )) + ) : ( + +

추천되는 게시글이 없습니다.

+
+ )}
{posts?.data.content.length !== 0 && ( @@ -252,9 +251,6 @@ export function MobileSharedPostsPage() { direction="left" disabled={isFirstPage} onClick={() => { - if (sharedPosts != null) { - setPrevSharedPosts(sharedPosts); - } handlePrevPage(); window.scrollTo({ top: 0, behavior: 'smooth' }); }} @@ -269,9 +265,6 @@ export function MobileSharedPostsPage() {