diff --git a/src/components/common/PostSection/index.tsx b/src/components/common/PostSection/index.tsx new file mode 100644 index 00000000..d4ff6c6a --- /dev/null +++ b/src/components/common/PostSection/index.tsx @@ -0,0 +1,46 @@ +import { Styled } from './styled' +import type { PostSectionProps } from './types' +import { useGetCategoriesQuery } from '@apis/post' +import { SearchOptions, CategorySlideFilter, ProductList } from '@components' + +export const PostSection = ({ + infinitePosts, + postsCount = 0, + searchOptions, + onChangeSearchOption +}: PostSectionProps) => { + const getCategoriesQuery = useGetCategoriesQuery() + const categories = + getCategoriesQuery.data?.map(({ code, name }) => ({ + code, + name + })) || [] + + return ( + <> + + onChangeSearchOption('category', code)} + /> + + + {postsCount > 0 ? ( + + ) : ( + + 검색 결과 없음 + + 찾으시는 검색 결과가 없어요 + + + )} + + ) +} diff --git a/src/components/common/PostSection/styled.ts b/src/components/common/PostSection/styled.ts new file mode 100644 index 00000000..34cff31e --- /dev/null +++ b/src/components/common/PostSection/styled.ts @@ -0,0 +1,38 @@ +import { css } from '@emotion/react' +import styled from '@emotion/styled' + +const CategorySliderWrapper = styled.div` + /* TODO: useMedia를 사용한 조건부 렌더링시 hydration 에러가 발생해 스타일로 우선 적용 했습니다. */ + ${({ theme }) => theme.mediaQuery.tablet} { + display: none; + } +` + +const Placeholder = styled.div` + width: 100%; + height: 100%; + margin: 120px 0; + + text-align: center; +` + +const PlaceholderTitle = styled.p` + margin-bottom: 8px; + + ${({ theme }) => theme.fonts.subtitle01B} +` + +const PlaceholderDescription = styled.p` + ${({ theme }) => css` + color: ${theme.colors.grayScale70}; + + ${theme.fonts.body01M}; + `} +` + +export const Styled = { + CategorySliderWrapper, + Placeholder, + PlaceholderTitle, + PlaceholderDescription +} diff --git a/src/components/common/PostSection/types.ts b/src/components/common/PostSection/types.ts new file mode 100644 index 00000000..2d0a6df9 --- /dev/null +++ b/src/components/common/PostSection/types.ts @@ -0,0 +1,12 @@ +import type { + SearchOptionsState, + OnChangeSearchOptions +} from '../../result/SearchOptions/types' +import type { ProductListProps } from '@components/home' + +export type PostSectionProps = { + postsCount?: number + infinitePosts: ProductListProps + searchOptions: SearchOptionsState + onChangeSearchOption: OnChangeSearchOptions +} diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 34c9d8bb..8111c6fb 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -3,3 +3,4 @@ export * from './CommonModal' export * from './Header' export * from './AlertModal' export * from './Dialog' +export * from './PostSection' diff --git a/src/components/home/CategorySlider/index.tsx b/src/components/home/CategorySlider/index.tsx index 917bdb89..9baa8263 100644 --- a/src/components/home/CategorySlider/index.tsx +++ b/src/components/home/CategorySlider/index.tsx @@ -4,6 +4,8 @@ import { useState, useEffect, useRef, useCallback } from 'react' import type { ReactElement, TouchEventHandler } from 'react' import { Styled } from './styled' import { useGetCategoriesQuery } from '@apis/post' +import { SORT_OPTIONS } from '@constants/app' +import { toQueryString } from '@utils/format' const CategorySlider = (): ReactElement => { const containerRef = useRef(null) @@ -84,7 +86,7 @@ const CategorySlider = (): ReactElement => { onTouchMove={isDrag ? onDragMove : undefined} onTouchStart={onDragStart}> {isDesktop && ( - + <> {isFirstCategory ? (
) : ( @@ -105,14 +107,17 @@ const CategorySlider = (): ReactElement => { onClick={handleRightArrowClick} /> )} - + )} {getCategoryQuery.data?.map(({ code, name, imageUrl }) => { return ( - + theme.fonts.headline02B} @@ -81,43 +82,46 @@ export const CateGoryBox = styled.div` } ` -export const ArrowBox = styled.div` +const arrowStyle = css` position: absolute; - z-index: 999; - display: flex; - align-self: center; - justify-content: space-between; - - width: 100%; - margin-bottom: 30px; -` + top: 32px; + z-index: ${theme.zIndex.common}; -export const LeftArrow = styled(IconButton)` width: 24px; height: 24px; border-radius: 100%; - background-color: ${({ theme }): string => theme.colors.white}; + background-color: ${theme.colors.white}; filter: drop-shadow(0 2px 6px rgb(0 0 0 / 25%)); ` -export const RightArrow = styled(IconButton)` - width: 24px; - height: 24px; - border-radius: 100%; +export const LeftArrow = styled(IconButton)` + left: 0; + + ${arrowStyle} +` - background-color: ${({ theme }): string => theme.colors.white}; +export const RightArrow = styled(IconButton)` + right: 0; - filter: drop-shadow(0 2px 6px rgb(0 0 0 / 25%)); + ${arrowStyle} transform: scaleX(-1); ` -export const CategoryItem = styled.div` +export const CategoryItem = styled.button` + display: flex; + justify-content: center; + width: 100%; max-width: 108px; height: 118px; + border: none; + + background: transparent; + + cursor: pointer; transition: 0.5s; @@ -142,6 +146,7 @@ export const CategoryImgWrapper = styled.div` width: 108px; height: 86px; + cursor: pointer; ${({ theme }) => css` border-radius: ${theme.radius.round12}; @@ -192,7 +197,6 @@ export const Styled = { CateGoryBoxWrapper, CateGoryBox, CategoryLink, - ArrowBox, LeftArrow, RightArrow, CategoryItem, diff --git a/src/components/home/ProductList/types.ts b/src/components/home/ProductList/types.ts index ccd76216..40f23696 100644 --- a/src/components/home/ProductList/types.ts +++ b/src/components/home/ProductList/types.ts @@ -3,14 +3,10 @@ import type { InfiniteData, InfiniteQueryObserverResult } from '@tanstack/react-query' -import type { GetPostsReq, GetPostsRes } from '@apis/post' +import type { GetPostsRes } from '@apis/post' export type ProductListProps = { postData?: GetPostsRes[] - filterOption?: Pick< - GetPostsReq, - 'sort' | 'category' | 'minPrice' | 'maxPrice' - > hasNextPage?: boolean fetchNextPage?( options?: FetchNextPageOptions diff --git a/src/components/result/CategoryHeader/index.tsx b/src/components/result/CategoryHeader/index.tsx index 2d697ae4..65c6716b 100644 --- a/src/components/result/CategoryHeader/index.tsx +++ b/src/components/result/CategoryHeader/index.tsx @@ -3,14 +3,12 @@ import { Styled } from './styled' import type { ResultHeaderProps } from './types' const ResultHeader = ({ - searchResult, + resultMessage, postsCount }: ResultHeaderProps): ReactElement => { return ( - - "{searchResult}"의 검색결과 - + {resultMessage} {postsCount}개 diff --git a/src/components/result/CategoryHeader/types.ts b/src/components/result/CategoryHeader/types.ts index 3ab37494..6f601d53 100644 --- a/src/components/result/CategoryHeader/types.ts +++ b/src/components/result/CategoryHeader/types.ts @@ -1,4 +1,4 @@ export type ResultHeaderProps = { - searchResult: string + resultMessage: string postsCount: number } diff --git a/src/components/result/SearchOptions/types.ts b/src/components/result/SearchOptions/types.ts index 9a25f870..9bd9f4c4 100644 --- a/src/components/result/SearchOptions/types.ts +++ b/src/components/result/SearchOptions/types.ts @@ -9,14 +9,15 @@ export type SearchOptionsState = { max?: number } } + export type OnChangeSearchOptions = ( name: KeyOf, value: ValueOf ) => void export type SearchOptionsProps = { - categories: Pick[] postsCount?: number + categories: Pick[] searchOptions: SearchOptionsState onChangeSearchOption: OnChangeSearchOptions } diff --git a/src/pages/categories/[categoryCode].tsx b/src/pages/categories/[categoryCode].tsx new file mode 100644 index 00000000..65eeb7f0 --- /dev/null +++ b/src/pages/categories/[categoryCode].tsx @@ -0,0 +1,138 @@ +import styled from '@emotion/styled' +import type { GetServerSideProps, NextPage } from 'next' +import { useRouter } from 'next/router' +import { useState } from 'react' +import type { + SearchOptionsState, + OnChangeSearchOptions +} from '@components/result/SearchOptions/types' +import { useGetCategoriesQuery, useGetInfinitePostsQuery } from '@apis' +import { PostSection, ResultHeader } from '@components' +import type { SortOptionCodes, TradeTypeCodes } from '@types' +import { find, removeNullish, toQueryString } from '@utils' + +const DEFAULT_PER_PAGE = 8 +// TODO: 포스트 전체 갯수 내려달라고 요청해놓았습니다 +const POSTS_COUNT_MOCK = 10 + +type CategoriesProps = { + category?: string + sort?: SortOptionCodes + minPrice?: number + maxPrice?: number + tradeType?: TradeTypeCodes +} + +export const getServerSideProps: GetServerSideProps = async ({ + query +}) => ({ + props: { + category: query.categoryCode as string, + sort: (query.sort as SortOptionCodes) || 'CREATED_DATE_DESC', + minPrice: Number(query.min_price), + maxPrice: Number(query.max_price), + tradeType: (query.tradeType as TradeTypeCodes) || null + } +}) + +const Categories: NextPage = ({ + category, + sort, + minPrice, + maxPrice, + tradeType +}: CategoriesProps) => { + const [searchOptions, setSearchOptions] = useState({ + category, + sort: 'CREATED_DATE_DESC', + priceRange: {} + }) + const router = useRouter() + + const getCategoriesQuery = useGetCategoriesQuery() + + const searchParams = removeNullish({ + minPrice: searchOptions.priceRange?.min ?? minPrice, + maxPrice: searchOptions.priceRange?.max ?? maxPrice, + tradeType: searchOptions.tradeType ?? tradeType, + sort: searchOptions.sort ?? sort + }) + + const categories = + getCategoriesQuery.data?.map(({ code, name }) => ({ code, name })) || [] + const currentCategory = find(categories, { code: searchOptions.category }) + + const infinitePosts = useGetInfinitePostsQuery({ + lastId: null, + limit: DEFAULT_PER_PAGE, + ...searchParams + }) + + const searchByCategory = ({ + category, + priceRange, + ...params + }: SearchOptionsState) => { + router.push( + `/categories/${category}?${toQueryString({ + ...params, + minPrice: priceRange.min, + maxPrice: priceRange.max + })}` + ) + } + + const handleChangeSearchOptions: OnChangeSearchOptions = (name, value) => { + const newSearchOptions = { + ...searchOptions, + [name]: value + } + + setSearchOptions(newSearchOptions) + searchByCategory(newSearchOptions) + } + + return ( + + + + + + + ) +} + +const ResultWrapper = styled.div` + width: 100%; + max-width: 1200px; + ${({ theme }): string => theme.mediaQuery.tablet} { + padding-right: 24px; + padding-left: 24px; + } + ${({ theme }): string => theme.mediaQuery.mobile} { + padding-right: 16px; + padding-left: 16px; + } +` + +const Layout = styled.div` + display: flex; + justify-content: center; + + width: 100%; + margin-top: 68px; +` + +export default Categories diff --git a/src/pages/index.tsx b/src/pages/index.tsx index b93e4b8d..3144c422 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -7,6 +7,10 @@ import { ProductList } from '../components/home/ProductList' import { useGetInfinitePostsQuery } from '@apis/post' import { CategorySlider, HomeBanner } from '@components' +const DEFAULT_PER_PAGE = 8 +// TODO: 포스트 전체 갯수 내려달라고 요청해놓았습니다 +const POSTS_COUNTS_MOCK = 10 + const Home: NextPage = () => { const { data: postList, @@ -14,20 +18,17 @@ const Home: NextPage = () => { hasNextPage } = useGetInfinitePostsQuery({ lastId: null, - limit: 8 + limit: DEFAULT_PER_PAGE }) const router = useRouter() - // TODO: 포스트 전체 갯수 내려달라고 요청해놓았습니다 - const postsCount = 0 - return ( 새로운 상품 - {postsCount > 0 ? ( + {POSTS_COUNTS_MOCK > 0 ? ( { - const getCategoriesQuery = useGetCategoriesQuery() const router = useRouter() const searchKeyword = useAtomValue(searchKeywordAtom) const [searchOptions, setSearchOptions] = useState({ @@ -67,64 +62,49 @@ const ResultPage: NextPage = ({ searchKeyword: currentKeyword }) - const categories = - getCategoriesQuery.data?.map(({ code, name }) => ({ code, name })) || [] - const infinitePosts = useGetInfinitePostsQuery({ lastId: null, - limit: DEFAULT_POST_PAGE_NUMBER, + limit: DEFAULT_PER_PAGE, ...searchParams }) - // TODO: 포스트 전체 갯수 내려달라고 요청해놓았습니다 - const postsCount = 0 + const searchByResult = ({ priceRange, ...params }: SearchOptionsState) => { + router.push( + `/result?${toQueryString({ + ...params, + minPrice: priceRange.min, + maxPrice: priceRange.max + })}` + ) + } const handleChangeSearchOptions: OnChangeSearchOptions = (name, value) => { - const nextSearchOptions = { + const newSearchOptions = { ...searchOptions, [name]: value } - setSearchOptions(nextSearchOptions) - router.push(`/result?${toQueryString(searchParams)}`) + setSearchOptions(newSearchOptions) + searchByResult(newSearchOptions) } return ( - - - handleChangeSearchOptions('category', code) - } - /> - - - {postsCount > 0 ? ( - - ) : ( - - 검색 결과 없음 - - 찾으시는 검색 결과가 없어요 - - - )} ) @@ -151,33 +131,4 @@ const Layout = styled.div` margin-top: 68px; ` -const CategorySliderWrapper = styled.div` - /* TODO: useMedia를 사용한 조건부 렌더링시 hydration 에러가 발생해 스타일로 우선 적용 했습니다. */ - ${({ theme }) => theme.mediaQuery.tablet} { - display: none; - } -` - -const Placeholder = styled.div` - width: 100%; - height: 100%; - margin: 120px 0; - - text-align: center; -` - -const PlaceholderTitle = styled.p` - margin-bottom: 8px; - - ${({ theme }) => theme.fonts.subtitle01B} -` - -const PlaceholderDescription = styled.p` - ${({ theme }) => css` - color: ${theme.colors.grayScale70}; - - ${theme.fonts.body01M}; - `} -` - export default ResultPage diff --git a/src/utils/common/index.ts b/src/utils/common/index.ts index 3b13e510..cecf87df 100644 --- a/src/utils/common/index.ts +++ b/src/utils/common/index.ts @@ -2,7 +2,7 @@ export const noop = (): void => undefined export const find = ( arr: T, - target: UnknownObject + target: UnknownObject ): T extends readonly any[] ? ValueOf : undefined => { const result = arr.find(item => { const splittedArr = splitObject(item) diff --git a/src/utils/format/index.ts b/src/utils/format/index.ts index fb3a31e3..5ee8f3a0 100644 --- a/src/utils/format/index.ts +++ b/src/utils/format/index.ts @@ -53,12 +53,14 @@ export const toLocaleCurrency = (value: number): string => { } export const toQueryString = (object: { - [key: string]: string | number + [key: string]: string | number | undefined }): URLSearchParams => { const searchParams = new URLSearchParams() Object.entries(object).forEach(([key, value]) => { - searchParams.set(String(key), String(value)) + if (value) { + searchParams.set(String(key), String(value)) + } }) return searchParams