diff --git a/src/apis/queryKeys.ts b/src/apis/queryKeys.ts index b2ee30a1..2a9a2ae8 100644 --- a/src/apis/queryKeys.ts +++ b/src/apis/queryKeys.ts @@ -8,6 +8,7 @@ const QUERY_KEYS = { CHAT_ROOM: 'chatRoom', PRODUCT_DETAIL: 'productDetail', ARTIST_PROFILE: 'artistProfile', + WISH_LIST: 'wishList', }; export default QUERY_KEYS; diff --git a/src/apis/users/useGetWishes.ts b/src/apis/users/useGetWishes.ts new file mode 100644 index 00000000..763cb29e --- /dev/null +++ b/src/apis/users/useGetWishes.ts @@ -0,0 +1,40 @@ +import { APIResponse, SearchProductsResponse } from '@/types'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import fetchInstance from '../instance'; +import QueryKeys from '../queryKeys'; + +async function getWishes({ + pageParam = 0, +}: { + pageParam: number; +}): Promise> { + const size = 20; + const response = await fetchInstance().get('/wishes', { + params: { + size, + page: pageParam, + }, + }); + return response.data; +} + +const useGetWishes = () => { + const queryResult = useSuspenseInfiniteQuery< + APIResponse, + Error, + APIResponse, + [string], + number + >({ + queryKey: [QueryKeys.WISH_LIST], + queryFn: ({ pageParam }) => getWishes({ pageParam }), + initialPageParam: 0, + getNextPageParam: (lastPage, allPages) => { + return lastPage.data.hasNext ? allPages.length : undefined; + }, + }); + + return queryResult; +}; + +export default useGetWishes; diff --git a/src/components/common/Loader/index.tsx b/src/components/common/Loader/index.tsx new file mode 100644 index 00000000..6b3005d0 --- /dev/null +++ b/src/components/common/Loader/index.tsx @@ -0,0 +1,36 @@ +import styled from '@emotion/styled'; + +const LoaderWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +`; + +const Spinner = styled.div` + border: 4px solid #f3f3f3; + border-top: 4px solid #3498db; + border-radius: 50%; + width: 50px; + height: 50px; + animation: spin 1.5s linear infinite; + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } +`; + +const Loader = () => { + return ( + + + + ); +}; + +export default Loader; diff --git a/src/pages/Discover/index.tsx b/src/pages/Discover/index.tsx index 62d8c22e..f483bd21 100644 --- a/src/pages/Discover/index.tsx +++ b/src/pages/Discover/index.tsx @@ -3,6 +3,7 @@ import { Suspense, useEffect } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import useGetFeed, { type Product } from '@/apis/products/useGetFeed'; +import Loader from '@/components/common/Loader'; import SearchBar from '@/components/layouts/SearchBar'; import { HEIGHTS } from '@/styles/constants'; @@ -12,7 +13,7 @@ const Discover = () => ( {/* todo: 폴백 UI 만들기 */} Error}> - Loading...}> + }> diff --git a/src/pages/My/index.tsx b/src/pages/My/index.tsx index 44831a8c..75a4e6a9 100644 --- a/src/pages/My/index.tsx +++ b/src/pages/My/index.tsx @@ -4,6 +4,7 @@ import { ErrorBoundary } from 'react-error-boundary'; import useGetArtist from '@/apis/artists/useGetArtist'; import useGetUser from '@/apis/users/useGetUser'; +import Loader from '@/components/common/Loader'; import Footer from '@/components/layouts/Footer'; import useModeStore from '@/store/useModeStore'; import { HEIGHTS } from '@/styles/constants'; @@ -15,7 +16,7 @@ import UserProfileBox from './components/UserProfileBox'; const My = () => { return ( Error...}> - Loading...}> + }> diff --git a/src/pages/MyFavorites/index.tsx b/src/pages/MyFavorites/index.tsx index 115f95b2..ea8a3f6f 100644 --- a/src/pages/MyFavorites/index.tsx +++ b/src/pages/MyFavorites/index.tsx @@ -2,15 +2,19 @@ import styled from '@emotion/styled'; import { useEffect, useState } from 'react'; import useGetFollow from '@/apis/users/useGetFollow'; +import useGetWishList from '@/apis/users/useGetWishes'; import ArtistItem from '@/components/common/ArtistItem'; import CategoryTabBar from '@/components/common/CategoryTabBar'; +import Loader from '@/components/common/Loader'; +import ProductItem from '@/components/common/ProductItem'; import * as G from '@/styles/globalStyles'; -import { User } from '@/types'; +import { SearchProductInfo, User } from '@/types'; const MyFavorites = () => { const categoryList = ['작품', '작가']; const [selectedTab, setSelectedTab] = useState('작품'); - const { data, status, refetch } = useGetFollow(); + const { data: artistResults, status, refetch } = useGetFollow(); + const { data: wishListResults } = useGetWishList(); useEffect(() => { if (selectedTab === '작가') { @@ -19,27 +23,46 @@ const MyFavorites = () => { }, [selectedTab, refetch]); if (status === 'pending') { - return

