diff --git a/frontend/src/apiHooks/new/useGetTopics.ts b/frontend/src/apiHooks/new/useGetTopics.ts new file mode 100644 index 000000000..ffb65693b --- /dev/null +++ b/frontend/src/apiHooks/new/useGetTopics.ts @@ -0,0 +1,14 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; + +import { getTopics } from '../../apis/new'; + +const useGetTopics = (url: string) => { + const { data: topics, refetch } = useSuspenseQuery({ + queryKey: ['topics', url], + queryFn: () => getTopics(url), + }); + + return { topics, refetch }; +}; + +export default useGetTopics; diff --git a/frontend/src/apis/new/http.ts b/frontend/src/apis/new/http.ts new file mode 100644 index 000000000..3c39ecd15 --- /dev/null +++ b/frontend/src/apis/new/http.ts @@ -0,0 +1,101 @@ +import axios, { + AxiosInstance, + AxiosRequestConfig, + AxiosRequestHeaders, +} from 'axios'; + +const API_POSTFIX = 'api'; +const BASE_URL = process.env.APP_URL || `https://mapbefine.com/${API_POSTFIX}`; +const token = localStorage.getItem('userToken'); + +const axiosInstance = axios.create({ + baseURL: BASE_URL, + headers: token ? { Authorization: `Bearer ${token}` } : {}, + // withCredentials: true, +}); + +let refreshResponse: Promise<Response> | null = null; + +export interface HttpClient extends AxiosInstance { + get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>; + post<T = unknown>( + url: string, + data?: any, + config?: AxiosRequestConfig, + ): Promise<T>; + patch<T = unknown>( + url: string, + data?: any, + config?: AxiosRequestConfig, + ): Promise<T>; + put<T = unknown>( + url: string, + data?: any, + config?: AxiosRequestConfig, + ): Promise<T>; + delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>; +} + +export const http: HttpClient = axiosInstance; + +http.interceptors.response.use((res) => res.data); +http.interceptors.request.use( + async (config) => { + const userToken = localStorage.getItem('userToken'); + + if (userToken && isTokenExpired(userToken)) { + await updateToken(config.headers); + } + return config; + }, + (error) => Promise.reject(error), +); + +const isTokenExpired = (token: string) => { + const decodedPayloadObject = decodeToken(token); + return decodedPayloadObject.exp * 1000 < Date.now(); +}; + +const decodeToken = (token: string) => { + const tokenParts = token.split('.'); + + if (tokenParts.length !== 3) { + throw new Error('토큰이 잘못되었습니다.'); + } + + const decodedPayloadString = atob(tokenParts[1]); + + return JSON.parse(decodedPayloadString); +}; + +async function updateToken(headers: AxiosRequestHeaders) { + const response = await refreshToken(headers); + const responseCloned = response.clone(); + const newToken = await responseCloned.json(); + + localStorage.setItem('userToken', newToken.accessToken); +} + +async function refreshToken(headers: AxiosRequestHeaders): Promise<Response> { + if (refreshResponse !== null) { + return refreshResponse; + } + + const accessToken = localStorage.getItem('userToken'); + refreshResponse = fetch(`${BASE_URL}/refresh-token`, { + method: 'POST', + headers, + body: JSON.stringify({ + accessToken, + }), + }); + + const responseData = await refreshResponse; + refreshResponse = null; + + if (!responseData.ok) { + throw new Error('Failed to refresh access token.'); + } + + return responseData; +} diff --git a/frontend/src/apis/new/index.ts b/frontend/src/apis/new/index.ts new file mode 100644 index 000000000..a5f81fec0 --- /dev/null +++ b/frontend/src/apis/new/index.ts @@ -0,0 +1,4 @@ +import { TopicCardProps } from '../../types/Topic'; +import { http } from './http'; + +export const getTopics = (url: string) => http.get<TopicCardProps[]>(url); diff --git a/frontend/src/components/Skeletons/TopicCardSkeleton.tsx b/frontend/src/components/Skeletons/TopicCardSkeleton.tsx index 1592b62e8..f0d73cc06 100644 --- a/frontend/src/components/Skeletons/TopicCardSkeleton.tsx +++ b/frontend/src/components/Skeletons/TopicCardSkeleton.tsx @@ -1,53 +1,21 @@ import { keyframes, styled } from 'styled-components'; -import Flex from '../common/Flex'; +import Box from '../common/Box'; import Space from '../common/Space'; +import SkeletonBox from './common/SkeletonBox'; function TopicCardSkeleton() { return ( - <Flex $flexDirection="row"> - <SkeletonImg /> - <Space size={2} /> - <Flex $flexDirection="column"> - <SkeletonTitle /> - <Space size={5} /> - <SkeletonDescription /> - </Flex> - </Flex> + <Box> + <SkeletonBox width="100%" $maxWidth={212} ratio="1.6 / 1" /> + <Space size={1} /> + <SkeletonBox width={212} height={25} /> + <Space size={5} /> + <SkeletonBox width={100} height={25} /> + <Space size={1} /> + <SkeletonBox width={212} height={46} /> + </Box> ); } -const skeletonAnimation = keyframes` - from { - opacity: 0.1; - } - to { - opacity: 1; - } -`; - -const SkeletonImg = styled.div` - width: 138px; - height: 138px; - - border-radius: 8px; - - background: ${({ theme }) => theme.color.lightGray}; - animation: ${skeletonAnimation} 1s infinite; -`; - -const SkeletonTitle = styled.div` - width: 172px; - height: 32px; - - border-radius: 8px; - - background: ${({ theme }) => theme.color.lightGray}; - animation: ${skeletonAnimation} 1s infinite; -`; - -const SkeletonDescription = styled(SkeletonTitle)` - height: 80px; -`; - export default TopicCardSkeleton; diff --git a/frontend/src/components/Skeletons/TopicListSkeleton.tsx b/frontend/src/components/Skeletons/TopicListSkeleton.tsx index 79deb153f..e5ba5620c 100644 --- a/frontend/src/components/Skeletons/TopicListSkeleton.tsx +++ b/frontend/src/components/Skeletons/TopicListSkeleton.tsx @@ -1,26 +1,40 @@ import { styled } from 'styled-components'; +import Space from '../common/Space'; +import SkeletonBox from './common/SkeletonBox'; import TopicCardSkeleton from './TopicCardSkeleton'; -function TopicCardContainerSkeleton() { +function TopicListSkeleton() { return ( - <Wrapper> - <TopicCardSkeleton /> - <TopicCardSkeleton /> - <TopicCardSkeleton /> - <TopicCardSkeleton /> - <TopicCardSkeleton /> - <TopicCardSkeleton /> - </Wrapper> + <> + <Space size={5} /> + <SkeletonBox width={160} height={32} /> + <Space size={4} /> + <Space size={5} /> + <TopicCardWrapper> + <TopicCardSkeleton /> + <TopicCardSkeleton /> + <TopicCardSkeleton /> + <TopicCardSkeleton /> + <TopicCardSkeleton /> + </TopicCardWrapper> + <Space size={4} /> + <TopicCardWrapper> + <TopicCardSkeleton /> + <TopicCardSkeleton /> + <TopicCardSkeleton /> + <TopicCardSkeleton /> + <TopicCardSkeleton /> + </TopicCardWrapper> + </> ); } -const Wrapper = styled.section` +const TopicCardWrapper = styled.section` display: flex; flex-wrap: wrap; gap: 20px; - width: 1036px; - height: 300px; + width: 1140px; `; -export default TopicCardContainerSkeleton; +export default TopicListSkeleton; diff --git a/frontend/src/components/Skeletons/common/SkeletonBox.ts b/frontend/src/components/Skeletons/common/SkeletonBox.ts new file mode 100644 index 000000000..da8d15590 --- /dev/null +++ b/frontend/src/components/Skeletons/common/SkeletonBox.ts @@ -0,0 +1,38 @@ +import styled, { keyframes } from 'styled-components'; + +import { convertCSS } from '../../../utils/convertCSS'; + +interface Props { + width?: number | string; + height?: number | string; + $maxWidth?: number | string; + $maxHeight?: number | string; + ratio?: string; + radius?: number | string; +} + +const skeletonAnimation = keyframes` + from { + opacity: 1; + } + 50% { + opacity: 0.6; + } + to { + opacity: 1; + } +`; + +const SkeletonBox = styled.div<Props>` + width: ${({ width }) => width && convertCSS(width)}; + height: ${({ height }) => height && convertCSS(height)}; + max-width: ${({ $maxWidth }) => $maxWidth && convertCSS($maxWidth)}; + max-height: ${({ $maxHeight }) => $maxHeight && convertCSS($maxHeight)}; + aspect-ratio: ${({ ratio }) => ratio}; + border-radius: ${({ radius, theme }) => + (radius && convertCSS(radius)) || theme.radius.small}; + background: ${({ theme }) => theme.color.lightGray}; + animation: ${skeletonAnimation} 1s infinite; +`; + +export default SkeletonBox; diff --git a/frontend/src/components/TopicCardList/index.tsx b/frontend/src/components/TopicCardList/index.tsx index b8c2643fd..a5675fb25 100644 --- a/frontend/src/components/TopicCardList/index.tsx +++ b/frontend/src/components/TopicCardList/index.tsx @@ -1,7 +1,5 @@ -import { useEffect, useState } from 'react'; -import { styled } from 'styled-components'; - -import { TopicCardProps } from '../../types/Topic'; +import { ReactNode } from 'react'; +import useGetTopics from '../../apiHooks/new/useGetTopics'; import Button from '../common/Button'; import Flex from '../common/Flex'; import Grid from '../common/Grid'; @@ -12,30 +10,26 @@ import useProfileList from '../../hooks/queries/useProfileList'; interface TopicCardListProps { url: string; - errorMessage: string; commentWhenEmpty: string; - pageCommentWhenEmpty: string; + routePageName: string; routePage: () => void; - children?: React.ReactNode; + svgElementWhenEmpty?: ReactNode; } function TopicCardList({ url, - errorMessage, commentWhenEmpty, - pageCommentWhenEmpty, + routePageName, routePage, - children, + svgElementWhenEmpty, }: TopicCardListProps) { - const { data: topics, refetch: refetchTopic } = useProfileList(); - - if (!topics) return null; + const { topics, refetch } = useGetTopics(url); if (topics.length === 0) { return ( - <EmptyWrapper> + <Flex height="240px" $flexDirection="column" $alignItems="center"> <Flex $alignItems="center"> - {children} + {svgElementWhenEmpty} <Space size={1} /> <Text color="black" $fontSize="default" $fontWeight="normal"> {commentWhenEmpty} @@ -44,14 +38,14 @@ function TopicCardList({ </Flex> <Space size={5} /> <Button variant="primary" onClick={routePage}> - {pageCommentWhenEmpty} + {routePageName} </Button> - </EmptyWrapper> + </Flex> ); } return ( - <Wrapper> + <Flex $flexWrap="wrap" $gap="20px"> <Grid rows="auto" columns={5} @@ -77,26 +71,13 @@ function TopicCardList({ bookmarkCount={topic.bookmarkCount} isInAtlas={topic.isInAtlas} isBookmarked={topic.isBookmarked} - getTopicsFromServer={refetchTopic} + getTopicsFromServer={refetch} /> </ul> ))} </Grid> - </Wrapper> + </Flex> ); } -const EmptyWrapper = styled.section` - height: 240px; - display: flex; - flex-direction: column; - align-items: center; -`; - -const Wrapper = styled.section` - display: flex; - flex-wrap: wrap; - gap: 20px; -`; - export default TopicCardList; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index ba31e7ac5..314804aff 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,3 +1,4 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import ReactDOM from 'react-dom/client'; import ReactGA from 'react-ga4'; import { ThemeProvider } from 'styled-components'; @@ -9,6 +10,17 @@ import GlobalStyle from './GlobalStyle'; import NotFound from './pages/NotFound'; import theme from './themes'; +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchInterval: false, + refetchOnReconnect: false, + }, + }, +}); + const rootElement = document.getElementById('root'); if (!rootElement) throw new Error('Failed to find the root element'); const root = ReactDOM.createRoot(rootElement); diff --git a/frontend/src/pages/Bookmark.tsx b/frontend/src/pages/Bookmark.tsx index 4e74c2d22..7efd8d309 100644 --- a/frontend/src/pages/Bookmark.tsx +++ b/frontend/src/pages/Bookmark.tsx @@ -1,4 +1,4 @@ -import { lazy, Suspense } from 'react'; +import { Suspense } from 'react'; import { styled } from 'styled-components'; import FavoriteNotFilledSVG from '../assets/favoriteBtn_notFilled.svg'; @@ -7,22 +7,18 @@ import Flex from '../components/common/Flex'; import Space from '../components/common/Space'; import MediaSpace from '../components/common/Space/MediaSpace'; import MediaText from '../components/common/Text/MediaText'; -import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; +import TopicListSkeleton from '../components/Skeletons/TopicListSkeleton'; +import TopicCardList from '../components/TopicCardList'; import { ARIA_FOCUS, FULLSCREEN } from '../constants'; import useNavigator from '../hooks/useNavigator'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; -const TopicCardList = lazy(() => import('../components/TopicCardList')); - function Bookmark() { - const { routePage } = useNavigator(); useSetLayoutWidth(FULLSCREEN); useSetNavbarHighlight('favorite'); - const goToHome = () => { - routePage('/'); - }; + const { routingHandlers } = useNavigator(); return ( <Wrapper> @@ -48,16 +44,14 @@ function Bookmark() { <MediaSpace size={6} /> - <Suspense fallback={<TopicCardContainerSkeleton />}> + <Suspense fallback={<TopicListSkeleton />}> <TopicCardList url="/members/my/bookmarks" - errorMessage="로그인 후 이용해주세요." commentWhenEmpty="버튼으로 지도를 즐겨찾기에 담아보세요." - pageCommentWhenEmpty="메인페이지로 가기" - routePage={goToHome} - > - <FavoriteNotFilledSVG /> - </TopicCardList> + routePageName="메인 페이지로 가기" + routePage={routingHandlers.home} + svgElementWhenEmpty={<FavoriteNotFilledSVG />} + /> </Suspense> <Space size={8} /> diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index baedc733e..d2276cf05 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -5,7 +5,7 @@ import Banner from '../components/Banner'; import Space from '../components/common/Space'; import MediaSpace from '../components/common/Space/MediaSpace'; import SearchBar from '../components/SearchBar/SearchBar'; -import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; +import TopicListSkeleton from '../components/Skeletons/TopicListSkeleton'; import { FULLSCREEN } from '../constants'; import { MarkerContext } from '../context/MarkerContext'; import { SeeTogetherContext } from '../context/SeeTogetherContext'; @@ -54,7 +54,7 @@ function Home() { <Banner /> <Space size={6} /> - <Suspense fallback={<TopicCardContainerSkeleton />}> + <Suspense fallback={<TopicListSkeleton />}> <TopicListContainer url="/topics/bests" containerTitle="인기 급상승할 지도?" @@ -65,7 +65,7 @@ function Home() { <MediaSpace size={9} /> - <Suspense fallback={<TopicCardContainerSkeleton />}> + <Suspense fallback={<TopicListSkeleton />}> <TopicListContainer url="/topics/newest" containerTitle="새로울 지도?" @@ -76,7 +76,7 @@ function Home() { <MediaSpace size={9} /> - <Suspense fallback={<TopicCardContainerSkeleton />}> + <Suspense fallback={<TopicListSkeleton />}> <TopicListContainer url="/topics" containerTitle="모두일 지도?" diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index cf42b7f87..e15f8e3df 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -7,7 +7,7 @@ import Space from '../components/common/Space'; import MediaSpace from '../components/common/Space/MediaSpace'; import MediaText from '../components/common/Text/MediaText'; import MyInfo from '../components/MyInfo'; -import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; +import TopicListSkeleton from '../components/Skeletons/TopicListSkeleton'; import { ARIA_FOCUS, FULLSCREEN } from '../constants'; import useNavigator from '../hooks/useNavigator'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; @@ -53,7 +53,7 @@ function Profile() { <MediaSpace size={6} /> - <Suspense fallback={<TopicCardContainerSkeleton />}> + <Suspense fallback={<TopicListSkeleton />}> <TopicCardList url="/members/my/topics" errorMessage="로그인 후 이용해주세요." diff --git a/frontend/src/pages/SeeAllNearTopics.tsx b/frontend/src/pages/SeeAllAllTopics.tsx similarity index 62% rename from frontend/src/pages/SeeAllNearTopics.tsx rename to frontend/src/pages/SeeAllAllTopics.tsx index 0a518bb71..0d2bb3985 100644 --- a/frontend/src/pages/SeeAllNearTopics.tsx +++ b/frontend/src/pages/SeeAllAllTopics.tsx @@ -1,29 +1,25 @@ -import { lazy, Suspense } from 'react'; +import { Suspense } from 'react'; import { styled } from 'styled-components'; import Box from '../components/common/Box'; import Space from '../components/common/Space'; import MediaSpace from '../components/common/Space/MediaSpace'; import MediaText from '../components/common/Text/MediaText'; -import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; +import TopicListSkeleton from '../components/Skeletons/TopicListSkeleton'; +import TopicCardList from '../components/TopicCardList'; import { ARIA_FOCUS, FULLSCREEN } from '../constants'; import useNavigator from '../hooks/useNavigator'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; -const TopicCardList = lazy(() => import('../components/TopicCardList')); - -function SeeAllNearTopics() { - const { routePage } = useNavigator(); +function SeeAllAllTopics() { useSetLayoutWidth(FULLSCREEN); useSetNavbarHighlight('home'); - const goToHome = () => { - routePage('/new-topic'); - }; + const { routingHandlers } = useNavigator(); return ( - <Wrapper as="section"> + <Wrapper> <Space size={5} /> <MediaText as="h2" @@ -38,13 +34,12 @@ function SeeAllNearTopics() { <MediaSpace size={6} /> - <Suspense fallback={<TopicCardContainerSkeleton />}> + <Suspense fallback={<TopicListSkeleton />}> <TopicCardList url="/topics" - errorMessage="로그인 후 이용해주세요." - commentWhenEmpty="지도가 없습니다. 추가하기 버튼을 눌러 지도를 추가해보세요." - pageCommentWhenEmpty="지도 만들러 가기" - routePage={goToHome} + commentWhenEmpty="생성된 지도가 없습니다. 지도를 만들어보세요." + routePageName="지도 만들러 가기" + routePage={routingHandlers.newTopic} /> </Suspense> @@ -63,4 +58,4 @@ const Wrapper = styled(Box)` } `; -export default SeeAllNearTopics; +export default SeeAllAllTopics; diff --git a/frontend/src/pages/SeeAllPopularTopics.tsx b/frontend/src/pages/SeeAllBestTopics.tsx similarity index 58% rename from frontend/src/pages/SeeAllPopularTopics.tsx rename to frontend/src/pages/SeeAllBestTopics.tsx index cdd3a8d97..0412968d6 100644 --- a/frontend/src/pages/SeeAllPopularTopics.tsx +++ b/frontend/src/pages/SeeAllBestTopics.tsx @@ -1,29 +1,25 @@ -import { lazy, Suspense } from 'react'; +import { Suspense } from 'react'; import { styled } from 'styled-components'; import Box from '../components/common/Box'; import Space from '../components/common/Space'; import MediaSpace from '../components/common/Space/MediaSpace'; import MediaText from '../components/common/Text/MediaText'; -import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; +import TopicListSkeleton from '../components/Skeletons/TopicListSkeleton'; +import TopicCardList from '../components/TopicCardList'; import { ARIA_FOCUS, FULLSCREEN } from '../constants'; import useNavigator from '../hooks/useNavigator'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; -const TopicCardList = lazy(() => import('../components/TopicCardList')); - -function SeeAllTopics() { - const { routePage } = useNavigator(); +function SeeAllAllTopics() { useSetLayoutWidth(FULLSCREEN); useSetNavbarHighlight('home'); - const goToHome = () => { - routePage('/'); - }; + const { routingHandlers } = useNavigator(); return ( - <Wrapper as="section"> + <Wrapper> <Space size={5} /> <MediaText as="h2" @@ -31,20 +27,19 @@ function SeeAllTopics() { $fontSize="extraLarge" $fontWeight="bold" tabIndex={ARIA_FOCUS} - aria-label="인기 급상승할 지도 전체보기 페이지 입니다." + aria-label="모두일 지도 전체보기 페이지 입니다." > - 인기 급상승할 지도? + 새로울 지도? </MediaText> <MediaSpace size={6} /> - <Suspense fallback={<TopicCardContainerSkeleton />}> + <Suspense fallback={<TopicListSkeleton />}> <TopicCardList url="/topics/bests" - errorMessage="로그인 후 이용해주세요." - commentWhenEmpty="즐겨찾기가 많이 된 지도가 없습니다." - pageCommentWhenEmpty="메인페이지로 가기" - routePage={goToHome} + commentWhenEmpty="생성된 지도가 없습니다. 지도를 만들어보세요." + routePageName="지도 만들러 가기" + routePage={routingHandlers.newTopic} /> </Suspense> @@ -63,4 +58,4 @@ const Wrapper = styled(Box)` } `; -export default SeeAllTopics; +export default SeeAllAllTopics; diff --git a/frontend/src/pages/SeeAllLatestTopics.tsx b/frontend/src/pages/SeeAllNewestTopics.tsx similarity index 58% rename from frontend/src/pages/SeeAllLatestTopics.tsx rename to frontend/src/pages/SeeAllNewestTopics.tsx index 34bcf6e17..afe2c0efc 100644 --- a/frontend/src/pages/SeeAllLatestTopics.tsx +++ b/frontend/src/pages/SeeAllNewestTopics.tsx @@ -1,29 +1,25 @@ -import { lazy, Suspense } from 'react'; +import { Suspense } from 'react'; import { styled } from 'styled-components'; import Box from '../components/common/Box'; import Space from '../components/common/Space'; import MediaSpace from '../components/common/Space/MediaSpace'; import MediaText from '../components/common/Text/MediaText'; -import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; +import TopicListSkeleton from '../components/Skeletons/TopicListSkeleton'; +import TopicCardList from '../components/TopicCardList'; import { ARIA_FOCUS, FULLSCREEN } from '../constants'; import useNavigator from '../hooks/useNavigator'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; -const TopicCardList = lazy(() => import('../components/TopicCardList')); - -function SeeAllLatestTopics() { - const { routePage } = useNavigator(); +function SeeAllAllTopics() { useSetLayoutWidth(FULLSCREEN); useSetNavbarHighlight('home'); - const goToHome = () => { - routePage('/'); - }; + const { routingHandlers } = useNavigator(); return ( - <Wrapper as="section"> + <Wrapper> <Space size={5} /> <MediaText as="h2" @@ -31,20 +27,19 @@ function SeeAllLatestTopics() { $fontSize="extraLarge" $fontWeight="bold" tabIndex={ARIA_FOCUS} - aria-label="새로울 지도 전체보기 페이지 입니다." + aria-label="모두일 지도 전체보기 페이지 입니다." > - 새로울 지도? + 인기 급상승할 지도? </MediaText> <MediaSpace size={6} /> - <Suspense fallback={<TopicCardContainerSkeleton />}> + <Suspense fallback={<TopicListSkeleton />}> <TopicCardList url="/topics/newest" - errorMessage="로그인 후 이용해주세요." - commentWhenEmpty="최근에 핀이 찍힌 지도가 없습니다." - pageCommentWhenEmpty="메인페이지로 가기" - routePage={goToHome} + commentWhenEmpty="생성된 지도가 없습니다. 지도를 만들어보세요." + routePageName="지도 만들러 가기" + routePage={routingHandlers.newTopic} /> </Suspense> @@ -63,4 +58,4 @@ const Wrapper = styled(Box)` } `; -export default SeeAllLatestTopics; +export default SeeAllAllTopics; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index b2f5f13b6..6ffab3bae 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -2,6 +2,7 @@ import { lazy, ReactNode, Suspense } from 'react'; import { createBrowserRouter } from 'react-router-dom'; import AuthLayout from './components/Layout/AuthLayout'; +import TopicListSkeleton from './components/Skeletons/TopicListSkeleton'; import Home from './pages/Home'; import NotFound from './pages/NotFound'; import RootPage from './pages/RootPage'; @@ -11,9 +12,9 @@ import TopicListSkeleton from './components/Skeletons/TopicListSkeleton'; const SelectedTopic = lazy(() => import('./pages/SelectedTopic')); const NewPin = lazy(() => import('./pages/NewPin')); const NewTopic = lazy(() => import('./pages/NewTopic')); -const SeeAllPopularTopics = lazy(() => import('./pages/SeeAllPopularTopics')); -const SeeAllNearTopics = lazy(() => import('./pages/SeeAllNearTopics')); -const SeeAllLatestTopics = lazy(() => import('./pages/SeeAllLatestTopics')); +const SeeAllBestTopics = lazy(() => import('./pages/SeeAllBestTopics')); +const SeeAllAllTopics = lazy(() => import('./pages/SeeAllAllTopics')); +const SeeAllNewestTopics = lazy(() => import('./pages/SeeAllNewestTopics')); const KakaoRedirect = lazy(() => import('./pages/KakaoRedirect')); const Profile = lazy(() => import('./pages/Profile')); const AskLogin = lazy(() => import('./pages/AskLogin')); @@ -80,36 +81,36 @@ const routes: routeElement[] = [ { path: 'see-all/popularity', element: ( - <SuspenseComp> - <SeeAllPopularTopics /> - </SuspenseComp> + <Suspense fallback={<TopicListSkeleton />}> + <SeeAllBestTopics /> + </Suspense> ), withAuth: false, }, { path: 'see-all/near', element: ( - <SuspenseComp> - <SeeAllNearTopics /> - </SuspenseComp> + <Suspense fallback={<TopicListSkeleton />}> + <SeeAllAllTopics /> + </Suspense> ), withAuth: false, }, { path: 'see-all/latest', element: ( - <SuspenseComp> - <SeeAllLatestTopics /> - </SuspenseComp> + <Suspense fallback={<TopicListSkeleton />}> + <SeeAllNewestTopics /> + </Suspense> ), withAuth: false, }, { path: 'favorite', element: ( - <SuspenseComp> + <Suspense fallback={<TopicListSkeleton />}> <Bookmark /> - </SuspenseComp> + </Suspense> ), withAuth: true, }, diff --git a/frontend/src/utils/convertCSS.ts b/frontend/src/utils/convertCSS.ts new file mode 100644 index 000000000..add6bd757 --- /dev/null +++ b/frontend/src/utils/convertCSS.ts @@ -0,0 +1,5 @@ +export const convertCSS = (property: number | string) => { + if (typeof property === 'number') return `${property}px`; + + return property; +};