diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 5ea0e3be4..963fcc6c8 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -102,6 +102,7 @@ module.exports = { 'import/no-unresolved': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/ban-types': 'off', + 'import/export': 'off', }, settings: { 'import/resolver': { diff --git a/frontend/src/components/Common/CategoryMenu/CategoryMenu.tsx b/frontend/src/components/Common/CategoryMenu/CategoryMenu.tsx index 90a7d500c..b262b8acd 100644 --- a/frontend/src/components/Common/CategoryMenu/CategoryMenu.tsx +++ b/frontend/src/components/Common/CategoryMenu/CategoryMenu.tsx @@ -11,14 +11,14 @@ interface CategoryMenuProps { } const CategoryMenu = ({ menuVariant }: CategoryMenuProps) => { - const { data: categoryList } = useCategoryQuery(menuVariant); + const { data: categories } = useCategoryQuery(menuVariant); const { categoryIds, selectCategory } = useCategoryContext(); const currentCategoryId = categoryIds[menuVariant]; return ( - {categoryList?.map((menu) => { + {categories.map((menu) => { const isSelected = menu.id === currentCategoryId; return (
  • diff --git a/frontend/src/components/Members/MemberReviewList/MemberReviewList.tsx b/frontend/src/components/Members/MemberReviewList/MemberReviewList.tsx index e91aad3c1..dfaa25cda 100644 --- a/frontend/src/components/Members/MemberReviewList/MemberReviewList.tsx +++ b/frontend/src/components/Members/MemberReviewList/MemberReviewList.tsx @@ -15,12 +15,12 @@ const MemberReviewList = ({ isMemberPage = false }: MemberReviewListProps) => { const scrollRef = useRef(null); const { fetchNextPage, hasNextPage, data } = useInfiniteMemberReviewQuery(); - const memberReviews = data?.pages.flatMap((page) => page.reviews); + const memberReviews = data.pages.flatMap((page) => page.reviews); const reviewsToDisplay = useDisplaySlice(isMemberPage, memberReviews); useIntersectionObserver(fetchNextPage, scrollRef, hasNextPage); - const totalReviewCount = data?.pages.flatMap((page) => page.page.totalDataCount); + const totalReviewCount = data.pages.flatMap((page) => page.page.totalDataCount); return ( @@ -31,7 +31,7 @@ const MemberReviewList = ({ isMemberPage = false }: MemberReviewListProps) => { )} - {reviewsToDisplay?.map((reviewRanking) => ( + {reviewsToDisplay.map((reviewRanking) => (
  • diff --git a/frontend/src/components/Product/PBProductList/PBProductList.tsx b/frontend/src/components/Product/PBProductList/PBProductList.tsx index b76030c46..0c297c1cc 100644 --- a/frontend/src/components/Product/PBProductList/PBProductList.tsx +++ b/frontend/src/components/Product/PBProductList/PBProductList.tsx @@ -18,13 +18,13 @@ const PBProductList = ({ isHomePage }: PBProductListProps) => { const { categoryIds } = useCategoryContext(); const { data: pbProductListResponse } = useInfiniteProductsQuery(categoryIds.store); - const pbProductList = pbProductListResponse?.pages.flatMap((page) => page.products); - const pbProductsToDisplay = displaySlice(isHomePage, pbProductList, 10); + const pbProducts = pbProductListResponse.pages.flatMap((page) => page.products); + const pbProductsToDisplay = displaySlice(isHomePage, pbProducts, 10); return ( <> - {pbProductsToDisplay?.map((pbProduct) => ( + {pbProductsToDisplay.map((pbProduct) => (
  • diff --git a/frontend/src/components/Product/ProductDetailItem/ProductDetailItem.tsx b/frontend/src/components/Product/ProductDetailItem/ProductDetailItem.tsx index 2c9620926..54058da90 100644 --- a/frontend/src/components/Product/ProductDetailItem/ProductDetailItem.tsx +++ b/frontend/src/components/Product/ProductDetailItem/ProductDetailItem.tsx @@ -10,16 +10,11 @@ interface ProductDetailItemProps { } const ProductDetailItem = ({ productId }: ProductDetailItemProps) => { - const theme = useTheme(); - const { data: productDetail } = useProductDetailQuery(productId); - - if (!productDetail) { - return null; - } - const { name, price, image, content, averageRating, tags, bookmark } = productDetail; + const theme = useTheme(); + return ( <> diff --git a/frontend/src/components/Product/ProductItem/ProductItem.tsx b/frontend/src/components/Product/ProductItem/ProductItem.tsx index 9fef53963..b0608d950 100644 --- a/frontend/src/components/Product/ProductItem/ProductItem.tsx +++ b/frontend/src/components/Product/ProductItem/ProductItem.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import PreviewImage from '@/assets/characters.svg'; import { SvgIcon } from '@/components/Common'; import type { Product } from '@/types/product'; + interface ProductItemProps { product: Product; } diff --git a/frontend/src/components/Product/ProductList/ProductList.tsx b/frontend/src/components/Product/ProductList/ProductList.tsx index ddb651eab..63e2cbaac 100644 --- a/frontend/src/components/Product/ProductList/ProductList.tsx +++ b/frontend/src/components/Product/ProductList/ProductList.tsx @@ -27,7 +27,7 @@ const ProductList = ({ category, isHomePage, selectedOption }: ProductListProps) categoryIds[category], selectedOption?.value ?? 'reviewCount,desc' ); - const productList = data?.pages.flatMap((page) => page.products); + const productList = data.pages.flatMap((page) => page.products); const productsToDisplay = displaySlice(isHomePage, productList); useIntersectionObserver(fetchNextPage, scrollRef, hasNextPage); @@ -35,7 +35,7 @@ const ProductList = ({ category, isHomePage, selectedOption }: ProductListProps) return ( <> - {productsToDisplay?.map((product) => ( + {productsToDisplay.map((product) => (
  • diff --git a/frontend/src/components/Product/ProductOverviewItem/ProductOverviewItem.tsx b/frontend/src/components/Product/ProductOverviewItem/ProductOverviewItem.tsx index 9441439b1..2bf72b2ca 100644 --- a/frontend/src/components/Product/ProductOverviewItem/ProductOverviewItem.tsx +++ b/frontend/src/components/Product/ProductOverviewItem/ProductOverviewItem.tsx @@ -4,18 +4,18 @@ import styled from 'styled-components'; import PreviewImage from '@/assets/characters.svg'; interface ProductOverviewItemProps { + name: string; + image: string | null; rank?: number; - name?: string; - image?: string; } -const ProductOverviewItem = ({ rank, name, image }: ProductOverviewItemProps) => { +const ProductOverviewItem = ({ name, image, rank }: ProductOverviewItemProps) => { return ( {rank ?? ''} - {image ? ( + {image !== null ? ( ) : ( diff --git a/frontend/src/components/Rank/ProductRankingList/ProductRankingList.tsx b/frontend/src/components/Rank/ProductRankingList/ProductRankingList.tsx index a1a8a2f19..08e37207b 100644 --- a/frontend/src/components/Rank/ProductRankingList/ProductRankingList.tsx +++ b/frontend/src/components/Rank/ProductRankingList/ProductRankingList.tsx @@ -5,17 +5,18 @@ import { ProductOverviewItem } from '@/components/Product'; import { PATH } from '@/constants/path'; import { useProductRankingQuery } from '@/hooks/queries/rank'; import displaySlice from '@/utils/displaySlice'; + interface ProductRankingListProps { isHomePage?: boolean; } -const ProductRankingList = ({ isHomePage }: ProductRankingListProps) => { +const ProductRankingList = ({ isHomePage = false }: ProductRankingListProps) => { const { data: productRankings } = useProductRankingQuery(); - const productsToDisplay = displaySlice(isHomePage, productRankings?.products, 3); + const productsToDisplay = displaySlice(isHomePage, productRankings.products, 3); return (
      - {productsToDisplay?.map(({ id, name, image, categoryType }, index) => ( + {productsToDisplay.map(({ id, name, image, categoryType }, index) => (
    • diff --git a/frontend/src/components/Rank/ReviewRankingList/ReviewRankingList.tsx b/frontend/src/components/Rank/ReviewRankingList/ReviewRankingList.tsx index 35039edce..2f3079cd7 100644 --- a/frontend/src/components/Rank/ReviewRankingList/ReviewRankingList.tsx +++ b/frontend/src/components/Rank/ReviewRankingList/ReviewRankingList.tsx @@ -9,13 +9,13 @@ interface ReviewRankingListProps { isHomePage?: boolean; } -const ReviewRankingList = ({ isHomePage }: ReviewRankingListProps) => { +const ReviewRankingList = ({ isHomePage = false }: ReviewRankingListProps) => { const { data: reviewRankings } = useReviewRankingQuery(); - const reviewsToDisplay = useDisplaySlice(isHomePage, reviewRankings?.reviews); + const reviewsToDisplay = useDisplaySlice(isHomePage, reviewRankings.reviews); return ( - {reviewsToDisplay?.map((reviewRanking) => ( + {reviewsToDisplay.map((reviewRanking) => (
    • diff --git a/frontend/src/components/Recipe/RecipeList/RecipeList.tsx b/frontend/src/components/Recipe/RecipeList/RecipeList.tsx index c89f1ca7e..9e56e32a8 100644 --- a/frontend/src/components/Recipe/RecipeList/RecipeList.tsx +++ b/frontend/src/components/Recipe/RecipeList/RecipeList.tsx @@ -18,10 +18,6 @@ const RecipeList = ({ selectedOption }: RecipeListProps) => { const { fetchNextPage, hasNextPage, data } = useInfiniteRecipesQuery(selectedOption.value); useIntersectionObserver(fetchNextPage, scrollRef, hasNextPage); - if (!data) { - return null; - } - const recipes = data.pages.flatMap((page) => page.recipes); return ( diff --git a/frontend/src/components/Review/ReviewList/ReviewList.tsx b/frontend/src/components/Review/ReviewList/ReviewList.tsx index dd64b78a1..a51bfb4b2 100644 --- a/frontend/src/components/Review/ReviewList/ReviewList.tsx +++ b/frontend/src/components/Review/ReviewList/ReviewList.tsx @@ -1,54 +1,53 @@ -import { Text, Link } from '@fun-eat/design-system'; import { useRef } from 'react'; -import { Link as RouterLink } from 'react-router-dom'; import styled from 'styled-components'; import ReviewItem from '../ReviewItem/ReviewItem'; -import { PATH } from '@/constants/path'; +import { Loading } from '@/components/Common'; import { useIntersectionObserver } from '@/hooks/common'; import { useInfiniteProductReviewsQuery } from '@/hooks/queries/product'; import type { SortOption } from '@/types/common'; -const LOGIN_ERROR_MESSAGE = - '로그인 해야 상품 리뷰를 볼 수 있어요.\n펀잇에 가입하고 편의점 상품의 리뷰를 확인해보세요 😊'; - interface ReviewListProps { productId: number; selectedOption: SortOption; } const ReviewList = ({ productId, selectedOption }: ReviewListProps) => { + const { fetchNextPage, hasNextPage, data, isFetchingNextPage } = useInfiniteProductReviewsQuery( + productId, + selectedOption.value + ); const scrollRef = useRef(null); - - const { fetchNextPage, hasNextPage, data, isError } = useInfiniteProductReviewsQuery(productId, selectedOption.value); useIntersectionObserver(fetchNextPage, scrollRef, hasNextPage); - if (isError) { - return ( - - - {LOGIN_ERROR_MESSAGE} - - - 로그인하러 가기 - - - ); - } + const reviews = data.pages.flatMap((page) => page.reviews); + + // TODO: 로그인 에러 페이지로 이동 예정. 다른 브랜치에서 작업중 + //if (isError) { + // return ( + // + // + // {LOGIN_ERROR_MESSAGE} + // + // + // 로그인하러 가기 + // + // + // ); + //} return ( <> - {data?.pages - .flatMap((page) => page.reviews) - .map((review) => ( -
    • - -
    • - ))} + {reviews.map((review) => ( +
    • + +
    • + ))}
      + {isFetchingNextPage && } ); }; @@ -60,21 +59,3 @@ const ReviewListContainer = styled.ul` flex-direction: column; row-gap: 60px; `; - -const ErrorContainer = styled.div` - display: flex; - flex-direction: column; - align-items: center; -`; - -const ErrorDescription = styled(Text)` - padding: 40px 0; - white-space: pre-line; - word-break: break-all; -`; - -const LoginLink = styled(Link)` - padding: 16px 24px; - border: 1px solid ${({ theme }) => theme.colors.gray4}; - border-radius: 8px; -`; diff --git a/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx b/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx index 2d3f9f134..54892819d 100644 --- a/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx +++ b/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx @@ -67,7 +67,7 @@ const ReviewRegisterForm = ({ productId, targetRef, closeReviewDialog }: ReviewR - + diff --git a/frontend/src/components/Review/ReviewTagList/ReviewTagList.tsx b/frontend/src/components/Review/ReviewTagList/ReviewTagList.tsx index 763694d0d..c3f0cb49a 100644 --- a/frontend/src/components/Review/ReviewTagList/ReviewTagList.tsx +++ b/frontend/src/components/Review/ReviewTagList/ReviewTagList.tsx @@ -14,11 +14,7 @@ interface ReviewTagListProps { const ReviewTagList = ({ selectedTags }: ReviewTagListProps) => { const { data: tagsData } = useReviewTagsQuery(); - const { minDisplayedTags, canShowMore, showMoreTags } = useDisplayTag(tagsData ?? [], MIN_DISPLAYED_TAGS_LENGTH); - - if (!tagsData) { - return null; - } + const { minDisplayedTags, canShowMore, showMoreTags } = useDisplayTag(tagsData, MIN_DISPLAYED_TAGS_LENGTH); return ( diff --git a/frontend/src/components/Search/RecommendList/RecommendList.tsx b/frontend/src/components/Search/RecommendList/RecommendList.tsx index 4ccc390e3..cbb53c212 100644 --- a/frontend/src/components/Search/RecommendList/RecommendList.tsx +++ b/frontend/src/components/Search/RecommendList/RecommendList.tsx @@ -17,10 +17,6 @@ const RecommendList = ({ searchQuery }: RecommendListProps) => { const scrollRef = useRef(null); useIntersectionObserver(fetchNextPage, scrollRef, hasNextPage); - if (!searchResponse) { - return null; - } - const products = searchResponse.pages.flatMap((page) => page.products); if (products.length === 0) { diff --git a/frontend/src/components/Search/SearchResultList/SearchResultList.tsx b/frontend/src/components/Search/SearchResultList/SearchResultList.tsx index 7cc9717ea..36108f719 100644 --- a/frontend/src/components/Search/SearchResultList/SearchResultList.tsx +++ b/frontend/src/components/Search/SearchResultList/SearchResultList.tsx @@ -17,10 +17,6 @@ const SearchResultList = ({ searchQuery }: SearchResultListProps) => { const scrollRef = useRef(null); useIntersectionObserver(fetchNextPage, scrollRef, hasNextPage); - if (!searchResponse) { - return null; - } - const products = searchResponse.pages.flatMap((page) => page.products); if (products.length === 0) { diff --git a/frontend/src/hooks/queries/index.ts b/frontend/src/hooks/queries/index.ts new file mode 100644 index 000000000..2288777da --- /dev/null +++ b/frontend/src/hooks/queries/index.ts @@ -0,0 +1,2 @@ +export * from './useSuspendedQuery'; +export * from './useSuspendedInfiniteQuery'; diff --git a/frontend/src/hooks/queries/members/useInfiniteMemberReviewQuery.ts b/frontend/src/hooks/queries/members/useInfiniteMemberReviewQuery.ts index 8e8d19570..40e0e088e 100644 --- a/frontend/src/hooks/queries/members/useInfiniteMemberReviewQuery.ts +++ b/frontend/src/hooks/queries/members/useInfiniteMemberReviewQuery.ts @@ -1,4 +1,4 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; +import { useSuspendedInfiniteQuery } from '..'; import { memberApi } from '@/apis'; import type { MemberReviewResponse } from '@/types/response'; @@ -10,9 +10,7 @@ const fetchMemberReview = async (pageParam: number) => { }; const useInfiniteMemberReviewQuery = () => { - return useInfiniteQuery({ - queryKey: ['member', 'reviews'], - queryFn: ({ pageParam = 0 }) => fetchMemberReview(pageParam), + return useSuspendedInfiniteQuery(['member', 'reviews'], ({ pageParam = 0 }) => fetchMemberReview(pageParam), { getNextPageParam: (prevResponse: MemberReviewResponse) => { const isLast = prevResponse.page.lastPage; const nextPage = prevResponse.page.requestPage + 1; diff --git a/frontend/src/hooks/queries/product/useCategoryQuery.ts b/frontend/src/hooks/queries/product/useCategoryQuery.ts index 55adee756..4b4c08c46 100644 --- a/frontend/src/hooks/queries/product/useCategoryQuery.ts +++ b/frontend/src/hooks/queries/product/useCategoryQuery.ts @@ -1,4 +1,4 @@ -import { useQuery } from '@tanstack/react-query'; +import { useSuspendedQuery } from '..'; import { categoryApi } from '@/apis'; import type { Category } from '@/types/common'; @@ -10,10 +10,7 @@ const fetchCategories = async (type: string) => { }; const useCategoryQuery = (type: string) => { - return useQuery({ - queryKey: ['categories', type], - queryFn: () => fetchCategories(type), - }); + return useSuspendedQuery(['categories', type], () => fetchCategories(type)); }; export default useCategoryQuery; diff --git a/frontend/src/hooks/queries/product/useInfiniteProductReviewsQuery.ts b/frontend/src/hooks/queries/product/useInfiniteProductReviewsQuery.ts index a2c85a848..9d6dc8a96 100644 --- a/frontend/src/hooks/queries/product/useInfiniteProductReviewsQuery.ts +++ b/frontend/src/hooks/queries/product/useInfiniteProductReviewsQuery.ts @@ -1,4 +1,4 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; +import { useSuspendedInfiniteQuery } from '..'; import { productApi } from '@/apis'; import type { ProductReviewResponse } from '@/types/response'; @@ -15,15 +15,17 @@ const fetchProductReviews = async (pageParam: number, productId: number, sort: s }; const useInfiniteProductReviewsQuery = (productId: number, sort: string) => { - return useInfiniteQuery({ - queryKey: ['productReviews', productId, sort], - queryFn: ({ pageParam = 0 }) => fetchProductReviews(pageParam, productId, sort), - getNextPageParam: (prevResponse: ProductReviewResponse) => { - const isLast = prevResponse.page.lastPage; - const nextPage = prevResponse.page.requestPage + 1; - return isLast ? undefined : nextPage; - }, - }); + return useSuspendedInfiniteQuery( + ['productReviews', productId, sort], + ({ pageParam = 0 }) => fetchProductReviews(pageParam, productId, sort), + { + getNextPageParam: (prevResponse: ProductReviewResponse) => { + const isLast = prevResponse.page.lastPage; + const nextPage = prevResponse.page.requestPage + 1; + return isLast ? undefined : nextPage; + }, + } + ); }; export default useInfiniteProductReviewsQuery; diff --git a/frontend/src/hooks/queries/product/useInfiniteProductsQuery.ts b/frontend/src/hooks/queries/product/useInfiniteProductsQuery.ts index 1158b9ccc..0f4652569 100644 --- a/frontend/src/hooks/queries/product/useInfiniteProductsQuery.ts +++ b/frontend/src/hooks/queries/product/useInfiniteProductsQuery.ts @@ -1,4 +1,4 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; +import { useSuspendedInfiniteQuery } from '..'; import { categoryApi } from '@/apis'; import type { CategoryProductResponse } from '@/types/response'; @@ -14,15 +14,17 @@ const fetchProducts = async (pageParam: number, categoryId: number, sort = 'revi }; const useInfiniteProductsQuery = (categoryId: number, sort = 'reviewCount,desc') => { - return useInfiniteQuery({ - queryKey: ['products', categoryId, sort], - queryFn: ({ pageParam = 0 }) => fetchProducts(pageParam, categoryId, sort), - getNextPageParam: (prevResponse: CategoryProductResponse) => { - const isLast = prevResponse.page.lastPage; - const nextPage = prevResponse.page.requestPage + 1; - return isLast ? undefined : nextPage; - }, - }); + return useSuspendedInfiniteQuery( + ['products', categoryId, sort], + ({ pageParam = 0 }) => fetchProducts(pageParam, categoryId, sort), + { + getNextPageParam: (prevResponse: CategoryProductResponse) => { + const isLast = prevResponse.page.lastPage; + const nextPage = prevResponse.page.requestPage + 1; + return isLast ? undefined : nextPage; + }, + } + ); }; export default useInfiniteProductsQuery; diff --git a/frontend/src/hooks/queries/product/useProductDetailQuery.ts b/frontend/src/hooks/queries/product/useProductDetailQuery.ts index 46e0df3b2..b68028efc 100644 --- a/frontend/src/hooks/queries/product/useProductDetailQuery.ts +++ b/frontend/src/hooks/queries/product/useProductDetailQuery.ts @@ -1,4 +1,4 @@ -import { useQuery } from '@tanstack/react-query'; +import { useSuspendedQuery } from '..'; import { productApi } from '@/apis'; import type { ProductDetail } from '@/types/product'; @@ -10,10 +10,7 @@ const fetchProductDetail = async (productId: number) => { }; const useProductDetailQuery = (productId: number) => { - return useQuery({ - queryKey: ['productDetail', productId], - queryFn: () => fetchProductDetail(productId), - }); + return useSuspendedQuery(['productDetail', productId], () => fetchProductDetail(productId)); }; export default useProductDetailQuery; diff --git a/frontend/src/hooks/queries/rank/useProductRankingQuery.ts b/frontend/src/hooks/queries/rank/useProductRankingQuery.ts index 2ef8bb74e..afc9b4f19 100644 --- a/frontend/src/hooks/queries/rank/useProductRankingQuery.ts +++ b/frontend/src/hooks/queries/rank/useProductRankingQuery.ts @@ -1,4 +1,4 @@ -import { useQuery } from '@tanstack/react-query'; +import { useSuspendedQuery } from '..'; import { rankApi } from '@/apis'; import type { ProductRankingResponse } from '@/types/response'; @@ -10,10 +10,7 @@ const fetchProductRanking = async () => { }; const useProductRankingQuery = () => { - return useQuery({ - queryKey: ['productRanking'], - queryFn: () => fetchProductRanking(), - }); + return useSuspendedQuery(['ranking', 'product'], () => fetchProductRanking()); }; export default useProductRankingQuery; diff --git a/frontend/src/hooks/queries/rank/useReviewRankingQuery.ts b/frontend/src/hooks/queries/rank/useReviewRankingQuery.ts index 30d37731d..edacfaaf8 100644 --- a/frontend/src/hooks/queries/rank/useReviewRankingQuery.ts +++ b/frontend/src/hooks/queries/rank/useReviewRankingQuery.ts @@ -1,4 +1,4 @@ -import { useQuery } from '@tanstack/react-query'; +import { useSuspendedQuery } from '..'; import { rankApi } from '@/apis'; import type { ReviewRankingResponse } from '@/types/response'; @@ -10,10 +10,7 @@ const fetchReviewRanking = async () => { }; const useReviewRankingQuery = () => { - return useQuery({ - queryKey: ['reviewRanking'], - queryFn: () => fetchReviewRanking(), - }); + return useSuspendedQuery(['ranking', 'review'], () => fetchReviewRanking()); }; export default useReviewRankingQuery; diff --git a/frontend/src/hooks/queries/recipe/useInfiniteRecipesQuery.ts b/frontend/src/hooks/queries/recipe/useInfiniteRecipesQuery.ts index ca2e65697..117212c27 100644 --- a/frontend/src/hooks/queries/recipe/useInfiniteRecipesQuery.ts +++ b/frontend/src/hooks/queries/recipe/useInfiniteRecipesQuery.ts @@ -1,4 +1,4 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; +import { useSuspendedInfiniteQuery } from '..'; import { recipeApi } from '@/apis'; import type { RecipeResponse } from '@/types/response'; @@ -10,9 +10,7 @@ const fetchRecipes = async (pageParam: number, sort: string) => { }; const useInfiniteRecipesQuery = (sort: string) => { - return useInfiniteQuery({ - queryKey: ['recipe', sort], - queryFn: ({ pageParam = 0 }) => fetchRecipes(pageParam, sort), + return useSuspendedInfiniteQuery(['recipe', sort], ({ pageParam = 0 }) => fetchRecipes(pageParam, sort), { getNextPageParam: (prevResponse: RecipeResponse) => { const isLast = prevResponse.page.lastPage; const nextPage = prevResponse.page.requestPage + 1; diff --git a/frontend/src/hooks/queries/recipe/useRecipeDetailQuery.ts b/frontend/src/hooks/queries/recipe/useRecipeDetailQuery.ts index 99ff7f150..8f8ad9c28 100644 --- a/frontend/src/hooks/queries/recipe/useRecipeDetailQuery.ts +++ b/frontend/src/hooks/queries/recipe/useRecipeDetailQuery.ts @@ -1,4 +1,4 @@ -import { useQuery } from '@tanstack/react-query'; +import { useSuspendedQuery } from '..'; import { recipeApi } from '@/apis'; import type { RecipeDetail } from '@/types/recipe'; @@ -10,10 +10,7 @@ const fetchRecipeDetail = async (recipeId: number) => { }; const useRecipeDetailQuery = (recipeId: number) => { - return useQuery({ - queryKey: ['recipeDetail', recipeId], - queryFn: () => fetchRecipeDetail(recipeId), - }); + return useSuspendedQuery(['recipeDetail', recipeId], () => fetchRecipeDetail(recipeId)); }; export default useRecipeDetailQuery; diff --git a/frontend/src/hooks/queries/review/useReviewTagsQuery.ts b/frontend/src/hooks/queries/review/useReviewTagsQuery.ts index 0ddf05195..3ba2b3409 100644 --- a/frontend/src/hooks/queries/review/useReviewTagsQuery.ts +++ b/frontend/src/hooks/queries/review/useReviewTagsQuery.ts @@ -1,4 +1,4 @@ -import { useQuery } from '@tanstack/react-query'; +import { useSuspendedQuery } from '..'; import { tagApi } from '@/apis'; import type { ReviewTag } from '@/types/review'; @@ -10,10 +10,7 @@ const fetchReviewTags = async () => { }; const useReviewTagsQuery = () => { - return useQuery({ - queryKey: ['reviewTags'], - queryFn: () => fetchReviewTags(), - }); + return useSuspendedQuery(['review', 'tags'], () => fetchReviewTags()); }; export default useReviewTagsQuery; diff --git a/frontend/src/hooks/queries/search/useInfiniteProductSearchAutocompleteQuery.ts b/frontend/src/hooks/queries/search/useInfiniteProductSearchAutocompleteQuery.ts index e224c4116..492f3d96e 100644 --- a/frontend/src/hooks/queries/search/useInfiniteProductSearchAutocompleteQuery.ts +++ b/frontend/src/hooks/queries/search/useInfiniteProductSearchAutocompleteQuery.ts @@ -1,4 +1,4 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; +import { useSuspendedInfiniteQuery } from '..'; import { searchApi } from '@/apis'; import type { ProductSearchAutocompleteResponse } from '@/types/response'; @@ -11,16 +11,17 @@ const fetchProductSearchAutocomplete = async (query: string, page: number) => { }; const useInfiniteProductSearchAutocompleteQuery = (query: string) => { - return useInfiniteQuery({ - queryKey: ['search', 'products', query], - queryFn: ({ pageParam = 0 }) => fetchProductSearchAutocomplete(query, pageParam), - getNextPageParam: (prevResponse: ProductSearchAutocompleteResponse) => { - const isLast = prevResponse.page.lastPage; - const nextPage = prevResponse.page.requestPage + 1; - return isLast ? undefined : nextPage; - }, - suspense: true, - }); + return useSuspendedInfiniteQuery( + ['search', 'products', query], + ({ pageParam = 0 }) => fetchProductSearchAutocomplete(query, pageParam), + { + getNextPageParam: (prevResponse: ProductSearchAutocompleteResponse) => { + const isLast = prevResponse.page.lastPage; + const nextPage = prevResponse.page.requestPage + 1; + return isLast ? undefined : nextPage; + }, + } + ); }; export default useInfiniteProductSearchAutocompleteQuery; diff --git a/frontend/src/hooks/queries/search/useInfiniteProductSearchResultsQuery.ts b/frontend/src/hooks/queries/search/useInfiniteProductSearchResultsQuery.ts index 0ae40327b..d49a07453 100644 --- a/frontend/src/hooks/queries/search/useInfiniteProductSearchResultsQuery.ts +++ b/frontend/src/hooks/queries/search/useInfiniteProductSearchResultsQuery.ts @@ -1,4 +1,4 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; +import { useSuspendedInfiniteQuery } from '..'; import { searchApi } from '@/apis'; import type { ProductSearchResultResponse } from '@/types/response'; @@ -11,16 +11,17 @@ const fetchProductSearchResults = async (query: string, page: number) => { }; const useInfiniteProductSearchResultsQuery = (query: string) => { - return useInfiniteQuery({ - queryKey: ['search', 'products', 'results', query], - queryFn: ({ pageParam = 0 }) => fetchProductSearchResults(query, pageParam), - getNextPageParam: (prevResponse: ProductSearchResultResponse) => { - const isLast = prevResponse.page.lastPage; - const nextPage = prevResponse.page.requestPage + 1; - return isLast ? undefined : nextPage; - }, - suspense: true, - }); + return useSuspendedInfiniteQuery( + ['search', 'products', 'results', query], + ({ pageParam = 0 }) => fetchProductSearchResults(query, pageParam), + { + getNextPageParam: (prevResponse: ProductSearchResultResponse) => { + const isLast = prevResponse.page.lastPage; + const nextPage = prevResponse.page.requestPage + 1; + return isLast ? undefined : nextPage; + }, + } + ); }; export default useInfiniteProductSearchResultsQuery; diff --git a/frontend/src/hooks/queries/useSuspendedInfiniteQuery.ts b/frontend/src/hooks/queries/useSuspendedInfiniteQuery.ts new file mode 100644 index 000000000..e9763cb5a --- /dev/null +++ b/frontend/src/hooks/queries/useSuspendedInfiniteQuery.ts @@ -0,0 +1,103 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import type { + InfiniteData, + QueryKey, + UseInfiniteQueryResult, + UseInfiniteQueryOptions, + QueryFunction, +} from '@tanstack/react-query'; + +export type UseSuspendedInfiniteQueryResult = Omit< + UseInfiniteQueryResult, + 'error' | 'isLoading' | 'isError' | 'isFetching' | 'status' | 'data' +>; + +export type UseSuspendedInfiniteQueryResultOnSuccess = UseSuspendedInfiniteQueryResult & { + data: InfiniteData; + status: 'success'; + isSuccess: true; + isIdle: false; +}; + +export type UseSuspendedInfiniteQueryResultOnIdle = UseSuspendedInfiniteQueryResult & { + data: undefined; + status: 'idle'; + isSuccess: false; + isIdle: true; +}; + +export type UseSuspendedInfiniteQueryOptions< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +> = Omit< + UseInfiniteQueryOptions, + 'suspense' | 'queryKey' | 'queryFn' +>; + +export type UseSuspendedInfiniteQueryOptionWithoutEnabled< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +> = Omit, 'enabled'>; + +export function useSuspendedInfiniteQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +>( + queryKey: TQueryKey, + queryFn: QueryFunction, + options?: UseSuspendedInfiniteQueryOptionWithoutEnabled +): UseSuspendedInfiniteQueryResultOnSuccess; + +export function useSuspendedInfiniteQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +>( + queryKey: TQueryKey, + queryFn: QueryFunction, + options?: UseSuspendedInfiniteQueryOptionWithoutEnabled & { + enabled?: true; + } +): UseSuspendedInfiniteQueryResultOnSuccess; + +export function useSuspendedInfiniteQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +>( + queryKey: TQueryKey, + queryFn: QueryFunction, + options: UseSuspendedInfiniteQueryOptionWithoutEnabled & { + enabled: false; + } +): UseSuspendedInfiniteQueryResultOnIdle; + +export function useSuspendedInfiniteQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +>( + queryKey: TQueryKey, + queryFn: QueryFunction, + options?: UseSuspendedInfiniteQueryOptions +) { + return useInfiniteQuery({ + queryKey, + queryFn, + suspense: true, + ...options, + }) as UseSuspendedInfiniteQueryResult; +} + +export default useSuspendedInfiniteQuery; diff --git a/frontend/src/hooks/queries/useSuspendedQuery.ts b/frontend/src/hooks/queries/useSuspendedQuery.ts new file mode 100644 index 000000000..22466ddb9 --- /dev/null +++ b/frontend/src/hooks/queries/useSuspendedQuery.ts @@ -0,0 +1,95 @@ +import { useQuery } from '@tanstack/react-query'; +import type { QueryFunction, QueryKey, UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; + +export type UseSuspendedQueryResult = Omit< + UseQueryResult, + 'error' | 'isLoading' | 'isError' | 'isFetching' | 'status' | 'data' +> & { data: TData; status: 'idle' | 'success' }; + +export type UseSuspendedQueryResultOnSuccess = UseSuspendedQueryResult & { + status: 'success'; + isSuccess: true; + isIdle: false; +}; + +export type UseSuspendedQueryResultOnIdle = UseSuspendedQueryResult & { + status: 'idle'; + isSuccess: false; + isIdle: true; +}; + +export type UseSuspendedQueryOptions< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +> = Omit, 'suspense' | 'queryKey' | 'queryFn'>; + +export type UseSuspendedQueryOptionWithoutEnabled< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +> = Omit, 'enabled'>; + +export function useSuspendedQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +>( + queryKey: TQueryKey, + queryFn: QueryFunction, + options?: UseSuspendedQueryOptionWithoutEnabled +): UseSuspendedQueryResultOnSuccess; + +export function useSuspendedQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +>( + queryKey: TQueryKey, + queryFn: QueryFunction, + options: UseSuspendedQueryOptionWithoutEnabled & { enabled?: true } +): UseSuspendedQueryResultOnSuccess; + +export function useSuspendedQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +>( + queryKey: TQueryKey, + queryFn: QueryFunction, + options: UseSuspendedQueryOptionWithoutEnabled & { enabled: false } +): UseSuspendedQueryResultOnIdle; + +export function useSuspendedQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +>( + queryKey: TQueryKey, + queryFn: QueryFunction, + options: UseSuspendedQueryOptionWithoutEnabled +): UseSuspendedQueryResultOnSuccess | UseSuspendedQueryResultOnIdle; + +export function useSuspendedQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +>( + queryKey: TQueryKey, + queryFn: QueryFunction, + options?: UseSuspendedQueryOptions +) { + return useQuery({ + queryKey, + queryFn, + suspense: true, + ...options, + }) as UseSuspendedQueryResult; +} diff --git a/frontend/src/mocks/handlers/reviewHandlers.ts b/frontend/src/mocks/handlers/reviewHandlers.ts index 26d1cd52d..884088830 100644 --- a/frontend/src/mocks/handlers/reviewHandlers.ts +++ b/frontend/src/mocks/handlers/reviewHandlers.ts @@ -14,7 +14,7 @@ export const reviewHandlers = [ const page = Number(req.url.searchParams.get('page')); if (!mockSessionId) { - return res(ctx.status(403)); + return res(ctx.status(401)); } if (sortOptions === null) { @@ -42,7 +42,8 @@ export const reviewHandlers = [ return res( ctx.status(200), - ctx.json({ page: sortedReviews.page, reviews: sortedReviews.reviews.slice(page * 5, (page + 1) * 5) }) + ctx.json({ page: sortedReviews.page, reviews: sortedReviews.reviews }), + ctx.delay(1000) ); }), diff --git a/frontend/src/pages/ProductDetailPage.tsx b/frontend/src/pages/ProductDetailPage.tsx index 940f94e19..9013d20bf 100644 --- a/frontend/src/pages/ProductDetailPage.tsx +++ b/frontend/src/pages/ProductDetailPage.tsx @@ -1,9 +1,17 @@ import { BottomSheet, Button, Spacing, useBottomSheet } from '@fun-eat/design-system'; -import { useState, useRef } from 'react'; +import { useState, useRef, Suspense } from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; -import { SortButton, SortOptionList, TabMenu, ScrollButton } from '@/components/Common'; +import { + SortButton, + SortOptionList, + TabMenu, + ScrollButton, + Loading, + ErrorBoundary, + ErrorComponent, +} from '@/components/Common'; import { ProductDetailItem } from '@/components/Product'; import { ReviewList, ReviewRegisterForm } from '@/components/Review'; import { REVIEW_SORT_OPTIONS } from '@/constants'; @@ -36,12 +44,16 @@ const ProductDetailPage = () => { {/* 나중에 API 수정하면 이 부분도 같이 수정해주세요 */} - - - -
      - -
      + + }> + + + +
      + +
      +
      +
      { const { recipeId } = useParams(); const { data: recipeDetail } = useRecipeDetailQuery(Number(recipeId)); - - if (!recipeDetail) { - return null; - } - const { id, images, title, content, author, products, totalPrice, favoriteCount, favorite, createdAt } = recipeDetail; return (