diff --git a/src/apis/artists/useGetProfile.ts b/src/apis/artists/useGetProfile.ts new file mode 100644 index 00000000..fad93f8a --- /dev/null +++ b/src/apis/artists/useGetProfile.ts @@ -0,0 +1,22 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; + +import { APIResponse, ProfileResponse } from '@/types'; +import fetchInstance from '../fetchInstance'; +import QUERY_KEYS from '../queryKeys'; + +async function getProfile(artistInfoId: number | null): Promise> { + const response = await fetchInstance().get(`/artists/${artistInfoId}`); + + return response.data; +} + +const useGetProfile = (artistInfoId: number | null) => { + const { data } = useSuspenseQuery, Error>({ + queryKey: [QUERY_KEYS.ARTIST_PROFILE, artistInfoId], + queryFn: () => getProfile(artistInfoId), + }); + + return { data }; +}; + +export default useGetProfile; diff --git a/src/apis/data/searchArtist.ts b/src/apis/data/searchArtist.ts deleted file mode 100644 index ecc702b9..00000000 --- a/src/apis/data/searchArtist.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { SearchArtist } from '@/types/index'; - -const searchArtist: SearchArtist[] = [ - { - id: 1, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - name: '작가1', - totalFollowers: 120, - totalLikes: 250, - followed: true, - }, - { - id: 2, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - name: '작가2', - totalFollowers: 85, - totalLikes: 190, - followed: false, - }, - { - id: 3, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - name: '작가3', - totalFollowers: 95, - totalLikes: 170, - followed: true, - }, - { - id: 4, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - name: '작가4', - totalFollowers: 60, - totalLikes: 140, - followed: false, - }, - { - id: 5, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - name: '작가5', - totalFollowers: 110, - totalLikes: 220, - followed: true, - }, - { - id: 6, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - name: '작가6', - totalFollowers: 50, - totalLikes: 130, - followed: false, - }, - { - id: 7, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - name: '작가7', - totalFollowers: 70, - totalLikes: 160, - followed: true, - }, - { - id: 8, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - name: '작가8', - totalFollowers: 90, - totalLikes: 180, - followed: true, - }, - { - id: 9, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - name: '작가9', - totalFollowers: 100, - totalLikes: 200, - followed: false, - }, -]; - -export default searchArtist; diff --git a/src/apis/data/searchWork.ts b/src/apis/data/searchWork.ts deleted file mode 100644 index 1ff54bd1..00000000 --- a/src/apis/data/searchWork.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { SearchWork } from '@/types/index'; - -const searchWork: SearchWork[] = [ - { - id: 1, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - title: '무제', - artist: '김영석', - price: 100000, - }, - { - id: 2, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - title: '현대 서양화', - artist: '이수정', - price: 200000, - }, - { - id: 3, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - title: '추상 민화', - artist: '박민지', - price: 300000, - }, - { - id: 4, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - title: '모던 동양화', - artist: '최윤영', - price: 250000, - }, - { - id: 5, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - title: '클래식 서양화', - artist: '김정민', - price: 150000, - }, - { - id: 6, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - title: '풍경 수묵화', - artist: '이성현', - price: 350000, - }, - { - id: 7, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - title: '인물 동양화', - artist: '박서윤', - price: 180000, - }, - { - id: 8, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - title: '정물 서양화', - artist: '이혜원', - price: 220000, - }, - { - id: 9, - src: 'https://i.pinimg.com/474x/45/37/93/4537932e76ebcc6186f86b75d9eeac87.jpg', - title: '동서양화', - artist: '김동욱', - price: 270000, - }, -]; - -export default searchWork; diff --git a/src/apis/products/useGetDetail.ts b/src/apis/products/useGetDetail.ts new file mode 100644 index 00000000..0b628190 --- /dev/null +++ b/src/apis/products/useGetDetail.ts @@ -0,0 +1,22 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; + +import { APIResponse, ProductResponse } from '@/types'; +import fetchInstance from '../fetchInstance'; +import QUERY_KEYS from '../queryKeys'; + +async function getDetail(productId: number | null): Promise> { + const response = await fetchInstance().get(`/products/${productId}`); + + return response.data; +} + +const useGetDetail = (productId: number | null) => { + const { data } = useSuspenseQuery, Error>({ + queryKey: [QUERY_KEYS.PRODUCT_DETAIL, productId], + queryFn: () => getDetail(productId), + }); + + return { data }; +}; + +export default useGetDetail; diff --git a/src/apis/queryKeys.ts b/src/apis/queryKeys.ts index f6d243b8..b2ee30a1 100644 --- a/src/apis/queryKeys.ts +++ b/src/apis/queryKeys.ts @@ -3,7 +3,11 @@ const QUERY_KEYS = { FEED: 'feed', FOLLOW_LIST: 'followList', USER_INFO: 'userInfo', + ARTIST_LIST: 'artistList', + PRODUCT_LIST: 'productList', CHAT_ROOM: 'chatRoom', + PRODUCT_DETAIL: 'productDetail', + ARTIST_PROFILE: 'artistProfile', }; export default QUERY_KEYS; diff --git a/src/apis/search/useSearchArtists.tsx b/src/apis/search/useSearchArtists.tsx new file mode 100644 index 00000000..3df92b09 --- /dev/null +++ b/src/apis/search/useSearchArtists.tsx @@ -0,0 +1,40 @@ +import { APIResponse, InfiniteAPIResponse, SearchArtistsResponse } from '@/types'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import fetchInstance from '../instance'; +import QueryKeys from '../queryKeys'; + +async function searchArtists( + { pageParam = 0 }: { pageParam: number }, + query: string, +): Promise> { + const size = 20; + const response = await fetchInstance().get('/artists/search', { + params: { + query, + size, + page: pageParam, + }, + }); + return response.data; +} + +const useSearchArtists = (query: string) => { + const queryResult = useSuspenseInfiniteQuery< + APIResponse, + Error, + InfiniteAPIResponse, + [string, string], + number + >({ + queryKey: [QueryKeys.ARTIST_LIST, query], + queryFn: ({ pageParam }) => searchArtists({ pageParam }, query), + initialPageParam: 0, + getNextPageParam: (lastPage, allPages) => { + return lastPage.data.hasNext ? allPages.length : undefined; + }, + }); + + return queryResult; +}; + +export default useSearchArtists; diff --git a/src/apis/search/useSearchProducts.tsx b/src/apis/search/useSearchProducts.tsx new file mode 100644 index 00000000..d9d2532f --- /dev/null +++ b/src/apis/search/useSearchProducts.tsx @@ -0,0 +1,42 @@ +import { APIResponse, InfiniteAPIResponse, SearchProductsResponse } from '@/types'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import fetchInstance from '../instance'; +import QueryKeys from '../queryKeys'; + +async function searchProducts( + { pageParam = 0 }: { pageParam: number }, + query: string, +): Promise> { + const size = 20; + const sort = 'LATEST'; + const response = await fetchInstance().get('/products', { + params: { + query, + size, + page: pageParam, + sort, + }, + }); + return response.data; +} + +const useSearchProducts = (query: string) => { + const queryResult = useSuspenseInfiniteQuery< + APIResponse, + Error, + InfiniteAPIResponse, + [string, string], + number + >({ + queryKey: [QueryKeys.PRODUCT_LIST, query], + queryFn: ({ pageParam }) => searchProducts({ pageParam }, query), + initialPageParam: 0, + getNextPageParam: (lastPage, allPages) => { + return lastPage.data.hasNext ? allPages.length : undefined; + }, + }); + + return queryResult; +}; + +export default useSearchProducts; diff --git a/src/apis/users/useDeleteLikes.ts b/src/apis/users/useDeleteLikes.ts new file mode 100644 index 00000000..3fb98ac3 --- /dev/null +++ b/src/apis/users/useDeleteLikes.ts @@ -0,0 +1,26 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import fetchInstance from '../fetchInstance'; +import QUERY_KEYS from '../queryKeys'; + +async function deleteLikes(productId: number): Promise { + await fetchInstance().delete(`/products/${productId}/likes`); +} + +const useDeleteLikes = () => { + const queryClient = useQueryClient(); + + const { mutate, status } = useMutation({ + mutationFn: (productId: number) => deleteLikes(productId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.PRODUCT_LIST] }); + }, + onError: (error) => { + console.error('Failed to delete likes:', error); + }, + }); + + return { mutate, status }; +}; + +export default useDeleteLikes; diff --git a/src/apis/users/usePostLikes.ts b/src/apis/users/usePostLikes.ts new file mode 100644 index 00000000..e9587384 --- /dev/null +++ b/src/apis/users/usePostLikes.ts @@ -0,0 +1,20 @@ +import { useMutation } from '@tanstack/react-query'; + +import fetchInstance from '../fetchInstance'; + +async function postLikes(productId: number): Promise { + await fetchInstance().post(`/products/${productId}/likes`, {}); +} + +const usePostLikes = () => { + const { mutate, status } = useMutation({ + mutationFn: (productId: number) => postLikes(productId), + onError: (error) => { + console.error('API call failed:', error); + }, + }); + + return { mutate, status }; +}; + +export default usePostLikes; diff --git a/src/components/common/ArtistItem/index.tsx b/src/components/common/ArtistItem/index.tsx index 800905ba..d58c8c07 100644 --- a/src/components/common/ArtistItem/index.tsx +++ b/src/components/common/ArtistItem/index.tsx @@ -49,7 +49,7 @@ const ArtistItem = ({ artistId, author, like, follower, src, alt, isFollow }: Ar return ( - +

{author}

{ +const ProductItem = ({ id, author, title, price, src, alt, isLiked }: ProductItemProps) => { + const [productLiked, setProductLiked] = useState(isLiked); + + const { mutate: postLike, status: isPostStatus } = usePostLikes(); + const { mutate: deleteLike, status: isDeleteStatus } = useDeleteLikes(); + + const handleHeartClick = () => { + if (productLiked) { + deleteLike(id, { + onSuccess: () => { + setProductLiked(false); + }, + onError: (error) => { + console.error('Failed to delete follow:', error); + alert('팔로우 취소에 실패했습니다.'); + }, + }); + } else { + postLike(id, { + onSuccess: () => { + setProductLiked(true); + }, + onError: (error) => { + console.error('Failed to post follow:', error); + alert('팔로우에 실패했습니다.'); + }, + }); + } + }; + return ( - + {author} {title} diff --git a/src/components/common/Thumbnail/index.tsx b/src/components/common/Thumbnail/index.tsx index 645f4bf2..425b1d0d 100644 --- a/src/components/common/Thumbnail/index.tsx +++ b/src/components/common/Thumbnail/index.tsx @@ -1,30 +1,52 @@ import { Image } from '@chakra-ui/react'; import styled from '@emotion/styled'; -import { useState } from 'react'; import FavoriteDefault from '@/assets/icons/favorite-default.svg?react'; +import { useNavigate } from 'react-router-dom'; interface ThumbnailProps { ratio?: 'square' | 'default'; src?: string; alt?: string; heart?: boolean; + handleHeartClick?: () => void; + productLiked?: boolean; + isPostStatus?: string; + isDeleteStatus?: string; + id: number; + type: 'artist' | 'product'; } const Thumbnail = ({ ratio = 'default', src, alt = 'thumbnail image', - heart = false, + heart, + handleHeartClick, + productLiked, + isPostStatus, + isDeleteStatus, + id, + type, }: ThumbnailProps) => { - const [isLike, setIsLike] = useState(false); - + const navigate = useNavigate(); return ( - + + type === 'product' ? navigate(`/products/${id}`) : navigate(`/artists/${id}`) + } + > {src && } {heart && ( - setIsLike(!isLike)}> - + { + e.stopPropagation(); + handleHeartClick?.(); + }} + disabled={isPostStatus === 'pending' || isDeleteStatus === 'pending'} + > + @@ -42,6 +64,7 @@ const Wrapper = styled.div<{ ratio: 'square' | 'default' }>` background-color: var(--color-gray-lt); border-radius: var(--border-radius); overflow: hidden; + cursor: pointer; `; const StyledImage = styled(Image)` @@ -50,7 +73,7 @@ const StyledImage = styled(Image)` object-fit: cover; `; -const FavoriteWrapper = styled.div` +const FavoriteWrapper = styled.button` position: absolute; top: 80%; right: 5%; diff --git a/src/pages/ArtistDetails/index.tsx b/src/pages/ArtistDetails/index.tsx index 24d4fd39..a0e768d2 100644 --- a/src/pages/ArtistDetails/index.tsx +++ b/src/pages/ArtistDetails/index.tsx @@ -1,5 +1,118 @@ +import { useNavigate, useParams } from 'react-router-dom'; + +import { Suspense } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; + +import useGetProfile from '@/apis/artists/useGetProfile'; +import IconButton from '@/components/common/IconButton'; +import Header from '@/components/layouts/Header'; +import styled from '@emotion/styled'; + +const ArtistDetailsContext = () => { + const navigate = useNavigate(); + const { artistId } = useParams<{ artistId: string }>(); + + const parsedId = artistId ? parseInt(artistId, 10) : null; + + const { data } = useGetProfile(parsedId); + + return ( + +
navigate(-1)} />} /> + + + + {data.data.nickname} + {data.data.description} + + + Followers + {data.data.totalFollowers} + + + Likes + {data.data.totalLikes} + + + {data.data.about} + + + + ); +}; + const ArtistDetails = () => { - return <>ArtistDetails; + return ( + Error Status}> + Loading Status}> + + + + ); }; export default ArtistDetails; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + height: 100%; + width: 100%; +`; + +const ContentWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + margin-top: 44px; + margin-bottom: 53px; +`; + +const ArtistProfileImage = styled.img` + width: 100%; + height: 300px; + object-fit: cover; + border-radius: 16px; +`; + +const ArtistInfoWrapper = styled.div` + padding: 24px; + display: flex; + flex-direction: column; + gap: 16px; +`; + +const ArtistName = styled.h2` + font-size: var(--font-size-xxl); + font-weight: bold; +`; + +const ArtistDescription = styled.p` + font-size: var(--font-size-md); +`; + +const ArtistStats = styled.div` + display: flex; + gap: 24px; +`; + +const ArtistStatItem = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; + +const ArtistStatLabel = styled.span` + font-size: var(--font-size-sm); + color: var(--color-gray-dk); +`; + +const ArtistStatValue = styled.span` + font-size: var(--font-size-lg); + font-weight: bold; +`; + +const ArtistAbout = styled.p` + font-size: var(--font-size-md); +`; diff --git a/src/pages/ProductDetails/index.tsx b/src/pages/ProductDetails/index.tsx index daf5bd5c..4071ae1f 100644 --- a/src/pages/ProductDetails/index.tsx +++ b/src/pages/ProductDetails/index.tsx @@ -1,18 +1,29 @@ -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import usePostChatRoom from '@/apis/chats/usePostChatRoom'; +import useGetDetail from '@/apis/products/useGetDetail'; import CTA, { CTAContainer } from '@/components/common/CTA'; -import useUserStore from '@/store/useUserStore'; +import IconButton from '@/components/common/IconButton'; +import Header from '@/components/layouts/Header'; import { RouterPath } from '@/routes/path'; +import useUserStore from '@/store/useUserStore'; +import styled from '@emotion/styled'; +import { Suspense } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; const USER_EMAIL_1 = 'ble6859@knu.ac.kr'; const USER_EMAIL_2 = 'user2@example.com'; -const ProductDetails = () => { +const ProductDetailsContext = () => { const { email } = useUserStore(); const userEmail1 = email || USER_EMAIL_1; // 사용자 본인 이메일 const userEmail2 = USER_EMAIL_2; // 상대방 이메일 const navigate = useNavigate(); + const { productId } = useParams<{ productId: string }>(); + + const parsedId = productId ? parseInt(productId, 10) : null; + + const { data } = useGetDetail(parsedId); const { mutate: postChatRoom } = usePostChatRoom(); @@ -33,14 +44,111 @@ const ProductDetails = () => { }, ); }; + console.log(data); return ( - <> - - - - + +
navigate(-1)} />} /> + + + + {data.data.name} + {data.data.category} + {data.data.size} + ₩{data.data.price.toLocaleString()} + {data.data.description} + Artist: {data.data.artistInfo.artistName} + + {data.data.hashTags.map((tag, index) => ( + #{tag} + ))} + + + + + + + + ); +}; + +const ProductDetails = () => { + return ( + Error Status}> + Loading Status}> + + + ); }; export default ProductDetails; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + height: 100%; + width: 100%; +`; + +const ContentWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + margin-top: 44px; + margin-bottom: 53px; +`; + +const ProductImage = styled.img` + width: 50%; + height: 100%; + object-fit: cover; + border-radius: 16px; +`; + +const ProductInfoWrapper = styled.div` + width: 50%; + display: flex; + flex-direction: column; + gap: 16px; + padding: 24px; +`; + +const ProductName = styled.h2` + font-size: var(--font-size-xxl); + font-weight: bold; +`; + +const ProductCategory = styled.p` + font-size: 16px; + color: var(--color-gray-dk); +`; + +const ProductSize = styled.p` + font-size: var(--font-size-md); +`; + +const ProductPrice = styled.p` + font-size: var(--font-size-xl); + font-weight: bold; +`; + +const ProductDescription = styled.p` + font-size: var(--font-size-md); +`; + +const ProductArtistInfo = styled.p` + font-size: var(--font-size-md); +`; + +const ProductHashTags = styled.div` + display: flex; + flex-wrap: wrap; + gap: 8px; +`; + +const Tag = styled.span` + font-size: var(--font-size-sm); + color: var(--color-gray-dk); +`; diff --git a/src/pages/SearchResults/components/ArtWorkContents.tsx b/src/pages/SearchResults/components/ArtWorkContents.tsx index c9ed7d3d..0806e5b5 100644 --- a/src/pages/SearchResults/components/ArtWorkContents.tsx +++ b/src/pages/SearchResults/components/ArtWorkContents.tsx @@ -1,14 +1,13 @@ -import searchWork from '@/apis/data/searchWork'; import ProductItem from '@/components/common/ProductItem'; import Grid from '@/components/styles/Grid'; -import { SearchWork } from '@/types'; +import { SearchProductInfo } from '@/types'; import styled from '@emotion/styled'; import { useEffect, useRef, useState } from 'react'; import DropdownButton from './Dropdown'; export type ArtWorkOptions = '최신순' | '가격순' | '제목순'; -const ArtWorkContents = () => { +const ArtWorkContents = ({ searchWork }: { searchWork: SearchProductInfo[] }) => { const searchWorkLen = searchWork.length; const originalSearchWork = useRef(searchWork); const [isOpen, setIsOpen] = useState(false); @@ -26,8 +25,8 @@ const ArtWorkContents = () => { setIsOpen(false); }; - const sortByPrice = (a: SearchWork, b: SearchWork) => a.price - b.price; - const sortByTitle = (a: SearchWork, b: SearchWork) => a.title.localeCompare(b.title); + const sortByPrice = (a: SearchProductInfo, b: SearchProductInfo) => a.price - b.price; + const sortByTitle = (a: SearchProductInfo, b: SearchProductInfo) => a.name.localeCompare(b.name); useEffect(() => { if (selectedOption === '최신순') { @@ -54,11 +53,13 @@ const ArtWorkContents = () => { {sortedWork.map((item) => ( ))} diff --git a/src/pages/SearchResults/components/ArtistContents.tsx b/src/pages/SearchResults/components/ArtistContents.tsx index dac29218..24026b7e 100644 --- a/src/pages/SearchResults/components/ArtistContents.tsx +++ b/src/pages/SearchResults/components/ArtistContents.tsx @@ -1,14 +1,13 @@ -import searchArtist from '@/apis/data/searchArtist'; import ArtistItem from '@/components/common/ArtistItem'; import Grid from '@/components/styles/Grid'; -import { SearchArtist } from '@/types'; +import { SearchArtistInfo } from '@/types'; import styled from '@emotion/styled'; import { useEffect, useRef, useState } from 'react'; import DropdownButton from './Dropdown'; export type ArtistOptions = '최신순' | '인기순' | '이름순' | '팔로우순'; -const ArtistContents = () => { +const ArtistContents = ({ searchArtist }: { searchArtist: SearchArtistInfo[] }) => { const searchArtistLen = searchArtist.length; const originalSearchArtist = useRef(searchArtist); @@ -27,9 +26,11 @@ const ArtistContents = () => { setIsOpen(false); }; - const sortByFollowed = (a: SearchArtist, b: SearchArtist) => b.totalFollowers - a.totalFollowers; - const sortByName = (a: SearchArtist, b: SearchArtist) => a.name.localeCompare(b.name); - const sortByHeart = (a: SearchArtist, b: SearchArtist) => b.totalLikes - a.totalLikes; + const sortByFollowed = (a: SearchArtistInfo, b: SearchArtistInfo) => + b.totalFollowers - a.totalFollowers; + const sortByName = (a: SearchArtistInfo, b: SearchArtistInfo) => + a.nickname.localeCompare(b.nickname); + const sortByHeart = (a: SearchArtistInfo, b: SearchArtistInfo) => b.totalLikes - a.totalLikes; useEffect(() => { if (selectedOption === '최신순') { @@ -59,12 +60,12 @@ const ArtistContents = () => { {sortedArtist.map((item) => ( ))} diff --git a/src/pages/SearchResults/components/HorizontalFrame.tsx b/src/pages/SearchResults/components/HorizontalFrame.tsx index f8264639..e3fed9c4 100644 --- a/src/pages/SearchResults/components/HorizontalFrame.tsx +++ b/src/pages/SearchResults/components/HorizontalFrame.tsx @@ -1,10 +1,10 @@ import ArtistItem from '@/components/common/ArtistItem'; import ProductItem from '@/components/common/ProductItem'; -import { SearchArtist, SearchWork } from '@/types/index'; +import { SearchArtistInfo, SearchProductInfo } from '@/types/index'; import styled from '@emotion/styled'; interface HorizontalFrameProps { - children: SearchArtist[] | SearchWork[]; + children: SearchArtistInfo[] | SearchProductInfo[]; } const HorizontalFrame = ({ children }: HorizontalFrameProps) => { @@ -12,25 +12,27 @@ const HorizontalFrame = ({ children }: HorizontalFrameProps) => { {children.map((item) => ( - {'title' in item && ( + {'name' in item && ( )} - {'name' in item && ( + {'nickname' in item && ( )} diff --git a/src/pages/SearchResults/index.tsx b/src/pages/SearchResults/index.tsx index 82da5e4b..8adfd48b 100644 --- a/src/pages/SearchResults/index.tsx +++ b/src/pages/SearchResults/index.tsx @@ -1,25 +1,36 @@ import { Z_INDEX } from '@/styles/constants'; import styled from '@emotion/styled'; -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { Suspense, useState } from 'react'; -import searchArtist from '@/apis/data/searchArtist'; -import searchWork from '@/apis/data/searchWork'; +import useSearchArtists from '@/apis/search/useSearchArtists'; +import useSearchProducts from '@/apis/search/useSearchProducts'; import CategoryTabBar from '@/components/common/CategoryTabBar'; import SearchBar from '@/components/layouts/SearchBar'; import Gap from '@/components/styles/Gap'; import { RouterPath } from '@/routes/path'; +import { ErrorBoundary } from 'react-error-boundary'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import ArtWorkContents from './components/ArtWorkContents'; import ArtistContents from './components/ArtistContents'; import HorizontalFrame from './components/HorizontalFrame'; import MoreButton from './components/MoreButton'; -const SearchResults = () => { +const SearchResultsContent = () => { const [selectedTab, setSelectedTab] = useState('전체'); + const [searchParams] = useSearchParams(); + const searchQuery = searchParams.get('query') || ''; + + console.log('searchQuery: ', searchQuery); + + const searchArtistResults = useSearchArtists(searchQuery); + const artistsData = searchArtistResults.data.pages.flatMap((page) => page.data.artists); + const searchProductResults = useSearchProducts(searchQuery); + const productsData = searchProductResults.data.pages.flatMap((page) => page.data.products); + const navigate = useNavigate(); - const searchLen = searchWork.length + searchArtist.length; - const searchWorkLen = searchWork.length; - const searchArtistLen = searchArtist.length; + const searchLen = productsData.length + artistsData.length; + const searchProductLen = productsData.length; + const searchArtistLen = artistsData.length; const categoryList = ['전체', '작품', '작가']; const goBack = () => { @@ -43,10 +54,10 @@ const SearchResults = () => { {searchLen}건의 결과
- 작품 ({searchWorkLen}) + 작품 ({searchProductLen}) - + handleTabClick('작품')}> 더보기
@@ -58,19 +69,29 @@ const SearchResults = () => { 작가 ({searchArtistLen}) - + handleTabClick('작가')}> 더보기 )} - {selectedTab === '작품' && } - {selectedTab === '작가' && } + {selectedTab === '작품' && } + {selectedTab === '작가' && } ); }; +const SearchResults = () => { + return ( + Error Status}> + Loading Status}> + + + + ); +}; + export default SearchResults; const PageContainer = styled.div` diff --git a/src/types/index.ts b/src/types/index.ts index 3d754275..499044d2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -60,8 +60,75 @@ export type ArtistInfo = { ImageUrl: string; }; +export type SearchArtistInfo = { + id: number; + nickname: string; + artistImageUrl: string; + totalFollowers: number; + totalLikes: number; + isFollowing: boolean; +}; + +export type SearchArtistsResponse = { + artists: SearchArtistInfo[]; + hasNext: boolean; +}; + export type APIResponse = { code: number; message: string; data: T; }; + +export type InfiniteAPIResponse = { + pages: APIResponse[]; + pageParams: number[]; +}; + +export type SearchProductInfo = { + id: number; + name: string; + artist: string; + thumbnailUrl: string; + price: number; +}; + +export type SearchProductsResponse = { + products: SearchProductInfo[]; + hasNext: boolean; +}; + +export type DetailArtistInfo = { + id: number; + userId: number; + artistName: string; + artistImageUrl: string; + artistType: string; + totalFollowers: number; + totalLikes: number; + about: string; +}; + +export type ProductResponse = { + id: number; + name: string; + category: string; + size: string; + price: number; + description: string; + preferredLocation: string; + hashTags: string[]; + artistInfo: DetailArtistInfo; + imageUrls: string[]; +}; + +export type ProfileResponse = { + id: number; + nickname: string; + description: string; + totalFollowers: number; + totalLikes: number; + about: string; + ImageUrl: string; + isFollowed: boolean; +};