diff --git a/frontend/.storybook/preview-body.html b/frontend/.storybook/preview-body.html
index 1461bde42..46ce41c44 100644
--- a/frontend/.storybook/preview-body.html
+++ b/frontend/.storybook/preview-body.html
@@ -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"
/>
+
+
+
diff --git a/frontend/src/components/Common/Svg/SvgIcon.tsx b/frontend/src/components/Common/Svg/SvgIcon.tsx
index 0705d543a..e31256ae6 100644
--- a/frontend/src/components/Common/Svg/SvgIcon.tsx
+++ b/frontend/src/components/Common/Svg/SvgIcon.tsx
@@ -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];
diff --git a/frontend/src/components/Common/Svg/SvgSprite.tsx b/frontend/src/components/Common/Svg/SvgSprite.tsx
index 450c870be..e0fa06dd5 100644
--- a/frontend/src/components/Common/Svg/SvgSprite.tsx
+++ b/frontend/src/components/Common/Svg/SvgSprite.tsx
@@ -86,6 +86,9 @@ const SvgSprite = () => {
+
+
+
);
};
diff --git a/frontend/src/components/Members/MemberRecipeList/MemberRecipeList.tsx b/frontend/src/components/Members/MemberRecipeList/MemberRecipeList.tsx
index 14558353f..740daddff 100644
--- a/frontend/src/components/Members/MemberRecipeList/MemberRecipeList.tsx
+++ b/frontend/src/components/Members/MemberRecipeList/MemberRecipeList.tsx
@@ -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(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(fetchNextPage, scrollRef, hasNextPage);
@@ -40,7 +40,7 @@ const MemberRecipeList = ({ isMemberPage = false }: MemberRecipeListProps) => {
return (
- {!isMemberPage && (
+ {!isPreview && (
총 {totalRecipeCount}개의 꿀조합을 남겼어요!
@@ -50,7 +50,7 @@ const MemberRecipeList = ({ isMemberPage = false }: MemberRecipeListProps) => {
{recipeToDisplay?.map((recipe) => (
-
+
))}
diff --git a/frontend/src/components/Members/MemberReviewItem/MemberReviewItem.stories.tsx b/frontend/src/components/Members/MemberReviewItem/MemberReviewItem.stories.tsx
new file mode 100644
index 000000000..89827a62c
--- /dev/null
+++ b/frontend/src/components/Members/MemberReviewItem/MemberReviewItem.stories.tsx
@@ -0,0 +1,35 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import MemberReviewItem from './MemberReviewItem';
+
+import ToastProvider from '@/contexts/ToastContext';
+
+const meta: Meta = {
+ title: 'members/MemberReviewItem',
+ component: MemberReviewItem,
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ args: {
+ review: {
+ reviewId: 1,
+ productId: 5,
+ productName: '구운감자슬림명란마요',
+ content:
+ '할머니가 먹을 거 같은 맛입니다. 1960년 전쟁 때 맛 보고 싶었는데 그때는 너무 가난해서 먹을 수 없었는데요 이것보다 긴 리뷰도 잘려 보인답니다',
+ rating: 4.0,
+ favoriteCount: 1256,
+ categoryType: 'food',
+ },
+ isMemberPage: true,
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/frontend/src/components/Members/MemberReviewItem/MemberReviewItem.tsx b/frontend/src/components/Members/MemberReviewItem/MemberReviewItem.tsx
new file mode 100644
index 000000000..1d4503853
--- /dev/null
+++ b/frontend/src/components/Members/MemberReviewItem/MemberReviewItem.tsx
@@ -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 = (e) => {
+ e.preventDefault();
+
+ const result = window.confirm('리뷰를 삭제하시겠습니까?');
+ if (!result) {
+ return;
+ }
+
+ mutate(reviewId, {
+ onSuccess: () => {
+ toast.success('리뷰를 삭제했습니다.');
+ },
+ onError: (error) => {
+ if (error instanceof Error) {
+ toast.error(error.message);
+ return;
+ }
+
+ toast.error('리뷰 좋아요를 다시 시도해주세요.');
+ },
+ });
+ };
+
+ return (
+
+
+
+ {productName}
+
+ {!isPreview && (
+
+ )}
+
+
+ {content}
+
+
+
+
+
+
+ {favoriteCount}
+
+
+
+
+
+ {rating.toFixed(1)}
+
+
+
+
+ );
+};
+
+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;
+ }
+`;
diff --git a/frontend/src/components/Members/MemberReviewList/MemberReviewList.tsx b/frontend/src/components/Members/MemberReviewList/MemberReviewList.tsx
index fde211413..b622d9f65 100644
--- a/frontend/src/components/Members/MemberReviewList/MemberReviewList.tsx
+++ b/frontend/src/components/Members/MemberReviewList/MemberReviewList.tsx
@@ -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(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(fetchNextPage, scrollRef, hasNextPage);
@@ -39,17 +40,17 @@ const MemberReviewList = ({ isMemberPage = false }: MemberReviewListProps) => {
return (
- {!isMemberPage && (
+ {!isPreview && (
총 {totalReviewCount}개의 리뷰를 남겼어요!
)}
- {reviewsToDisplay.map((reviewRanking) => (
-
-
-
+ {reviewsToDisplay.map((review) => (
+
+
+
))}
diff --git a/frontend/src/components/Members/index.ts b/frontend/src/components/Members/index.ts
index a295e2728..4e31460ee 100644
--- a/frontend/src/components/Members/index.ts
+++ b/frontend/src/components/Members/index.ts
@@ -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';
diff --git a/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.tsx b/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.tsx
index c2004c20e..ca2aed3dc 100644
--- a/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.tsx
+++ b/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.tsx
@@ -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';
@@ -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 (
-
+
{productName}
@@ -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)`
diff --git a/frontend/src/hooks/queries/members/index.ts b/frontend/src/hooks/queries/members/index.ts
index 9e45e5239..cbd6e7468 100644
--- a/frontend/src/hooks/queries/members/index.ts
+++ b/frontend/src/hooks/queries/members/index.ts
@@ -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';
diff --git a/frontend/src/hooks/queries/members/useDeleteReview.ts b/frontend/src/hooks/queries/members/useDeleteReview.ts
new file mode 100644
index 000000000..a88169bce
--- /dev/null
+++ b/frontend/src/hooks/queries/members/useDeleteReview.ts
@@ -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;
diff --git a/frontend/src/mocks/handlers/memberHandlers.ts b/frontend/src/mocks/handlers/memberHandlers.ts
index 2abb3d7fb..31ee93536 100644
--- a/frontend/src/mocks/handlers/memberHandlers.ts
+++ b/frontend/src/mocks/handlers/memberHandlers.ts
@@ -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));
+ }),
];
diff --git a/frontend/src/pages/MemberPage.tsx b/frontend/src/pages/MemberPage.tsx
index 7026e4768..58aa6d8e4 100644
--- a/frontend/src/pages/MemberPage.tsx
+++ b/frontend/src/pages/MemberPage.tsx
@@ -20,14 +20,14 @@ export const MemberPage = () => {
}>
-
+
}>
-
+
diff --git a/frontend/src/types/review.ts b/frontend/src/types/review.ts
index ced1d2f58..dd0273ddf 100644
--- a/frontend/src/types/review.ts
+++ b/frontend/src/types/review.ts
@@ -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[];