From 26796f91ebc99c8ccffec5ca3da701f099d388b2 Mon Sep 17 00:00:00 2001
From: Hyejin Yang
Date: Wed, 29 Nov 2023 19:10:37 +0900
Subject: [PATCH 01/11] =?UTF-8?q?feat(volunteer):=20=EB=B4=89=EC=82=AC=20?=
=?UTF-8?q?=ED=9B=84=EA=B8=B0=20=EC=88=98=EC=A0=95=20=ED=8E=98=EC=9D=B4?=
=?UTF-8?q?=EC=A7=80=20=EC=A7=84=EC=9E=85=20=EC=8B=9C,=20=EA=B8=B0?=
=?UTF-8?q?=EC=A1=B4=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20form=20=EC=97=90=20?=
=?UTF-8?q?=EB=84=A3=EC=96=B4=EC=A3=BC=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?=
=?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20msw=20=EC=97=B0=EA=B2=B0=20(#2?=
=?UTF-8?q?29)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(volunteer): getVolunteerReviewDetail 함수의 api 요청 url 수정
* fix(volunteer): reviewDetailResponse type 수정
* feat(volunteer): 봉사 리뷰 상세조회 mock API 추가
* feat(volunteer): useUploadPhoto hook 에 setImageUrls 함수 추가
* feat(volunteer): 봉사 후기 수정 페이지 진입 시, 기존 후기 데이터를input 에 set 해주는 기능 추가
* fix(volunteer): 사용하지 않는 변수 제거
---
apps/volunteer/src/apis/review.ts | 2 +-
apps/volunteer/src/mocks/handlers/review.ts | 17 ++++++++-
.../pages/shelters/reviews/update/index.tsx | 38 +++++++++++++++++--
apps/volunteer/src/types/apis/review.ts | 4 +-
packages/shared/hooks/useUploadPhoto.ts | 11 ++++++
5 files changed, 64 insertions(+), 8 deletions(-)
diff --git a/apps/volunteer/src/apis/review.ts b/apps/volunteer/src/apis/review.ts
index e69c0930..3c7a446d 100644
--- a/apps/volunteer/src/apis/review.ts
+++ b/apps/volunteer/src/apis/review.ts
@@ -9,7 +9,7 @@ import {
} from '@/types/apis/review';
export const getVolunteerReviewDetail = (reviewId: number) =>
- axiosInstance.get(`/reviews/${reviewId}`);
+ axiosInstance.get(`/volunteers/reviews/${reviewId}`);
export const createVolunteerReview = (reqeust: ReviewCreateRequest) =>
axiosInstance.post(
diff --git a/apps/volunteer/src/mocks/handlers/review.ts b/apps/volunteer/src/mocks/handlers/review.ts
index 9727882b..9f9df04a 100644
--- a/apps/volunteer/src/mocks/handlers/review.ts
+++ b/apps/volunteer/src/mocks/handlers/review.ts
@@ -5,8 +5,23 @@ export const handlers = [
await delay(200);
return HttpResponse.json(null, { status: 200 });
}),
- http.put('/volunteers/reviews/:id', async () => {
+ http.patch('/volunteers/reviews/:id', async () => {
await delay(200);
return HttpResponse.json(null, { status: 200 });
}),
+ http.get('/volunteers/reviews/:id', async () => {
+ await delay(200);
+ return HttpResponse.json(
+ {
+ reviewId: 1,
+ reviewContent: `정말 강아지들이 귀여워서 봉사할 맛이 났어요!\n유기견 입양 한다면 꼭 여기서 입양할 거에요!`,
+ reviewImageUrls: [
+ 'https://source.unsplash.com/random/1',
+ 'https://source.unsplash.com/random/2',
+ 'https://source.unsplash.com/random/3',
+ ],
+ },
+ { status: 200 },
+ );
+ }),
];
diff --git a/apps/volunteer/src/pages/shelters/reviews/update/index.tsx b/apps/volunteer/src/pages/shelters/reviews/update/index.tsx
index 2b7ecdca..dc4e881b 100644
--- a/apps/volunteer/src/pages/shelters/reviews/update/index.tsx
+++ b/apps/volunteer/src/pages/shelters/reviews/update/index.tsx
@@ -10,13 +10,14 @@ import {
} from '@chakra-ui/react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQuery } from '@tanstack/react-query';
+import { useCallback, useEffect } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import EditPhotoList from 'shared/components/EditPhotoList';
import ProfileInfo from 'shared/components/ProfileInfo';
import { useUploadPhoto } from 'shared/hooks/useUploadPhoto';
-import { updateVolunteerReview } from '@/apis/review';
+import { getVolunteerReviewDetail, updateVolunteerReview } from '@/apis/review';
import { getSimpleShelterProfile } from '@/apis/shelter';
import PATH from '@/constants/path';
import ReviewSubmitButton from '@/pages/shelters/reviews/_components/ReviewSubmitButton';
@@ -40,7 +41,7 @@ export default function SheltersReviewsUpdatePage() {
navigate(-1);
}
- const { data } = useQuery({
+ const { data: shelterInfo } = useQuery({
queryKey: ['shelterProfile', Number(shelterId)],
queryFn: async () => {
return (await getSimpleShelterProfile(Number(shelterId))).data;
@@ -53,6 +54,18 @@ export default function SheltersReviewsUpdatePage() {
},
});
+ const { data: reviewDetail } = useQuery({
+ queryKey: ['review', 'detail', Number(reviewId)],
+ queryFn: async () => {
+ return (await getVolunteerReviewDetail(Number(reviewId))).data;
+ },
+ initialData: {
+ reviewId: 0,
+ reviewContent: '',
+ reviewImageUrls: [],
+ },
+ });
+
const { mutate, isPending } = useMutation({
mutationFn: ({
reviewId,
@@ -66,22 +79,39 @@ export default function SheltersReviewsUpdatePage() {
},
});
- const { shelterName, shelterImageUrl, shelterAddress, shelterEmail } = data;
+ const { shelterName, shelterImageUrl, shelterAddress, shelterEmail } =
+ shelterInfo;
const {
register,
handleSubmit,
watch,
+ setValue,
+ setFocus,
formState: { errors },
} = useForm({
resolver: zodResolver(reviewSchema),
});
- const { photos, handleUploadPhoto, handleDeletePhoto } =
+ const { photos, setImageUrls, handleUploadPhoto, handleDeletePhoto } =
useUploadPhoto(UPLOAD_LIMIT);
const contentLength = watch('content')?.length ?? 0;
+ const setReviewFormvalues = useCallback(
+ (content: string, imageUrls: string[]) => {
+ setValue('content', content);
+ setImageUrls(imageUrls);
+ },
+ [],
+ );
+
+ useEffect(() => {
+ const { reviewContent, reviewImageUrls } = reviewDetail;
+ setReviewFormvalues(reviewContent, reviewImageUrls);
+ setFocus('content');
+ }, [reviewDetail, setReviewFormvalues, setFocus]);
+
const onSubmit: SubmitHandler = (data: ReviewSchema) => {
const { content } = data;
diff --git a/apps/volunteer/src/types/apis/review.ts b/apps/volunteer/src/types/apis/review.ts
index 222c7533..0ba3264a 100644
--- a/apps/volunteer/src/types/apis/review.ts
+++ b/apps/volunteer/src/types/apis/review.ts
@@ -10,8 +10,8 @@ export type Pagination = {
export type ReviewDetailResponse = {
reviewId: number;
- content: string;
- imageUrls: string[];
+ reviewContent: string;
+ reviewImageUrls: string[];
};
export type ReviewCreateRequest = {
diff --git a/packages/shared/hooks/useUploadPhoto.ts b/packages/shared/hooks/useUploadPhoto.ts
index 9546a51f..4f68817b 100644
--- a/packages/shared/hooks/useUploadPhoto.ts
+++ b/packages/shared/hooks/useUploadPhoto.ts
@@ -66,6 +66,16 @@ export const useUploadPhoto = (uploadLimit: number) => {
const toast = useToast();
+ const setImageUrls = (imageUrls: string[]) => {
+ const newPhotos: Photo[] =
+ imageUrls.map((url) => ({
+ id: getRandomId(),
+ url,
+ })) || [];
+
+ setPhotos(newPhotos);
+ };
+
const handleUploadPhoto = (files: FileList | null) => {
if (!files) {
return;
@@ -110,6 +120,7 @@ export const useUploadPhoto = (uploadLimit: number) => {
return {
photos,
+ setImageUrls,
handleUploadPhoto,
handleDeletePhoto,
};
From 34ecce65f9d95178af506094a0d6b22cf8d0a232 Mon Sep 17 00:00:00 2001
From: Hyejin Yang
Date: Thu, 30 Nov 2023 14:11:42 +0900
Subject: [PATCH 02/11] =?UTF-8?q?fix(shared):=20global=20theme=20=EC=9D=98?=
=?UTF-8?q?=20overscrollbehavior=20css=20=EC=86=8D=EC=84=B1=EC=9D=84=20emo?=
=?UTF-8?q?tion=20=EC=9D=84=20=EC=B9=B4=EB=A9=9C=20=EC=BC=80=EC=9D=B4?=
=?UTF-8?q?=EC=8A=A4=EB=A1=9C=20=EC=88=98=EC=A0=95=20(#235)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/shared/theme/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/shared/theme/index.ts b/packages/shared/theme/index.ts
index 2e975dbf..1b305036 100644
--- a/packages/shared/theme/index.ts
+++ b/packages/shared/theme/index.ts
@@ -8,7 +8,7 @@ const theme = extendTheme({
styles: {
global: {
body: {
- 'overscroll-behavior': 'none',
+ overscrollBehavior: 'none',
},
},
},
From a0031ab5080afa8baa52bbfe68ac62d28d4f57e0 Mon Sep 17 00:00:00 2001
From: Hyejin Yang
Date: Thu, 30 Nov 2023 15:22:02 +0900
Subject: [PATCH 03/11] =?UTF-8?q?feat(shelter):=20=EB=B4=89=EC=82=AC?=
=?UTF-8?q?=EC=9E=90=20=EB=AA=A8=EC=A7=91=EA=B8=80=20=EB=A6=AC=EC=8A=A4?=
=?UTF-8?q?=ED=8A=B8=EC=97=90=EC=84=9C=20=EC=95=84=EC=9D=B4=ED=85=9C=20?=
=?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=8B=9C,=20=EC=BA=90=EC=8B=B1=EB=90=9C=20?=
=?UTF-8?q?=EC=84=9C=EB=B2=84=20=EC=83=81=ED=85=8C=20=EC=97=85=EB=8D=B0?=
=?UTF-8?q?=EC=9D=B4=ED=8A=B8=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?=
=?UTF-8?q?=EC=B6=94=EA=B0=80=20(#232)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(shelter): 봉사자 모집 아이템 수정 또는 삭제 시, 캐싱된 서버 데이터 업데이트하는 로직 추가
* feat(shelter): 봉사 모집글 리스트 Mock API 의 response 로 항상 다른 응답이 가는 기능 추가
---
.../shelter/src/mocks/handlers/recruitment.ts | 39 +++++++-------
.../_hooks/useVolunteerRecruitItem.ts | 54 ++++++++++++++++++-
2 files changed, 73 insertions(+), 20 deletions(-)
diff --git a/apps/shelter/src/mocks/handlers/recruitment.ts b/apps/shelter/src/mocks/handlers/recruitment.ts
index 057c76a4..79ca0e06 100644
--- a/apps/shelter/src/mocks/handlers/recruitment.ts
+++ b/apps/shelter/src/mocks/handlers/recruitment.ts
@@ -1,22 +1,25 @@
import { delay, http, HttpResponse } from 'msw';
-const DUMMY_RECRUITMENT = {
- recruitmentId: 1,
- recruitmentTitle: '봉사자를 모집합니다',
- recruitmentStartTime: '2021-11-08T11:44:30.327959',
- recruitmentEndTime: '2021-11-08T11:44:30.327959',
- recruitmentDeadline: '2023-11-20T11:44:30.327959',
- recruitmentIsClosed: false,
- recruitmentApplicantCount: 15,
- recruitmentCapacity: 15,
-};
+const randomDate = (start: Date, end: Date) =>
+ new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
-// eslint-disable-next-line
-// @ts-ignore
-const DUMMY_RECRUITMENT_LIST = Array.from(
- { length: 4 },
- () => DUMMY_RECRUITMENT,
-);
+const currentDate = new Date();
+const startDate = new Date(currentDate);
+const endDate = new Date(currentDate);
+startDate.setDate(currentDate.getDate() - 30);
+endDate.setDate(endDate.getDate() + 30);
+
+const getRandomDummyRecruitment = () => ({
+ recruitmentId: Number(String(Math.random()).slice(2)),
+ shelterId: Number(String(Math.random()).slice(2)),
+ recruitmentTitle: '봉사자를 모집합니다',
+ recruitmentStartTime: randomDate(startDate, endDate),
+ recruitmentEndTime: randomDate(startDate, endDate),
+ recruitmentDeadline: randomDate(startDate, endDate),
+ recruitmentIsClosed: Boolean(Math.floor(Math.random() * 2)),
+ recruitmentApplicantCount: Math.floor(Math.random() * 15),
+ recruitmentCapacity: 20,
+});
export const DUMMY_APPLICANT = {
applicantId: 10,
@@ -44,9 +47,7 @@ export const handlers = [
hasNext: true,
},
recruitments: Array.from({ length: 4 }, () => ({
- ...DUMMY_RECRUITMENT,
- recruitmentId: Math.random(),
- shelterId: Math.random(),
+ ...getRandomDummyRecruitment(),
})),
},
{ status: 200 },
diff --git a/apps/shelter/src/pages/volunteers/_hooks/useVolunteerRecruitItem.ts b/apps/shelter/src/pages/volunteers/_hooks/useVolunteerRecruitItem.ts
index 82335c35..5208e17e 100644
--- a/apps/shelter/src/pages/volunteers/_hooks/useVolunteerRecruitItem.ts
+++ b/apps/shelter/src/pages/volunteers/_hooks/useVolunteerRecruitItem.ts
@@ -1,5 +1,10 @@
import { useDisclosure } from '@chakra-ui/react';
-import { useMutation } from '@tanstack/react-query';
+import {
+ InfiniteData,
+ useMutation,
+ useQueryClient,
+} from '@tanstack/react-query';
+import { AxiosResponse } from 'axios';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -7,6 +12,7 @@ import {
closeShelterRecruitment,
deleteShelterRecruitment,
} from '@/apis/recruitment';
+import { RecruitementsResponse, Recruitment } from '@/types/apis/recruitment';
export const useVolunteerRecruitItem = () => {
const navigate = useNavigate();
@@ -19,11 +25,42 @@ export const useVolunteerRecruitItem = () => {
onClick: () => {},
});
+ const queryClient = useQueryClient();
+
+ const getNewPages = (
+ data: InfiniteData>,
+ processRecruitments: (recruitments: Recruitment[]) => Recruitment[],
+ ) =>
+ data.pages.map((page) => ({
+ ...page,
+ data: {
+ pageInfo: page.data.pageInfo,
+ recruitments: processRecruitments(page.data.recruitments),
+ },
+ }));
+
const closeRecruitment = useMutation({
mutationFn: async (recruitmentId: number) => {
await closeShelterRecruitment(recruitmentId);
onClose();
},
+ onSuccess: (_, recruitmentId) => {
+ queryClient.setQueryData(
+ ['recruitments'],
+ (data: InfiniteData>) => {
+ return {
+ pages: getNewPages(data, (recruitments) =>
+ recruitments.map((recruitment) =>
+ recruitment.recruitmentId === recruitmentId
+ ? { ...recruitment, recruitmentIsClosed: true }
+ : recruitment,
+ ),
+ ),
+ pageParams: data.pageParams,
+ };
+ },
+ );
+ },
});
const deleteRecruitment = useMutation({
@@ -31,6 +68,21 @@ export const useVolunteerRecruitItem = () => {
await deleteShelterRecruitment(recruitmentId);
onClose();
},
+ onSuccess: (_, recruitmentId) => {
+ queryClient.setQueryData(
+ ['recruitments'],
+ (data: InfiniteData>) => {
+ return {
+ pages: getNewPages(data, (recruitments) =>
+ recruitments.filter(
+ (recruitment) => recruitment.recruitmentId !== recruitmentId,
+ ),
+ ),
+ pageParams: data.pageParams,
+ };
+ },
+ );
+ },
});
const goVolunteersDetail = (recruitmentId: number) => {
From c227e695bfc1481482d2be0541c7e6f7785f87c6 Mon Sep 17 00:00:00 2001
From: Hyejin Yang
Date: Thu, 30 Nov 2023 15:27:56 +0900
Subject: [PATCH 04/11] =?UTF-8?q?feat(shelter):=20=EB=B4=89=EC=82=AC?=
=?UTF-8?q?=EC=9E=90=20=EB=AA=A8=EC=A7=91=EA=B8=80=20=EC=88=98=EC=A0=95=20?=
=?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A7=84=EC=9E=85=20=EC=8B=9C,?=
=?UTF-8?q?=20=EA=B8=B0=EC=A1=B4=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=A6=AC?=
=?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B3=B4=EC=97=AC=EC=A3=BC=EB=8A=94=20?=
=?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#231)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(shelter): 이미지 업로드 mock API 추가
* feat(shelter): 봉사 모집글 작성을 위한 RecruitmentDetail 타입 정의
* feat(shelter): 봉사자 모집글 수정페이지에서 기존 이미지 리스트 보여주는 기능 추가
---
apps/shelter/src/mocks/browser.ts | 6 ++--
apps/shelter/src/mocks/handlers/image.ts | 14 +++++++++
.../detail/_hooks/useGetVolunteerDetail.ts | 18 ++++++++++-
.../src/pages/volunteers/update/index.tsx | 30 ++++++++++++-------
4 files changed, 55 insertions(+), 13 deletions(-)
create mode 100644 apps/shelter/src/mocks/handlers/image.ts
diff --git a/apps/shelter/src/mocks/browser.ts b/apps/shelter/src/mocks/browser.ts
index 3808427e..12e74902 100644
--- a/apps/shelter/src/mocks/browser.ts
+++ b/apps/shelter/src/mocks/browser.ts
@@ -1,6 +1,7 @@
import { setupWorker } from 'msw/browser';
import { handlers as authHandlers } from './handlers/auth';
+import { handlers as imageHandlers } from './handlers/image';
import { handlers as manageHandlers } from './handlers/manage';
import { handlers as recruitmentHandler } from './handlers/recruitment';
import { handlers as recruitmentDetailHandler } from './handlers/recruitmentDetail';
@@ -9,9 +10,10 @@ import { handlers as volunteerHandlers } from './handlers/volunteers';
export const worker = setupWorker(
...authHandlers,
- ...shelterHandlers,
+ ...imageHandlers,
+ ...manageHandlers,
...recruitmentHandler,
...recruitmentDetailHandler,
- ...manageHandlers,
+ ...shelterHandlers,
...volunteerHandlers,
);
diff --git a/apps/shelter/src/mocks/handlers/image.ts b/apps/shelter/src/mocks/handlers/image.ts
new file mode 100644
index 00000000..d1284b5c
--- /dev/null
+++ b/apps/shelter/src/mocks/handlers/image.ts
@@ -0,0 +1,14 @@
+import { delay, http, HttpResponse } from 'msw';
+
+export const handlers = [
+ http.post('/images', async () => {
+ await delay(200);
+
+ return HttpResponse.json(
+ {
+ imageUrls: ['https://source.unsplash.com/random'],
+ },
+ { status: 200 },
+ );
+ }),
+];
diff --git a/apps/shelter/src/pages/volunteers/detail/_hooks/useGetVolunteerDetail.ts b/apps/shelter/src/pages/volunteers/detail/_hooks/useGetVolunteerDetail.ts
index 95172ad3..3edd91f3 100644
--- a/apps/shelter/src/pages/volunteers/detail/_hooks/useGetVolunteerDetail.ts
+++ b/apps/shelter/src/pages/volunteers/detail/_hooks/useGetVolunteerDetail.ts
@@ -4,7 +4,23 @@ import {
RecruitmentDetailResponse,
} from 'shared/apis/common/Recruitments';
-const createRecruitmentDetail = (recruitment: RecruitmentDetailResponse) => {
+export type RecruitmentDetail = {
+ title: string;
+ content: string;
+ applicant: number;
+ capacity: number;
+ startTime: string;
+ endTime: string;
+ deadline: string;
+ createdAt: string;
+ updatedAt: string;
+ imageUrls: string[];
+ isClosed: boolean;
+};
+
+const createRecruitmentDetail = (
+ recruitment: RecruitmentDetailResponse,
+): RecruitmentDetail => {
const {
recruitmentTitle: title,
recruitmentContent: content,
diff --git a/apps/shelter/src/pages/volunteers/update/index.tsx b/apps/shelter/src/pages/volunteers/update/index.tsx
index af0ee44b..c1365c9f 100644
--- a/apps/shelter/src/pages/volunteers/update/index.tsx
+++ b/apps/shelter/src/pages/volunteers/update/index.tsx
@@ -13,7 +13,7 @@ import {
} from '@chakra-ui/react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { useEffect } from 'react';
+import { useCallback, useEffect } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import EditPhotoList from 'shared/components/EditPhotoList';
@@ -23,7 +23,9 @@ import * as z from 'zod';
import { updateShelterRecruitment } from '@/apis/recruitment';
import type { RecruitmentUpdateRequest } from '@/types/apis/recruitment';
-import useGetVolunteerDetail from '../detail/_hooks/useGetVolunteerDetail';
+import useGetVolunteerDetail, {
+ RecruitmentDetail,
+} from '../detail/_hooks/useGetVolunteerDetail';
const recruitmentSchema = z
.object({
@@ -74,7 +76,7 @@ export default function VolunteersUpdatePage() {
} = useForm({
resolver: zodResolver(recruitmentSchema),
});
- const { photos, handleUploadPhoto, handleDeletePhoto } =
+ const { photos, setImageUrls, handleUploadPhoto, handleDeletePhoto } =
useUploadPhoto(UPLOAD_LIMIT);
const contentLength = watch('content')?.length ?? 0;
@@ -118,15 +120,23 @@ export default function VolunteersUpdatePage() {
});
};
+ const setVolunteersRecruitmentFormvalues = useCallback(
+ (recruitment: RecruitmentDetail) => {
+ setValue('title', recruitment.title);
+ setValue('startTime', new Date(recruitment.startTime));
+ setValue('endTime', new Date(recruitment.endTime));
+ setValue('deadline', new Date(recruitment.deadline));
+ setValue('capacity', recruitment.capacity);
+ setValue('content', recruitment?.content ?? '');
+ setImageUrls(recruitment.imageUrls);
+ },
+ [],
+ );
+
useEffect(() => {
- setValue('title', recruitment.title);
- setValue('startTime', new Date(recruitment.startTime));
- setValue('endTime', new Date(recruitment.endTime));
- setValue('deadline', new Date(recruitment.deadline));
- setValue('capacity', recruitment.capacity);
- setValue('content', recruitment?.content ?? '');
setFocus('title');
- }, [recruitment, setFocus, setValue]);
+ setVolunteersRecruitmentFormvalues(recruitment);
+ }, [recruitment, setVolunteersRecruitmentFormvalues, setFocus]);
if (isRecruitFetchLoading) {
return ...로딩중
;
From 5254a300ffb6930405c4a430e4115a9e773a63de Mon Sep 17 00:00:00 2001
From: woo hyeonji <117665863+Eosdia@users.noreply.github.com>
Date: Thu, 30 Nov 2023 15:56:46 +0900
Subject: [PATCH 05/11] =?UTF-8?q?feat=20=EB=B4=89=EC=82=AC=EC=9E=90=20?=
=?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=EC=9D=98=20?=
=?UTF-8?q?=EB=B4=89=EC=82=AC=ED=9B=84=EA=B8=B0=20msw=20=EC=97=B0=EA=B2=B0?=
=?UTF-8?q?=20(#233)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 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 연결, 삭제 확인 모달 추가
---
apps/volunteer/src/apis/review.ts | 2 +-
apps/volunteer/src/apis/volunteer.ts | 29 +++-
.../volunteer/src/mocks/handlers/volunteer.ts | 36 +++++
.../src/pages/my/_components/MyReviews.tsx | 133 ++++++++++++++++++
apps/volunteer/src/pages/my/index.tsx | 3 +-
5 files changed, 200 insertions(+), 3 deletions(-)
create mode 100644 apps/volunteer/src/pages/my/_components/MyReviews.tsx
diff --git a/apps/volunteer/src/apis/review.ts b/apps/volunteer/src/apis/review.ts
index 3c7a446d..a5e0fd18 100644
--- a/apps/volunteer/src/apis/review.ts
+++ b/apps/volunteer/src/apis/review.ts
@@ -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 = (
diff --git a/apps/volunteer/src/apis/volunteer.ts b/apps/volunteer/src/apis/volunteer.ts
index 01f9a251..e69e522c 100644
--- a/apps/volunteer/src/apis/volunteer.ts
+++ b/apps/volunteer/src/apis/volunteer.ts
@@ -1,5 +1,7 @@
import axiosInstance from 'shared/apis/axiosInstance';
+import { PageInfo } from '@/types/apis/recruitment';
+
export type MyInfoResponse = {
volunteerId: number;
volunteerEmail: string;
@@ -63,4 +65,29 @@ type ApplicantsResponse = {
export const getVolunteerApplicantList = () =>
axiosInstance.get('/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('/volunteers/me/reviews', {
+ params: {
+ page,
+ size,
+ },
+ });
diff --git a/apps/volunteer/src/mocks/handlers/volunteer.ts b/apps/volunteer/src/mocks/handlers/volunteer.ts
index 34af7b55..26d8ebf8 100644
--- a/apps/volunteer/src/mocks/handlers/volunteer.ts
+++ b/apps/volunteer/src/mocks/handlers/volunteer.ts
@@ -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);
@@ -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 });
+ }),
];
diff --git a/apps/volunteer/src/pages/my/_components/MyReviews.tsx b/apps/volunteer/src/pages/my/_components/MyReviews.tsx
new file mode 100644
index 00000000..097f9bef
--- /dev/null
+++ b/apps/volunteer/src/pages/my/_components/MyReviews.tsx
@@ -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>) => ({
+ ...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 (
+
+
+ 작성한 후기 {pages[0].pageInfo.totalElements}개
+
+
+ {reviews.map((review) => {
+ const { reviewId, shelterId } = review;
+ return (
+
+ navigate(`/shelters/${shelterId}/reviews/write/${reviewId}`)
+ }
+ onDelete={() => openDeleteModal(reviewId)}
+ >
+
+
+ {review.shelterName}
+
+
+
+
+ );
+ })}
+
+
+ {
+ setDeleteReviewId(0);
+ onClose();
+ }}
+ onClick={() => deleteReveiw.mutate(deleteReviewId)}
+ />
+
+ );
+}
+
+export default function MyReviewsTab() {
+ return (
+ '로딩 중 입니다..'
}>
+
+
+ );
+}
diff --git a/apps/volunteer/src/pages/my/index.tsx b/apps/volunteer/src/pages/my/index.tsx
index 659afa3d..2c7d0032 100644
--- a/apps/volunteer/src/pages/my/index.tsx
+++ b/apps/volunteer/src/pages/my/index.tsx
@@ -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() {
@@ -38,7 +39,7 @@ export default function MyPage() {
],
- ['작성한 봉사 후기', ],
+ ['작성한 봉사 후기', ],
]}
/>
From 3774c3159c25fc12fa4c5b40536893c805534702 Mon Sep 17 00:00:00 2001
From: Choi Won Suk
Date: Thu, 30 Nov 2023 16:37:16 +0900
Subject: [PATCH 06/11] =?UTF-8?q?feat:=20=EB=B4=89=EC=82=AC=EC=9E=90=20?=
=?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C?=
=?UTF-8?q?=20=EC=8B=A0=EC=B2=AD=ED=95=9C=20=EB=B4=89=EC=82=AC=ED=9B=84?=
=?UTF-8?q?=EA=B8=B0=20UI=20=EB=B0=8F=20msw=20=EC=97=B0=EA=B2=B0=20(#236)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(volunteer): MyPage에 사용되는 applicant 상태 constant 추가
* feat(volunteer): MyPage 관련 type volunteer에 추가
* feat(volunteer): 봉사 리스트 조회 api volunteer에 추가
* feat(volunteer): Mypage의 봉사 리스트 관련 mock handler 추가
* feat(volunteer): ApplyRecruitments 컴포넌트 추가
* feat(volunteer): Mypage에 ApplyRecruitments 추가
* fix(volunteer): 봉사 후기 작성 이동 주소 변경
---
apps/volunteer/src/apis/volunteer.ts | 38 ++----
.../src/constants/applicantStatus.ts | 7 +
.../volunteer/src/mocks/handlers/volunteer.ts | 37 ++++++
.../my/_components/ApplyRecruitments.tsx | 122 ++++++++++++++++++
apps/volunteer/src/pages/my/index.tsx | 3 +-
apps/volunteer/src/types/apis/volunteer.ts | 29 +++++
6 files changed, 205 insertions(+), 31 deletions(-)
create mode 100644 apps/volunteer/src/constants/applicantStatus.ts
create mode 100644 apps/volunteer/src/pages/my/_components/ApplyRecruitments.tsx
create mode 100644 apps/volunteer/src/types/apis/volunteer.ts
diff --git a/apps/volunteer/src/apis/volunteer.ts b/apps/volunteer/src/apis/volunteer.ts
index e69e522c..71bb4869 100644
--- a/apps/volunteer/src/apis/volunteer.ts
+++ b/apps/volunteer/src/apis/volunteer.ts
@@ -1,6 +1,10 @@
import axiosInstance from 'shared/apis/axiosInstance';
import { PageInfo } from '@/types/apis/recruitment';
+import {
+ PagenationRequestParams,
+ VolunteerApplicantsResponseData,
+} from '@/types/apis/volunteer';
export type MyInfoResponse = {
volunteerId: number;
@@ -17,20 +21,6 @@ export type MyInfoResponse = {
export const getMyVolunteerInfo = () =>
axiosInstance.get('/volunteers/me');
-type PasswordUpdateParams = {
- newPassword: string;
- oldPassword: string;
-};
-
-export const updateVolunteerPassword = (
- passwordUpdateParams: PasswordUpdateParams,
-) => {
- return axiosInstance.patch(
- '/volunteers/me/password',
- passwordUpdateParams,
- );
-};
-
export type UpdateUserInfoParams = {
name: string;
gender: 'FEMALE' | 'MALE';
@@ -47,23 +37,11 @@ export const updateVolunteerUserInfo = (
updateUserInfoParams,
);
-type Applicant = {
- recruitmentId: number;
- recruitmentTitle: string;
- recruitmentStartTime: string;
- shelterName: string;
- applicantId: number;
- applicantStatus: string;
- applicantIsWritedReview: boolean;
-};
-
-type ApplicantsResponse = {
- applicants: Applicant[];
-};
-
//봉사자가 신청한 봉사 리스트 조회
-export const getVolunteerApplicantList = () =>
- axiosInstance.get('/volunteers/applicants');
+export const getVolunteerApplicants = (params: PagenationRequestParams) =>
+ axiosInstance.get('/volunteers/applicants', {
+ params,
+ });
type Pagination = {
pageSize: number;
diff --git a/apps/volunteer/src/constants/applicantStatus.ts b/apps/volunteer/src/constants/applicantStatus.ts
new file mode 100644
index 00000000..edc00728
--- /dev/null
+++ b/apps/volunteer/src/constants/applicantStatus.ts
@@ -0,0 +1,7 @@
+export const APPLICANT_STATUS = {
+ PENDING: { ENG: 'PENDING', KOR: '대기중', COLOR: 'YELLOW' },
+ REFUSED: { ENG: 'REFUSED', KOR: '거절됨', COLOR: 'RED' },
+ ATTENDANCE: { ENG: 'ATTENDANCE', KOR: '승인완료', COLOR: 'ORANGE' },
+ APPROVED: { ENG: 'APPROVED', KOR: '출석완료', COLOR: 'GREEN' },
+ NOSHOW: { ENG: 'NOSHOW', KOR: '불참', COLOR: 'RED' },
+} as const;
diff --git a/apps/volunteer/src/mocks/handlers/volunteer.ts b/apps/volunteer/src/mocks/handlers/volunteer.ts
index 26d8ebf8..eb62f228 100644
--- a/apps/volunteer/src/mocks/handlers/volunteer.ts
+++ b/apps/volunteer/src/mocks/handlers/volunteer.ts
@@ -1,5 +1,32 @@
import { delay, http, HttpResponse } from 'msw';
+import { Applicant } from '@/types/apis/volunteer';
+
+const DUMMY_APPLY_RECRUITMENT_DATA: Applicant[] = Array.from(
+ { length: 6 },
+ (_, i) => {
+ return {
+ shelterId: i + 1,
+ recruitmentId: i + 1,
+ applicantId: i + 1,
+ recruitmentTitle: '봉사자 모집합니다' + i,
+ shelterName: '양천구 보호소',
+ applicantStatus:
+ i === 0
+ ? 'PENDING'
+ : i === 1
+ ? 'REFUSED'
+ : i === 2
+ ? 'ATTENDANCE'
+ : i === 3 || i === 4
+ ? 'APPROVED'
+ : 'NOSHOW',
+ applicantIsWritedReview: i === 4 ? true : false,
+ recruitmentStartTime: '2023-12-29T22:24:04.565688',
+ };
+ },
+);
+
const DUMMY_MYREVIEW = {
reviewId: 32,
shelterName: '해피퍼피',
@@ -32,6 +59,16 @@ export const handlers = [
console.log(updateVolunteer);
return new HttpResponse(null, { status: 204 });
}),
+ http.get('/volunteers/applicants', async () => {
+ await delay(200);
+ return HttpResponse.json({
+ pageInfo: {
+ hasNext: true,
+ totalElements: 20,
+ },
+ applicants: DUMMY_APPLY_RECRUITMENT_DATA,
+ });
+ }),
http.get('/volunteers/me/reviews', async ({ request }) => {
await delay(1000);
const url = new URL(request.url);
diff --git a/apps/volunteer/src/pages/my/_components/ApplyRecruitments.tsx b/apps/volunteer/src/pages/my/_components/ApplyRecruitments.tsx
new file mode 100644
index 00000000..d4cf6bb6
--- /dev/null
+++ b/apps/volunteer/src/pages/my/_components/ApplyRecruitments.tsx
@@ -0,0 +1,122 @@
+import {
+ Box,
+ Button,
+ Card,
+ CardBody,
+ Heading,
+ Text,
+ useToken,
+} from '@chakra-ui/react';
+import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
+import { useNavigate } from 'react-router-dom';
+import Label from 'shared/components/Label';
+import useIntersect from 'shared/hooks/useIntersection';
+import { createFormattedTime } from 'shared/utils/date';
+
+import { getVolunteerApplicants } from '@/apis/volunteer';
+import { APPLICANT_STATUS } from '@/constants/applicantStatus';
+import type { Applicant } from '@/types/apis/volunteer';
+
+type ApplyRecruitmentItemProps = {
+ applyRecruitment: Applicant;
+};
+
+export default function ApplyRecruitments() {
+ const {
+ data: { pages },
+ hasNextPage,
+ isFetchingNextPage,
+ fetchNextPage,
+ } = useSuspenseInfiniteQuery({
+ queryKey: ['my', 'apply', 'recruitments'],
+ queryFn: ({ pageParam }) =>
+ getVolunteerApplicants({ page: pageParam, size: 10 }),
+ initialPageParam: 0,
+ getNextPageParam: ({ data: { pageInfo } }, _, lastPageParam) =>
+ pageInfo.hasNext ? lastPageParam + 1 : null,
+ });
+
+ const totalApplyRecruitments = pages[0].data.pageInfo.totalElements;
+ const applyRecruitments = pages.flatMap(
+ ({ data: { applicants } }) => applicants,
+ );
+
+ const ref = useIntersect((entry, observer) => {
+ observer.unobserve(entry.target);
+
+ if (hasNextPage && !isFetchingNextPage) {
+ fetchNextPage();
+ }
+ });
+
+ return (
+
+
+ {`신청한 봉사 ${totalApplyRecruitments}개`}
+
+ {applyRecruitments.map((applyRecruitment, index) => (
+
+ ))}
+
+
+ );
+}
+
+function ApplyRecruitmentItem({
+ applyRecruitment: {
+ applicantStatus,
+ recruitmentTitle,
+ shelterName,
+ recruitmentStartTime,
+ applicantIsWritedReview,
+ shelterId,
+ applicantId,
+ },
+}: ApplyRecruitmentItemProps) {
+ const [orange400] = useToken('colors', ['orange.400']);
+ const navigate = useNavigate();
+
+ return (
+
+
+
+ {recruitmentTitle}
+
+ {shelterName}
+
+
+
+ 봉사일
+
+
+ {` ${createFormattedTime(new Date(recruitmentStartTime))}`}
+
+
+ {applicantStatus === APPLICANT_STATUS.APPROVED.ENG &&
+ applicantIsWritedReview && (
+
+ )}
+
+
+ );
+}
diff --git a/apps/volunteer/src/pages/my/index.tsx b/apps/volunteer/src/pages/my/index.tsx
index 2c7d0032..8fcca32b 100644
--- a/apps/volunteer/src/pages/my/index.tsx
+++ b/apps/volunteer/src/pages/my/index.tsx
@@ -3,6 +3,7 @@ import Label from 'shared/components/Label';
import ProfileInfo from 'shared/components/ProfileInfo';
import Tabs from 'shared/components/Tabs';
+import ApplyRecruitments from './_components/ApplyRecruitments';
import MyReviewsTab from './_components/MyReviews';
import useFetchMyVolunteer from './_hooks/useFetchMyVolunteer';
@@ -38,7 +39,7 @@ export default function MyPage() {
],
+ ['신청한 봉사 목록', ],
['작성한 봉사 후기', ],
]}
/>
diff --git a/apps/volunteer/src/types/apis/volunteer.ts b/apps/volunteer/src/types/apis/volunteer.ts
new file mode 100644
index 00000000..bab04ce5
--- /dev/null
+++ b/apps/volunteer/src/types/apis/volunteer.ts
@@ -0,0 +1,29 @@
+import { APPLICANT_STATUS } from '@/constants/applicantStatus';
+
+type PageInfo = {
+ totalElements: number;
+ hasNext: boolean;
+};
+
+export type PagenationRequestParams = {
+ page: number;
+ size: number;
+};
+
+export type ApplicantStatus = keyof typeof APPLICANT_STATUS;
+
+export type Applicant = {
+ shelterId: number;
+ recruitmentId: number;
+ recruitmentTitle: string;
+ recruitmentStartTime: string;
+ shelterName: string;
+ applicantId: number;
+ applicantStatus: ApplicantStatus;
+ applicantIsWritedReview: boolean;
+};
+
+export type VolunteerApplicantsResponseData = {
+ pageInfo: PageInfo;
+ applicants: Applicant[];
+};
From d99cf565421f1671bff93bd93ad991e98f3a4c50 Mon Sep 17 00:00:00 2001
From: Hyejin Yang
Date: Thu, 30 Nov 2023 17:30:46 +0900
Subject: [PATCH 07/11] =?UTF-8?q?feat(shelter):=20=EB=B3=B4=ED=98=B8?=
=?UTF-8?q?=EC=86=8C=20=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20?=
=?UTF-8?q?=ED=86=A0=EA=B8=80=20=EB=B2=84=ED=8A=BC=20=EB=94=94=EC=9E=90?=
=?UTF-8?q?=EC=9D=B8=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20useMutation=20?=
=?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?=
=?UTF-8?q?=EC=A0=95=20(#242)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(shelter): 보호소 마이페이지 토글 버튼 디자인 수정
* feat(shelter): useMyPage hook 에서 useMutation 사용하도록 수정
* feat(shelter): 보호소 앱의 queryClient stale time 50초로 변경
---
apps/shelter/src/App.tsx | 8 +++-
apps/shelter/src/pages/my/_hooks/useMyPage.ts | 48 +++++++++++--------
apps/shelter/src/pages/my/index.tsx | 5 ++
3 files changed, 39 insertions(+), 22 deletions(-)
diff --git a/apps/shelter/src/App.tsx b/apps/shelter/src/App.tsx
index e222a5e1..53a5f598 100644
--- a/apps/shelter/src/App.tsx
+++ b/apps/shelter/src/App.tsx
@@ -7,7 +7,13 @@ import theme from 'shared/theme';
import { router } from '@/routes';
-const queryClient = new QueryClient();
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 50000,
+ },
+ },
+});
export default function App() {
return (
diff --git a/apps/shelter/src/pages/my/_hooks/useMyPage.ts b/apps/shelter/src/pages/my/_hooks/useMyPage.ts
index 2d781c66..1c18b875 100644
--- a/apps/shelter/src/pages/my/_hooks/useMyPage.ts
+++ b/apps/shelter/src/pages/my/_hooks/useMyPage.ts
@@ -1,5 +1,5 @@
-import { useQuery } from '@tanstack/react-query';
-import { useState } from 'react';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { ChangeEvent, useEffect, useState } from 'react';
import { getShelterInfoAPI, updateAddressStatusAPI } from '@/apis/shelter';
import { ShelterInfo } from '@/types/apis/shetler';
@@ -33,34 +33,40 @@ const createProfile = (response: ShelterInfo): ShelterProfile => {
};
export const useMyPage = () => {
- const [isAddressPublic, setIsAddressPublic] = useState(false);
+ const queryClient = useQueryClient();
- const updateAddressStatus = async () => {
- try {
- await updateAddressStatusAPI(!isAddressPublic);
- setIsAddressPublic(!isAddressPublic);
- } catch (error) {
- console.error(error);
- }
- };
+ const { mutate: updateAddressStatus } = useMutation({
+ mutationFn: async (event: ChangeEvent) => {
+ updateAddressStatusAPI(!event.target.checked);
+ },
+ onSuccess: () => {
+ const isOpenedAddress = !isAddressPublic;
+ setIsAddressPublic(isOpenedAddress);
+ queryClient.setQueryData(['shelterProfile'], (data: ShelterProfile) => ({
+ ...data,
+ isAddressPublic: isOpenedAddress,
+ }));
+ },
+ });
const { data } = useQuery({
queryKey: ['shelterProfile'],
queryFn: async () => {
const response = (await getShelterInfoAPI()).data;
- setIsAddressPublic(response.shelterIsOpenedAddress);
-
return createProfile(response);
},
- initialData: {
- shelterName: '',
- email: '',
- phoneNumber: '',
- sparePhoneNumber: '',
- shelterAddress: '',
- isAddressPublic: false,
- },
});
+ const [isAddressPublic, setIsAddressPublic] = useState(
+ data?.isAddressPublic ?? false,
+ );
+
+ useEffect(() => {
+ if (data) {
+ const { isAddressPublic } = data;
+ setIsAddressPublic(isAddressPublic);
+ }
+ }, [data]);
+
return { shelterProfile: data, isAddressPublic, updateAddressStatus };
};
diff --git a/apps/shelter/src/pages/my/index.tsx b/apps/shelter/src/pages/my/index.tsx
index 7648b4d1..32222008 100644
--- a/apps/shelter/src/pages/my/index.tsx
+++ b/apps/shelter/src/pages/my/index.tsx
@@ -12,6 +12,10 @@ export default function MyPage() {
const navigate = useNavigate();
const { shelterProfile, isAddressPublic, updateAddressStatus } = useMyPage();
+ if (!shelterProfile) {
+ return null;
+ }
+
const { shelterName, email, phoneNumber, sparePhoneNumber, shelterAddress } =
shelterProfile;
@@ -33,6 +37,7 @@ export default function MyPage() {
From 087e835e17f0ebc1fd5fdba7c3c8b1a4ce9ae7e8 Mon Sep 17 00:00:00 2001
From: Choi Won Suk
Date: Thu, 30 Nov 2023 17:30:53 +0900
Subject: [PATCH 08/11] =?UTF-8?q?feat(shared):=20validations=20=EC=A0=84?=
=?UTF-8?q?=ED=99=94=EB=B2=88=ED=98=B8=20=ED=98=95=EC=8B=9D=EC=97=90=20'-'?=
=?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#243)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/shared/utils/validations.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/shared/utils/validations.ts b/packages/shared/utils/validations.ts
index bc98b2e9..6b167f42 100644
--- a/packages/shared/utils/validations.ts
+++ b/packages/shared/utils/validations.ts
@@ -40,12 +40,12 @@ export const isOpenedAddress = z.boolean();
export const phoneNumber = z
.string()
.min(1, '보호소 전화번호 정보는 필수입니다')
- .regex(/^\d{2,3}\d{3,4}\d{4}$/, '전화번호 형식이 올바르지 않습니다');
+ .regex(/^\d{2,3}-\d{3,4}-\d{4}$/, '전화번호 형식이 올바르지 않습니다');
export const sparePhoneNumber = z.union([
z.literal(''),
z
.string()
- .regex(/^\d{2,3}\d{3,4}\d{4}$/, '전화번호 형식이 올바르지 않습니다'),
+ .regex(/^\d{2,3}-\d{3,4}-\d{4}$/, '전화번호 형식이 올바르지 않습니다'),
]);
export const gender = z.enum(['FEMALE', 'MALE']);
export const birthDate = z
From 9e6b8670eb10951f0c4e58c0716002d488a656b4 Mon Sep 17 00:00:00 2001
From: Dongja
Date: Thu, 30 Nov 2023 17:36:58 +0900
Subject: [PATCH 09/11] =?UTF-8?q?feat=20:=20=EB=A6=AC=EB=B7=B0=20=EC=8A=A4?=
=?UTF-8?q?=EC=BC=88=EB=A0=88=ED=86=A4=20(#238)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(shared): review Item skeleton 컴포넌트 생성
* feat(shelter): review list skeleton 컴포넌트 추가 및 보호소앱 리뷰 페이지에 적용
* feat(shelter): 리뷰 페이지에서 봉사자 프로필 페이지로 이동하는 기능 추가
---
apps/shelter/src/apis/shelter.ts | 1 +
apps/shelter/src/mocks/handlers/shelter.ts | 2 +-
.../src/pages/my/reviews/VolunteerProfile.tsx | 4 ++-
apps/shelter/src/pages/my/reviews/index.tsx | 13 +++++--
.../shared/components/ReviewItemSkeleton.tsx | 35 +++++++++++++++++++
.../components/ReviewItemSkeletonList.tsx | 19 ++++++++++
6 files changed, 69 insertions(+), 5 deletions(-)
create mode 100644 packages/shared/components/ReviewItemSkeleton.tsx
create mode 100644 packages/shared/components/ReviewItemSkeletonList.tsx
diff --git a/apps/shelter/src/apis/shelter.ts b/apps/shelter/src/apis/shelter.ts
index d512eeb7..3d2b17e4 100644
--- a/apps/shelter/src/apis/shelter.ts
+++ b/apps/shelter/src/apis/shelter.ts
@@ -47,6 +47,7 @@ export const getShelterReviewList = (pageParams: PageParams) =>
volunteerTemperature: number;
volunteerReviewCount: number;
volunteerImageUrl: string;
+ volunteerId: number;
}[];
}>(`/shelters/me/reviews`, {
params: pageParams,
diff --git a/apps/shelter/src/mocks/handlers/shelter.ts b/apps/shelter/src/mocks/handlers/shelter.ts
index 8418542f..64be3ba1 100644
--- a/apps/shelter/src/mocks/handlers/shelter.ts
+++ b/apps/shelter/src/mocks/handlers/shelter.ts
@@ -37,7 +37,7 @@ export const handlers = [
return HttpResponse.json({ status: 200 });
}),
http.get('/shelters/me/reviews', async () => {
- await delay(200);
+ await delay(3000);
return HttpResponse.json(
{
pageInfo: {
diff --git a/apps/shelter/src/pages/my/reviews/VolunteerProfile.tsx b/apps/shelter/src/pages/my/reviews/VolunteerProfile.tsx
index 9701bbcd..d557d386 100644
--- a/apps/shelter/src/pages/my/reviews/VolunteerProfile.tsx
+++ b/apps/shelter/src/pages/my/reviews/VolunteerProfile.tsx
@@ -9,6 +9,7 @@ type VolunteerProfileprops = {
volunteerReviewCount: number;
volunteerImageUrl: string;
reviewCreatedAt: string;
+ onClickNextButton: VoidFunction;
};
export default function VolunteerProfile({
@@ -17,6 +18,7 @@ export default function VolunteerProfile({
volunteerReviewCount,
volunteerImageUrl,
reviewCreatedAt,
+ onClickNextButton,
}: VolunteerProfileprops) {
return (
@@ -26,7 +28,7 @@ export default function VolunteerProfile({
{volunteerName}
-
+
diff --git a/apps/shelter/src/pages/my/reviews/index.tsx b/apps/shelter/src/pages/my/reviews/index.tsx
index cb62093e..b991d0b3 100644
--- a/apps/shelter/src/pages/my/reviews/index.tsx
+++ b/apps/shelter/src/pages/my/reviews/index.tsx
@@ -1,6 +1,8 @@
import { Box, Heading, VStack } from '@chakra-ui/react';
import { Suspense } from 'react';
+import { useNavigate } from 'react-router-dom';
import ReviewItem from 'shared/components/ReviewItem';
+import ReviewItemSkeletonList from 'shared/components/ReviewItemSkeletonList';
import useIntersect from 'shared/hooks/useIntersection';
import { createFormattedTime } from 'shared/utils/date';
@@ -10,7 +12,7 @@ import VolunteerProfile from './VolunteerProfile';
const PAGE_SIZE = 10;
function Reviews() {
- //TODO 봉사자 옆에 화살표 버튼 클릭시 봉사자 프로필 페이지로 가는 기능추가
+ const navigate = useNavigate();
const {
data: { pages },
@@ -28,6 +30,10 @@ function Reviews() {
}
});
+ const goVolunteerProfile = (id: number) => {
+ navigate(`/volunteers/profile/${id}`);
+ };
+
return (
goVolunteerProfile(review.volunteerId)}
reviewCreatedAt={createFormattedTime(
new Date(review.reviewCreatedAt),
'YY.MM.DD.',
@@ -62,14 +69,14 @@ function Reviews() {
))}
-
+ {isFetchingNextPage ? : }
);
}
export default function MyReviewsPage() {
return (
- 글목록 로딩중...}>
+ }>
);
diff --git a/packages/shared/components/ReviewItemSkeleton.tsx b/packages/shared/components/ReviewItemSkeleton.tsx
new file mode 100644
index 00000000..892b8923
--- /dev/null
+++ b/packages/shared/components/ReviewItemSkeleton.tsx
@@ -0,0 +1,35 @@
+import {
+ Card,
+ HStack,
+ Skeleton,
+ SkeletonCircle,
+ SkeletonText,
+ Stack,
+} from '@chakra-ui/react';
+
+export default function ReviewItemSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/shared/components/ReviewItemSkeletonList.tsx b/packages/shared/components/ReviewItemSkeletonList.tsx
new file mode 100644
index 00000000..64ded630
--- /dev/null
+++ b/packages/shared/components/ReviewItemSkeletonList.tsx
@@ -0,0 +1,19 @@
+import { Box, Skeleton, VStack } from '@chakra-ui/react';
+
+import ReviewItemSkeleton from './ReviewItemSkeleton';
+
+export default function ReviewItemSkeletonList({
+ showTitle = false,
+}: {
+ showTitle?: boolean;
+}) {
+ return (
+
+ {showTitle && }
+
+
+
+
+
+ );
+}
From 816d9ecc317da2df8df8dfd72626af62627de6b4 Mon Sep 17 00:00:00 2001
From: Dongja
Date: Thu, 30 Nov 2023 17:37:18 +0900
Subject: [PATCH 10/11] =?UTF-8?q?feat=20:=20withlogin,=20layout=20?=
=?UTF-8?q?=EC=88=98=EC=A0=95=20(#244)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(volunteer): withlogin 컴포넌트 early return 추가
* feat(shared): 보호소 어플 회원가입 페이지에 접근가능하도록 허용, layout console 제거
* fix(shared): 봉사자 앱, 보호소 앱 react strict mode 제거
---
apps/shelter/src/main.tsx | 7 +------
apps/volunteer/src/components/WithLogin.tsx | 2 +-
apps/volunteer/src/main.tsx | 7 +------
packages/shared/layout/index.tsx | 3 +++
4 files changed, 6 insertions(+), 13 deletions(-)
diff --git a/apps/shelter/src/main.tsx b/apps/shelter/src/main.tsx
index f6444594..f2480c6d 100644
--- a/apps/shelter/src/main.tsx
+++ b/apps/shelter/src/main.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
@@ -16,9 +15,5 @@ async function deferRender() {
}
deferRender().then(() => {
- ReactDOM.createRoot(document.getElementById('root')!).render(
-
-
- ,
- );
+ ReactDOM.createRoot(document.getElementById('root')!).render();
});
diff --git a/apps/volunteer/src/components/WithLogin.tsx b/apps/volunteer/src/components/WithLogin.tsx
index 1cf3b81e..4f0af1a1 100644
--- a/apps/volunteer/src/components/WithLogin.tsx
+++ b/apps/volunteer/src/components/WithLogin.tsx
@@ -6,7 +6,7 @@ export default function WithLogin({ children }: { children: ReactNode }) {
const { user } = useAuthStore();
if (user) {
- children;
+ return children;
}
return ;
diff --git a/apps/volunteer/src/main.tsx b/apps/volunteer/src/main.tsx
index d7ba7b9a..fb8d28e7 100644
--- a/apps/volunteer/src/main.tsx
+++ b/apps/volunteer/src/main.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
@@ -16,9 +15,5 @@ async function deferRender() {
}
deferRender().then(() => {
- ReactDOM.createRoot(document.getElementById('root')!).render(
-
-
- ,
- );
+ ReactDOM.createRoot(document.getElementById('root')!).render();
});
diff --git a/packages/shared/layout/index.tsx b/packages/shared/layout/index.tsx
index f79f2ae2..995f65ce 100644
--- a/packages/shared/layout/index.tsx
+++ b/packages/shared/layout/index.tsx
@@ -21,6 +21,9 @@ export default function Layout({ appType }: LayoutProps) {
if (appType === 'VOLUNTEER_APP' && pathname === '/') {
navigate('/volunteers');
} else if (appType === 'SHELTER_APP') {
+ if (pathname === '/signup' || pathname === '/signin') {
+ return;
+ }
navigate('/signin');
}
From df3b1b8802e186eebf21d0bf671a3d35d51f2822 Mon Sep 17 00:00:00 2001
From: woo hyeonji <117665863+Eosdia@users.noreply.github.com>
Date: Thu, 30 Nov 2023 17:50:37 +0900
Subject: [PATCH 11/11] =?UTF-8?q?feat:=20=EB=B4=89=EC=82=AC=EB=AA=A8?=
=?UTF-8?q?=EC=A7=91=20=EC=83=81=EC=84=B8=ED=8E=98=EC=9D=B4=EC=A7=80=20?=
=?UTF-8?q?=EB=B3=B4=ED=98=B8=EC=86=8C=ED=94=84=EB=A1=9C=ED=95=84=20?=
=?UTF-8?q?=EC=9D=B4=EB=8F=99,=20=EB=B4=89=EC=82=AC=20=EC=8B=A0=EC=B2=AD?=
=?UTF-8?q?=20msw=20=EC=B6=94=EA=B0=80=20(#245)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* refactor(volunteer): 봉사모집상세조회, 보호소 간단정보조회 fetch hook 분리
* refactor(volunteer): 봉사모집상세조회 데이터 받아오기
* feat(volunteer): 봉사신청 mock api 추가
* fix(volunteer): 봉사신청 api 수정
* feat(volunteer): 봉사신청 msw 연결
* fix(volunteer): 채팅하기 버튼 제거
* feat(volunteer): 보호소 정보 클릭하면 보호소 프로필 페이지로 이동
* feat(volunteer): 봉사신청 자동마감 된 경우 추가
---
apps/volunteer/src/apis/recruitment.ts | 4 +-
.../src/mocks/handlers/recruitment.ts | 5 +
.../_hooks/useFetchRecruitmentDetail.ts | 9 +
.../_hooks/useFetchSimpleShelterInfo.ts | 11 ++
.../detail/_hooks/useFetchVolunteerDetail.ts | 74 -------
.../src/pages/volunteers/detail/index.tsx | 181 +++++++++++-------
6 files changed, 138 insertions(+), 146 deletions(-)
create mode 100644 apps/volunteer/src/pages/volunteers/detail/_hooks/useFetchRecruitmentDetail.ts
create mode 100644 apps/volunteer/src/pages/volunteers/detail/_hooks/useFetchSimpleShelterInfo.ts
delete mode 100644 apps/volunteer/src/pages/volunteers/detail/_hooks/useFetchVolunteerDetail.ts
diff --git a/apps/volunteer/src/apis/recruitment.ts b/apps/volunteer/src/apis/recruitment.ts
index cb3541b6..b367051b 100644
--- a/apps/volunteer/src/apis/recruitment.ts
+++ b/apps/volunteer/src/apis/recruitment.ts
@@ -5,8 +5,8 @@ import {
RecruitmentsRequest,
} from '@/types/apis/recruitment';
-export const ApplyRecruitments = (recruitmentId: string) =>
- axiosInstance.post(`/recruitments/${recruitmentId}/apply`);
+export const applyRecruitments = (recruitmentId: number) =>
+ axiosInstance.post(`/volunteers/recruitments/${recruitmentId}/apply`);
export const getRecruitments = (request: Partial) =>
axiosInstance.get(
diff --git a/apps/volunteer/src/mocks/handlers/recruitment.ts b/apps/volunteer/src/mocks/handlers/recruitment.ts
index dbdb35be..d6df7485 100644
--- a/apps/volunteer/src/mocks/handlers/recruitment.ts
+++ b/apps/volunteer/src/mocks/handlers/recruitment.ts
@@ -54,4 +54,9 @@ export const handlers = [
{ status: 200 },
);
}),
+ http.post('/volunteers/recruitments/:recruitmentId/apply', async () => {
+ return new HttpResponse(null, {
+ status: 204,
+ });
+ }),
];
diff --git a/apps/volunteer/src/pages/volunteers/detail/_hooks/useFetchRecruitmentDetail.ts b/apps/volunteer/src/pages/volunteers/detail/_hooks/useFetchRecruitmentDetail.ts
new file mode 100644
index 00000000..c705e9d2
--- /dev/null
+++ b/apps/volunteer/src/pages/volunteers/detail/_hooks/useFetchRecruitmentDetail.ts
@@ -0,0 +1,9 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+import { getRecruitmentDetail } from 'shared/apis/common/Recruitments';
+
+const useFetchRecruitmentDetail = (recruitmentId: number) =>
+ useSuspenseQuery({
+ queryKey: ['recruitment', recruitmentId],
+ queryFn: async () => (await getRecruitmentDetail(recruitmentId)).data,
+ });
+export default useFetchRecruitmentDetail;
diff --git a/apps/volunteer/src/pages/volunteers/detail/_hooks/useFetchSimpleShelterInfo.ts b/apps/volunteer/src/pages/volunteers/detail/_hooks/useFetchSimpleShelterInfo.ts
new file mode 100644
index 00000000..6d7cfd64
--- /dev/null
+++ b/apps/volunteer/src/pages/volunteers/detail/_hooks/useFetchSimpleShelterInfo.ts
@@ -0,0 +1,11 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import { getSimpleShelterProfile } from '@/apis/shelter';
+
+const useFetchSimpleShelterInfo = (shelterId: number) =>
+ useSuspenseQuery({
+ queryKey: ['shelter', 'simpleProfile', shelterId],
+ queryFn: async () => (await getSimpleShelterProfile(shelterId)).data,
+ });
+
+export default useFetchSimpleShelterInfo;
diff --git a/apps/volunteer/src/pages/volunteers/detail/_hooks/useFetchVolunteerDetail.ts b/apps/volunteer/src/pages/volunteers/detail/_hooks/useFetchVolunteerDetail.ts
deleted file mode 100644
index 55a8b42d..00000000
--- a/apps/volunteer/src/pages/volunteers/detail/_hooks/useFetchVolunteerDetail.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import { useQuery } from '@tanstack/react-query';
-import { getRecruitmentDetail } from 'shared/apis/common/Recruitments';
-import {
- createFormattedTime,
- createWeekDayLocalString,
-} from 'shared/utils/date';
-
-import { getSimpleShelterProfile } from '@/apis/shelter';
-
-const useFetchVolunteerDetail = (recruitmentId: number) =>
- useQuery({
- queryKey: ['volunteer', recruitmentId],
- queryFn: async () => {
- const { shelterId, ...recruitmentInfo } = (
- await getRecruitmentDetail(recruitmentId)
- ).data;
- const shelterSimpleInfo = (await getSimpleShelterProfile(shelterId)).data;
-
- return {
- ...recruitmentInfo,
- shelterInfo: { shelterId, ...shelterSimpleInfo },
- };
- },
- select: (data) => {
- const startDate = new Date(data.recruitmentStartTime);
- const endDate = new Date(data.recruitmentEndTime);
- const deadLine = new Date(data.recruitmentDeadline);
-
- return {
- imageUrls: data.recruitmentImageUrls,
- title: data.recruitmentTitle,
- content: data.recruitmentContent,
- applicant: data.recruitmentApplicantCount,
- capacity: data.recruitmentCapacity,
- volunteerDay: `${createFormattedTime(
- startDate,
- )}(${createWeekDayLocalString(startDate)})`,
- recruitmentDeadline: `${createFormattedTime(
- deadLine,
- )}(${createWeekDayLocalString(deadLine)}) ${createFormattedTime(
- deadLine,
- 'hh:mm',
- )}`,
- volunteerStartTime: createFormattedTime(startDate, 'hh:mm'),
- volunteerEndTime: createFormattedTime(endDate, 'hh:mm'),
- recruitmentCreatedAt: createFormattedTime(
- new Date(data.recruitmentCreatedAt),
- ),
- recruitmentIsClosed: data.recruitmentIsClosed,
- shelterInfo: data.shelterInfo,
- };
- },
- initialData: {
- recruitmentTitle: '',
- recruitmentApplicantCount: 0,
- recruitmentCapacity: 0,
- recruitmentContent: '',
- recruitmentStartTime: '',
- recruitmentEndTime: '',
- recruitmentIsClosed: false,
- recruitmentDeadline: '',
- recruitmentCreatedAt: '',
- recruitmentUpdatedAt: '',
- recruitmentImageUrls: [],
- shelterInfo: {
- shelterId: 0,
- shelterName: '',
- shelterImageUrl: '',
- shelterAddress: '',
- shelterEmail: '',
- },
- },
- });
-export default useFetchVolunteerDetail;
diff --git a/apps/volunteer/src/pages/volunteers/detail/index.tsx b/apps/volunteer/src/pages/volunteers/detail/index.tsx
index ace21194..3e96d174 100644
--- a/apps/volunteer/src/pages/volunteers/detail/index.tsx
+++ b/apps/volunteer/src/pages/volunteers/detail/index.tsx
@@ -5,120 +5,160 @@ import {
HStack,
Text,
useDisclosure,
+ useToast,
VStack,
} from '@chakra-ui/react';
-import { useEffect, useState } from 'react';
+import { useMutation } from '@tanstack/react-query';
+import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import AlertModal from 'shared/components/AlertModal';
import ImageCarousel from 'shared/components/ImageCarousel';
import InfoTextList from 'shared/components/InfoTextList';
-import { LabelProps } from 'shared/components/Label';
+import Label from 'shared/components/Label';
import LabelText from 'shared/components/LabelText';
import ProfileInfo from 'shared/components/ProfileInfo';
-import { getDDay } from 'shared/utils/date';
+import {
+ createFormattedTime,
+ createWeekDayLocalString,
+ getDDay,
+} from 'shared/utils/date';
+
+import { applyRecruitments } from '@/apis/recruitment';
-import useFetchVolunteerDetail from './_hooks/useFetchVolunteerDetail';
+import useFetchVolunteerDetail from './_hooks/useFetchRecruitmentDetail';
+import useFetchSimpleShelterInfo from './_hooks/useFetchSimpleShelterInfo';
export default function VolunteersDetailPage() {
+ const toast = useToast();
const navigate = useNavigate();
- const { id } = useParams();
+
+ const { id } = useParams<{ id: string }>();
const recruitmentId = Number(id);
+
const { isOpen, onOpen, onClose } = useDisclosure();
- const [label, setLabel] = useState({
- labelTitle: '모집중',
- type: 'GREEN',
+
+ const { data } = useFetchVolunteerDetail(recruitmentId);
+
+ const [isRecruitmentClosed, setIsRecruitmentClosed] = useState(
+ data.recruitmentIsClosed,
+ );
+
+ const volunteerDay = new Date(data.recruitmentStartTime);
+ const deadline = new Date(data.recruitmentDeadline);
+ const createdAt = new Date(data.recruitmentCreatedAt);
+ const updatedAt = new Date(data.recruitmentUpdatedAt);
+
+ const { data: shelter } = useFetchSimpleShelterInfo(data.shelterId);
+
+ const { mutate: applyRecruitment } = useMutation({
+ mutationFn: async () => await applyRecruitments(recruitmentId),
+ onSuccess: () => {
+ toast({
+ position: 'top',
+ description: '봉사 신청이 완료되었습니다.',
+ status: 'success',
+ duration: 1500,
+ });
+ },
+ onError: (error) => {
+ if (error.response?.status === 409) {
+ toast({
+ position: 'top',
+ description: '봉사 모집이 마감되었습니다.',
+ status: 'error',
+ duration: 1500,
+ });
+ setIsRecruitmentClosed(!isRecruitmentClosed);
+ }
+ },
});
- const { data } = useFetchVolunteerDetail(3);
-
- const {
- imageUrls,
- title,
- content,
- applicant,
- capacity,
- volunteerDay,
- recruitmentDeadline,
- volunteerStartTime,
- volunteerEndTime,
- recruitmentCreatedAt,
- recruitmentIsClosed,
- shelterInfo,
- } = data;
- const { shelterName, shelterImageUrl, shelterAddress, shelterEmail } =
- shelterInfo;
-
- useEffect(() => {
- if (recruitmentIsClosed) {
- setLabel({ labelTitle: '마감완료', type: 'GRAY' });
- }
- }, [recruitmentIsClosed]);
-
- const goChatting = () => {
- //TODO 채팅방 생성 API
- navigate(`/chattings/${recruitmentId}`);
+ const goShelterProfilePage = () => {
+ navigate(`/shelters/profile/${data.shelterId}`);
};
const onApplyRecruitment = () => {
onClose();
- //TODO 봉사신청 API
- //TODO 봉사신청완료 toast
+ applyRecruitment();
};
return (
-
+
-
+ {isRecruitmentClosed ? (
+
+ ) : (
+
+ )}
- {title}
+ {data.recruitmentTitle}
- 작성일 | {recruitmentCreatedAt}(수정됨)
+ 작성일 |{' '}
+ {updatedAt
+ ? `${createFormattedTime(updatedAt)} (수정됨)`
+ : createFormattedTime(createdAt)}
- {content}
+ {data.recruitmentContent}
-
+
+
+
-
-
+