Skip to content

Commit

Permalink
[FE] feat: 꿀조합 전체 목록 조회 요청 쿼리 추가 (#449)
Browse files Browse the repository at this point in the history
* feat: 꿀조합 정렬 옵션 타입 추가

* feat: 꿀조합 전체 목록 조회 목 핸들러 추가

* feat: 상품 리뷰 조회 최신순 정렬 로직 추가

* feat: 꿀조합 전체 목록 조회 쿼리 추가

* refactor: 꿀조합 목 핸들러 중복 삭제
  • Loading branch information
Leejin-Yang authored Aug 15, 2023
1 parent 1db44ac commit 7add336
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 19 deletions.
43 changes: 30 additions & 13 deletions frontend/src/components/Recipe/RecipeList/RecipeList.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,42 @@
import { Link } from '@fun-eat/design-system';
import { useRef } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import styled from 'styled-components';

import RecipeItem from '../RecipeItem/RecipeItem';

import recipeResponse from '@/mocks/data/recipes.json';
import { useIntersectionObserver } from '@/hooks/common';
import { useInfiniteRecipesQuery } from '@/hooks/queries/recipe';
import type { SortOption } from '@/types/common';

const RecipeList = () => {
// TODO: 임시 데이터, API 연동 후 수정
const { recipes } = recipeResponse;
interface RecipeListProps {
selectedOption: SortOption;
}

const RecipeList = ({ selectedOption }: RecipeListProps) => {
const scrollRef = useRef<HTMLDivElement>(null);
const { fetchNextPage, hasNextPage, data } = useInfiniteRecipesQuery(selectedOption.value);
useIntersectionObserver<HTMLDivElement>(fetchNextPage, scrollRef, hasNextPage);

if (!data) {
return null;
}

const recipes = data.pages.flatMap((page) => page.recipes);

return (
<RecipeListContainer>
{recipes.map((recipe) => (
<li key={recipe.id}>
<Link as={RouterLink} to={`${recipe.id}`}>
<RecipeItem recipe={recipe} />
</Link>
</li>
))}
</RecipeListContainer>
<>
<RecipeListContainer>
{recipes.map((recipe) => (
<li key={recipe.id}>
<Link as={RouterLink} to={`${recipe.id}`}>
<RecipeItem recipe={recipe} />
</Link>
</li>
))}
</RecipeListContainer>
<div ref={scrollRef} aria-hidden />
</>
);
};

Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/queries/recipe/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as useRecipeDetailQuery } from './useRecipeDetailQuery';
export { default as useRecipeRegisterFormMutation } from './useRecipeRegisterFormMutation';
export { default as useInfiniteRecipesQuery } from './useInfiniteRecipesQuery';
24 changes: 24 additions & 0 deletions frontend/src/hooks/queries/recipe/useInfiniteRecipesQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useInfiniteQuery } from '@tanstack/react-query';

import { recipeApi } from '@/apis';
import type { RecipeResponse } from '@/types/response';

const fetchRecipes = async (pageParam: number, sort: string) => {
const response = await recipeApi.get({ queries: `?sort=${sort}&page=${pageParam}` });
const data: RecipeResponse = await response.json();
return data;
};

const useInfiniteRecipesQuery = (sort: string) => {
return useInfiniteQuery({
queryKey: ['recipe', sort],
queryFn: ({ pageParam = 0 }) => fetchRecipes(pageParam, sort),
getNextPageParam: (prevResponse: RecipeResponse) => {
const isLast = prevResponse.page.lastPage;
const nextPage = prevResponse.page.requestPage + 1;
return isLast ? undefined : nextPage;
},
});
};

export default useInfiniteRecipesQuery;
35 changes: 35 additions & 0 deletions frontend/src/mocks/handlers/recipeHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { rest } from 'msw';

import { isRecipeSortOption, isSortOrder } from './utils';
import recipeDetail from '../data/recipeDetail.json';
import mockRecipes from '../data/recipes.json';

