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"