diff --git a/frontend/src/components/Common/ScrollButton/ScrollButton.tsx b/frontend/src/components/Common/ScrollButton/ScrollButton.tsx index 09827b2b4..6bfcc5095 100644 --- a/frontend/src/components/Common/ScrollButton/ScrollButton.tsx +++ b/frontend/src/components/Common/ScrollButton/ScrollButton.tsx @@ -1,5 +1,5 @@ import { Button } from '@fun-eat/design-system'; -import { useState, useEffect } from 'react'; +import type { RefObject } from 'react'; import { styled } from 'styled-components'; import SvgIcon from '../Svg/SvgIcon'; @@ -7,24 +7,18 @@ import SvgIcon from '../Svg/SvgIcon'; import { useScroll } from '@/hooks/common'; interface ScrollButtonProps { + targetRef: RefObject; isRecipePage?: boolean; } -const ScrollButton = ({ isRecipePage = false }: ScrollButtonProps) => { +const ScrollButton = ({ targetRef, isRecipePage = false }: ScrollButtonProps) => { const { scrollToTop } = useScroll(); - const [scrollTop, setScrollTop] = useState(false); const handleScroll = () => { - setScrollTop(true); - }; - - useEffect(() => { - const mainElement = document.getElementById('main'); - if (mainElement) { - scrollToTop(mainElement); - setScrollTop(false); + if (targetRef) { + scrollToTop(targetRef); } - }, [scrollTop]); + }; return ( { customWidth="45px" customHeight="45px" variant="filled" - color="gray5" + color="white" onClick={handleScroll} > - + ); }; export default ScrollButton; -const ScrollButtonWrapper = styled(Button)` +const ScrollButtonWrapper = styled(Button)>` position: fixed; bottom: ${({ isRecipePage }) => (isRecipePage ? '210px' : '90px')}; right: 20px; border-radius: 50%; + box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 4px; @media screen and (min-width: 600px) { left: calc(50% + 234px); @@ -56,4 +51,8 @@ const ScrollButtonWrapper = styled(Button)` transform: scale(1.1); transition: all 200ms ease-in-out; } + + svg { + rotate: 90deg; + } `; diff --git a/frontend/src/components/Product/ProductList/ProductList.tsx b/frontend/src/components/Product/ProductList/ProductList.tsx index 7a796ecd1..114405aab 100644 --- a/frontend/src/components/Product/ProductList/ProductList.tsx +++ b/frontend/src/components/Product/ProductList/ProductList.tsx @@ -6,7 +6,7 @@ import styled from 'styled-components'; import ProductItem from '../ProductItem/ProductItem'; import { PATH } from '@/constants/path'; -import { useIntersectionObserver, useScrollRestoration } from '@/hooks/common'; +import { useIntersectionObserver } from '@/hooks/common'; import { useCategoryValueContext } from '@/hooks/context'; import { useInfiniteProductsQuery } from '@/hooks/queries/product'; import type { CategoryVariant, SortOption } from '@/types/common'; @@ -18,8 +18,6 @@ interface ProductListProps { const ProductList = ({ category, selectedOption }: ProductListProps) => { const scrollRef = useRef(null); - const productListRef = useRef(null); - const { categoryIds } = useCategoryValueContext(); const { fetchNextPage, hasNextPage, data } = useInfiniteProductsQuery( @@ -29,13 +27,11 @@ const ProductList = ({ category, selectedOption }: ProductListProps) => { useIntersectionObserver(fetchNextPage, scrollRef, hasNextPage); - useScrollRestoration(categoryIds[category], productListRef); - const productList = data.pages.flatMap((page) => page.products); return ( - - + <> + {productList.map((product) => (
  • @@ -43,19 +39,15 @@ const ProductList = ({ category, selectedOption }: ProductListProps) => {
  • ))} -
    +
    - + ); }; -export default ProductList; -const ProductListContainer = styled.div` - height: calc(100% - 150px); - overflow-y: auto; -`; +export default ProductList; -const ProductListWrapper = styled.ul` +const ProductListContainer = styled.ul` display: flex; flex-direction: column; diff --git a/frontend/src/contexts/CategoryContext.tsx b/frontend/src/contexts/CategoryContext.tsx index 46e008a97..e77029003 100644 --- a/frontend/src/contexts/CategoryContext.tsx +++ b/frontend/src/contexts/CategoryContext.tsx @@ -8,7 +8,7 @@ const initialState = { store: 7, }; -type CategoryIds = { +export type CategoryIds = { [k in CategoryVariant]: number; }; diff --git a/frontend/src/hooks/common/useScroll.ts b/frontend/src/hooks/common/useScroll.ts index cfcd52a5e..b340dcf22 100644 --- a/frontend/src/hooks/common/useScroll.ts +++ b/frontend/src/hooks/common/useScroll.ts @@ -1,13 +1,18 @@ import type { RefObject } from 'react'; const useScroll = () => { - const scrollToTop = (mainElement: HTMLElement) => { - mainElement.scrollTo(0, 0); + const scrollToTop = (ref: RefObject) => { + if (ref.current) { + ref.current.scrollTo(0, 0); + } }; - const scrollToPosition = (ref?: RefObject) => { - setTimeout(() => { - ref?.current?.scrollIntoView({ behavior: 'smooth' }); + const scrollToPosition = (ref: RefObject) => { + const timeout = setTimeout(() => { + if (ref.current) { + ref.current.scrollIntoView({ behavior: 'smooth' }); + clearTimeout(timeout); + } }, 100); }; diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index d4a288936..306285fb1 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -4,7 +4,7 @@ import { Suspense } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import styled from 'styled-components'; -import { ScrollButton, Loading, ErrorBoundary, ErrorComponent, CategoryList } from '@/components/Common'; +import { Loading, ErrorBoundary, ErrorComponent, CategoryList } from '@/components/Common'; import { ProductRankingList, ReviewRankingList, RecipeRankingList } from '@/components/Rank'; import { IMAGE_URL } from '@/constants'; import channelTalk from '@/service/channelTalk'; @@ -75,7 +75,6 @@ const HomePage = () => { - ); }; diff --git a/frontend/src/pages/MemberRecipePage.tsx b/frontend/src/pages/MemberRecipePage.tsx index f6885edbd..90bddc44b 100644 --- a/frontend/src/pages/MemberRecipePage.tsx +++ b/frontend/src/pages/MemberRecipePage.tsx @@ -1,6 +1,6 @@ import { Spacing } from '@fun-eat/design-system'; import { useQueryErrorResetBoundary } from '@tanstack/react-query'; -import { Suspense } from 'react'; +import { Suspense, useRef } from 'react'; import styled from 'styled-components'; import { ErrorBoundary, ErrorComponent, Loading, ScrollButton, SectionTitle } from '@/components/Common'; @@ -8,9 +8,10 @@ import { MemberRecipeList } from '@/components/Members'; const MemberRecipePage = () => { const { reset } = useQueryErrorResetBoundary(); + const memberRecipeRef = useRef(null); return ( - + @@ -18,7 +19,7 @@ const MemberRecipePage = () => { - + ); @@ -27,5 +28,7 @@ const MemberRecipePage = () => { export default MemberRecipePage; const MemberRecipePageContainer = styled.div` + height: 100%; padding: 20px 20px 0; + overflow-y: auto; `; diff --git a/frontend/src/pages/MemberReviewPage.tsx b/frontend/src/pages/MemberReviewPage.tsx index e0159e648..9f15905fd 100644 --- a/frontend/src/pages/MemberReviewPage.tsx +++ b/frontend/src/pages/MemberReviewPage.tsx @@ -1,6 +1,6 @@ import { Spacing } from '@fun-eat/design-system'; import { useQueryErrorResetBoundary } from '@tanstack/react-query'; -import { Suspense } from 'react'; +import { Suspense, useRef } from 'react'; import styled from 'styled-components'; import { ErrorBoundary, ErrorComponent, Loading, ScrollButton, SectionTitle } from '@/components/Common'; @@ -8,9 +8,10 @@ import { MemberReviewList } from '@/components/Members'; const MemberReviewPage = () => { const { reset } = useQueryErrorResetBoundary(); + const memberReviewRef = useRef(null); return ( - + @@ -18,7 +19,7 @@ const MemberReviewPage = () => { - + ); @@ -27,5 +28,7 @@ const MemberReviewPage = () => { export default MemberReviewPage; const MemberReviewPageContainer = styled.div` + height: 100%; padding: 20px 20px 0; + overflow-y: auto; `; diff --git a/frontend/src/pages/ProductDetailPage.tsx b/frontend/src/pages/ProductDetailPage.tsx index acbcef743..11f668dd2 100644 --- a/frontend/src/pages/ProductDetailPage.tsx +++ b/frontend/src/pages/ProductDetailPage.tsx @@ -36,18 +36,19 @@ const ProductDetailPage = () => { const { data: productDetail } = useProductDetailQuery(Number(productId)); const { reset } = useQueryErrorResetBoundary(); - const tabMenus = [`리뷰 ${productDetail.reviewCount}`, '꿀조합']; const { selectedTabMenu, isFirstTabMenu: isReviewTab, handleTabMenuClick, initTabMenu } = useTabMenu(); const tabRef = useRef(null); - const sortOptions = isReviewTab ? REVIEW_SORT_OPTIONS : RECIPE_SORT_OPTIONS; - const initialSortOption = isReviewTab ? REVIEW_SORT_OPTIONS[0] : RECIPE_SORT_OPTIONS[0]; - - const { selectedOption, selectSortOption } = useSortOption(initialSortOption); + const { selectedOption, selectSortOption } = useSortOption(REVIEW_SORT_OPTIONS[0]); const { ref, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); - const [activeSheet, setActiveSheet] = useState<'registerReview' | 'sortOption'>('sortOption'); + const productDetailPageRef = useRef(null); + + const tabMenus = [`리뷰 ${productDetail.reviewCount}`, '꿀조합']; + const sortOptions = isReviewTab ? REVIEW_SORT_OPTIONS : RECIPE_SORT_OPTIONS; + const currentSortOption = isReviewTab ? REVIEW_SORT_OPTIONS[0] : RECIPE_SORT_OPTIONS[0]; + if (!category) { return null; } @@ -64,7 +65,7 @@ const ProductDetailPage = () => { const handleTabMenuSelect = (index: number) => { handleTabMenuClick(index); - selectSortOption(initialSortOption); + selectSortOption(currentSortOption); ReactGA.event({ category: '버튼', @@ -74,7 +75,7 @@ const ProductDetailPage = () => { }; return ( - <> + @@ -122,7 +123,7 @@ const ProductDetailPage = () => { onClick={handleOpenRegisterReviewSheet} /> - + {activeSheet === 'registerReview' ? ( @@ -142,12 +143,21 @@ const ProductDetailPage = () => { /> )} - + ); }; export default ProductDetailPage; +const ProductDetailPageContainer = styled.div` + height: 100%; + overflow-y: auto; + + &::-webkit-scrollbar { + display: none; + } +`; + const SortButtonWrapper = styled.div` display: flex; justify-content: flex-end; diff --git a/frontend/src/pages/ProductListPage.tsx b/frontend/src/pages/ProductListPage.tsx index 6ae559618..05c2de50e 100644 --- a/frontend/src/pages/ProductListPage.tsx +++ b/frontend/src/pages/ProductListPage.tsx @@ -1,6 +1,6 @@ import { BottomSheet, Spacing, useBottomSheet } from '@fun-eat/design-system'; import { useQueryErrorResetBoundary } from '@tanstack/react-query'; -import { Suspense } from 'react'; +import { Suspense, useRef } from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; @@ -16,18 +16,25 @@ import { import { ProductTitle, ProductList } from '@/components/Product'; import { PRODUCT_SORT_OPTIONS } from '@/constants'; import { PATH } from '@/constants/path'; -import { useSortOption } from '@/hooks/common'; +import type { CategoryIds } from '@/contexts/CategoryContext'; +import { useScrollRestoration, useSortOption } from '@/hooks/common'; +import { useCategoryValueContext } from '@/hooks/context'; import { isCategoryVariant } from '@/types/common'; const PAGE_TITLE = { food: '공통 상품', store: 'PB 상품' }; const ProductListPage = () => { const { category } = useParams(); + const productListRef = useRef(null); const { ref, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); const { selectedOption, selectSortOption } = useSortOption(PRODUCT_SORT_OPTIONS[0]); const { reset } = useQueryErrorResetBoundary(); + const { categoryIds } = useCategoryValueContext(); + + useScrollRestoration(categoryIds[category as keyof CategoryIds], productListRef); + if (!category || !isCategoryVariant(category)) { return null; } @@ -43,16 +50,18 @@ const ProductListPage = () => { - - }> - - - - - - + + + }> + + + + + + + - + { ); }; + export default ProductListPage; const ProductListSection = styled.section` @@ -75,3 +85,8 @@ const SortButtonWrapper = styled.div` justify-content: flex-end; margin-top: 20px; `; + +const ProductListContainer = styled.div` + height: calc(100% - 150px); + overflow-y: auto; +`; diff --git a/frontend/src/pages/RecipePage.tsx b/frontend/src/pages/RecipePage.tsx index 706e6b941..52a7f0928 100644 --- a/frontend/src/pages/RecipePage.tsx +++ b/frontend/src/pages/RecipePage.tsx @@ -1,6 +1,6 @@ import { BottomSheet, Heading, Link, Spacing, useBottomSheet } from '@fun-eat/design-system'; import { useQueryErrorResetBoundary } from '@tanstack/react-query'; -import { Suspense, useState } from 'react'; +import { Suspense, useRef, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import styled from 'styled-components'; @@ -30,6 +30,8 @@ const RecipePage = () => { const { ref, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); const { reset } = useQueryErrorResetBoundary(); + const recipeRef = useRef(null); + const handleOpenRegisterRecipeSheet = () => { setActiveSheet('registerRecipe'); handleOpenBottomSheet(); @@ -53,7 +55,9 @@ const RecipePage = () => { - + + + @@ -64,7 +68,7 @@ const RecipePage = () => { onClick={handleOpenRegisterRecipeSheet} /> - + {activeSheet === 'sortOption' ? (