export const recipeHandlers = [
rest.get('/api/recipes/:recipeId', (req, res, ctx) => {
Expand All @@ -16,4 +18,37 @@ export const recipeHandlers = [

return res(ctx.status(200), ctx.json({ message: '꿀조합이 등록되었습니다.' }), ctx.set('Location', '/recipes/1'));
}),

rest.get('/api/recipes', (req, res, ctx) => {
const sortOptions = req.url.searchParams.get('sort');
const page = Number(req.url.searchParams.get('page'));

if (sortOptions === null) {
return res(ctx.status(400));
}

const [key, sortOrder] = sortOptions.split(',');

if (!isRecipeSortOption(key) || !isSortOrder(sortOrder)) {
return res(ctx.status(400));
}

const sortedRecipes = {
...mockRecipes,
recipes: [...mockRecipes.recipes].sort((cur, next) => {
if (key === 'createdAt') {
return sortOrder === 'asc'
? new Date(cur[key]).getTime() - new Date(next[key]).getTime()
: new Date(next[key]).getTime() - new Date(cur[key]).getTime();
}

return sortOrder === 'asc' ? cur[key] - next[key] : next[key] - cur[key];
}),
};

return res(
ctx.status(200),
ctx.json({ page: sortedRecipes.page, recipes: sortedRecipes.recipes.slice(page * 5, (page + 1) * 5) })
);
}),
];
12 changes: 9 additions & 3 deletions frontend/src/mocks/handlers/reviewHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ export const reviewHandlers = [

const sortedReviews = {
...mockReviews,
reviews: [...mockReviews.reviews].sort((cur, next) =>
sortOrder === 'asc' ? cur[key] - next[key] : next[key] - cur[key]
),
reviews: [...mockReviews.reviews].sort((cur, next) => {
if (key === 'createdAt') {
return sortOrder === 'asc'
? new Date(cur[key]).getTime() - new Date(next[key]).getTime()
: new Date(next[key]).getTime() - new Date(cur[key]).getTime();
}

return sortOrder === 'asc' ? cur[key] - next[key] : next[key] - cur[key];
}),
};

return res(
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/mocks/handlers/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ProductSortOption, ReviewSortOption } from '@/types/common';
import type { ProductSortOption, RecipeSortOption, ReviewSortOption } from '@/types/common';

export const isProductSortOption = (sortKey: string): sortKey is ProductSortOption =>
sortKey === 'price' || sortKey === 'averageRating' || sortKey === 'reviewCount';
Expand All @@ -7,3 +7,6 @@ export const isReviewSortOption = (sortKey: string): sortKey is ReviewSortOption
sortKey === 'favoriteCount' || sortKey === 'rating' || sortKey === 'createdAt';

export const isSortOrder = (sortOrder: string) => sortOrder === 'asc' || sortOrder === 'desc';

export const isRecipeSortOption = (sortKey: string): sortKey is RecipeSortOption =>
sortKey === 'favoriteCount' || sortKey === 'createdAt';
2 changes: 1 addition & 1 deletion frontend/src/pages/RecipePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const RecipePage = () => {
<SortButtonWrapper>
<SortButton option={selectedOption} onClick={handleOpenSortOptionSheet} />
</SortButtonWrapper>
<RecipeList />
<RecipeList selectedOption={selectedOption} />
<Spacing size={80} />
<RecipeRegisterButtonWrapper>
<RecipeRegisterButton
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export interface NavigationMenu {

export type ProductSortOption = 'price' | 'averageRating' | 'reviewCount';

export type ReviewSortOption = 'favoriteCount' | 'rating';
export type ReviewSortOption = 'favoriteCount' | 'rating' | 'createdAt';

export type RecipeSortOption = 'favoriteCount' | 'createdAt';

export type SortOption = (typeof PRODUCT_SORT_OPTIONS)[number] | (typeof REVIEW_SORT_OPTIONS)[number];

Expand Down

0 comments on commit 7add336

Please sign in to comment.