Skip to content

Commit

Permalink
feat: 보호소의 봉사자 프로필 페이지 api를 포함한 로직 추가 (#216)
Browse files Browse the repository at this point in the history
* feat(shelter): volunteers api 관련 type 추가

* feat(shelter): volunteers api에 봉사자 프로필 관련 추가

* feat(shelter): volunteers에 봉사자 프로필 관련 handlers 추가

* feat(shelter): volunteers handler를 browser에 추가

* feat(shelter): VolunteersRecords를 VolunteerRecruitments로 변경 및 api를 포함한 로직 추가

* feat(shelter): VolunteerReviews에 api를 포함한 로직 추가

* feat(shelter): VolunteerProfilePage에 api를 포함한 로직 추가
  • Loading branch information
sukvvon authored Nov 29, 2023
1 parent 10a294e commit 62a7226
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 111 deletions.
31 changes: 31 additions & 0 deletions apps/shelter/src/apis/volunteers.ts
Original file line number Diff line number Diff line change
@@ -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<VolunteerProfileResponseData>(
`/shelters/volunteers/${volunteerId}/profile`,
);

export const getVolunteerReviewsOnVolunteer = (
volunteerId: number,
params: VolunteerCompletedsRequestParams,
) =>
axiosInstance.get<VolunteerReviewsOnVolunteerResponseData>(
`/shelters/volunteers/${volunteerId}/reviews`,
{ params },
);

export const getVolunteerRecruitmentsOnVolunteer = (
volunteerId: number,
params: VolunteerCompletedsRequestParams,
) =>
axiosInstance.get<VoluteerRecruitmentsOnVolunteerResponseData>(
`/shelters/volunteers/${volunteerId}/recruitments/completed`,
{ params },
);
2 changes: 2 additions & 0 deletions apps/shelter/src/mocks/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ 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,
...shelterHandlers,
...recruitmentHandler,
...recruitmentDetailHandler,
...manageHandlers,
...volunteerHandlers,
);
62 changes: 62 additions & 0 deletions apps/shelter/src/mocks/handlers/volunteers.ts
Original file line number Diff line number Diff line change
@@ -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: '[email protected]',
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),
});
},
),
];

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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 (
<Box>
<Heading fontWeight={600} fontSize="md" py={4}>
봉사 이력 {totalRecruitments}
</Heading>
{recruitments.map((recruitment, index) => (
<VolunteerRecruitmentItem key={index} {...recruitment} />
))}
<Box ref={ref} />
</Box>
);
}

function VolunteerRecruitmentItem({
shelterName,
recruitmentTitle,
recruitmentStartTime,
}: VolunteerRecruitmentItemProps) {
return (
<Card p={4} pb={3.5} mb={2}>
<CardBody pos="relative" p={0}>
<Text pb={2} fontWeight={600}>
{recruitmentTitle}
</Text>
<Text fontSize="sm" color="gray.400">
{shelterName}
</Text>
<Text fontSize="sm" color="black">
{`봉사일 | ${createFormattedTime(new Date(recruitmentStartTime))}`}
</Text>
</CardBody>
</Card>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<Box>
<Heading fontSize="md" py={4}>
봉사 후기 {DUMMY_DATA.reviews.length}
봉사 후기 {totalReviews}
</Heading>
<VStack spacing={2}>
{DUMMY_DATA.reviews.map(
({ shelterName, reviewContent, reviewCreatedAt, images }, index) => {
return (
<ReviewItem key={index} content={reviewContent} images={images}>
<Box>
<HStack mb={1}>
<Text fontWeight={600}>{shelterName}</Text>
</HStack>
<InfoSubtext
title="작성일"
content={createFormattedTime(new Date(reviewCreatedAt))}
/>
</Box>
</ReviewItem>
);
},
{reviews.map(
(
{ shelterName, reviewContent, reviewCreatedAt, reviewImageUrls },
index,
) => (
<ReviewItem
key={index}
content={reviewContent}
images={reviewImageUrls}
>
<Box>
<HStack mb={1}>
<Text fontWeight={600}>{shelterName}</Text>
</HStack>
<InfoSubtext
title="작성일"
content={createFormattedTime(new Date(reviewCreatedAt))}
/>
</Box>
</ReviewItem>
),
)}
</VStack>
<Box ref={ref} />
</Box>
);
}
Loading

0 comments on commit 62a7226

Please sign in to comment.