From d7c95de8c66fcfe671b03ce38f0a57a4c1cd10f4 Mon Sep 17 00:00:00 2001 From: Leejin Yang Date: Wed, 26 Jul 2023 13:33:56 +0900 Subject: [PATCH] =?UTF-8?q?[FE]=20feat:=20=EC=A0=95=EB=A0=AC=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=B6=84=EB=A6=AC=20(#141)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 펀잇 디자인 시스템 버전 업데이트 * feat: 리뷰 정렬 옵션 상수 추가 및 정렬 옵션 타입 추가 * refactor: SortButton에서 BottomSheet 제거 * refactor: SortOptionList에서 options prop 추가 * refactor: BottomSheet 페이지로 이동 * style: SortOption -> SortOptionButton으로 수정 * feat: SortButton 스토리에 args 추가 * refactor: useSortOption 커스텀 훅 분리 --- frontend/package.json | 2 +- .../Common/SortButton/SortButton.stories.tsx | 3 + .../Common/SortButton/SortButton.tsx | 47 ++++++---------- .../SortOptionList/SortOptionList.stories.tsx | 15 +++-- .../Common/SortOptionList/SortOptionList.tsx | 55 ++++++++++--------- frontend/src/constants/index.ts | 8 ++- frontend/src/hooks/useSortOption.ts | 13 +++++ frontend/src/pages/ProductDetailPage.tsx | 22 ++++++-- frontend/src/pages/ProductListPage.tsx | 41 +++++++++----- frontend/src/types/common.ts | 3 + frontend/yarn.lock | 8 +-- 11 files changed, 133 insertions(+), 84 deletions(-) create mode 100644 frontend/src/hooks/useSortOption.ts diff --git a/frontend/package.json b/frontend/package.json index 48753fc8f..d9c4d29d2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,7 @@ "build-storybook": "storybook build" }, "dependencies": { - "@fun-eat/design-system": "^0.3.1", + "@fun-eat/design-system": "^0.3.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.14.2", diff --git a/frontend/src/components/Common/SortButton/SortButton.stories.tsx b/frontend/src/components/Common/SortButton/SortButton.stories.tsx index dcf579ed4..f0f5d16cf 100644 --- a/frontend/src/components/Common/SortButton/SortButton.stories.tsx +++ b/frontend/src/components/Common/SortButton/SortButton.stories.tsx @@ -5,6 +5,9 @@ import SortButton from './SortButton'; const meta: Meta = { title: 'common/SortButton', component: SortButton, + args: { + option: '높은 가격순', + }, }; export default meta; diff --git a/frontend/src/components/Common/SortButton/SortButton.tsx b/frontend/src/components/Common/SortButton/SortButton.tsx index 1f57a2cb7..86d65d192 100644 --- a/frontend/src/components/Common/SortButton/SortButton.tsx +++ b/frontend/src/components/Common/SortButton/SortButton.tsx @@ -1,46 +1,31 @@ -import { BottomSheet, Button, Text, theme } from '@fun-eat/design-system'; -import { useState } from 'react'; +import { Button, Text, useTheme } from '@fun-eat/design-system'; import styled from 'styled-components'; -import BottomSheetContent from '../SortOptionList/SortOptionList'; import SvgIcon from '../Svg/SvgIcon'; -import { SORT_OPTIONS } from '@/constants'; -import useBottomSheet from '@/hooks/useBottomSheet'; +interface SortButtonProps { + option: string; + onClick: () => void; +} -const SortButton = () => { - const { ref, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); - const [selectedOption, setSelectedOption] = useState(0); - - const selectSortOption = (optionIndex: number) => { - setSelectedOption(optionIndex); - }; +const SortButton = ({ option, onClick }: SortButtonProps) => { + const theme = useTheme(); return ( - <> - - - - - + + + + {option} + + ); }; export default SortButton; -const SortButtonWrapper = styled.div` +const SortButtonContainer = styled(Button)` display: flex; align-items: center; - gap: 3px; + column-gap: 4px; + padding: 0; `; diff --git a/frontend/src/components/Common/SortOptionList/SortOptionList.stories.tsx b/frontend/src/components/Common/SortOptionList/SortOptionList.stories.tsx index 3396db948..f54b0cb93 100644 --- a/frontend/src/components/Common/SortOptionList/SortOptionList.stories.tsx +++ b/frontend/src/components/Common/SortOptionList/SortOptionList.stories.tsx @@ -4,6 +4,8 @@ import { useEffect, useRef, useState } from 'react'; import SortOptionList from './SortOptionList'; +import { PRODUCT_SORT_OPTIONS } from '@/constants'; + const meta: Meta = { title: 'common/SortOptionList', component: SortOptionList, @@ -15,7 +17,7 @@ type Story = StoryObj; export const Default: Story = { render: () => { const ref = useRef(null); - const [selectedOption, setSelectedOption] = useState(0); + const [selectedOption, setSelectedOption] = useState(PRODUCT_SORT_OPTIONS[0].label); useEffect(() => { ref.current?.showModal(); @@ -25,13 +27,18 @@ export const Default: Story = { ref.current?.close(); }; - const selectSortOption = (optionIndex: number) => { - setSelectedOption(optionIndex); + const selectSortOption = (selectedOptionLabel: string) => { + setSelectedOption(selectedOptionLabel); }; return ( - + ); }, diff --git a/frontend/src/components/Common/SortOptionList/SortOptionList.tsx b/frontend/src/components/Common/SortOptionList/SortOptionList.tsx index 20e3ad2c3..3fa8cb583 100644 --- a/frontend/src/components/Common/SortOptionList/SortOptionList.tsx +++ b/frontend/src/components/Common/SortOptionList/SortOptionList.tsx @@ -1,33 +1,28 @@ import { Button, theme } from '@fun-eat/design-system'; import styled from 'styled-components'; -import { SORT_OPTIONS } from '@/constants'; +import type { SortOption } from '@/types/common'; interface SortOptionListProps { - selectedOption: number; - selectSortOption: (optionIndex: number) => void; + options: readonly SortOption[]; + selectedOption: string; + selectSortOption: (selectedOptionLabel: string) => void; close: () => void; } -const SortOptionList = ({ selectedOption, selectSortOption, close }: SortOptionListProps) => { - const handleSelectedOption = (optionIndex: number) => { - selectSortOption(optionIndex); +const SortOptionList = ({ options, selectedOption, selectSortOption, close }: SortOptionListProps) => { + const handleSelectedOption = (selectedOptionLabel: string) => { + selectSortOption(selectedOptionLabel); close(); }; return ( - {SORT_OPTIONS.map((option, index) => { - const isSelected = index === selectedOption; - const isLastItem = index < SORT_OPTIONS.length - 1; + {options.map(({ label }) => { + const isSelected = label === selectedOption; return ( - - + handleSelectedOption(index)} + onClick={() => handleSelectedOption(label)} > - {option.label} - - + {label} + + ); })} @@ -50,15 +45,25 @@ export default SortOptionList; const SortOptionListContainer = styled.ul` padding: 20px; -`; -const SortOptionItem = styled.li``; + & > li { + height: 60px; + border-bottom: 1px solid ${({ theme }) => theme.dividerColors.disabled}; + line-height: 60px; + } + + & > li:last-of-type { + border: none; + } +`; -const SortOption = styled(Button)` - margin: 20px 0 10px 0; - padding: 0; +const SortOptionButton = styled(Button)` + width: 100%; + height: 100%; + padding: 10px 0; border: none; outline: transparent; + text-align: left; &:hover { font-weight: ${({ theme }) => theme.fontWeights.bold}; diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index 92db9bd73..caa8ffe90 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -30,7 +30,13 @@ export const NAVIGATION_MENU: NavigationMenu[] = [ }, ]; -export const SORT_OPTIONS = [ +export const PRODUCT_SORT_OPTIONS = [ { label: '높은 가격순', value: 'price,desc' }, { label: '낮은 가격순', value: 'price,asc' }, ] as const; + +export const REVIEW_SORT_OPTIONS = [ + { label: '높은 평점순', value: 'rating,desc' }, + { label: '낮은 평점순', value: 'rating,asc' }, + { label: '추천순', value: 'favoriteCount,asc' }, +] as const; diff --git a/frontend/src/hooks/useSortOption.ts b/frontend/src/hooks/useSortOption.ts new file mode 100644 index 000000000..6a7f2cc3a --- /dev/null +++ b/frontend/src/hooks/useSortOption.ts @@ -0,0 +1,13 @@ +import { useState } from 'react'; + +const useSortOption = (initialOption: string) => { + const [selectedOption, setSelectedOption] = useState(initialOption); + + const selectSortOption = (selectedOptionLabel: string) => { + setSelectedOption(selectedOptionLabel); + }; + + return { selectedOption, selectSortOption }; +}; + +export default useSortOption; diff --git a/frontend/src/pages/ProductDetailPage.tsx b/frontend/src/pages/ProductDetailPage.tsx index 2b42af8c0..416c2c71d 100644 --- a/frontend/src/pages/ProductDetailPage.tsx +++ b/frontend/src/pages/ProductDetailPage.tsx @@ -1,15 +1,20 @@ -import { Spacing } from '@fun-eat/design-system'; +import { BottomSheet, Spacing } from '@fun-eat/design-system'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; -import { SortButton, TabMenu } from '@/components/Common'; +import { SortButton, SortOptionList, TabMenu } from '@/components/Common'; import { ProductDetailItem, ProductTitle } from '@/components/Product'; import { ReviewItem } from '@/components/Review'; +import { REVIEW_SORT_OPTIONS } from '@/constants'; +import useBottomSheet from '@/hooks/useBottomSheet'; +import useSortOption from '@/hooks/useSortOption'; import productDetails from '@/mocks/data/productDetails.json'; import mockReviews from '@/mocks/data/reviews.json'; const ProductDetailPage = () => { const { productId } = useParams(); + const { ref, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); + const { selectedOption, selectSortOption } = useSortOption(REVIEW_SORT_OPTIONS[0].label); // TODO: productId param으로 api 요청 보내면 바뀔 로직 const targetProductDetail = @@ -24,11 +29,9 @@ const ProductDetailPage = () => { - - + -
{reviews && ( @@ -40,6 +43,14 @@ const ProductDetailPage = () => { )}
+ + + ); }; @@ -50,6 +61,7 @@ const SortButtonWrapper = styled.div` display: flex; align-items: center; justify-content: flex-end; + margin: 20px 0; `; const ReviewItemWrapper = styled.ul` diff --git a/frontend/src/pages/ProductListPage.tsx b/frontend/src/pages/ProductListPage.tsx index 82181f535..404be026b 100644 --- a/frontend/src/pages/ProductListPage.tsx +++ b/frontend/src/pages/ProductListPage.tsx @@ -1,23 +1,37 @@ -import { Spacing } from '@fun-eat/design-system'; +import { BottomSheet, Spacing } from '@fun-eat/design-system'; import styled from 'styled-components'; -import { CategoryMenu } from '@/components/Common'; -import SortButton from '@/components/Common/SortButton/SortButton'; -import Title from '@/components/Common/Title/Title'; +import { CategoryMenu, SortButton, SortOptionList, Title } from '@/components/Common'; import { ProductList } from '@/components/Product'; +import { PRODUCT_SORT_OPTIONS } from '@/constants'; +import useBottomSheet from '@/hooks/useBottomSheet'; +import useSortOption from '@/hooks/useSortOption'; import foodCategory from '@/mocks/data/foodCategory.json'; const ProductListPage = () => { + const { ref, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); + const { selectedOption, selectSortOption } = useSortOption(PRODUCT_SORT_OPTIONS[0].label); + return ( -
- - <Spacing size={30} /> - <CategoryMenu menuList={foodCategory} menuVariant="food" /> - <SortButtonWrapper> - <SortButton /> - </SortButtonWrapper> - <ProductList /> - </section> + <> + <section> + <Title headingTitle="공통 상품" /> + <Spacing size={30} /> + <CategoryMenu menuList={foodCategory} menuVariant="food" /> + <SortButtonWrapper> + <SortButton option={selectedOption} onClick={handleOpenBottomSheet} /> + </SortButtonWrapper> + <ProductList /> + </section> + <BottomSheet ref={ref} maxWidth="600px" close={handleCloseBottomSheet}> + <SortOptionList + options={PRODUCT_SORT_OPTIONS} + selectedOption={selectedOption} + selectSortOption={selectSortOption} + close={handleCloseBottomSheet} + /> + </BottomSheet> + </> ); }; @@ -26,4 +40,5 @@ export default ProductListPage; const SortButtonWrapper = styled.div` display: flex; justify-content: flex-end; + margin: 20px 0; `; diff --git a/frontend/src/types/common.ts b/frontend/src/types/common.ts index 456990edc..2815e8f08 100644 --- a/frontend/src/types/common.ts +++ b/frontend/src/types/common.ts @@ -1,4 +1,5 @@ import type { SvgIconVariant } from '@/components/Common/Svg/SvgIcon'; +import type { PRODUCT_SORT_OPTIONS, REVIEW_SORT_OPTIONS } from '@/constants'; import type { PATH } from '@/constants/path'; export interface Category { @@ -20,3 +21,5 @@ export interface NavigationMenu { export type ProductSortOption = 'price' | 'averageRating' | 'reviewCount'; export type ReviewSortOption = 'favoriteCount' | 'rating'; + +export type SortOption = (typeof PRODUCT_SORT_OPTIONS)[number] | (typeof REVIEW_SORT_OPTIONS)[number]; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index baf43a3a7..943ef0fc5 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1565,10 +1565,10 @@ resolved "https://registry.yarnpkg.com/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz#c05ed35ad82df8e6ac616c68b92c2282bd083ba4" integrity sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ== -"@fun-eat/design-system@^0.3.1": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@fun-eat/design-system/-/design-system-0.3.1.tgz#3cc7887e2fa8e15301af3b6a95e6889921702a3f" - integrity sha512-oMSNE9E2PsJklqtgdXFB3elthWCohqDNdhdtIR0cNIoAZU7Jf85a79ktav+enaqyDbU87MupvLkwEs74QNVtqQ== +"@fun-eat/design-system@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@fun-eat/design-system/-/design-system-0.3.2.tgz#3b49b3d9868f8283df09a2605d1c6fa047943329" + integrity sha512-19pGO+VqblqQGivbayRtN2EpzdLBQ5PGWI/kRz9Mg8n/661MTrciiKyTjJ4if/QtBWufTXNFshsfz2SCMUGPFA== "@humanwhocodes/config-array@^0.11.10": version "0.11.10"