Skip to content

Commit

Permalink
feat 봉사자 마이페이지의 봉사후기 msw 연결 (#233)
Browse files Browse the repository at this point in the history
* feat(volunteer): 마이페이지의  봉사자가 작성한 봉사후기 UI 추가

* feat(volunteer): 봉사자가 작성한 리뷰 조회 mock api 추가

* feat(volunteer): 봉사자가 작성한 봉사리뷰 조회 api 추가

* feat(volunteer): 봉사자가 작성한 봉사리뷰 조회 msw 연결

* feat(volunteer): 리뷰수정하기 추가

* feat(volunteer): 봉사자 리뷰 삭제 mock api 추가

* fix(volunteer): 봉사자 리뷰 삭제 api 타입 수정

* feat(volunteer): 봉사자 리뷰 삭제 msw 연결, 삭제 확인 모달 추가
  • Loading branch information
Eosdia authored Nov 30, 2023
1 parent c227e69 commit 5254a30
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 3 deletions.
2 changes: 1 addition & 1 deletion apps/volunteer/src/apis/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const updateVolunteerReview = (
reqeust,
);

export const deleteVolunteerReview = (reviewId: string) =>
export const deleteVolunteerReview = (reviewId: number) =>
axiosInstance.delete(`/volunteers/reviews/${reviewId}`);

export const getVolunteerReviewsOnShelter = (
Expand Down
29 changes: 28 additions & 1 deletion apps/volunteer/src/apis/volunteer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import axiosInstance from 'shared/apis/axiosInstance';

import { PageInfo } from '@/types/apis/recruitment';

export type MyInfoResponse = {
volunteerId: number;
volunteerEmail: string;
Expand Down Expand Up @@ -63,4 +65,29 @@ type ApplicantsResponse = {
export const getVolunteerApplicantList = () =>
axiosInstance.get<ApplicantsResponse>('/volunteers/applicants');

//TODO 봉사자가 작성한 후기 리스트 조회
type Pagination = {
pageSize: number;
pageNumber: number;
};

type MyReview = {
reviewId: number;
shelterId: number;
shelterName: string;
reviewCreatedAt: string;
reviewContent: string;
reviewImageUrls: string[];
};

export type MyReviewsResponse = {
pageInfo: PageInfo;
reviews: MyReview[];
};

export const getMyReviewsAPI = (page: number, size: number) =>
axiosInstance.get<MyReviewsResponse, Pagination>('/volunteers/me/reviews', {
params: {
page,
size,
},
});
36 changes: 36 additions & 0 deletions apps/volunteer/src/mocks/handlers/volunteer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { delay, http, HttpResponse } from 'msw';

const DUMMY_MYREVIEW = {
reviewId: 32,
shelterName: '해피퍼피',
shelterId: 1,
reviewCreatedAt: '2023-03-16T18:00',
reviewContent: '시설이 너무 깨끗하고 강아지도...',
reviewImageUrls: [
'https://source.unsplash.com/random',
'https://source.unsplash.com/random',
],
};

export const handlers = [
http.get('/volunteers/me', async () => {
await delay(200);
Expand All @@ -20,4 +32,28 @@ export const handlers = [
console.log(updateVolunteer);
return new HttpResponse(null, { status: 204 });
}),
http.get('/volunteers/me/reviews', async ({ request }) => {
await delay(1000);
const url = new URL(request.url);
const page = url.searchParams.get('page');

return HttpResponse.json(
{
pageInfo: {
totalElements: 30,
hasNext: page === '3' ? false : true,
},
reviews: Array.from({ length: 10 }, () => ({
...DUMMY_MYREVIEW,
reviewId: Math.random(),
})),
},
{
status: 200,
},
);
}),
http.delete('/volunteers/reviews/:reviewId', async () => {
return new HttpResponse(null, { status: 204 });
}),
];
133 changes: 133 additions & 0 deletions apps/volunteer/src/pages/my/_components/MyReviews.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { Box, Heading, Text, useDisclosure, VStack } from '@chakra-ui/react';
import {
InfiniteData,
useMutation,
useQueryClient,
useSuspenseInfiniteQuery,
} from '@tanstack/react-query';
import { AxiosResponse } from 'axios';
import { Suspense, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import AlertModal from 'shared/components/AlertModal';
import InfoSubtext from 'shared/components/InfoSubtext';
import ReviewItem from 'shared/components/ReviewItem';
import useIntersect from 'shared/hooks/useIntersection';
import { createFormattedTime } from 'shared/utils/date';

import { deleteVolunteerReview } from '@/apis/review';
import { getMyReviewsAPI, MyReviewsResponse } from '@/apis/volunteer';

function MyReviews() {
const navigate = useNavigate();

const queryClient = useQueryClient();

const { isOpen, onOpen, onClose } = useDisclosure();

const [deleteReviewId, setDeleteReviewId] = useState(0);

const {
data: { pages },
hasNextPage,
isFetchingNextPage,
fetchNextPage,
} = useSuspenseInfiniteQuery({
queryKey: ['myreviews'],
queryFn: async ({ pageParam }) =>
(await getMyReviewsAPI(pageParam, 10)).data,
initialPageParam: 1,
getNextPageParam: ({ pageInfo }, _, lastPageParam) =>
pageInfo.hasNext ? lastPageParam + 1 : null,
});

const reviews = pages.flatMap((item) => item.reviews);

const ref = useIntersect(async (entry, observer) => {
observer.unobserve(entry.target);
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
});

const deleteReveiw = useMutation({
mutationFn: async (reviewId: number) =>
await deleteVolunteerReview(reviewId),
onSuccess: (_, reviewId) => {
queryClient.setQueryData(
['myreviews'],
(data: InfiniteData<AxiosResponse<MyReviewsResponse>>) => ({
...data,
pages: data.pages.map((page) => ({
...page,
reviews: reviews.filter((review) => review.reviewId !== reviewId),
})),
}),
);
setDeleteReviewId(0);
onClose();
},
});

const openDeleteModal = (reviewId: number) => {
onOpen();
setDeleteReviewId(reviewId);
};

return (
<Box minH={500}>
<Heading fontWeight={600} fontSize="md" py={4}>
작성한 후기 {pages[0].pageInfo.totalElements}
</Heading>
<VStack spacing={2}>
{reviews.map((review) => {
const { reviewId, shelterId } = review;
return (
<ReviewItem
key={reviewId}
showMenuButton={true}
content={review.reviewContent}
images={review.reviewImageUrls}
onUpdate={() =>
navigate(`/shelters/${shelterId}/reviews/write/${reviewId}`)
}
onDelete={() => openDeleteModal(reviewId)}
>
<Box>
<Text fontWeight={600} mb={2}>
{review.shelterName}
</Text>
<InfoSubtext
title="작성일"
content={createFormattedTime(
new Date(review.reviewCreatedAt),
'YY.MM.DD',
)}
/>
</Box>
</ReviewItem>
);
})}
</VStack>
<div ref={ref} />
<AlertModal
modalTitle="리뷰 삭제"
modalContent="리뷰를 삭제하시겠어요?"
btnTitle="삭제하기"
isOpen={isOpen}
onClose={() => {
setDeleteReviewId(0);
onClose();
}}
onClick={() => deleteReveiw.mutate(deleteReviewId)}
/>
</Box>
);
}

export default function MyReviewsTab() {
return (
<Suspense fallback={<p>'로딩 중 입니다..'</p>}>
<MyReviews />
</Suspense>
);
}
3 changes: 2 additions & 1 deletion apps/volunteer/src/pages/my/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Label from 'shared/components/Label';
import ProfileInfo from 'shared/components/ProfileInfo';
import Tabs from 'shared/components/Tabs';

import MyReviewsTab from './_components/MyReviews';
import useFetchMyVolunteer from './_hooks/useFetchMyVolunteer';

export default function MyPage() {
Expand Down Expand Up @@ -38,7 +39,7 @@ export default function MyPage() {
<Tabs
tabs={[
['신청한 봉사 목록', <Box key={1} minH={500} />],
['작성한 봉사 후기', <Box key={2} minH={500} />],
['작성한 봉사 후기', <MyReviewsTab key={2} />],
]}
/>
</Box>
Expand Down

0 comments on commit 5254a30

Please sign in to comment.