Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE] feat: 꿀조합 전체 목록 조회 요청 쿼리 추가 #449

Merged
merged 5 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading