diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index e733d261d..d77ffa4b2 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -18,6 +18,11 @@ logging: path: { LOG_DIR } server: + tomcat: + threads: + max: { MAX_THREADS } + max-connections: { MAX_CONNECTIONS } + accept-count: { ACCEPT_COUNT } servlet: session: cookie: diff --git a/frontend/package.json b/frontend/package.json index a0953eadb..ddc1de60a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,7 @@ "test:coverage": "jest --watchAll --coverage" }, "dependencies": { - "@fun-eat/design-system": "^0.3.11", + "@fun-eat/design-system": "^0.3.12", "@tanstack/react-query": "^4.32.6", "@tanstack/react-query-devtools": "^4.32.6", "dayjs": "^1.11.9", diff --git a/frontend/public/index.html b/frontend/public/index.html index d6d5ee3aa..21156c6ad 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -18,6 +18,13 @@ + 펀잇 diff --git a/frontend/src/components/Common/CategoryFoodList/CategoryFoodList.stories.tsx b/frontend/src/components/Common/CategoryFoodList/CategoryFoodList.stories.tsx new file mode 100644 index 000000000..e597dc53c --- /dev/null +++ b/frontend/src/components/Common/CategoryFoodList/CategoryFoodList.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import CategoryFoodList from './CategoryFoodList'; + +const meta: Meta = { + title: 'common/CategoryFoodList', + component: CategoryFoodList, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/frontend/src/components/Common/CategoryFoodList/CategoryFoodList.tsx b/frontend/src/components/Common/CategoryFoodList/CategoryFoodList.tsx new file mode 100644 index 000000000..ec2e95c6b --- /dev/null +++ b/frontend/src/components/Common/CategoryFoodList/CategoryFoodList.tsx @@ -0,0 +1,33 @@ +import { Link } from '@fun-eat/design-system'; +import { Link as RouterLink } from 'react-router-dom'; +import styled from 'styled-components'; + +import CategoryItem from '../CategoryItem/CategoryItem'; + +import { CATEGORY_TYPE } from '@/constants'; +import { useCategoryFoodQuery } from '@/hooks/queries/product'; + +const category = CATEGORY_TYPE.FOOD; + +const CategoryFoodList = () => { + const { data: categories } = useCategoryFoodQuery(category); + + return ( +
+ + {categories.map((menu) => ( + + + + ))} + +
+ ); +}; + +export default CategoryFoodList; + +const CategoryFoodListWrapper = styled.div` + display: flex; + gap: 16px; +`; diff --git a/frontend/src/components/Common/CategoryFoodTab/CategoryFoodTab.stories.tsx b/frontend/src/components/Common/CategoryFoodTab/CategoryFoodTab.stories.tsx new file mode 100644 index 000000000..7cbb6e1f6 --- /dev/null +++ b/frontend/src/components/Common/CategoryFoodTab/CategoryFoodTab.stories.tsx @@ -0,0 +1,22 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import CategoryFoodTab from './CategoryFoodTab'; + +import CategoryProvider from '@/contexts/CategoryContext'; + +const meta: Meta = { + title: 'common/CategoryFoodTab', + component: CategoryFoodTab, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/frontend/src/components/Common/CategoryTab/CategoryTab.tsx b/frontend/src/components/Common/CategoryFoodTab/CategoryFoodTab.tsx similarity index 52% rename from frontend/src/components/Common/CategoryTab/CategoryTab.tsx rename to frontend/src/components/Common/CategoryFoodTab/CategoryFoodTab.tsx index 8796c65f1..1e672994b 100644 --- a/frontend/src/components/Common/CategoryTab/CategoryTab.tsx +++ b/frontend/src/components/Common/CategoryFoodTab/CategoryFoodTab.tsx @@ -1,23 +1,20 @@ import { Button, theme } from '@fun-eat/design-system'; import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; -import type { CSSProp } from 'styled-components'; import styled from 'styled-components'; -import { useCategoryValueContext, useCategoryActionContext } from '@/hooks/context'; -import { useCategoryQuery } from '@/hooks/queries/product'; -import type { CategoryVariant } from '@/types/common'; +import { CATEGORY_TYPE } from '@/constants'; +import { useCategoryActionContext, useCategoryValueContext } from '@/hooks/context'; +import { useCategoryFoodQuery } from '@/hooks/queries/product/useCategoryQuery'; -interface CategoryMenuProps { - menuVariant: CategoryVariant; -} +const category = CATEGORY_TYPE.FOOD; -const CategoryTab = ({ menuVariant }: CategoryMenuProps) => { - const { data: categories } = useCategoryQuery(menuVariant); +const CategoryFoodTab = () => { + const { data: categories } = useCategoryFoodQuery(category); const { categoryIds } = useCategoryValueContext(); const { selectCategory } = useCategoryActionContext(); - const currentCategoryId = categoryIds[menuVariant]; + const currentCategoryId = categoryIds[category]; const location = useLocation(); const queryParams = new URLSearchParams(location.search); @@ -25,9 +22,9 @@ const CategoryTab = ({ menuVariant }: CategoryMenuProps) => { useEffect(() => { if (categoryIdFromURL) { - selectCategory(menuVariant, parseInt(categoryIdFromURL)); + selectCategory(category, parseInt(categoryIdFromURL)); } - }, [location]); + }, [category]); return ( @@ -43,8 +40,7 @@ const CategoryTab = ({ menuVariant }: CategoryMenuProps) => { weight="bold" variant={isSelected ? 'filled' : 'outlined'} isSelected={isSelected} - menuVariant={menuVariant} - onClick={() => selectCategory(menuVariant, menu.id)} + onClick={() => selectCategory(category, menu.id)} aria-pressed={isSelected} > {menu.name} @@ -56,9 +52,7 @@ const CategoryTab = ({ menuVariant }: CategoryMenuProps) => { ); }; -export default CategoryTab; - -type CategoryMenuStyleProps = Pick; +export default CategoryFoodTab; const CategoryMenuContainer = styled.ul` display: flex; @@ -71,18 +65,13 @@ const CategoryMenuContainer = styled.ul` } `; -const CategoryButton = styled(Button)<{ isSelected: boolean } & CategoryMenuStyleProps>` +const CategoryButton = styled(Button)<{ isSelected: boolean }>` padding: 6px 12px; - ${({ isSelected, menuVariant }) => (isSelected ? selectedCategoryMenuStyles[menuVariant] : '')} + ${({ isSelected }) => + isSelected + ? ` + background: ${theme.colors.gray5}; + color: ${theme.textColors.white}; + ` + : ''} `; - -const selectedCategoryMenuStyles: Record = { - food: ` - background: ${theme.colors.gray5}; - color: ${theme.textColors.white}; - `, - store: ` - background: ${theme.colors.primary}; - color: ${theme.textColors.default}; - `, -}; diff --git a/frontend/src/components/Common/CategoryList/CategoryList.stories.tsx b/frontend/src/components/Common/CategoryList/CategoryList.stories.tsx deleted file mode 100644 index ca76f7b46..000000000 --- a/frontend/src/components/Common/CategoryList/CategoryList.stories.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import CategoryList from './CategoryList'; - -const meta: Meta = { - title: 'common/CategoryList', - component: CategoryList, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/frontend/src/components/Common/CategoryList/CategoryList.tsx b/frontend/src/components/Common/CategoryList/CategoryList.tsx deleted file mode 100644 index 675ab02fe..000000000 --- a/frontend/src/components/Common/CategoryList/CategoryList.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Link } from '@fun-eat/design-system'; -import { Link as RouterLink } from 'react-router-dom'; -import styled from 'styled-components'; - -import CategoryItem from '../CategoryItem/CategoryItem'; - -import { CATEGORY_TYPE } from '@/constants'; -import { useCategoryQuery } from '@/hooks/queries/product'; - -interface CategoryListProps { - menuVariant: keyof typeof CATEGORY_TYPE; -} - -const CategoryList = ({ menuVariant }: CategoryListProps) => { - const { data: categories } = useCategoryQuery(CATEGORY_TYPE[menuVariant]); - - return ( - - - {categories.map((menu) => ( - - - - ))} - - - ); -}; - -export default CategoryList; - -const CategoryListContainer = styled.div` - overflow-x: auto; - overflow-y: hidden; - - @media screen and (min-width: 500px) { - display: flex; - flex-direction: column; - align-items: center; - } - - &::-webkit-scrollbar { - display: none; - } -`; - -const CategoryListWrapper = styled.div` - display: flex; - gap: 20px; -`; diff --git a/frontend/src/components/Common/CategoryStoreList/CategoryStoreList.stories.tsx b/frontend/src/components/Common/CategoryStoreList/CategoryStoreList.stories.tsx new file mode 100644 index 000000000..2592fe4c9 --- /dev/null +++ b/frontend/src/components/Common/CategoryStoreList/CategoryStoreList.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import CategoryStoreList from './CategoryStoreList'; + +const meta: Meta = { + title: 'common/CategoryStoreList', + component: CategoryStoreList, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/frontend/src/components/Common/CategoryStoreList/CategoryStoreList.tsx b/frontend/src/components/Common/CategoryStoreList/CategoryStoreList.tsx new file mode 100644 index 000000000..46f4958c1 --- /dev/null +++ b/frontend/src/components/Common/CategoryStoreList/CategoryStoreList.tsx @@ -0,0 +1,33 @@ +import { Link } from '@fun-eat/design-system'; +import { Link as RouterLink } from 'react-router-dom'; +import styled from 'styled-components'; + +import CategoryItem from '../CategoryItem/CategoryItem'; + +import { CATEGORY_TYPE } from '@/constants'; +import { useCategoryStoreQuery } from '@/hooks/queries/product'; + +const category = CATEGORY_TYPE.STORE; + +const CategoryStoreList = () => { + const { data: categories } = useCategoryStoreQuery(category); + + return ( +
+ + {categories.map((menu) => ( + + + + ))} + +
+ ); +}; + +export default CategoryStoreList; + +const CategoryStoreListWrapper = styled.div` + display: flex; + gap: 16px; +`; diff --git a/frontend/src/components/Common/CategoryStoreTab/CategoryStoreTab.stories.tsx b/frontend/src/components/Common/CategoryStoreTab/CategoryStoreTab.stories.tsx new file mode 100644 index 000000000..7fca5880b --- /dev/null +++ b/frontend/src/components/Common/CategoryStoreTab/CategoryStoreTab.stories.tsx @@ -0,0 +1,22 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import CategoryStoreTab from './CategoryStoreTab'; + +import CategoryProvider from '@/contexts/CategoryContext'; + +const meta: Meta = { + title: 'common/CategoryStoreTab', + component: CategoryStoreTab, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/frontend/src/components/Common/CategoryStoreTab/CategoryStoreTab.tsx b/frontend/src/components/Common/CategoryStoreTab/CategoryStoreTab.tsx new file mode 100644 index 000000000..b93d83ab0 --- /dev/null +++ b/frontend/src/components/Common/CategoryStoreTab/CategoryStoreTab.tsx @@ -0,0 +1,77 @@ +import { Button, theme } from '@fun-eat/design-system'; +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; +import styled from 'styled-components'; + +import { CATEGORY_TYPE } from '@/constants'; +import { useCategoryActionContext, useCategoryValueContext } from '@/hooks/context'; +import { useCategoryStoreQuery } from '@/hooks/queries/product/useCategoryQuery'; + +const category = CATEGORY_TYPE.STORE; + +const CategoryStoreTab = () => { + const { data: categories } = useCategoryStoreQuery(category); + + const { categoryIds } = useCategoryValueContext(); + const { selectCategory } = useCategoryActionContext(); + const currentCategoryId = categoryIds[category]; + + const location = useLocation(); + const queryParams = new URLSearchParams(location.search); + const categoryIdFromURL = queryParams.get('category'); + + useEffect(() => { + if (categoryIdFromURL) { + selectCategory(category, parseInt(categoryIdFromURL)); + } + }, [category]); + + return ( + + {categories.map((menu) => { + const isSelected = menu.id === currentCategoryId; + return ( +
  • + selectCategory(category, menu.id)} + aria-pressed={isSelected} + > + {menu.name} + +
  • + ); + })} +
    + ); +}; + +export default CategoryStoreTab; + +const CategoryMenuContainer = styled.ul` + display: flex; + gap: 8px; + white-space: nowrap; + overflow-x: auto; + + &::-webkit-scrollbar { + display: none; + } +`; + +const CategoryButton = styled(Button)<{ isSelected: boolean }>` + padding: 6px 12px; + ${({ isSelected }) => + isSelected + ? ` + background: ${theme.colors.primary}; + color: ${theme.textColors.default}; + ` + : ''} +`; diff --git a/frontend/src/components/Common/CategoryTab/CategoryTab.stories.tsx b/frontend/src/components/Common/CategoryTab/CategoryTab.stories.tsx deleted file mode 100644 index 45bfac20d..000000000 --- a/frontend/src/components/Common/CategoryTab/CategoryTab.stories.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import CategoryTab from './CategoryTab'; - -const meta: Meta = { - title: 'common/CategoryTab', - component: CategoryTab, -}; - -export default meta; -type Story = StoryObj; - -export const FoodCategory: Story = { - args: { - menuVariant: 'food', - }, -}; - -export const StoreCategory: Story = { - args: { - menuVariant: 'store', - }, -}; diff --git a/frontend/src/components/Common/Loading/Loading.tsx b/frontend/src/components/Common/Loading/Loading.tsx index 17ce56a87..4ef7d374e 100644 --- a/frontend/src/components/Common/Loading/Loading.tsx +++ b/frontend/src/components/Common/Loading/Loading.tsx @@ -1,5 +1,49 @@ -const Loading = () => { - return
    로딩중..
    ; +import { Text } from '@fun-eat/design-system'; +import styled, { keyframes } from 'styled-components'; + +import PlateImage from '@/assets/plate.svg'; + +const DEFAULT_DESCRIPTION = '잠시만 기다려주세요 🥄'; + +interface LoadingProps { + customHeight?: string; + description?: string; +} + +const Loading = ({ customHeight = '100%', description = DEFAULT_DESCRIPTION }: LoadingProps) => { + return ( + + + + + {description} + + ); }; export default Loading; + +type LoadingContainerStyleProps = Pick; + +const LoadingContainer = styled.div` + display: flex; + flex-direction: column; + row-gap: 36px; + justify-content: center; + align-items: center; + height: ${({ customHeight }) => customHeight}; +`; + +const rotate = keyframes` + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(-360deg); + } +`; + +const PlateImageWrapper = styled.div` + animation: ${rotate} 1.5s ease-in-out infinite; +`; diff --git a/frontend/src/components/Common/MoreButton/MoreButton.stories.tsx b/frontend/src/components/Common/MoreButton/MoreButton.stories.tsx deleted file mode 100644 index 292e12c06..000000000 --- a/frontend/src/components/Common/MoreButton/MoreButton.stories.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import MoreButton from './MoreButton'; - -const meta: Meta = { - title: 'common/MoreButton', - component: MoreButton, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/frontend/src/components/Common/MoreButton/MoreButton.tsx b/frontend/src/components/Common/MoreButton/MoreButton.tsx deleted file mode 100644 index 479aec96d..000000000 --- a/frontend/src/components/Common/MoreButton/MoreButton.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Link, Text } from '@fun-eat/design-system'; -import { Link as RouterLink } from 'react-router-dom'; -import styled from 'styled-components'; - -import SvgIcon from '../Svg/SvgIcon'; - -import { PATH } from '@/constants/path'; - -const MoreButton = () => { - return ( - - - - - - - 더보기 - - - - ); -}; - -export default MoreButton; - -const MoreButtonWrapper = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 110px; - height: 110px; - border-radius: 5px; - background: ${({ theme }) => theme.colors.gray1}; -`; - -const PlusIconWrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; - width: 40px; - height: 40px; - margin-bottom: 5px; - border-radius: 50%; - background: ${({ theme }) => theme.colors.white}; -`; diff --git a/frontend/src/components/Common/Skeleton/Skeleton.stories.tsx b/frontend/src/components/Common/Skeleton/Skeleton.stories.tsx new file mode 100644 index 000000000..e8952c316 --- /dev/null +++ b/frontend/src/components/Common/Skeleton/Skeleton.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Skeleton from './Skeleton'; + +const meta: Meta = { + title: 'common/Skeleton', + component: Skeleton, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + width: 100, + height: 100, + }, +}; diff --git a/frontend/src/components/Common/Skeleton/Skeleton.tsx b/frontend/src/components/Common/Skeleton/Skeleton.tsx new file mode 100644 index 000000000..857d03079 --- /dev/null +++ b/frontend/src/components/Common/Skeleton/Skeleton.tsx @@ -0,0 +1,36 @@ +import type { ComponentPropsWithoutRef } from 'react'; +import styled from 'styled-components'; + +interface SkeletonProps extends ComponentPropsWithoutRef<'div'> { + width?: string | number; + height?: string | number; +} + +const Skeleton = ({ width, height }: SkeletonProps) => { + return ; +}; + +export default Skeleton; + +export const SkeletonContainer = styled.div` + position: absolute; + width: ${({ width }) => (typeof width === 'number' ? width + 'px' : width)}; + height: ${({ height }) => (typeof height === 'number' ? height + 'px' : height)}; + border-radius: 8px; + background: linear-gradient(-90deg, #dddddd, #f7f7f7, #dddddd, #f7f7f7); + background-size: 400%; + overflow: hidden; + animation: skeleton-gradient 5s infinite ease-out; + + @keyframes skeleton-gradient { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } + } +`; diff --git a/frontend/src/components/Common/index.ts b/frontend/src/components/Common/index.ts index 493fe72bb..8668ceb21 100644 --- a/frontend/src/components/Common/index.ts +++ b/frontend/src/components/Common/index.ts @@ -1,4 +1,5 @@ -export { default as CategoryTab } from './CategoryTab/CategoryTab'; +export { default as CategoryFoodTab } from './CategoryFoodTab/CategoryFoodTab'; +export { default as CategoryStoreTab } from './CategoryStoreTab/CategoryStoreTab'; export { default as Header } from './Header/Header'; export { default as NavigationBar } from './NavigationBar/NavigationBar'; export { default as SortButton } from './SortButton/SortButton'; @@ -15,9 +16,10 @@ export { default as ErrorBoundary } from './ErrorBoundary/ErrorBoundary'; export { default as ErrorComponent } from './ErrorComponent/ErrorComponent'; export { default as Loading } from './Loading/Loading'; export { default as MarkedText } from './MarkedText/MarkedText'; -export { default as MoreButton } from './MoreButton/MoreButton'; export { default as NavigableSectionTitle } from './NavigableSectionTitle/NavigableSectionTitle'; export { default as Carousel } from './Carousel/Carousel'; export { default as RegisterButton } from './RegisterButton/RegisterButton'; export { default as CategoryItem } from './CategoryItem/CategoryItem'; -export { default as CategoryList } from './CategoryList/CategoryList'; +export { default as CategoryFoodList } from './CategoryFoodList/CategoryFoodList'; +export { default as CategoryStoreList } from './CategoryStoreList/CategoryStoreList'; +export { default as Skeleton } from './Skeleton/Skeleton'; diff --git a/frontend/src/components/Members/MembersInfo/MembersInfo.tsx b/frontend/src/components/Members/MembersInfo/MembersInfo.tsx index 5ed9e89e3..1b2e94839 100644 --- a/frontend/src/components/Members/MembersInfo/MembersInfo.tsx +++ b/frontend/src/components/Members/MembersInfo/MembersInfo.tsx @@ -60,4 +60,5 @@ const MembersImage = styled.img` margin-right: 16px; border: 2px solid ${({ theme }) => theme.colors.primary}; border-radius: 50%; + object-fit: cover; `; diff --git a/frontend/src/components/Product/PBProductItem/PBProductItem.stories.tsx b/frontend/src/components/Product/PBProductItem/PBProductItem.stories.tsx deleted file mode 100644 index 175918ec5..000000000 --- a/frontend/src/components/Product/PBProductItem/PBProductItem.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import PBProductItem from './PBProductItem'; - -import pbProducts from '@/mocks/data/pbProducts.json'; - -const meta: Meta = { - title: 'product/PBProductItem', - component: PBProductItem, - args: { - pbProduct: pbProducts.products[0], - }, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/frontend/src/components/Product/PBProductItem/PBProductItem.tsx b/frontend/src/components/Product/PBProductItem/PBProductItem.tsx deleted file mode 100644 index 63cf774ef..000000000 --- a/frontend/src/components/Product/PBProductItem/PBProductItem.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { Text, theme } from '@fun-eat/design-system'; -import styled from 'styled-components'; - -import PBPreviewImage from '@/assets/samgakgimbab.svg'; -import { SvgIcon } from '@/components/Common'; -import type { PBProduct } from '@/types/product'; - -interface PBProductItemProps { - pbProduct: PBProduct; -} - -const PBProductItem = ({ pbProduct }: PBProductItemProps) => { - const { name, price, image, averageRating } = pbProduct; - - return ( - - {image !== null ? ( - - ) : ( - - )} - - {name} - - - - - {averageRating} - - - - {price.toLocaleString('ko-KR')}원 - - - - - ); -}; - -export default PBProductItem; - -const PBProductItemContainer = styled.div` - width: 110px; -`; - -const PBProductImage = styled.img` - object-fit: cover; -`; - -const PBProductInfoWrapper = styled.div` - height: 50%; - margin-top: 10px; -`; - -const PBProductName = styled(Text)` - display: inline-block; - width: 100%; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; -`; - -const PBProductReviewWrapper = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - margin: 5px 0; -`; - -const RatingWrapper = styled.div` - display: flex; - align-items: center; - column-gap: 4px; - - & > svg { - padding-bottom: 2px; - } -`; diff --git a/frontend/src/components/Product/PBProductList/PBProductList.stories.tsx b/frontend/src/components/Product/PBProductList/PBProductList.stories.tsx deleted file mode 100644 index dc1804f08..000000000 --- a/frontend/src/components/Product/PBProductList/PBProductList.stories.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import PBProductList from './PBProductList'; - -const meta: Meta = { - title: 'product/PBProductList', - component: PBProductList, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/frontend/src/components/Product/PBProductList/PBProductList.tsx b/frontend/src/components/Product/PBProductList/PBProductList.tsx deleted file mode 100644 index 576ea05a4..000000000 --- a/frontend/src/components/Product/PBProductList/PBProductList.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Link } from '@fun-eat/design-system'; -import { Link as RouterLink } from 'react-router-dom'; -import styled from 'styled-components'; - -import PBProductItem from '../PBProductItem/PBProductItem'; - -import { MoreButton } from '@/components/Common'; -import { PATH } from '@/constants/path'; -import { useCategoryValueContext } from '@/hooks/context'; -import { useInfiniteProductsQuery } from '@/hooks/queries/product'; - -const PBProductList = () => { - const { categoryIds } = useCategoryValueContext(); - - const { data: pbProductListResponse } = useInfiniteProductsQuery(categoryIds.store); - const pbProducts = pbProductListResponse.pages.flatMap((page) => page.products); - - return ( - <> - - {pbProducts.map((pbProduct) => ( -
  • - - - -
  • - ))} -
  • - -
  • -
    - - ); -}; - -export default PBProductList; - -const PBProductListContainer = styled.ul` - display: flex; - gap: 40px; - overflow-x: auto; - overflow-y: hidden; - - &::-webkit-scrollbar { - display: none; - } -`; diff --git a/frontend/src/components/Product/ProductItem/ProductItem.tsx b/frontend/src/components/Product/ProductItem/ProductItem.tsx index 6fb9ce673..74fe7accb 100644 --- a/frontend/src/components/Product/ProductItem/ProductItem.tsx +++ b/frontend/src/components/Product/ProductItem/ProductItem.tsx @@ -1,8 +1,9 @@ import { Text, useTheme } from '@fun-eat/design-system'; +import { memo, useState } from 'react'; import styled from 'styled-components'; import PreviewImage from '@/assets/characters.svg'; -import { SvgIcon } from '@/components/Common'; +import { Skeleton, SvgIcon } from '@/components/Common'; import type { Product } from '@/types/product'; interface ProductItemProps { @@ -12,11 +13,22 @@ interface ProductItemProps { const ProductItem = ({ product }: ProductItemProps) => { const theme = useTheme(); const { name, price, image, averageRating, reviewCount } = product; + const [isImageLoading, setIsImageLoading] = useState(true); return ( {image !== null ? ( - + <> + setIsImageLoading(false)} + /> + {isImageLoading && } + ) : ( )} @@ -46,9 +58,10 @@ const ProductItem = ({ product }: ProductItemProps) => { ); }; -export default ProductItem; +export default memo(ProductItem); const ProductItemContainer = styled.div` + position: relative; display: flex; align-items: center; padding: 12px 0; diff --git a/frontend/src/components/Product/ProductOverviewItem/ProductOverviewItem.tsx b/frontend/src/components/Product/ProductOverviewItem/ProductOverviewItem.tsx index bcc593be0..ef2811072 100644 --- a/frontend/src/components/Product/ProductOverviewItem/ProductOverviewItem.tsx +++ b/frontend/src/components/Product/ProductOverviewItem/ProductOverviewItem.tsx @@ -20,9 +20,9 @@ const ProductOverviewItem = ({ name, image, rank }: ProductOverviewItemProps) => ) : ( )} - + {name} - + ); }; @@ -34,7 +34,7 @@ const ProductOverviewContainer = styled.div theme.borderRadius.xs}; background: ${({ theme, rank }) => (rank ? theme.colors.gray1 : theme.colors.white)}; `; @@ -51,3 +51,10 @@ const ProductPreviewImage = styled(PreviewImage)` border-radius: 50%; background-color: ${({ theme }) => theme.colors.white}; `; + +const ProductOverviewText = styled(Text)` + white-space: nowrap; + text-overflow: ellipsis; + word-break: break-all; + overflow: hidden; +`; diff --git a/frontend/src/components/Product/index.ts b/frontend/src/components/Product/index.ts index 47ce2703d..25a06f28f 100644 --- a/frontend/src/components/Product/index.ts +++ b/frontend/src/components/Product/index.ts @@ -2,6 +2,5 @@ export { default as ProductDetailItem } from './ProductDetailItem/ProductDetailI export { default as ProductItem } from './ProductItem/ProductItem'; export { default as ProductList } from './ProductList/ProductList'; export { default as ProductOverviewItem } from './ProductOverviewItem/ProductOverviewItem'; -export { default as PBProductList } from './PBProductList/PBProductList'; export { default as ProductRecipeList } from './ProductRecipeList/ProductRecipeList'; export { default as ProductTitle } from './ProductTitle/ProductTitle'; diff --git a/frontend/src/components/Rank/RecipeRankingItem/RecipeRankingItem.tsx b/frontend/src/components/Rank/RecipeRankingItem/RecipeRankingItem.tsx index b9ac0cab6..81086851d 100644 --- a/frontend/src/components/Rank/RecipeRankingItem/RecipeRankingItem.tsx +++ b/frontend/src/components/Rank/RecipeRankingItem/RecipeRankingItem.tsx @@ -1,8 +1,9 @@ import { Spacing, Text, useTheme } from '@fun-eat/design-system'; +import { useState } from 'react'; import styled from 'styled-components'; import RecipePreviewImage from '@/assets/plate.svg'; -import { SvgIcon } from '@/components/Common'; +import { Skeleton, SvgIcon } from '@/components/Common'; import type { RecipeRanking } from '@/types/ranking'; interface RecipeRankingItemProps { @@ -18,6 +19,7 @@ const RecipeRankingItem = ({ rank, recipe }: RecipeRankingItemProps) => { author: { nickname, profileImage }, favoriteCount, } = recipe; + const [isImageLoading, setIsImageLoading] = useState(true); return ( @@ -26,7 +28,16 @@ const RecipeRankingItem = ({ rank, recipe }: RecipeRankingItemProps) => { {image !== null ? ( - + <> + setIsImageLoading(false)} + /> + {isImageLoading && } + ) : ( )} @@ -74,6 +85,7 @@ const RankingRecipeWrapper = styled.div` const RecipeImage = styled.img` border-radius: 5px; + object-fit: cover; `; const TitleFavoriteWrapper = styled.div` @@ -100,4 +112,5 @@ const AuthorWrapper = styled.div` const AuthorImage = styled.img` border: 2px solid ${({ theme }) => theme.colors.primary}; border-radius: 50%; + object-fit: cover; `; diff --git a/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.tsx b/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.tsx index d519c931a..70fd89c32 100644 --- a/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.tsx +++ b/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.tsx @@ -1,4 +1,5 @@ import { Spacing, Text, theme } from '@fun-eat/design-system'; +import { memo } from 'react'; import styled from 'styled-components'; import { SvgIcon } from '@/components/Common'; @@ -38,7 +39,7 @@ const ReviewRankingItem = ({ reviewRanking }: ReviewRankingItemProps) => { ); }; -export default ReviewRankingItem; +export default memo(ReviewRankingItem); const ReviewRankingItemContainer = styled.div` display: flex; diff --git a/frontend/src/components/Recipe/RecipeItem/RecipeItem.tsx b/frontend/src/components/Recipe/RecipeItem/RecipeItem.tsx index 2ad70c90a..5846bd872 100644 --- a/frontend/src/components/Recipe/RecipeItem/RecipeItem.tsx +++ b/frontend/src/components/Recipe/RecipeItem/RecipeItem.tsx @@ -1,9 +1,9 @@ import { Heading, Text, useTheme } from '@fun-eat/design-system'; -import { Fragment } from 'react'; +import { Fragment, memo, useState } from 'react'; import styled from 'styled-components'; import PreviewImage from '@/assets/plate.svg'; -import { SvgIcon } from '@/components/Common'; +import { Skeleton, SvgIcon } from '@/components/Common'; import type { MemberRecipe, Recipe } from '@/types/recipe'; import { getFormattedDate } from '@/utils/date'; @@ -15,6 +15,7 @@ interface RecipeItemProps { const RecipeItem = ({ recipe, isMemberPage = false }: RecipeItemProps) => { const { image, title, createdAt, favoriteCount, products } = recipe; const author = 'author' in recipe ? recipe.author : null; + const [isImageLoading, setIsImageLoading] = useState(true); const theme = useTheme(); return ( @@ -22,7 +23,10 @@ const RecipeItem = ({ recipe, isMemberPage = false }: RecipeItemProps) => { {!isMemberPage && ( {image !== null ? ( - + <> + setIsImageLoading(false)} /> + {isImageLoading && } + ) : ( )} @@ -55,7 +59,7 @@ const RecipeItem = ({ recipe, isMemberPage = false }: RecipeItemProps) => { ); }; -export default RecipeItem; +export default memo(RecipeItem); const ImageWrapper = styled.div` position: relative; diff --git a/frontend/src/components/Review/ReviewItem/ReviewItem.tsx b/frontend/src/components/Review/ReviewItem/ReviewItem.tsx index 782bdd928..112824ada 100644 --- a/frontend/src/components/Review/ReviewItem/ReviewItem.tsx +++ b/frontend/src/components/Review/ReviewItem/ReviewItem.tsx @@ -1,5 +1,5 @@ import { Badge, Button, Text, useTheme } from '@fun-eat/design-system'; -import { useState } from 'react'; +import { memo, useState } from 'react'; import styled from 'styled-components'; import { SvgIcon, TagList } from '@/components/Common'; @@ -91,7 +91,7 @@ const ReviewItem = ({ productId, review }: ReviewItemProps) => { ); }; -export default ReviewItem; +export default memo(ReviewItem); const ReviewItemContainer = styled.div` display: flex; @@ -118,6 +118,7 @@ const RebuyBadge = styled(Badge)` const ReviewerImage = styled.img` border: 2px solid ${({ theme }) => theme.colors.primary}; border-radius: 50%; + object-fit: cover; `; const RatingIconWrapper = styled.div` diff --git a/frontend/src/hooks/queries/product/index.ts b/frontend/src/hooks/queries/product/index.ts index b49fd5aca..9da39eff9 100644 --- a/frontend/src/hooks/queries/product/index.ts +++ b/frontend/src/hooks/queries/product/index.ts @@ -1,4 +1,5 @@ -export { default as useCategoryQuery } from './useCategoryQuery'; +export { useCategoryFoodQuery } from './useCategoryQuery'; +export { useCategoryStoreQuery } from './useCategoryQuery'; export { default as useInfiniteProductsQuery } from './useInfiniteProductsQuery'; export { default as useInfiniteProductReviewsQuery } from './useInfiniteProductReviewsQuery'; export { default as useProductDetailQuery } from './useProductDetailQuery'; diff --git a/frontend/src/hooks/queries/product/useCategoryQuery.ts b/frontend/src/hooks/queries/product/useCategoryQuery.ts index 4b4c08c46..e79987d93 100644 --- a/frontend/src/hooks/queries/product/useCategoryQuery.ts +++ b/frontend/src/hooks/queries/product/useCategoryQuery.ts @@ -1,16 +1,22 @@ import { useSuspendedQuery } from '..'; import { categoryApi } from '@/apis'; -import type { Category } from '@/types/common'; +import type { Category, CategoryVariant, Food, Store } from '@/types/common'; -const fetchCategories = async (type: string) => { +const fetchCategories = async (type: CategoryVariant) => { const response = await categoryApi.get({ queries: `?type=${type}` }); const data: Category[] = await response.json(); return data; }; -const useCategoryQuery = (type: string) => { - return useSuspendedQuery(['categories', type], () => fetchCategories(type)); +export const useCategoryFoodQuery = (type: Food) => { + return useSuspendedQuery(['categories', type], () => fetchCategories(type), { + staleTime: Infinity, + }); }; -export default useCategoryQuery; +export const useCategoryStoreQuery = (type: Store) => { + return useSuspendedQuery(['categories', type], () => fetchCategories(type), { + staleTime: Infinity, + }); +}; diff --git a/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts b/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts index 7d5e8ad7f..c97b392af 100644 --- a/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts +++ b/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { recipeApi } from '@/apis'; import type { RecipeFavoriteRequestBody } from '@/types/recipe'; @@ -10,8 +10,11 @@ const patchRecipeFavorite = (recipeId: number, body: RecipeFavoriteRequestBody) }; const useRecipeFavoriteMutation = (recipeId: number) => { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: (body: RecipeFavoriteRequestBody) => patchRecipeFavorite(recipeId, body), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['recipeDetail', recipeId] }), }); }; diff --git a/frontend/src/hooks/queries/review/useReviewFavoriteMutation.ts b/frontend/src/hooks/queries/review/useReviewFavoriteMutation.ts index 1c72b593b..0d35ab610 100644 --- a/frontend/src/hooks/queries/review/useReviewFavoriteMutation.ts +++ b/frontend/src/hooks/queries/review/useReviewFavoriteMutation.ts @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { productApi } from '@/apis'; import type { ReviewFavoriteRequestBody } from '@/types/review'; @@ -10,8 +10,11 @@ const patchReviewFavorite = (productId: number, reviewId: number, body: ReviewFa }; const useReviewFavoriteMutation = (productId: number, reviewId: number) => { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: (body: ReviewFavoriteRequestBody) => patchReviewFavorite(productId, reviewId, body), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['product', productId, 'review'] }), }); }; diff --git a/frontend/src/hooks/search/useSearch.ts b/frontend/src/hooks/search/useSearch.ts index 67dc6a281..5405ecfc3 100644 --- a/frontend/src/hooks/search/useSearch.ts +++ b/frontend/src/hooks/search/useSearch.ts @@ -1,5 +1,5 @@ -import type { ChangeEventHandler, FormEventHandler, MouseEventHandler, RefObject } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import type { ChangeEventHandler, FormEventHandler, MouseEventHandler } from 'react'; +import { useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; const useSearch = () => { @@ -12,12 +12,8 @@ const useSearch = () => { const [isSubmitted, setIsSubmitted] = useState(!!currentSearchQuery); const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(searchQuery.length > 0); - useEffect(() => { - setIsAutocompleteOpen(searchQuery.length > 0); - }, [searchQuery]); - const focusInput = () => { - if (inputRef?.current) { + if (inputRef.current) { inputRef.current.focus(); } }; @@ -25,6 +21,7 @@ const useSearch = () => { const handleSearchQuery: ChangeEventHandler = (event) => { setIsSubmitted(false); setSearchQuery(event.currentTarget.value); + setIsAutocompleteOpen(event.currentTarget.value.length > 0); }; const handleSearch: FormEventHandler = (event) => { @@ -35,7 +32,7 @@ const useSearch = () => { if (!trimmedSearchQuery) { alert('검색어를 입력해주세요'); focusInput(); - setSearchQuery(''); + resetSearchQuery(); return; } diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 9be413e9f..113f22f69 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -42,7 +42,7 @@ root.render( - + ...loading

    } />
    diff --git a/frontend/src/mocks/data/productRankingList.json b/frontend/src/mocks/data/productRankingList.json index 217613d45..344b3a3d4 100644 --- a/frontend/src/mocks/data/productRankingList.json +++ b/frontend/src/mocks/data/productRankingList.json @@ -2,7 +2,7 @@ "products": [ { "id": 3, - "name": "구운감자슬림명란마요", + "name": "구운감자슬림명란마요요요요요요요요요요요요용요요용요요ㅛ", "image": "https://t3.ftcdn.net/jpg/06/06/91/70/240_F_606917032_4ujrrMV8nspZDX8nTgGrTpJ69N9JNxOL.jpg", "categoryType": "food" }, diff --git a/frontend/src/pages/AuthPage.tsx b/frontend/src/pages/AuthPage.tsx index d5fd87b8e..a840cb978 100644 --- a/frontend/src/pages/AuthPage.tsx +++ b/frontend/src/pages/AuthPage.tsx @@ -5,7 +5,7 @@ import { loginApi } from '@/apis'; import { PATH } from '@/constants/path'; import { useMemberQuery } from '@/hooks/queries/members'; -const AuthPage = () => { +export const AuthPage = () => { const { authProvider } = useParams(); const [searchParams] = useSearchParams(); const code = searchParams.get('code'); @@ -57,5 +57,3 @@ const AuthPage = () => { return <>; }; - -export default AuthPage; diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index e6e701b07..061b66f32 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -3,12 +3,12 @@ import { useQueryErrorResetBoundary } from '@tanstack/react-query'; import { Suspense } from 'react'; import styled from 'styled-components'; -import { Loading, ErrorBoundary, ErrorComponent, CategoryList } from '@/components/Common'; +import { Loading, ErrorBoundary, ErrorComponent, CategoryFoodList, CategoryStoreList } from '@/components/Common'; import { ProductRankingList, ReviewRankingList, RecipeRankingList } from '@/components/Rank'; import { IMAGE_URL } from '@/constants'; import channelTalk from '@/service/channelTalk'; -const HomePage = () => { +export const HomePage = () => { const { reset } = useQueryErrorResetBoundary(); channelTalk.loadScript(); @@ -21,7 +21,7 @@ const HomePage = () => { <>
    - +
    @@ -31,8 +31,10 @@ const HomePage = () => { - - + + + + @@ -77,12 +79,26 @@ const HomePage = () => { ); }; -export default HomePage; - const Banner = styled.img` width: 100%; + height: auto; `; const SectionWrapper = styled.section` padding: 0 20px; `; + +const CategoryListWrapper = styled.div` + overflow-x: auto; + overflow-y: hidden; + + @media screen and (min-width: 500px) { + display: flex; + flex-direction: column; + align-items: center; + } + + &::-webkit-scrollbar { + display: none; + } +`; diff --git a/frontend/src/pages/IntegratedSearchPage.tsx b/frontend/src/pages/IntegratedSearchPage.tsx index 285fc5c2e..77f781f32 100644 --- a/frontend/src/pages/IntegratedSearchPage.tsx +++ b/frontend/src/pages/IntegratedSearchPage.tsx @@ -12,7 +12,7 @@ import { useSearch } from '@/hooks/search'; const PRODUCT_PLACEHOLDER = '상품 이름을 검색해보세요.'; const RECIPE_PLACEHOLDER = '꿀조합에 포함된 상품을 입력해보세요.'; -const IntegratedSearchPage = () => { +export const IntegratedSearchPage = () => { const { inputRef, searchQuery, @@ -79,7 +79,7 @@ const IntegratedSearchPage = () => { handleTabMenuSelect={handleTabMenuClick} /> - {isSubmitted && debouncedSearchQuery ? ( + {isSubmitted && searchQuery ? ( <> '{searchQuery}'에 대한 검색결과입니다. @@ -88,9 +88,9 @@ const IntegratedSearchPage = () => { }> {isProductSearchTab ? ( - + ) : ( - + )} @@ -103,8 +103,6 @@ const IntegratedSearchPage = () => { ); }; -export default IntegratedSearchPage; - const SearchSection = styled.section` position: relative; `; diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 6cefd12d4..a64bcbb24 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -13,7 +13,7 @@ const KAKAO_LOGIN = '카카오 로그인'; const loginLink = process.env.NODE_ENV === 'development' ? '/login/kakao?code=qwe' : '/api/auth/kakao'; -const LoginPage = () => { +export const LoginPage = () => { const { routeBack } = useRoutePage(); const { data: member } = useMemberQuery(); @@ -47,8 +47,6 @@ const LoginPage = () => { ); }; -export default LoginPage; - const LoginPageContainer = styled.div` display: flex; flex-direction: column; diff --git a/frontend/src/pages/MemberModifyPage.tsx b/frontend/src/pages/MemberModifyPage.tsx index 50e98f690..c6c43a8c3 100644 --- a/frontend/src/pages/MemberModifyPage.tsx +++ b/frontend/src/pages/MemberModifyPage.tsx @@ -11,7 +11,7 @@ import { useFormData, useImageUploader } from '@/hooks/common'; import { useMemberModifyMutation, useMemberQuery } from '@/hooks/queries/members'; import type { MemberRequest } from '@/types/member'; -const MemberModifyPage = () => { +export const MemberModifyPage = () => { const { data: member } = useMemberQuery(); const { mutate } = useMemberModifyMutation(); @@ -96,8 +96,6 @@ const MemberModifyPage = () => { ); }; -export default MemberModifyPage; - const MemberImageUploaderContainer = styled.div` display: flex; justify-content: center; diff --git a/frontend/src/pages/MemberPage.tsx b/frontend/src/pages/MemberPage.tsx index bb5fca6bb..7026e4768 100644 --- a/frontend/src/pages/MemberPage.tsx +++ b/frontend/src/pages/MemberPage.tsx @@ -7,7 +7,7 @@ import { ErrorBoundary, ErrorComponent, Loading, NavigableSectionTitle } from '@ import { MembersInfo, MemberReviewList, MemberRecipeList } from '@/components/Members'; import { PATH } from '@/constants/path'; -const MemberPage = () => { +export const MemberPage = () => { const { reset } = useQueryErrorResetBoundary(); return ( @@ -35,8 +35,6 @@ const MemberPage = () => { ); }; -export default MemberPage; - const MemberPageContainer = styled.div` padding: 20px 20px 0; `; diff --git a/frontend/src/pages/MemberRecipePage.tsx b/frontend/src/pages/MemberRecipePage.tsx index 90bddc44b..5bbedf533 100644 --- a/frontend/src/pages/MemberRecipePage.tsx +++ b/frontend/src/pages/MemberRecipePage.tsx @@ -6,7 +6,7 @@ import styled from 'styled-components'; import { ErrorBoundary, ErrorComponent, Loading, ScrollButton, SectionTitle } from '@/components/Common'; import { MemberRecipeList } from '@/components/Members'; -const MemberRecipePage = () => { +export const MemberRecipePage = () => { const { reset } = useQueryErrorResetBoundary(); const memberRecipeRef = useRef(null); @@ -25,8 +25,6 @@ const MemberRecipePage = () => { ); }; -export default MemberRecipePage; - const MemberRecipePageContainer = styled.div` height: 100%; padding: 20px 20px 0; diff --git a/frontend/src/pages/MemberReviewPage.tsx b/frontend/src/pages/MemberReviewPage.tsx index 9f15905fd..08dd6390d 100644 --- a/frontend/src/pages/MemberReviewPage.tsx +++ b/frontend/src/pages/MemberReviewPage.tsx @@ -6,7 +6,7 @@ import styled from 'styled-components'; import { ErrorBoundary, ErrorComponent, Loading, ScrollButton, SectionTitle } from '@/components/Common'; import { MemberReviewList } from '@/components/Members'; -const MemberReviewPage = () => { +export const MemberReviewPage = () => { const { reset } = useQueryErrorResetBoundary(); const memberReviewRef = useRef(null); @@ -25,8 +25,6 @@ const MemberReviewPage = () => { ); }; -export default MemberReviewPage; - const MemberReviewPageContainer = styled.div` height: 100%; padding: 20px 20px 0; diff --git a/frontend/src/pages/ProductDetailPage.tsx b/frontend/src/pages/ProductDetailPage.tsx index 11f668dd2..e216ea9a3 100644 --- a/frontend/src/pages/ProductDetailPage.tsx +++ b/frontend/src/pages/ProductDetailPage.tsx @@ -30,7 +30,7 @@ const LOGIN_ERROR_MESSAGE_REVIEW = const LOGIN_ERROR_MESSAGE_RECIPE = '로그인 후 상품 꿀조합을 볼 수 있어요.\n펀잇에 가입하고 편의점 상품 꿀조합을 확인해보세요 😊'; -const ProductDetailPage = () => { +export const ProductDetailPage = () => { const { category, productId } = useParams(); const { data: member } = useMemberQuery(); const { data: productDetail } = useProductDetailQuery(Number(productId)); @@ -147,8 +147,6 @@ const ProductDetailPage = () => { ); }; -export default ProductDetailPage; - const ProductDetailPageContainer = styled.div` height: 100%; overflow-y: auto; diff --git a/frontend/src/pages/ProductListPage.tsx b/frontend/src/pages/ProductListPage.tsx index b4961e746..d98c79ddc 100644 --- a/frontend/src/pages/ProductListPage.tsx +++ b/frontend/src/pages/ProductListPage.tsx @@ -5,7 +5,8 @@ import { useParams } from 'react-router-dom'; import styled from 'styled-components'; import { - CategoryTab, + CategoryFoodTab, + CategoryStoreTab, SortButton, SortOptionList, ScrollButton, @@ -23,7 +24,7 @@ import { isCategoryVariant } from '@/types/common'; const PAGE_TITLE = { food: '공통 상품', store: 'PB 상품' }; -const ProductListPage = () => { +export const ProductListPage = () => { const { category } = useParams(); const productListRef = useRef(null); @@ -47,9 +48,7 @@ const ProductListPage = () => { routeDestination={PATH.PRODUCT_LIST + '/' + (category === 'store' ? 'food' : 'store')} /> - - - + {category === 'food' ? : } @@ -75,8 +74,6 @@ const ProductListPage = () => { ); }; -export default ProductListPage; - const ProductListSection = styled.section` height: 100%; `; diff --git a/frontend/src/pages/RecipeDetailPage.tsx b/frontend/src/pages/RecipeDetailPage.tsx index cc77efb77..61b1ab585 100644 --- a/frontend/src/pages/RecipeDetailPage.tsx +++ b/frontend/src/pages/RecipeDetailPage.tsx @@ -8,7 +8,7 @@ import { RecipeFavorite } from '@/components/Recipe'; import { useRecipeDetailQuery } from '@/hooks/queries/recipe'; import { getFormattedDate } from '@/utils/date'; -const RecipeDetailPage = () => { +export const RecipeDetailPage = () => { const { recipeId } = useParams(); const { data: recipeDetail } = useRecipeDetailQuery(Number(recipeId)); @@ -70,8 +70,6 @@ const RecipeDetailPage = () => { ); }; -export default RecipeDetailPage; - const RecipeDetailPageContainer = styled.div` padding: 20px 20px 0; `; diff --git a/frontend/src/pages/RecipePage.tsx b/frontend/src/pages/RecipePage.tsx index 579639542..3d72fdf78 100644 --- a/frontend/src/pages/RecipePage.tsx +++ b/frontend/src/pages/RecipePage.tsx @@ -24,7 +24,7 @@ const RECIPE_PAGE_TITLE = '🍯 꿀조합'; const REGISTER_RECIPE = '꿀조합 작성하기'; const REGISTER_RECIPE_AFTER_LOGIN = '로그인 후 꿀조합을 작성할 수 있어요'; -const RecipePage = () => { +export const RecipePage = () => { const [activeSheet, setActiveSheet] = useState<'registerRecipe' | 'sortOption'>('sortOption'); const { selectedOption, selectSortOption } = useSortOption(RECIPE_SORT_OPTIONS[0]); const { ref, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); @@ -50,17 +50,17 @@ const RecipePage = () => { + }> - - - + + + - { ); }; -export default RecipePage; - const TitleWrapper = styled.div` display: flex; justify-content: space-between; @@ -103,11 +101,10 @@ const Title = styled(Heading)` const SortButtonWrapper = styled.div` display: flex; justify-content: flex-end; - margin: 20px 0; `; const RecipeListWrapper = styled.div` - height: calc(100% - 190px); + height: calc(100% - 130px); overflow-y: auto; `; diff --git a/frontend/src/pages/SearchPage.tsx b/frontend/src/pages/SearchPage.tsx index f63232dba..b14c4d6a4 100644 --- a/frontend/src/pages/SearchPage.tsx +++ b/frontend/src/pages/SearchPage.tsx @@ -15,7 +15,7 @@ const RECIPE_PLACEHOLDER = '꿀조합에 포함된 상품을 입력해보세요. type SearchPageType = keyof typeof SEARCH_PAGE_VARIANTS; -const SearchPage = () => { +export const SearchPage = () => { const { inputRef, searchQuery, @@ -97,7 +97,7 @@ const SearchPage = () => { )} - {isSubmitted && debouncedSearchQuery ? ( + {isSubmitted && searchQuery ? ( <> '{searchQuery}'에 대한 검색결과입니다. @@ -105,8 +105,8 @@ const SearchPage = () => { }> - {isProductSearchPage && } - {isRecipeSearchPage && } + {isProductSearchPage && } + {isRecipeSearchPage && } @@ -118,8 +118,6 @@ const SearchPage = () => { ); }; -export default SearchPage; - const SearchSection = styled.section` position: relative; `; diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index 7bf9565ea..0103d0fc7 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -5,74 +5,78 @@ import App from './App'; import { AuthLayout } from '@/components/Layout'; import { PATH } from '@/constants/path'; import CategoryProvider from '@/contexts/CategoryContext'; -import AuthPage from '@/pages/AuthPage'; -import HomePage from '@/pages/HomePage'; -import IntegratedSearchPage from '@/pages/IntegratedSearchPage'; -import LoginPage from '@/pages/LoginPage'; -import MemberModifyPage from '@/pages/MemberModifyPage'; -import MemberPage from '@/pages/MemberPage'; -import MemberRecipePage from '@/pages/MemberRecipePage'; -import MemberReviewPage from '@/pages/MemberReviewPage'; import NotFoundPage from '@/pages/NotFoundPage'; -import ProductDetailPage from '@/pages/ProductDetailPage'; -import ProductListPage from '@/pages/ProductListPage'; -import RecipeDetailPage from '@/pages/RecipeDetailPage'; -import RecipePage from '@/pages/RecipePage'; -import SearchPage from '@/pages/SearchPage'; const router = createBrowserRouter([ { path: '/', element: ( - + - + ), errorElement: , children: [ - { - index: true, - element: , - }, { path: `${PATH.RECIPE}/:recipeId`, - element: ( - - - - ), + async lazy() { + const { RecipeDetailPage } = await import( + /* webpackChunkName: "RecipeDetailPage" */ '@/pages/RecipeDetailPage' + ); + return { Component: RecipeDetailPage }; + }, }, { path: PATH.MEMBER, - element: ( - - - - ), + async lazy() { + const { MemberPage } = await import(/* webpackChunkName: "MemberPage" */ '@/pages/MemberPage'); + return { Component: MemberPage }; + }, }, { path: `${PATH.MEMBER}/modify`, - element: ( - - - - ), + async lazy() { + const { MemberModifyPage } = await import( + /* webpackChunkName: "MemberModifyPage" */ '@/pages/MemberModifyPage' + ); + return { Component: MemberModifyPage }; + }, }, { path: `${PATH.MEMBER}/review`, - element: ( - - - - ), + async lazy() { + const { MemberReviewPage } = await import( + /* webpackChunkName: "MemberReviewPage" */ '@/pages/MemberReviewPage' + ); + return { Component: MemberReviewPage }; + }, }, { path: `${PATH.MEMBER}/recipe`, - element: ( - - - - ), + async lazy() { + const { MemberRecipePage } = await import( + /* webpackChunkName: "MemberRecipePage" */ '@/pages/MemberRecipePage' + ); + return { Component: MemberRecipePage }; + }, + }, + ], + }, + { + path: '/', + element: ( + + + + ), + errorElement: , + children: [ + { + index: true, + async lazy() { + const { HomePage } = await import(/* webpackChunkName: "HomePage" */ '@/pages/HomePage'); + return { Component: HomePage }; + }, }, ], }, @@ -83,11 +87,17 @@ const router = createBrowserRouter([ children: [ { path: PATH.LOGIN, - element: , + async lazy() { + const { LoginPage } = await import(/* webpackChunkName: "LoginPage" */ '@/pages/LoginPage'); + return { Component: LoginPage }; + }, }, { path: `${PATH.LOGIN}/:authProvider`, - element: , + async lazy() { + const { AuthPage } = await import(/* webpackChunkName: "AuthPage" */ '@/pages/AuthPage'); + return { Component: AuthPage }; + }, }, ], }, @@ -102,7 +112,12 @@ const router = createBrowserRouter([ children: [ { path: `${PATH.PRODUCT_LIST}/:category/:productId`, - element: , + async lazy() { + const { ProductDetailPage } = await import( + /* webpackChunkName: "ProductDetailPage" */ '@/pages/ProductDetailPage' + ); + return { Component: ProductDetailPage }; + }, }, ], }, @@ -117,19 +132,33 @@ const router = createBrowserRouter([ children: [ { path: `${PATH.PRODUCT_LIST}/:category`, - element: , + async lazy() { + const { ProductListPage } = await import(/* webpackChunkName: "ProductListPage" */ '@/pages/ProductListPage'); + return { Component: ProductListPage }; + }, }, { path: PATH.RECIPE, - element: , + async lazy() { + const { RecipePage } = await import(/* webpackChunkName: "RecipePage" */ '@/pages/RecipePage'); + return { Component: RecipePage }; + }, }, { path: `${PATH.SEARCH}/integrated`, - element: , + async lazy() { + const { IntegratedSearchPage } = await import( + /* webpackChunkName: "IntegratedSearchPage" */ '@/pages/IntegratedSearchPage' + ); + return { Component: IntegratedSearchPage }; + }, }, { path: `${PATH.SEARCH}/:searchVariant`, - element: , + async lazy() { + const { SearchPage } = await import(/* webpackChunkName: "SearchPage" */ '@/pages/SearchPage'); + return { Component: SearchPage }; + }, }, ], }, diff --git a/frontend/src/styles/font.ts b/frontend/src/styles/font.ts new file mode 100644 index 000000000..52122fdf7 --- /dev/null +++ b/frontend/src/styles/font.ts @@ -0,0 +1,14 @@ +import { css } from 'styled-components'; + +const fonts = css` + body, + button, + input, + textarea { + font-family: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, + 'Helvetica Neue', 'Segoe UI', 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', sans-serif; + } +`; + +export default fonts; diff --git a/frontend/src/styles/index.ts b/frontend/src/styles/index.ts index 56318656e..e90d4bd13 100644 --- a/frontend/src/styles/index.ts +++ b/frontend/src/styles/index.ts @@ -1,6 +1,10 @@ import { createGlobalStyle } from 'styled-components'; +import fonts from './font'; + const GlobalStyle = createGlobalStyle` +${fonts} + #root { position: absolute; top: 0; diff --git a/frontend/src/types/common.ts b/frontend/src/types/common.ts index 3bfcc8576..d7cf4d8bd 100644 --- a/frontend/src/types/common.ts +++ b/frontend/src/types/common.ts @@ -1,11 +1,20 @@ import type { ReactNode } from 'react'; import type { SvgIconVariant } from '@/components/Common/Svg/SvgIcon'; -import type { TAG_TITLE, PRODUCT_SORT_OPTIONS, REVIEW_SORT_OPTIONS, RECIPE_SORT_OPTIONS } from '@/constants'; +import type { + TAG_TITLE, + PRODUCT_SORT_OPTIONS, + REVIEW_SORT_OPTIONS, + RECIPE_SORT_OPTIONS, + CATEGORY_TYPE, +} from '@/constants'; import type { PATH } from '@/constants/path'; export type CategoryVariant = 'food' | 'store'; +export type Food = (typeof CATEGORY_TYPE)['FOOD']; +export type Store = (typeof CATEGORY_TYPE)['STORE']; + export const isCategoryVariant = (value: string): value is CategoryVariant => { return value === 'store' || value === 'food'; }; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index e3dff3aa1..321f1c029 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -15,7 +15,8 @@ "baseUrl": ".", "paths": { "@/*": ["src/*"] - } + }, + "removeComments": false }, "include": ["src", "__tests__"], "exclude": ["node_modules", "dist"] diff --git a/frontend/webpack.common.js b/frontend/webpack.common.js index 9c705c237..036200393 100644 --- a/frontend/webpack.common.js +++ b/frontend/webpack.common.js @@ -5,7 +5,7 @@ module.exports = { entry: './src/index.tsx', output: { path: path.join(__dirname, 'dist'), - filename: 'bundle.js', + filename: '[name].[chunkhash].js', clean: true, publicPath: '/', }, diff --git a/frontend/webpack.prod.js b/frontend/webpack.prod.js index 63d76544c..3d7d802ab 100644 --- a/frontend/webpack.prod.js +++ b/frontend/webpack.prod.js @@ -22,4 +22,9 @@ module.exports = merge(common, { ], }), ], + optimization: { + splitChunks: { + chunks: 'all', + }, + }, }); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index d83613fbd..c78fae96a 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1375,10 +1375,10 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.1.tgz#1a5b1959a528e374e8037c4396c3e825d6cf4a83" integrity sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw== -"@fun-eat/design-system@^0.3.11": - version "0.3.11" - resolved "https://registry.yarnpkg.com/@fun-eat/design-system/-/design-system-0.3.11.tgz#899f7994c99ad46a8af89ca44df5fbf3453d9fa3" - integrity sha512-ECuTyNVnDoNzC2mgstkjceySPaXhoyitlLU4wxW5q0eSVx3T6Cl8knhXtUqM5hL+euvJD8KKKYvin138qcvSfw== +"@fun-eat/design-system@^0.3.12": + version "0.3.12" + resolved "https://registry.yarnpkg.com/@fun-eat/design-system/-/design-system-0.3.12.tgz#a5e431287b1c4e4685c748ade392aa2602076d27" + integrity sha512-rHnhO6EVQ0FY9ahaFJLCuEUKwm2vwSJ42zY565Ph0/i/dMnQscLGalL4YHUF+O3hzKslqj6Aw/SOwivQDY2wWQ== "@humanwhocodes/config-array@^0.11.11": version "0.11.11"