diff --git a/frontend/src/components/Common/CategoryMenu/CategoryMenu.tsx b/frontend/src/components/Common/CategoryMenu/CategoryMenu.tsx index b262b8acd..2cb59148f 100644 --- a/frontend/src/components/Common/CategoryMenu/CategoryMenu.tsx +++ b/frontend/src/components/Common/CategoryMenu/CategoryMenu.tsx @@ -2,7 +2,7 @@ import { Button, theme } from '@fun-eat/design-system'; import type { CSSProp } from 'styled-components'; import styled from 'styled-components'; -import { useCategoryContext } from '@/hooks/context'; +import { useCategoryValueContext, useCategoryActionContext } from '@/hooks/context'; import { useCategoryQuery } from '@/hooks/queries/product'; import type { CategoryVariant } from '@/types/common'; @@ -12,8 +12,8 @@ interface CategoryMenuProps { const CategoryMenu = ({ menuVariant }: CategoryMenuProps) => { const { data: categories } = useCategoryQuery(menuVariant); - const { categoryIds, selectCategory } = useCategoryContext(); - + const { categoryIds } = useCategoryValueContext(); + const { selectCategory } = useCategoryActionContext(); const currentCategoryId = categoryIds[menuVariant]; return ( diff --git a/frontend/src/components/Layout/DefaultLayout.tsx b/frontend/src/components/Layout/DefaultLayout.tsx index fe6391912..4daf634ee 100644 --- a/frontend/src/components/Layout/DefaultLayout.tsx +++ b/frontend/src/components/Layout/DefaultLayout.tsx @@ -25,7 +25,7 @@ const DefaultLayoutContainer = styled.div` const MainWrapper = styled.main` position: relative; height: calc(100% - 120px); - padding: 20px; + padding: 0 20px; overflow-x: hidden; overflow-y: auto; `; diff --git a/frontend/src/components/Product/PBProductList/PBProductList.tsx b/frontend/src/components/Product/PBProductList/PBProductList.tsx index 105415ff7..087fdbed5 100644 --- a/frontend/src/components/Product/PBProductList/PBProductList.tsx +++ b/frontend/src/components/Product/PBProductList/PBProductList.tsx @@ -6,7 +6,7 @@ import PBProductItem from '../PBProductItem/PBProductItem'; import { MoreButton } from '@/components/Common'; import { PATH } from '@/constants/path'; -import { useCategoryContext } from '@/hooks/context'; +import { useCategoryValueContext } from '@/hooks/context'; import { useInfiniteProductsQuery } from '@/hooks/queries/product'; import displaySlice from '@/utils/displaySlice'; @@ -15,7 +15,7 @@ interface PBProductListProps { } const PBProductList = ({ isHomePage }: PBProductListProps) => { - const { categoryIds } = useCategoryContext(); + const { categoryIds } = useCategoryValueContext(); const { data: pbProductListResponse } = useInfiniteProductsQuery(categoryIds.store); const pbProducts = pbProductListResponse.pages.flatMap((page) => page.products); diff --git a/frontend/src/components/Product/ProductList/ProductList.tsx b/frontend/src/components/Product/ProductList/ProductList.tsx index 63e2cbaac..40b492f73 100644 --- a/frontend/src/components/Product/ProductList/ProductList.tsx +++ b/frontend/src/components/Product/ProductList/ProductList.tsx @@ -6,8 +6,8 @@ import styled from 'styled-components'; import ProductItem from '../ProductItem/ProductItem'; import { PATH } from '@/constants/path'; -import { useIntersectionObserver } from '@/hooks/common'; -import { useCategoryContext } from '@/hooks/context'; +import { useIntersectionObserver, useScrollRestoration } from '@/hooks/common'; +import { useCategoryValueContext } from '@/hooks/context'; import { useInfiniteProductsQuery } from '@/hooks/queries/product'; import type { CategoryVariant, SortOption } from '@/types/common'; import displaySlice from '@/utils/displaySlice'; @@ -20,21 +20,25 @@ interface ProductListProps { const ProductList = ({ category, isHomePage, selectedOption }: ProductListProps) => { const scrollRef = useRef(null); + const productListRef = useRef(null); - const { categoryIds } = useCategoryContext(); + const { categoryIds } = useCategoryValueContext(); const { fetchNextPage, hasNextPage, data } = useInfiniteProductsQuery( categoryIds[category], selectedOption?.value ?? 'reviewCount,desc' ); - const productList = data.pages.flatMap((page) => page.products); - const productsToDisplay = displaySlice(isHomePage, productList); useIntersectionObserver(fetchNextPage, scrollRef, hasNextPage); + useScrollRestoration(categoryIds[category], productListRef); + + const productList = data.pages.flatMap((page) => page.products); + const productsToDisplay = displaySlice(isHomePage, productList); + return ( - <> - + + {productsToDisplay.map((product) => (
  • @@ -42,14 +46,19 @@ const ProductList = ({ category, isHomePage, selectedOption }: ProductListProps)
  • ))} -
    +
    - + ); }; export default ProductList; -const ProductListContainer = styled.ul` +const ProductListContainer = styled.div` + height: calc(100% - 150px); + overflow-y: auto; +`; + +const ProductListWrapper = styled.ul` display: flex; flex-direction: column; diff --git a/frontend/src/contexts/CategoryContext.tsx b/frontend/src/contexts/CategoryContext.tsx index a7ceb895c..46e008a97 100644 --- a/frontend/src/contexts/CategoryContext.tsx +++ b/frontend/src/contexts/CategoryContext.tsx @@ -12,29 +12,46 @@ type CategoryIds = { [k in CategoryVariant]: number; }; -interface CategoryState { +interface CategoryValue { categoryIds: CategoryIds; + currentTabScroll: { [key: number]: number }; +} + +interface CategoryAction { selectCategory: (menuVariant: string, categoryId: number) => void; + saveCurrentTabScroll: (categoryId: number, scrollY: number) => void; } -export const CategoryContext = createContext({ - categoryIds: initialState, - selectCategory: () => {}, -}); +export const CategoryValueContext = createContext(null); +export const CategoryActionContext = createContext(null); const CategoryProvider = ({ children }: PropsWithChildren) => { const [categoryIds, setCategoryIds] = useState(initialState); + const [currentTabScroll, setCurrentTabScroll] = useState({}); const selectCategory = (menuVariant: string, categoryId: number) => { setCategoryIds((prevCategory) => ({ ...prevCategory, [menuVariant]: categoryId })); }; - const categoryState: CategoryState = { + const saveCurrentTabScroll = (categoryId: number, scrollY: number) => { + setCurrentTabScroll((prevState) => ({ ...prevState, [categoryId]: scrollY })); + }; + + const categoryValue = { categoryIds, + currentTabScroll, + }; + + const categoryAction = { selectCategory, + saveCurrentTabScroll, }; - return {children}; + return ( + + {children} + + ); }; export default CategoryProvider; diff --git a/frontend/src/hooks/common/index.ts b/frontend/src/hooks/common/index.ts index 0f4756f2a..d3c8894c0 100644 --- a/frontend/src/hooks/common/index.ts +++ b/frontend/src/hooks/common/index.ts @@ -8,3 +8,4 @@ export { default as useImageUploader } from './useImageUploader'; export { default as useFormData } from './useFormData'; export { default as useTimeout } from './useTimeout'; export { default as useRouteChangeTracker } from './useRouteChangeTracker'; +export { default as useScrollRestoration } from './useScrollRestoration'; diff --git a/frontend/src/hooks/common/useScrollRestoration.ts b/frontend/src/hooks/common/useScrollRestoration.ts new file mode 100644 index 000000000..6f3d00bf9 --- /dev/null +++ b/frontend/src/hooks/common/useScrollRestoration.ts @@ -0,0 +1,36 @@ +import type { RefObject } from 'react'; +import { useEffect } from 'react'; + +import useTimeout from './useTimeout'; +import { useCategoryActionContext, useCategoryValueContext } from '../context'; + +const useScrollRestoration = (currentCategoryId: number, ref: RefObject) => { + const { saveCurrentTabScroll } = useCategoryActionContext(); + const { currentTabScroll } = useCategoryValueContext(); + + const handleScroll = () => { + if (!ref.current) { + return; + } + saveCurrentTabScroll(currentCategoryId, ref.current.scrollTop); + }; + + const [timeoutFn] = useTimeout(handleScroll, 300); + + useEffect(() => { + if (!ref.current) { + return; + } + + ref.current.addEventListener('scroll', timeoutFn); + + const scrollY = currentTabScroll[currentCategoryId]; + ref.current.scrollTo(0, scrollY); + + return () => { + ref.current?.removeEventListener('scroll', timeoutFn); + }; + }, [currentCategoryId]); +}; + +export default useScrollRestoration; diff --git a/frontend/src/hooks/context/index.ts b/frontend/src/hooks/context/index.ts index a0a01b373..56470cfbb 100644 --- a/frontend/src/hooks/context/index.ts +++ b/frontend/src/hooks/context/index.ts @@ -1,4 +1,5 @@ -export { default as useCategoryContext } from './useCategoryContext'; +export { default as useCategoryValueContext } from './useCategoryValueContext'; +export { default as useCategoryActionContext } from './useCategoryActionContext'; export { default as useReviewFormActionContext } from './useReviewFormActionContext'; export { default as useReviewFormValueContext } from './useReviewFormValueContext'; export { default as useRecipeFormActionContext } from './useRecipeFormActionContext'; diff --git a/frontend/src/hooks/context/useCategoryActionContext.ts b/frontend/src/hooks/context/useCategoryActionContext.ts new file mode 100644 index 000000000..bc353818d --- /dev/null +++ b/frontend/src/hooks/context/useCategoryActionContext.ts @@ -0,0 +1,14 @@ +import { useContext } from 'react'; + +import { CategoryActionContext } from '@/contexts/CategoryContext'; + +const useCategoryActionContext = () => { + const categoryAction = useContext(CategoryActionContext); + if (categoryAction === null || categoryAction === undefined) { + throw new Error('useCategoryActionContext는 Category Provider 안에서 사용해야 합니다.'); + } + + return categoryAction; +}; + +export default useCategoryActionContext; diff --git a/frontend/src/hooks/context/useCategoryContext.ts b/frontend/src/hooks/context/useCategoryContext.ts deleted file mode 100644 index b67a9d0f5..000000000 --- a/frontend/src/hooks/context/useCategoryContext.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from 'react'; - -import { CategoryContext } from '@/contexts/CategoryContext'; - -const useCategoryContext = () => { - const { categoryIds, selectCategory } = useContext(CategoryContext); - - return { categoryIds, selectCategory }; -}; - -export default useCategoryContext; diff --git a/frontend/src/hooks/context/useCategoryValueContext.ts b/frontend/src/hooks/context/useCategoryValueContext.ts new file mode 100644 index 000000000..2f159e2dd --- /dev/null +++ b/frontend/src/hooks/context/useCategoryValueContext.ts @@ -0,0 +1,14 @@ +import { useContext } from 'react'; + +import { CategoryValueContext } from '@/contexts/CategoryContext'; + +const useCategoryValueContext = () => { + const categoryValue = useContext(CategoryValueContext); + if (categoryValue === null || categoryValue === undefined) { + throw new Error('useCategoryValueContext는 Category Provider 안에서 사용해야 합니다.'); + } + + return categoryValue; +}; + +export default useCategoryValueContext; diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 9048e1492..9da58fc2f 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -90,6 +90,7 @@ const HomePage = () => { + ); diff --git a/frontend/src/pages/ProductListPage.tsx b/frontend/src/pages/ProductListPage.tsx index 32ecc5475..5930a7958 100644 --- a/frontend/src/pages/ProductListPage.tsx +++ b/frontend/src/pages/ProductListPage.tsx @@ -22,23 +22,19 @@ import { isCategoryVariant } from '@/types/common'; const PAGE_TITLE = { food: '공통 상품', store: 'PB 상품' }; const ProductListPage = () => { + const { category } = useParams(); + const { ref, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); const { selectedOption, selectSortOption } = useSortOption(PRODUCT_SORT_OPTIONS[0]); const { reset } = useQueryErrorResetBoundary(); - const { category } = useParams(); - - if (!category) { - return null; - } - - if (!isCategoryVariant(category)) { + if (!category || !isCategoryVariant(category)) { return null; } return ( <> -
    + { -
    + { }; export default ProductListPage; +const ProductListSection = styled.section` + height: 100%; +`; + const SortButtonWrapper = styled.div` display: flex; justify-content: flex-end; - margin: 20px 0; + margin-top: 20px; `; diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index 0eaa96fb6..7bf9565ea 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -23,16 +23,16 @@ import SearchPage from '@/pages/SearchPage'; const router = createBrowserRouter([ { path: '/', - element: , + element: ( + + + + ), errorElement: , children: [ { index: true, - element: ( - - - - ), + element: , }, { path: `${PATH.RECIPE}/:recipeId`, @@ -93,7 +93,11 @@ const router = createBrowserRouter([ }, { path: '/', - element: , + element: ( + + + + ), errorElement: , children: [ { @@ -104,16 +108,16 @@ const router = createBrowserRouter([ }, { path: '/', - element: , + element: ( + + + + ), errorElement: , children: [ { path: `${PATH.PRODUCT_LIST}/:category`, - element: ( - - - - ), + element: , }, { path: PATH.RECIPE,