Loading...

; + return ; } - if (status === 'error' || !data) { + if (status === 'error' || !artistResults) { return

Error... console.log('Error:', error);

; } const handleTabClick = (tab: string) => { setSelectedTab(tab); }; + return ( <> {selectedTab === '작품' ? ( - 작품 // 현재 이부분 api가 없어 비워두었습니다. + + {wishListResults.data.products.length === 0 ? ( +

찜한 작품이 없습니다.

+ ) : ( + + {wishListResults.data.products.map((product: SearchProductInfo) => ( + + ))} + + )} +
) : ( - {data?.data.content?.length === 0 ? ( + {artistResults?.data.content?.length === 0 ? (

팔로우한 작가가 없습니다.

) : ( - {data?.data.content?.map((artist: User) => ( + {artistResults?.data.content?.map((artist: User) => ( { const ProductDetails = () => { return ( Error Status}> - Loading Status}> + }> diff --git a/src/pages/SearchResults/components/ArtWorkContents.tsx b/src/pages/SearchResults/components/ArtWorkContents.tsx index 66b45c6b..39a3880d 100644 --- a/src/pages/SearchResults/components/ArtWorkContents.tsx +++ b/src/pages/SearchResults/components/ArtWorkContents.tsx @@ -51,19 +51,23 @@ const ArtWorkContents = ({ searchWork }: { searchWork: SearchProductInfo[] }) => handleSelect={handleSelect} /> - - {sortedWork.map((item) => ( - - ))} - + {searchWorkLen === 0 ? ( + 데이터가 없습니다. + ) : ( + + {sortedWork.map((item) => ( + + ))} + + )} ); }; @@ -83,3 +87,13 @@ const ResultWrapper = styled.div` align-items: center; width: 100%; `; + +const NoDataMessage = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + padding: 40px 0; + font-weight: 600; + color: var(--color-black); +`; diff --git a/src/pages/SearchResults/components/ArtistContents.tsx b/src/pages/SearchResults/components/ArtistContents.tsx index d648647b..9ea57f17 100644 --- a/src/pages/SearchResults/components/ArtistContents.tsx +++ b/src/pages/SearchResults/components/ArtistContents.tsx @@ -57,19 +57,23 @@ const ArtistContents = ({ searchArtist }: { searchArtist: SearchArtistInfo[] }) handleSelect={handleSelect} /> - - {sortedArtist.map((item) => ( - - ))} - + {searchArtistLen === 0 ? ( + 데이터가 없습니다. + ) : ( + + {sortedArtist.map((item) => ( + + ))} + + )} ); }; @@ -89,3 +93,13 @@ const ResultWrapper = styled.div` align-items: center; width: 100%; `; + +const NoDataMessage = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + padding: 40px 0; + font-weight: 600; + color: var(--color-black); +`; diff --git a/src/pages/SearchResults/index.tsx b/src/pages/SearchResults/index.tsx index e3185c33..f98daa0c 100644 --- a/src/pages/SearchResults/index.tsx +++ b/src/pages/SearchResults/index.tsx @@ -7,6 +7,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom'; import useSearchArtists from '@/apis/search/useSearchArtists'; import useSearchProducts from '@/apis/search/useSearchProducts'; import CategoryTabBar from '@/components/common/CategoryTabBar'; +import Loader from '@/components/common/Loader'; import SearchBar from '@/components/layouts/SearchBar'; import { RouterPath } from '@/routes/path'; import * as G from '@/styles/globalStyles'; @@ -56,20 +57,28 @@ const SearchResultsContent = () => { 작품 ({searchProductLen}) - - - handleTabClick('작품')}> 더보기 - + {searchProductLen === 0 ? ( + 데이터가 없습니다. + ) : ( + + + handleTabClick('작품')}> 더보기 + + )}
작가 ({searchArtistLen}) - - - handleTabClick('작가')}> 더보기 - + {searchArtistLen === 0 ? ( + 데이터가 없습니다. + ) : ( + + + handleTabClick('작가')}> 더보기 + + )}
)} @@ -83,7 +92,7 @@ const SearchResultsContent = () => { const SearchResults = () => { return ( Error Status}> - Loading Status}> + }> @@ -149,3 +158,13 @@ const HorizontalWRapper = styled.div` justify-content: center; align-items: center; `; + +const NoDataMessage = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + padding: 20px 0; + font-weight: 600; + color: var(--color-black); +`; diff --git a/src/pages/chats/ChatList/components/ChatItem/index.tsx b/src/pages/chats/ChatList/components/ChatItem/index.tsx index 99600346..7cbb9a7a 100644 --- a/src/pages/chats/ChatList/components/ChatItem/index.tsx +++ b/src/pages/chats/ChatList/components/ChatItem/index.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import { useNavigate } from 'react-router-dom'; import ProfileImage from '@/components/common/ProfileImage'; -import { formatDate } from '@/utils'; +import { formatDate } from '@/utils/dates'; type ChatItemProps = { chatRoomId: number; diff --git a/src/pages/chats/ChatRoom/components/ChatInput/index.tsx b/src/pages/chats/ChatRoom/components/ChatInput/index.tsx index 2d4a6a40..c2d50be6 100644 --- a/src/pages/chats/ChatRoom/components/ChatInput/index.tsx +++ b/src/pages/chats/ChatRoom/components/ChatInput/index.tsx @@ -7,7 +7,7 @@ import CancelIcon from '@/assets/icons/cancel-default.svg?react'; import ImageIcon from '@/assets/icons/image.svg?react'; import SendIcon from '@/assets/icons/send.svg?react'; import type { User } from '@/types/chats'; -import { countNonSpaceChars } from '@/utils'; +import { countNonSpaceChars } from '@/utils/strings'; type ChatInputProps = { client: CompatClient | null; diff --git a/src/pages/chats/ChatRoom/components/MessageItem/index.tsx b/src/pages/chats/ChatRoom/components/MessageItem/index.tsx index 7fdb6312..93958ca1 100644 --- a/src/pages/chats/ChatRoom/components/MessageItem/index.tsx +++ b/src/pages/chats/ChatRoom/components/MessageItem/index.tsx @@ -1,8 +1,8 @@ +import { Box } from '@chakra-ui/react'; import styled from '@emotion/styled'; import ProfileImage from '@/components/common/ProfileImage'; -import { formatTimestamp } from '@/utils'; -import { Box } from '@chakra-ui/react'; +import { formatTimestamp } from '@/utils/dates'; export type MessageItemProps = { senderName: string; diff --git a/src/pages/chats/ChatRoom/components/MessageList/index.tsx b/src/pages/chats/ChatRoom/components/MessageList/index.tsx index 502c185d..f3e94348 100644 --- a/src/pages/chats/ChatRoom/components/MessageList/index.tsx +++ b/src/pages/chats/ChatRoom/components/MessageList/index.tsx @@ -3,7 +3,7 @@ import { isSameDay } from 'date-fns'; import { Fragment, useRef } from 'react'; import type { ChatMessage } from '@/types/chats'; -import { formatDate, getDay } from '@/utils'; +import { formatDate, getDay } from '@/utils/dates'; import MessageItem from '../MessageItem'; type MessageListProps = { diff --git a/src/routes/ProtectedRoute.tsx b/src/routes/ProtectedRoute.tsx index 294315d0..360e7c0e 100644 --- a/src/routes/ProtectedRoute.tsx +++ b/src/routes/ProtectedRoute.tsx @@ -9,7 +9,7 @@ export const ProtectedRoute = () => { const navigate = useNavigate(); // accessToken이 없으면 로그인 페이지로 리다이렉트, 있으면 자식 요소(페이지) 렌더링 - // 로그인 구현에 따라 추후 변동 가능성 O + // todo: 로그인 구현에 따라 추후 변동 가능성 O useEffect(() => { const currentPath = window.location.pathname; diff --git a/src/types/index.ts b/src/types/index.ts index a1def416..5e24f543 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,20 +1,3 @@ -export type SearchWork = { - id: number; - src: string; - title: string; - artist: string; - price: number; -}; - -export type SearchArtist = { - id: number; - name: string; - src: string; - totalFollowers: number; - totalLikes: number; - followed: boolean; -}; - export type User = { userId: number; nickname: string; diff --git a/src/utils/index.ts b/src/utils/dates.ts similarity index 80% rename from src/utils/index.ts rename to src/utils/dates.ts index 22eebd7d..becab33e 100644 --- a/src/utils/index.ts +++ b/src/utils/dates.ts @@ -43,17 +43,3 @@ export function getDay(date: Date): string { return day; } - -/** - * 문자열 관련 함수 - */ - -// 공백 제거 함수 -export function eliminateSpaces(str: string): string { - return str.replace(/\s/g, ''); -} - -// 공백 제거하고 문자열 길이 세는 함수 -export function countNonSpaceChars(str: string): number { - return eliminateSpaces(str).length; -} diff --git a/src/utils/queryParams.ts b/src/utils/queryParams.ts index fd7f8fd0..5852c09e 100644 --- a/src/utils/queryParams.ts +++ b/src/utils/queryParams.ts @@ -1,3 +1,7 @@ +/** + * URL 쿼리 파라미터 관련 함수 + */ + // query param으로 만들어 반환 export function getQueryParams(params: Record): URLSearchParams { const queryParams = new URLSearchParams(); diff --git a/src/utils/strings.ts b/src/utils/strings.ts new file mode 100644 index 00000000..522be75d --- /dev/null +++ b/src/utils/strings.ts @@ -0,0 +1,13 @@ +/** + * 문자열 관련 함수 + */ + +// 공백 제거 함수 +export function eliminateSpaces(str: string): string { + return str.replace(/\s/g, ''); +} + +// 공백 제거하고 문자열 길이 세는 함수 +export function countNonSpaceChars(str: string): number { + return eliminateSpaces(str).length; +}