-
Notifications
You must be signed in to change notification settings - Fork 0
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
[FE] 리뷰 삭제 기능 구현 #780
Changes from 4 commits
de8ec76
b99cb90
9d4f85c
28cf569
d990fe1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 = {}; |
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; | ||
} | ||
|
||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 클릭이벤트에 어떤 동작을 막기 위해 썼나요? 궁금합니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그 리뷰를 클릭하면 리뷰 상세페이지로 이동하는게 그거 막으려고 썼습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 리스트에 링크가 있군요..! 좋습니다 👍 |
||
|
||
const result = window.confirm('리뷰를 삭제하시겠습니까?'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
@@ -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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
회원 리뷰면 이 prop은 없어도 될거 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이게 마이페이지에서 리뷰 전체 목록을 눌렀을 때만 삭제 아이콘이 뜨게끔 하기 위해서 저 prop을 추가한거에요! 만약 네이밍이 헷갈리다면
isMemberReviewListPage
이런식으로 바꿀까요??There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
타미 의견도 좋고 미리보기를 기준으로 한다면 isPreview도 괜찮아보여요.!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
d990fe1
수정했슴다~