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 4 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
@@ -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;
isMemberPage: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

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

회원 리뷰면 이 prop은 없어도 될거 같아요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

이게 마이페이지에서 리뷰 전체 목록을 눌렀을 때만 삭제 아이콘이 뜨게끔 하기 위해서 저 prop을 추가한거에요! 만약 네이밍이 헷갈리다면 isMemberReviewListPage 이런식으로 바꿀까요??

Copy link
Collaborator

Choose a reason for hiding this comment

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

타미 의견도 좋고 미리보기를 기준으로 한다면 isPreview도 괜찮아보여요.!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

d990fe1
수정했슴다~

}

const MemberReviewItem = ({ review, isMemberPage }: 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>
{!isMemberPage && (
<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,7 +3,8 @@ 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';
Expand Down Expand Up @@ -46,10 +47,10 @@ const MemberReviewList = ({ isMemberPage = false }: MemberReviewListProps) => {
)}
<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} isMemberPage={isMemberPage} />
</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));
}),
];
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