diff --git a/frontend/__tests__/hooks/useTabMenu.test.ts b/frontend/__tests__/hooks/useTabMenu.test.ts new file mode 100644 index 000000000..d8ca53178 --- /dev/null +++ b/frontend/__tests__/hooks/useTabMenu.test.ts @@ -0,0 +1,30 @@ +import { useTabMenu } from '@/hooks/common'; +import { renderHook, act } from '@testing-library/react'; + +it('선택된 탭 초기 상태는 0번 인덱스이다.', () => { + const { result } = renderHook(() => useTabMenu()); + + expect(result.current.selectedTabMenu).toBe(0); + expect(result.current.isFirstTabMenu).toBe(true); +}); + +it('handleTabMenuClick를 사용하여 선택한 탭 인덱스를 저장할 수 있다. ', () => { + const { result } = renderHook(() => useTabMenu()); + + act(() => { + result.current.handleTabMenuClick(1); + }); + + expect(result.current.selectedTabMenu).toBe(1); +}); + +it('initTabMenu를 사용하여 선택된 탭을 맨 처음 탭으로 초기화할 수 있다.', () => { + const { result } = renderHook(() => useTabMenu()); + + act(() => { + result.current.handleTabMenuClick(1); + result.current.initTabMenu(); + }); + + expect(result.current.selectedTabMenu).toBe(0); +}); diff --git a/frontend/src/components/Common/TabMenu/TabMenu.tsx b/frontend/src/components/Common/TabMenu/TabMenu.tsx index 44cd8d012..548b2f36b 100644 --- a/frontend/src/components/Common/TabMenu/TabMenu.tsx +++ b/frontend/src/components/Common/TabMenu/TabMenu.tsx @@ -5,18 +5,26 @@ import styled from 'styled-components'; interface TabMenuProps { tabMenus: readonly string[]; - selectedTabMenu: string; - handleTabMenuSelect: MouseEventHandler; + selectedTabMenu: number; + handleTabMenuSelect: (index: number) => void; } const TabMenu = ( { tabMenus, selectedTabMenu, handleTabMenuSelect }: TabMenuProps, ref: ForwardedRef ) => { + const handleTabMenuClick: MouseEventHandler = (event) => { + const { index } = event.currentTarget.dataset; + + if (index) { + handleTabMenuSelect(Number(index)); + } + }; + return ( - {tabMenus.map((menu) => { - const isSelected = selectedTabMenu === menu; + {tabMenus.map((menu, index) => { + const isSelected = selectedTabMenu === index; return ( {menu} diff --git a/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx b/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx index 5956d2cae..a75ea5ee6 100644 --- a/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx +++ b/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx @@ -24,9 +24,10 @@ interface ReviewRegisterFormProps { productId: number; targetRef: RefObject; closeReviewDialog: () => void; + initTabMenu: () => void; } -const ReviewRegisterForm = ({ productId, targetRef, closeReviewDialog }: ReviewRegisterFormProps) => { +const ReviewRegisterForm = ({ productId, targetRef, closeReviewDialog, initTabMenu }: ReviewRegisterFormProps) => { const { scrollToPosition } = useScroll(); const { previewImage, imageFile, uploadImage, deleteImage } = useImageUploader(); @@ -61,6 +62,7 @@ const ReviewRegisterForm = ({ productId, targetRef, closeReviewDialog }: ReviewR mutate(formData, { onSuccess: () => { resetAndCloseForm(); + initTabMenu(); scrollToPosition(targetRef); }, onError: (error) => { diff --git a/frontend/src/hooks/common/index.ts b/frontend/src/hooks/common/index.ts index d3c8894c0..5c0ce9f37 100644 --- a/frontend/src/hooks/common/index.ts +++ b/frontend/src/hooks/common/index.ts @@ -8,4 +8,5 @@ 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 useTabMenu } from './useTabMenu'; export { default as useScrollRestoration } from './useScrollRestoration'; diff --git a/frontend/src/hooks/common/useTabMenu.ts b/frontend/src/hooks/common/useTabMenu.ts new file mode 100644 index 000000000..f778e5348 --- /dev/null +++ b/frontend/src/hooks/common/useTabMenu.ts @@ -0,0 +1,26 @@ +import { useState } from 'react'; + +const INIT_TAB_INDEX = 0; + +const useTabMenu = () => { + const [selectedTabMenu, setSelectedTabMenu] = useState(INIT_TAB_INDEX); + + const isFirstTabMenu = selectedTabMenu === INIT_TAB_INDEX; + + const handleTabMenuClick = (index: number) => { + setSelectedTabMenu(index); + }; + + const initTabMenu = () => { + setSelectedTabMenu(INIT_TAB_INDEX); + }; + + return { + selectedTabMenu, + isFirstTabMenu, + handleTabMenuClick, + initTabMenu, + }; +}; + +export default useTabMenu; diff --git a/frontend/src/pages/IntegratedSearchPage.tsx b/frontend/src/pages/IntegratedSearchPage.tsx index 64d66d155..285fc5c2e 100644 --- a/frontend/src/pages/IntegratedSearchPage.tsx +++ b/frontend/src/pages/IntegratedSearchPage.tsx @@ -1,18 +1,16 @@ import { Button, Heading, Spacing, Text } from '@fun-eat/design-system'; import { useQueryErrorResetBoundary } from '@tanstack/react-query'; -import type { MouseEventHandler } from 'react'; import { Suspense, useEffect, useState } from 'react'; import styled from 'styled-components'; import { ErrorBoundary, ErrorComponent, Input, Loading, SvgIcon, TabMenu } from '@/components/Common'; import { RecommendList, ProductSearchResultList, RecipeSearchResultList } from '@/components/Search'; import { SEARCH_TAB_VARIANTS } from '@/constants'; -import { useDebounce } from '@/hooks/common'; +import { useDebounce, useTabMenu } from '@/hooks/common'; import { useSearch } from '@/hooks/search'; -const isProductSearchTab = (tabMenu: string) => tabMenu === SEARCH_TAB_VARIANTS[0]; -const getInputPlaceholder = (tabMenu: string) => - isProductSearchTab(tabMenu) ? '상품 이름을 검색해보세요.' : '꿀조합에 포함된 상품을 입력해보세요.'; +const PRODUCT_PLACEHOLDER = '상품 이름을 검색해보세요.'; +const RECIPE_PLACEHOLDER = '꿀조합에 포함된 상품을 입력해보세요.'; const IntegratedSearchPage = () => { const { @@ -26,12 +24,10 @@ const IntegratedSearchPage = () => { handleAutocompleteClose, } = useSearch(); const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery || ''); - const [selectedTabMenu, setSelectedTabMenu] = useState(SEARCH_TAB_VARIANTS[0]); const { reset } = useQueryErrorResetBoundary(); - const handleTabMenuSelect: MouseEventHandler = (event) => { - setSelectedTabMenu(event.currentTarget.value); - }; + const { selectedTabMenu, isFirstTabMenu: isProductSearchTab, handleTabMenuClick } = useTabMenu(); + const inputPlaceholder = isProductSearchTab ? PRODUCT_PLACEHOLDER : RECIPE_PLACEHOLDER; useDebounce( () => { @@ -53,7 +49,7 @@ const IntegratedSearchPage = () => {
@@ -80,7 +76,7 @@ const IntegratedSearchPage = () => { {isSubmitted && debouncedSearchQuery ? ( @@ -91,7 +87,7 @@ const IntegratedSearchPage = () => { }> - {isProductSearchTab(selectedTabMenu) ? ( + {isProductSearchTab ? ( ) : ( @@ -100,7 +96,7 @@ const IntegratedSearchPage = () => { ) : ( - {selectedTabMenu}을 검색해보세요. + {SEARCH_TAB_VARIANTS[selectedTabMenu]}을 검색해보세요. )} diff --git a/frontend/src/pages/ProductDetailPage.tsx b/frontend/src/pages/ProductDetailPage.tsx index 338279145..acbcef743 100644 --- a/frontend/src/pages/ProductDetailPage.tsx +++ b/frontend/src/pages/ProductDetailPage.tsx @@ -1,6 +1,5 @@ import { BottomSheet, Spacing, useBottomSheet, Text, Link } from '@fun-eat/design-system'; import { useQueryErrorResetBoundary } from '@tanstack/react-query'; -import type { MouseEventHandler } from 'react'; import { useState, useRef, Suspense } from 'react'; import ReactGA from 'react-ga4'; import { useParams, Link as RouterLink } from 'react-router-dom'; @@ -22,7 +21,7 @@ import { ReviewList, ReviewRegisterForm } from '@/components/Review'; import { RECIPE_SORT_OPTIONS, REVIEW_SORT_OPTIONS } from '@/constants'; import { PATH } from '@/constants/path'; import ReviewFormProvider from '@/contexts/ReviewFormContext'; -import { useSortOption } from '@/hooks/common'; +import { useSortOption, useTabMenu } from '@/hooks/common'; import { useMemberQuery } from '@/hooks/queries/members'; import { useProductDetailQuery } from '@/hooks/queries/product'; @@ -38,10 +37,9 @@ const ProductDetailPage = () => { const { reset } = useQueryErrorResetBoundary(); const tabMenus = [`리뷰 ${productDetail.reviewCount}`, '꿀조합']; - const [selectedTabMenu, setSelectedTabMenu] = useState(tabMenus[0]); + const { selectedTabMenu, isFirstTabMenu: isReviewTab, handleTabMenuClick, initTabMenu } = useTabMenu(); const tabRef = useRef(null); - const isReviewTab = selectedTabMenu === tabMenus[0]; const sortOptions = isReviewTab ? REVIEW_SORT_OPTIONS : RECIPE_SORT_OPTIONS; const initialSortOption = isReviewTab ? REVIEW_SORT_OPTIONS[0] : RECIPE_SORT_OPTIONS[0]; @@ -64,8 +62,8 @@ const ProductDetailPage = () => { handleOpenBottomSheet(); }; - const handleTabMenuSelect: MouseEventHandler = (event) => { - setSelectedTabMenu(event.currentTarget.value); + const handleTabMenuSelect = (index: number) => { + handleTabMenuClick(index); selectSortOption(initialSortOption); ReactGA.event({ @@ -132,6 +130,7 @@ const ProductDetailPage = () => { targetRef={tabRef} productId={Number(productId)} closeReviewDialog={handleCloseBottomSheet} + initTabMenu={initTabMenu} /> ) : ( diff --git a/frontend/src/pages/SearchPage.tsx b/frontend/src/pages/SearchPage.tsx index 6a1c9f892..f63232dba 100644 --- a/frontend/src/pages/SearchPage.tsx +++ b/frontend/src/pages/SearchPage.tsx @@ -10,9 +10,8 @@ import { SEARCH_PAGE_VARIANTS } from '@/constants'; import { useDebounce, useRoutePage } from '@/hooks/common'; import { useSearch } from '@/hooks/search'; -const isProductSearchPage = (path: string) => path === 'products'; -const getInputPlaceholder = (path: string) => - isProductSearchPage(path) ? '상품 이름을 검색해보세요.' : '꿀조합에 포함된 상품을 입력해보세요.'; +const PRODUCT_PLACEHOLDER = '상품 이름을 검색해보세요.'; +const RECIPE_PLACEHOLDER = '꿀조합에 포함된 상품을 입력해보세요.'; type SearchPageType = keyof typeof SEARCH_PAGE_VARIANTS; @@ -57,6 +56,10 @@ const SearchPage = () => { return null; } + const isProductSearchPage = searchVariant === 'products'; + const isRecipeSearchPage = searchVariant === 'recipes'; + const inputPlaceholder = isProductSearchPage ? PRODUCT_PLACEHOLDER : RECIPE_PLACEHOLDER; + return ( <> @@ -70,7 +73,7 @@ const SearchPage = () => { @@ -102,11 +105,8 @@ const SearchPage = () => { }> - {isProductSearchPage(searchVariant) ? ( - - ) : ( - - )} + {isProductSearchPage && } + {isRecipeSearchPage && }