diff --git a/src/drawer/apis/getSearchData.ts b/src/drawer/apis/getSearchData.ts new file mode 100644 index 00000000..052d4a95 --- /dev/null +++ b/src/drawer/apis/getSearchData.ts @@ -0,0 +1,16 @@ +import { soomsilClient } from '@/apis'; + +import { ProductSearchRequestParams } from '../types/ProductRequestParams.type'; +import { ProductResponses } from '../types/product.type'; + +export const getSearchData = async ({ keyword, category, page }: ProductSearchRequestParams) => { + const response = await soomsilClient.get('/v2/drawer/search', { + params: { + keyword, + category, + page, + }, + }); + + return response.data; +}; diff --git a/src/drawer/components/Header/Header.tsx b/src/drawer/components/Header/Header.tsx index 311d3804..49279f05 100644 --- a/src/drawer/components/Header/Header.tsx +++ b/src/drawer/components/Header/Header.tsx @@ -1,5 +1,10 @@ + +import { useState } from 'react'; + +import { useNavigate } from 'react-router-dom'; import { IcSearchLine } from '@yourssu/design-system-react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; + import { useTheme } from 'styled-components'; import soomsil from '@/assets/soomsil_logo.svg'; @@ -22,6 +27,12 @@ export const Header = () => { const setDialog = useSetRecoilState(DialogState); const theme = useTheme(); + const [searchValue, setSearchValue] = useState(''); + const navigate = useNavigate(); + + const handleInputChange = (event: React.ChangeEvent) => { + setSearchValue(event.target.value); + }; const handleClickAuthTab = (event: React.MouseEvent) => { if (isLoggedIn) return; @@ -46,7 +57,16 @@ export const Header = () => { - + { + if (e.key === 'Enter') { + navigate(`/drawer/search?keyword=${searchValue}`); + } + }} + /> diff --git a/src/drawer/hooks/useGetSearchData.ts b/src/drawer/hooks/useGetSearchData.ts new file mode 100644 index 00000000..a5d21e7c --- /dev/null +++ b/src/drawer/hooks/useGetSearchData.ts @@ -0,0 +1,29 @@ +import { InfiniteData, useSuspenseInfiniteQuery } from '@tanstack/react-query'; + +import { getSearchData } from '../apis/getSearchData'; +import { PRODUCTS_PER_PAGE } from '../constants/page.constant'; +import { ProductSearchRequestParams } from '../types/ProductRequestParams.type'; +import { ProductResponses, ProductResult } from '../types/product.type'; + +type ProductRequestParamsWithoutPage = Omit; + +export const useGetSearchData = ({ keyword, category }: ProductRequestParamsWithoutPage) => { + return useSuspenseInfiniteQuery< + ProductResponses[], + Error, + InfiniteData, + string[], + number + >({ + queryKey: ['searchData', keyword ?? '', category ?? ''], + queryFn: ({ pageParam }) => + getSearchData({ keyword, category, page: pageParam }).then( + (data) => data.productList + ) as Promise, + initialPageParam: 0, + getNextPageParam: (lastPage, allPages) => { + return lastPage.length === PRODUCTS_PER_PAGE ? allPages.length : undefined; + }, + retry: false, + }); +}; diff --git a/src/drawer/pages/DrawerSearch/DrawerSearch.style.ts b/src/drawer/pages/DrawerSearch/DrawerSearch.style.ts new file mode 100644 index 00000000..788cad60 --- /dev/null +++ b/src/drawer/pages/DrawerSearch/DrawerSearch.style.ts @@ -0,0 +1,36 @@ +import styled from 'styled-components'; + +export const StyledContainer = styled.div` + display: flex; + flex-direction: row; + gap: 8.56rem; +`; + +interface StyledRankingCategoryContainerProps { + $isSmallDesktop?: boolean; +} + +export const StyledRankingContainer = styled.section` + display: flex; + flex-direction: column; + gap: 3rem; + margin-bottom: 4rem; +`; + +export const StyledEmptyContainer = styled.section` + width: 80rem; +`; + +export const StyledCardContainer = styled.section` + display: grid; + grid-template-columns: repeat(3, 25.5rem); + gap: 1.75rem; + margin-top: 2rem; +`; + +export const StyledDescription = styled.p` + ${({ theme }) => theme.color.textPrimary}; + width: 80rem; + + ${({ theme }) => theme.typo.subtitle2}; +`; diff --git a/src/drawer/pages/DrawerSearch/DrawerSearch.tsx b/src/drawer/pages/DrawerSearch/DrawerSearch.tsx new file mode 100644 index 00000000..26fc2960 --- /dev/null +++ b/src/drawer/pages/DrawerSearch/DrawerSearch.tsx @@ -0,0 +1,77 @@ +import { useCallback, useEffect, useRef } from 'react'; + +import { useSearchParams } from 'react-router-dom'; +import { useRecoilState } from 'recoil'; + +import { CardLayout } from '@/drawer/components/CardLayout/CardLayout'; +import { CategoryDropdownMenu } from '@/drawer/components/Category/CategoryDropdownMenu/CategoryDropdownMenu'; +import { RankingCategory } from '@/drawer/components/Category/RankingCategory'; +import { EmptyScreen } from '@/drawer/components/EmptyScreen/EmptyScreen'; +import { SMALL_DESKTOP_MEDIA_QUERY } from '@/drawer/constants/mobileview.constant'; +import { useGetSearchData } from '@/drawer/hooks/useGetSearchData'; +import { CategoryState } from '@/drawer/recoil/CategoryState'; +import { useMediaQuery } from '@/hooks/useMediaQuery'; + +import { + StyledCardContainer, + StyledContainer, + StyledEmptyContainer, + StyledRankingContainer, +} from './DrawerSearch.style'; + +export const DrawerSearch = () => { + const [searchParams] = useSearchParams(); + const keyword = searchParams.get('keyword') || ''; + const [selectedCategory, setSelectedCategory] = useRecoilState(CategoryState); + + const { + data: searchResults, + isLoading, + fetchNextPage, + hasNextPage, + } = useGetSearchData({ keyword, category: selectedCategory }); + + useEffect(() => { + setSelectedCategory(''); + }, []); + + const observer = useRef(); + + const lastElementRef = useCallback( + (node: HTMLDivElement) => { + if (isLoading) return; + + if (observer.current) observer.current.disconnect(); + observer.current = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && hasNextPage) { + fetchNextPage(); + } + }); + if (node) observer.current.observe(node); + }, + [isLoading, hasNextPage] + ); + + const allProducts = searchResults ? searchResults.pages.flat() : []; + + const isSmallDesktop = useMediaQuery(SMALL_DESKTOP_MEDIA_QUERY); + + return ( + + {!isSmallDesktop && } + + {isSmallDesktop && } + {allProducts.length === 0 ? ( + + + + ) : ( + + + + )} + {hasNextPage &&
} + + + ); +}; diff --git a/src/drawer/types/ProductRequestParams.type.ts b/src/drawer/types/ProductRequestParams.type.ts index 8456eb67..f0812559 100644 --- a/src/drawer/types/ProductRequestParams.type.ts +++ b/src/drawer/types/ProductRequestParams.type.ts @@ -3,3 +3,9 @@ export interface ProductRequestParams { category?: string; page: number; } + +export interface ProductSearchRequestParams { + keyword?: string; + category?: string; + page: number; +} diff --git a/src/router.tsx b/src/router.tsx index 0261c4fa..858481d9 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -82,6 +82,12 @@ const Withdraw = lazy(() => })) ); +const DrawerSearch = lazy(() => + import('./drawer/pages/DrawerSearch/DrawerSearch').then(({ DrawerSearch }) => ({ + default: DrawerSearch, + })) +); + export const Router = () => { const [state, setState] = useState({ isAnimating: false, @@ -129,6 +135,7 @@ export const Router = () => { } /> } /> } /> + } /> } />