diff --git a/apps/shelter/src/apis/volunteers.ts b/apps/shelter/src/apis/volunteers.ts new file mode 100644 index 00000000..43ce4fae --- /dev/null +++ b/apps/shelter/src/apis/volunteers.ts @@ -0,0 +1,31 @@ +import axiosInstance from 'shared/apis/axiosInstance'; + +import { + VolunteerCompletedsRequestParams, + VolunteerProfileResponseData, + VolunteerReviewsOnVolunteerResponseData, + VoluteerRecruitmentsOnVolunteerResponseData, +} from '@/types/apis/volunteers'; + +export const getVolunteerProfile = (volunteerId: number) => + axiosInstance.get( + `/shelters/volunteers/${volunteerId}/profile`, + ); + +export const getVolunteerReviewsOnVolunteer = ( + volunteerId: number, + params: VolunteerCompletedsRequestParams, +) => + axiosInstance.get( + `/shelters/volunteers/${volunteerId}/reviews`, + { params }, + ); + +export const getVolunteerRecruitmentsOnVolunteer = ( + volunteerId: number, + params: VolunteerCompletedsRequestParams, +) => + axiosInstance.get( + `/shelters/volunteers/${volunteerId}/recruitments/completed`, + { params }, + ); diff --git a/apps/shelter/src/mocks/browser.ts b/apps/shelter/src/mocks/browser.ts index f6d52450..3808427e 100644 --- a/apps/shelter/src/mocks/browser.ts +++ b/apps/shelter/src/mocks/browser.ts @@ -5,6 +5,7 @@ import { handlers as manageHandlers } from './handlers/manage'; import { handlers as recruitmentHandler } from './handlers/recruitment'; import { handlers as recruitmentDetailHandler } from './handlers/recruitmentDetail'; import { handlers as shelterHandlers } from './handlers/shelter'; +import { handlers as volunteerHandlers } from './handlers/volunteers'; export const worker = setupWorker( ...authHandlers, @@ -12,4 +13,5 @@ export const worker = setupWorker( ...recruitmentHandler, ...recruitmentDetailHandler, ...manageHandlers, + ...volunteerHandlers, ); diff --git a/apps/shelter/src/mocks/handlers/volunteers.ts b/apps/shelter/src/mocks/handlers/volunteers.ts new file mode 100644 index 00000000..8d8da9bb --- /dev/null +++ b/apps/shelter/src/mocks/handlers/volunteers.ts @@ -0,0 +1,62 @@ +import { delay, http, HttpResponse } from 'msw'; + +const DUMMY_REVIEWS_DATA = { + reviewId: 36, + shelterName: '남양주 보호소', + reviewCreatedAt: '2023-03-16T18:00', + reviewContent: '아이들이 너무 귀여워서 봉사하는 시간이 즐거웠습니다~!', + reviewImageUrls: [ + 'https://source.unsplash.com/random', + 'https://source.unsplash.com/random', + 'https://source.unsplash.com/random', + ], +}; + +const DUMMY_RECRUITMENTS = { + recruitmentId: 1, + recruitmentTitle: '봉사자를 모집합니다', + recruitmentStartTime: '2023-03-16T18:00:00', + shelterName: '마석 보호소', +}; + +export const handlers = [ + http.get('/shelters/volunteers/:volunteerId/profile', async () => { + await delay(200); + return HttpResponse.json( + { + volunteerEmail: 'test@naver.com', + volunteerName: '홍길동', + volunteerTemperature: 36, + volunteerImageUrl: 'https://source.unsplash.com/random', + volunteerPhoneNumber: '010-8237-1847', + }, + { status: 200 }, + ); + }), + http.get('/shelters/volunteers/:volunteerId/reviews', async () => { + await delay(2000); + return HttpResponse.json( + { + pageInfo: { + totalElements: 20, + hasNext: true, + }, + reviews: Array.from({ length: 10 }, () => DUMMY_REVIEWS_DATA), + }, + { status: 200 }, + ); + }), + http.get( + '/shelters/volunteers/:volunteerId/recruitments/completed', + async () => { + await delay(2000); + return HttpResponse.json({ + pageInfo: { + totalElements: 20, + hasNext: true, + }, + recruitments: Array.from({ length: 10 }, () => DUMMY_RECRUITMENTS), + }); + }, + ), +]; diff --git a/apps/shelter/src/pages/volunteers/profile/_components/VolunteerRecords.tsx b/apps/shelter/src/pages/volunteers/profile/_components/VolunteerRecords.tsx deleted file mode 100644 index 954baa3a..00000000 --- a/apps/shelter/src/pages/volunteers/profile/_components/VolunteerRecords.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Box, Card, CardBody, Heading, Text } from '@chakra-ui/react'; -import { createFormattedTime } from 'shared/utils/date'; - -type VolunteerRecordItemProps = { - recruitmentTitle: string; - recruitmentStartTime: string; - shelterName: string; -}; - -const DUMMY_RECRUITMENTS = { - recruitmentId: 1, - recruitmentTitle: '봉사자를 모집합니다', - recruitmentStartTime: '2023-03-16T18:00:00', - shelterName: '마석 보호소', -}; - -const DUMMY_DATA = { - pageInfo: { - totalElements: 200, - hasNext: true, - }, - recruitments: Array.from({ length: 8 }, () => DUMMY_RECRUITMENTS), -}; - -export default function VolunteerRecords() { - return ( - - - 봉사 이력 {DUMMY_DATA.recruitments.length}개 - - {DUMMY_DATA.recruitments.map((recruitment, index) => ( - - ))} - - ); -} - -function VolunteerRecordItem({ - shelterName, - recruitmentTitle, - recruitmentStartTime, -}: VolunteerRecordItemProps) { - return ( - - - - {recruitmentTitle} - - - {shelterName} - - - {`봉사일 | ${createFormattedTime(new Date(recruitmentStartTime))}`} - - - - ); -} diff --git a/apps/shelter/src/pages/volunteers/profile/_components/VolunteerRecruitments.tsx b/apps/shelter/src/pages/volunteers/profile/_components/VolunteerRecruitments.tsx new file mode 100644 index 00000000..4a75c7c4 --- /dev/null +++ b/apps/shelter/src/pages/volunteers/profile/_components/VolunteerRecruitments.tsx @@ -0,0 +1,81 @@ +import { Box, Card, CardBody, Heading, Text } from '@chakra-ui/react'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import useIntersect from 'shared/hooks/useIntersection'; +import { createFormattedTime } from 'shared/utils/date'; + +import { getVolunteerRecruitmentsOnVolunteer } from '@/apis/volunteers'; + +type VolunteerRecruitmentItemProps = { + recruitmentTitle: string; + recruitmentStartTime: string; + shelterName: string; +}; + +type VolunteerRecruitmentsProps = { + id: number; +}; + +export default function VolunteerRecruitments({ + id, +}: VolunteerRecruitmentsProps) { + const { + data: { pages }, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + } = useSuspenseInfiniteQuery({ + queryKey: ['volunteer', 'profile', 'recruitments', id], + queryFn: ({ pageParam }) => + getVolunteerRecruitmentsOnVolunteer(id, { page: pageParam, size: 10 }), + initialPageParam: 0, + getNextPageParam: ({ data: { pageInfo } }, _, lastPageParam) => + pageInfo.hasNext ? lastPageParam + 1 : null, + }); + + const totalRecruitments = pages[0].data.pageInfo.totalElements; + const recruitments = pages.flatMap( + ({ data: { recruitments } }) => recruitments, + ); + + const ref = useIntersect((entry, observer) => { + observer.unobserve(entry.target); + + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }); + + return ( + + + 봉사 이력 {totalRecruitments}개 + + {recruitments.map((recruitment, index) => ( + + ))} + + + ); +} + +function VolunteerRecruitmentItem({ + shelterName, + recruitmentTitle, + recruitmentStartTime, +}: VolunteerRecruitmentItemProps) { + return ( + + + + {recruitmentTitle} + + + {shelterName} + + + {`봉사일 | ${createFormattedTime(new Date(recruitmentStartTime))}`} + + + + ); +} diff --git a/apps/shelter/src/pages/volunteers/profile/_components/VolunteerReviews.tsx b/apps/shelter/src/pages/volunteers/profile/_components/VolunteerReviews.tsx index 26ffee11..3d6fc0f5 100644 --- a/apps/shelter/src/pages/volunteers/profile/_components/VolunteerReviews.tsx +++ b/apps/shelter/src/pages/volunteers/profile/_components/VolunteerReviews.tsx @@ -1,53 +1,72 @@ import { Box, Heading, HStack, Text, VStack } from '@chakra-ui/react'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; import InfoSubtext from 'shared/components/InfoSubtext'; import ReviewItem from 'shared/components/ReviewItem'; +import useIntersect from 'shared/hooks/useIntersection'; import { createFormattedTime } from 'shared/utils/date'; -const DUMMY_REVIEWS_DATA = { - reviewId: 36, - shelterName: '남양주 보호소', - reviewCreatedAt: '2023-03-16T18:00', - reviewContent: '아이들이 너무 귀여워서 봉사하는 시간이 즐거웠습니다~!', - images: [ - 'https://source.unsplash.com/random', - 'https://source.unsplash.com/random', - 'https://source.unsplash.com/random', - ], -}; +import { getVolunteerReviewsOnVolunteer } from '@/apis/volunteers'; -const DUMMY_DATA = { - pageInfo: { - totalElements: 32, - hasNext: false, - }, - reviews: Array.from({ length: 5 }, () => DUMMY_REVIEWS_DATA), +type VolunteerReviewsProps = { + id: number; }; -export default function VolunteerReviews() { +export default function VolunteerReviews({ id }: VolunteerReviewsProps) { + const { + data: { pages }, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + } = useSuspenseInfiniteQuery({ + queryKey: ['volunteer', 'profile', 'reviews', id], + queryFn: ({ pageParam }) => + getVolunteerReviewsOnVolunteer(id, { page: pageParam, size: 10 }), + initialPageParam: 0, + getNextPageParam: ({ data: { pageInfo } }, _, lastPageParam) => + pageInfo.hasNext ? lastPageParam + 1 : null, + }); + + const totalReviews = pages[0].data.pageInfo.totalElements; + const reviews = pages.flatMap(({ data: { reviews } }) => reviews); + + const ref = useIntersect((entry, observer) => { + observer.unobserve(entry.target); + + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }); + return ( - 봉사 후기 {DUMMY_DATA.reviews.length}개 + 봉사 후기 {totalReviews}개 - {DUMMY_DATA.reviews.map( - ({ shelterName, reviewContent, reviewCreatedAt, images }, index) => { - return ( - - - - {shelterName} - - - - - ); - }, + {reviews.map( + ( + { shelterName, reviewContent, reviewCreatedAt, reviewImageUrls }, + index, + ) => ( + + + + {shelterName} + + + + + ), )} + ); } diff --git a/apps/shelter/src/pages/volunteers/profile/index.tsx b/apps/shelter/src/pages/volunteers/profile/index.tsx index 92fa6b27..6df24498 100644 --- a/apps/shelter/src/pages/volunteers/profile/index.tsx +++ b/apps/shelter/src/pages/volunteers/profile/index.tsx @@ -1,40 +1,50 @@ import { Box, useToken } from '@chakra-ui/react'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; import Label from 'shared/components/Label'; import ProfileInfo from 'shared/components/ProfileInfo'; import Tabs from 'shared/components/Tabs'; -import VolunteerRecords from './_components/VolunteerRecords'; -import VolunteerReviews from './_components/VolunteerReviews'; +import { getVolunteerProfile } from '@/apis/volunteers'; -const DUMMY_DATA = { - volunteerEmail: 'test@naver.com', - volunteerName: '김철수', - volunteerTemperate: 36, - volunteerImageUrl: 'www.aws.s3.com/asfqwe', - volunteerPhoneNumber: '010-8237-1847', -}; +import VolunteerRecruitments from './_components/VolunteerRecruitments'; +import VolunteerReviews from './_components/VolunteerReviews'; export default function VolunteersProfilePage() { + const { id } = useParams<{ id: string }>(); const [gray200] = useToken('colors', ['gray.200']); + const { + data: { + volunteerEmail, + volunteerImageUrl, + volunteerName, + volunteerPhoneNumber, + volunteerTemperature, + }, + } = useSuspenseQuery({ + queryKey: ['volunteer', 'profile', id], + queryFn: () => getVolunteerProfile(Number(id)), + select: (data) => { + return { ...data.data }; + }, + }); + return ( - ], - ['봉사 이력', ], + ['봉사 후기', ], + ['봉사 이력', ], ]} /> diff --git a/apps/shelter/src/types/apis/volunteers.ts b/apps/shelter/src/types/apis/volunteers.ts new file mode 100644 index 00000000..aaf03b90 --- /dev/null +++ b/apps/shelter/src/types/apis/volunteers.ts @@ -0,0 +1,42 @@ +type PageInfo = { + totalElements: number; + hasNext: boolean; +}; + +type Recruitment = { + recruitmentId: number; + recruitmentTitle: string; + recruitmentStartTime: string; + shelterName: string; +}; + +type Review = { + reviewId: number; + shelterName: string; + reviewCreatedAt: string; + reviewContent: string; + reviewImageUrls: string[]; +}; + +export type VolunteerProfileResponseData = { + volunteerEmail: string; + volunteerName: string; + volunteerTemperature: number; + volunteerImageUrl: string; + volunteerPhoneNumber: string; +}; + +export type VoluteerRecruitmentsOnVolunteerResponseData = { + pageInfo: PageInfo; + recruitments: Recruitment[]; +}; + +export type VolunteerCompletedsRequestParams = { + page: number; + size: number; +}; + +export type VolunteerReviewsOnVolunteerResponseData = { + pageInfo: PageInfo; + reviews: Review[]; +};