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] 리뷰 삭제 기능 구현 #780

Merged
merged 5 commits into from
Oct 18, 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
3 changes: 3 additions & 0 deletions frontend/.storybook/preview-body.html
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@
d="M8 7a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0v-3A.5.5 0 0 1 8 7zm0-.75a.749.749 0 1 0 0-1.5.749.749 0 0 0 0 1.498zM2 8a6 6 0 1 1 12 0A6 6 0 0 1 2 8zm6-5a5 5 0 1 0 0 10A5 5 0 0 0 8 3z"
/>
</symbol>
<symbol id="trashcan" viewBox="0 0 24 24">
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
</symbol>
</svg>
</div>
<div id="toast-container"></div>
3 changes: 2 additions & 1 deletion frontend/src/components/Common/Svg/SvgIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export const SVG_ICON_VARIANTS = [
'camera',
'link',
'plane',
'info'
'info',
'trashcan',
] as const;
export type SvgIconVariant = (typeof SVG_ICON_VARIANTS)[number];

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/Common/Svg/SvgSprite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ const SvgSprite = () => {
<symbol id="info" viewBox="0 0 16 16">
<path d="M8 7a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0v-3A.5.5 0 0 1 8 7zm0-.75a.749.749 0 1 0 0-1.5.749.749 0 0 0 0 1.498zM2 8a6 6 0 1 1 12 0A6 6 0 0 1 2 8zm6-5a5 5 0 1 0 0 10A5 5 0 0 0 8 3z" />
</symbol>
<symbol id="trashcan" viewBox="0 0 24 24">
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
</symbol>
</svg>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import { useInfiniteMemberRecipeQuery } from '@/hooks/queries/members';
import useDisplaySlice from '@/utils/displaySlice';

interface MemberRecipeListProps {
isMemberPage?: boolean;
isPreview?: boolean;
}

const MemberRecipeList = ({ isMemberPage = false }: MemberRecipeListProps) => {
const MemberRecipeList = ({ isPreview = false }: MemberRecipeListProps) => {
const scrollRef = useRef<HTMLDivElement>(null);

const { fetchNextPage, hasNextPage, data } = useInfiniteMemberRecipeQuery();
const memberRecipes = data?.pages.flatMap((page) => page.recipes);
const recipeToDisplay = useDisplaySlice(isMemberPage, memberRecipes);
const recipeToDisplay = useDisplaySlice(isPreview, memberRecipes);

useIntersectionObserver<HTMLDivElement>(fetchNextPage, scrollRef, hasNextPage);

Expand All @@ -40,7 +40,7 @@ const MemberRecipeList = ({ isMemberPage = false }: MemberRecipeListProps) => {

return (
<MemberRecipeListContainer>
{!isMemberPage && (
{!isPreview && (
<TotalRecipeCount color={theme.colors.gray4}>
총 <strong>{totalRecipeCount}</strong>개의 꿀조합을 남겼어요!
</TotalRecipeCount>
Expand All @@ -50,7 +50,7 @@ const MemberRecipeList = ({ isMemberPage = false }: MemberRecipeListProps) => {
{recipeToDisplay?.map((recipe) => (
<li key={recipe.id}>
<Link as={RouterLink} to={`${PATH.RECIPE}/${recipe.id}`}>
<RecipeItem recipe={recipe} isMemberPage={isMemberPage} />
<RecipeItem recipe={recipe} isMemberPage={isPreview} />
</Link>
</li>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Meta, StoryObj } from '@storybook/react';

import MemberReviewItem from './MemberReviewItem';

import ToastProvider from '@/contexts/ToastContext';

const meta: Meta<typeof MemberReviewItem> = {
title: 'members/MemberReviewItem',
component: MemberReviewItem,
decorators: [
(Story) => (
<ToastProvider>
<Story />
</ToastProvider>
),
],
args: {
review: {
reviewId: 1,
productId: 5,
productName: '구운감자슬림명란마요',
content:
'할머니가 먹을 거 같은 맛입니다. 1960년 전쟁 때 맛 보고 싶었는데 그때는 너무 가난해서 먹을 수 없었는데요 이것보다 긴 리뷰도 잘려 보인답니다',
rating: 4.0,
favoriteCount: 1256,
categoryType: 'food',
},
isMemberPage: true,
},
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};
123 changes: 123 additions & 0 deletions frontend/src/components/Members/MemberReviewItem/MemberReviewItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { useTheme, Spacing, Text, Button } from '@fun-eat/design-system';
import type { MouseEventHandler } from 'react';
import styled from 'styled-components';

import { SvgIcon } from '@/components/Common';
import { useToastActionContext } from '@/hooks/context';
import { useDeleteReview } from '@/hooks/queries/members';
import type { MemberReview } from '@/types/review';

interface MemberReviewItemProps {
review: MemberReview;
isPreview: boolean;
}

const MemberReviewItem = ({ review, isPreview }: MemberReviewItemProps) => {
const theme = useTheme();

const { mutate } = useDeleteReview();

const { toast } = useToastActionContext();

const { reviewId, productName, content, rating, favoriteCount } = review;

const handleReviewDelete: MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클릭이벤트에 어떤 동작을 막기 위해 썼나요? 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그 리뷰를 클릭하면 리뷰 상세페이지로 이동하는게 그거 막으려고 썼습니다!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리스트에 링크가 있군요..! 좋습니다 👍


const result = window.confirm('리뷰를 삭제하시겠습니까?');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if (!result) {
return;
}

mutate(reviewId, {
onSuccess: () => {
toast.success('리뷰를 삭제했습니다.');
},
onError: (error) => {
if (error instanceof Error) {
toast.error(error.message);
return;
}

toast.error('리뷰 좋아요를 다시 시도해주세요.');
},
});
};

return (
<ReviewRankingItemContainer>
<ProductNameIconWrapper>
<Text size="sm" weight="bold">
{productName}
</Text>
{!isPreview && (
<Button variant="transparent" customHeight="auto" onClick={handleReviewDelete}>
<SvgIcon variant="trashcan" width={20} height={20} />
</Button>
)}
</ProductNameIconWrapper>
<ReviewText size="sm" color={theme.textColors.info}>
{content}
</ReviewText>
<Spacing size={4} />
<FavoriteStarWrapper>
<FavoriteIconWrapper aria-label={`좋아요 ${favoriteCount}개`}>
<SvgIcon variant="favoriteFilled" color="red" width={11} height={13} />
<Text size="xs" weight="bold">
{favoriteCount}
</Text>
</FavoriteIconWrapper>
<RatingIconWrapper aria-label={`${rating.toFixed(1)}점`}>
<SvgIcon variant="star" color={theme.colors.secondary} width={16} height={16} />
<Text size="xs" weight="bold">
{rating.toFixed(1)}
</Text>
</RatingIconWrapper>
</FavoriteStarWrapper>
</ReviewRankingItemContainer>
);
};

export default MemberReviewItem;

const ReviewRankingItemContainer = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px 0;
border-bottom: ${({ theme }) => `1px solid ${theme.borderColors.disabled}`};
`;

const ProductNameIconWrapper = styled.div`
display: flex;
justify-content: space-between;
`;

const ReviewText = styled(Text)`
display: -webkit-inline-box;
text-overflow: ellipsis;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
`;

const FavoriteStarWrapper = styled.div`
display: flex;
gap: 4px;
`;

const FavoriteIconWrapper = styled.div`
display: flex;
gap: 4px;
align-items: center;
`;

const RatingIconWrapper = styled.div`
display: flex;
gap: 2px;
align-items: center;

& > svg {
padding-bottom: 2px;
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@ import { useRef } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import styled from 'styled-components';

import { ReviewRankingItem } from '@/components/Rank';
import MemberReviewItem from '../MemberReviewItem/MemberReviewItem';

import { PATH } from '@/constants/path';
import { useIntersectionObserver } from '@/hooks/common';
import { useInfiniteMemberReviewQuery } from '@/hooks/queries/members';
import useDisplaySlice from '@/utils/displaySlice';

interface MemberReviewListProps {
isMemberPage?: boolean;
isPreview?: boolean;
}

const MemberReviewList = ({ isMemberPage = false }: MemberReviewListProps) => {
const MemberReviewList = ({ isPreview = false }: MemberReviewListProps) => {
const scrollRef = useRef<HTMLDivElement>(null);
const { fetchNextPage, hasNextPage, data } = useInfiniteMemberReviewQuery();
const memberReviews = data.pages.flatMap((page) => page.reviews);
const reviewsToDisplay = useDisplaySlice(isMemberPage, memberReviews);
const reviewsToDisplay = useDisplaySlice(isPreview, memberReviews);

useIntersectionObserver<HTMLDivElement>(fetchNextPage, scrollRef, hasNextPage);

Expand All @@ -39,17 +40,17 @@ const MemberReviewList = ({ isMemberPage = false }: MemberReviewListProps) => {

return (
<MemberReviewListContainer>
{!isMemberPage && (
{!isPreview && (
<TotalReviewCount color={theme.colors.gray4}>
총 <strong>{totalReviewCount}</strong>개의 리뷰를 남겼어요!
</TotalReviewCount>
)}
<Spacing size={20} />
<MemberReviewListWrapper>
{reviewsToDisplay.map((reviewRanking) => (
<li key={reviewRanking.reviewId}>
<Link as={RouterLink} to={`${PATH.REVIEW}/${reviewRanking.reviewId}`} block>
<ReviewRankingItem reviewRanking={reviewRanking} isMemberPage />
{reviewsToDisplay.map((review) => (
<li key={review.reviewId}>
<Link as={RouterLink} to={`${PATH.REVIEW}/${review.reviewId}`} block>
<MemberReviewItem review={review} isPreview={isPreview} />
</Link>
</li>
))}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/Members/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { default as MembersInfo } from './MembersInfo/MembersInfo';
export { default as MemberReviewList } from './MemberReviewList/MemberReviewList';
export { default as MemberRecipeList } from './MemberRecipeList/MemberRecipeList';
export { default as MemberModifyInput } from './MemberModifyInput/MemberModifyInput';
export { default as MemberReviewItem } from './MemberReviewItem/MemberReviewItem';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Spacing, Text, theme } from '@fun-eat/design-system';
import { Spacing, Text, useTheme } from '@fun-eat/design-system';
import { memo } from 'react';
import styled from 'styled-components';

Expand All @@ -7,14 +7,15 @@ import type { ReviewRanking } from '@/types/ranking';

interface ReviewRankingItemProps {
reviewRanking: ReviewRanking;
isMemberPage?: boolean;
}

const ReviewRankingItem = ({ reviewRanking, isMemberPage = false }: ReviewRankingItemProps) => {
const ReviewRankingItem = ({ reviewRanking }: ReviewRankingItemProps) => {
const theme = useTheme();

const { productName, content, rating, favoriteCount } = reviewRanking;

return (
<ReviewRankingItemContainer isMemberPage={isMemberPage}>
<ReviewRankingItemContainer>
<Text size="sm" weight="bold">
{productName}
</Text>
Expand Down Expand Up @@ -42,14 +43,13 @@ const ReviewRankingItem = ({ reviewRanking, isMemberPage = false }: ReviewRankin

export default memo(ReviewRankingItem);

const ReviewRankingItemContainer = styled.div<{ isMemberPage: boolean }>`
const ReviewRankingItemContainer = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px;
border: ${({ isMemberPage, theme }) => (isMemberPage ? 'none' : `1px solid ${theme.borderColors.disabled}`)};
border-bottom: ${({ theme }) => `1px solid ${theme.borderColors.disabled}`};
border-radius: ${({ isMemberPage, theme }) => (isMemberPage ? 0 : theme.borderRadius.sm)};
border: ${({ theme }) => `1px solid ${theme.borderColors.disabled}`};
border-radius: ${({ theme }) => theme.borderRadius.sm};
`;

const ReviewText = styled(Text)`
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/queries/members/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { default as useMemberQuery } from './useMemberQuery';
export { default as useInfiniteMemberRecipeQuery } from './useInfiniteMemberRecipeQuery';
export { default as useMemberModifyMutation } from './useMemberModifyMutation';
export { default as useLogoutMutation } from './useLogoutMutation';
export { default as useDeleteReview } from './useDeleteReview';
20 changes: 20 additions & 0 deletions frontend/src/hooks/queries/members/useDeleteReview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { memberApi } from '@/apis';

const headers = { 'Content-Type': 'application/json' };

const deleteReview = async (reviewId: number) => {
return memberApi.delete({ params: `/reviews/${reviewId}`, credentials: true }, headers);
};

const useDeleteReview = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (reviewId: number) => deleteReview(reviewId),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['member', 'review'] }),
});
};

export default useDeleteReview;
4 changes: 4 additions & 0 deletions frontend/src/mocks/handlers/memberHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,8 @@ export const memberHandlers = [

return res(ctx.status(200), ctx.json(mockMemberRecipes));
}),

rest.delete('/api/members/reviews/:reviewId', (req, res, ctx) => {
return res(ctx.status(204));
}),
];
4 changes: 2 additions & 2 deletions frontend/src/pages/MemberPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ export const MemberPage = () => {
<Spacing size={5} />
<ErrorBoundary fallback={ErrorComponent} handleReset={reset}>
<Suspense fallback={<Loading />}>
<MemberReviewList isMemberPage />
<MemberReviewList isPreview />
</Suspense>
</ErrorBoundary>
<Spacing size={45} />
<NavigableSectionTitle title="내가 작성한 꿀조합" routeDestination={`${PATH.MEMBER}/recipe`} />
<ErrorBoundary fallback={ErrorComponent} handleReset={reset}>
<Suspense fallback={<Loading />}>
<MemberRecipeList isMemberPage />
<MemberRecipeList isPreview />
</Suspense>
</ErrorBoundary>
<Spacing size={40} />
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/types/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ export interface ReviewDetail extends Review {
productName: string;
}

export interface MemberReview {
reviewId: number;
productId: number;
productName: string;
content: string;
rating: number;
favoriteCount: number;
categoryType: CategoryVariant;
}

export interface ReviewTag {
tagType: TagVariants;
tags: Tag[];
Expand Down
Loading