diff --git a/.eslintrc.js b/.eslintrc.js index 62dd04f..8d64621 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,5 +17,6 @@ module.exports = { 'prettier/prettier': ['error', { endOfLine: 'auto' }], '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/no-misused-promises': 'off', }, }; diff --git a/src/components/atoms/RadioButtonField.tsx b/src/components/atoms/RadioButtonField.tsx index 242e0dc..1ecd77d 100644 --- a/src/components/atoms/RadioButtonField.tsx +++ b/src/components/atoms/RadioButtonField.tsx @@ -7,7 +7,7 @@ const RadioButtonField: React.FC<{ checked: boolean; }> = ({ label, onClick, checked }) => { return ( - diff --git a/src/components/atoms/ShareStatusBadge.tsx b/src/components/atoms/ShareStatusBadge.tsx new file mode 100644 index 0000000..29dbd98 --- /dev/null +++ b/src/components/atoms/ShareStatusBadge.tsx @@ -0,0 +1,35 @@ +import React, { useMemo } from 'react'; + +import type { ShareStatusType } from '@/types/friendship'; + +const ShareStatusBadge: React.FC<{ status: ShareStatusType }> = ({ status }) => { + const text = useMemo(() => { + switch (status) { + case 'SHARE_START': + return '나눔 신청'; + case 'SHARE_IN_PROGRESS': + return '나눔 중'; + case 'SHARE_COMPLETE': + return '나눔 완료'; + default: + return ''; + } + }, [status]); + + const className = useMemo(() => { + switch (status) { + case 'SHARE_START': + return 'bg-[#DCF3ED] text-primary2'; + case 'SHARE_IN_PROGRESS': + return 'bg-[#FFEBE6] text-point3'; + case 'SHARE_COMPLETE': + return 'bg-gray0 text-gray4'; + default: + return ''; + } + }, [status]); + + return
{text}
; +}; + +export default ShareStatusBadge; diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts index 7c06d92..994dd2f 100644 --- a/src/components/atoms/index.ts +++ b/src/components/atoms/index.ts @@ -18,3 +18,4 @@ export { default as MiniButton } from './MiniButton'; export { default as ExclamationAlertSpan } from './ExclamationAlertSpan'; export { default as Lottie } from './Lottie'; export { default as CheckBox } from './CheckBox'; +export { default as ShareStatusBadge } from './ShareStatusBadge'; diff --git a/src/components/organisms/ShareDetailAuthorBottomWrapper.tsx b/src/components/organisms/ShareDetailAuthorBottomWrapper.tsx index 0bc7959..7c7f8c0 100644 --- a/src/components/organisms/ShareDetailAuthorBottomWrapper.tsx +++ b/src/components/organisms/ShareDetailAuthorBottomWrapper.tsx @@ -1,22 +1,30 @@ +import { Button, RadioButtonField } from '@/components/atoms'; import { Modal, ModalBody, ModalContent, ModalOverlay, useDisclosure } from '@chakra-ui/react'; +import React, { useEffect, useState } from 'react'; +import { useGetShareApplicants, usePutShareStatus } from '@/hooks/queries/share'; -import { Button, RadioButtonField } from '@/components/atoms'; -import React from 'react'; -import { type SortLabel } from '@/types/common'; -import { useGetShareApplicants } from '@/hooks/queries/share'; import ShareApplicantListItem from './ShareApplicantListItem'; +import type { ShareStatusType } from '@/types/friendship'; + +export interface ShareStatusKeyValue { + label: string; + value: ShareStatusType; +} -const SHARE_STATUSES = [ - { label: '나눔 신청', value: 'enroll' }, - { label: '나눔 중', value: 'proceeding' }, - { label: '나눔 완료', value: 'complete' }, +const SHARE_STATUSES: ShareStatusKeyValue[] = [ + { label: '나눔 신청', value: 'SHARE_START' }, + { label: '나눔 중', value: 'SHARE_IN_PROGRESS' }, + { label: '나눔 완료', value: 'SHARE_COMPLETE' }, ]; const ShareDetailAuthorBottomWrapper: React.FC<{ id: string | string[] | undefined; - curStatus: SortLabel; - onChangeStatus: React.Dispatch>; -}> = ({ id, curStatus, onChangeStatus }) => { + refetch: () => void; + curStatus: ShareStatusType; +}> = ({ id, refetch, curStatus }) => { + const [selectedStatus, setSelectedStatus] = useState( + SHARE_STATUSES.find((ele) => ele.value === curStatus) as ShareStatusKeyValue, + ); const { isOpen: isStatusModalOpen, onOpen: onStatusModalOpen, onClose: onStatusModalClose } = useDisclosure(); const { isOpen: isParticipantsModalOpen, @@ -25,6 +33,22 @@ const ShareDetailAuthorBottomWrapper: React.FC<{ } = useDisclosure(); const applicants = useGetShareApplicants({ id }); + const modifyShareStatus = usePutShareStatus({ + id: Number(id), + status: selectedStatus.value as ShareStatusType, + onSuccessParam: refetch, + }); + + const onModifyShareStatus = () => { + onStatusModalClose(); + modifyShareStatus.mutate({}); + }; + + useEffect(() => { + const initialStatus = SHARE_STATUSES.find((ele) => ele.value === curStatus); + console.log(initialStatus); + setSelectedStatus(initialStatus as { label: string; value: ShareStatusType }); + }, []); return ( <> @@ -53,13 +77,13 @@ const ShareDetailAuthorBottomWrapper: React.FC<{ key={ele.value} label={ele.label} onClick={() => { - onChangeStatus(ele); + setSelectedStatus(ele); }} - checked={ele.value === curStatus.value} + checked={ele.value === selectedStatus.value} /> ))}
-
diff --git a/src/components/organisms/ShareDetailFriendBottomWrapper.tsx b/src/components/organisms/ShareDetailFriendBottomWrapper.tsx new file mode 100644 index 0000000..639c750 --- /dev/null +++ b/src/components/organisms/ShareDetailFriendBottomWrapper.tsx @@ -0,0 +1,61 @@ +import { useApplyShare, useDeleteApplyShare } from '@/hooks/queries/share'; + +import React from 'react'; +import type { ShareStatusType } from '@/types/friendship'; +import useToast from '@/hooks/useToast'; + +const APPLY_SUCCESS_MESSAGE = '나눔 신청이 완료되었습니다.'; +const APPLY_DELETE_SUCCESS_MESSAGE = '나눔 신청이 취소되었습니다.'; + +const ShareDetailFriendBottomWrapper: React.FC<{ + id: string | string[] | undefined; + isApplied: boolean; + curStatus: ShareStatusType; + refetch: () => void; +}> = ({ id, isApplied, curStatus, refetch }) => { + const { showToast } = useToast(); + const applyShare = useApplyShare({ + onSuccess: () => { + showToast(APPLY_SUCCESS_MESSAGE, 'success'); + refetch(); + }, + }); + const deleteShare = useDeleteApplyShare({ + id: Number(id), + onSuccess: () => { + showToast(APPLY_DELETE_SUCCESS_MESSAGE, 'success'); + refetch(); + }, + }); + + const onApply = () => { + applyShare.mutate({ shareId: Number(id) }); + }; + + const onApplyCancel = () => { + deleteShare.mutate({}); + }; + + if (curStatus === 'SHARE_COMPLETE') { + return ( +
+

+ 나눔 신청 종료 +

+
+ ); + } + + return ( +
+ +
+ ); +}; + +export default ShareDetailFriendBottomWrapper; diff --git a/src/components/organisms/index.ts b/src/components/organisms/index.ts index e97f17b..09ec9bd 100644 --- a/src/components/organisms/index.ts +++ b/src/components/organisms/index.ts @@ -15,3 +15,4 @@ export { default as FriendListItem } from './FriendListItem'; export { default as BulletNoticeBox } from './BulletNoticeBox'; export { default as SelectFridgeModal } from './SelectFridgeModal'; export { default as SelectFridgeBoard } from './SelectFridgeBoard'; +export { default as ShareDetailFriendBottomWrapper } from './ShareDetailFriendBottomWrapper'; diff --git a/src/hooks/queries/queryKeys.ts b/src/hooks/queries/queryKeys.ts index 7a65efa..f266b3c 100644 --- a/src/hooks/queries/queryKeys.ts +++ b/src/hooks/queries/queryKeys.ts @@ -27,6 +27,9 @@ export const queryKeys = { SHARE_APPLICANTS: () => ['shareApplicants'], UPLOAD: () => ['upload'], ADD_SHARE: () => ['addShare'], + MODIFY_SHARE_STATUS: () => ['modify_share_status'], + APPLY_SHARE: () => ['apply_share'], + DELETE_APPLY_SHARE: () => ['delete_share'], } as const; export type QueryKeys = (typeof queryKeys)[keyof typeof queryKeys]; diff --git a/src/hooks/queries/share/index.ts b/src/hooks/queries/share/index.ts index 83f0f78..d06b1a6 100644 --- a/src/hooks/queries/share/index.ts +++ b/src/hooks/queries/share/index.ts @@ -3,3 +3,6 @@ export { default as useGetShareDetail } from './useGetShareDetail'; export { default as useGetShareApplicants } from './useGetShareApplicants'; export { default as usePostUpload } from './usePostUpload'; export { default as usePostShare } from './usePostShare'; +export { default as usePutShareStatus } from './usePutShareStatus'; +export { default as useApplyShare } from './useApplyShare'; +export { default as useDeleteApplyShare } from './useDeleteApplyShare'; diff --git a/src/hooks/queries/share/useApplyShare.ts b/src/hooks/queries/share/useApplyShare.ts new file mode 100644 index 0000000..d352f2c --- /dev/null +++ b/src/hooks/queries/share/useApplyShare.ts @@ -0,0 +1,7 @@ +import { queryKeys } from '../queryKeys'; +import { useBaseMutation } from '../useBaseMutation'; + +const useApplyShare = ({ onSuccess }: { onSuccess: () => void }) => { + return useBaseMutation<{ shareId: number }>(queryKeys.APPLY_SHARE(), `/shares/applies`, onSuccess); +}; +export default useApplyShare; diff --git a/src/hooks/queries/share/useDeleteApplyShare.ts b/src/hooks/queries/share/useDeleteApplyShare.ts new file mode 100644 index 0000000..de74aa0 --- /dev/null +++ b/src/hooks/queries/share/useDeleteApplyShare.ts @@ -0,0 +1,7 @@ +import { queryKeys } from '../queryKeys'; +import { useBaseMutation } from '../useBaseMutation'; + +const useDeleteApplyShare = ({ id, onSuccess }: { id: number; onSuccess: () => void }) => { + return useBaseMutation(queryKeys.DELETE_APPLY_SHARE(), `/shares/applies/${id}`, onSuccess, 'DELETE'); +}; +export default useDeleteApplyShare; diff --git a/src/hooks/queries/share/useGetShareDetail.ts b/src/hooks/queries/share/useGetShareDetail.ts index d902849..175eb3c 100644 --- a/src/hooks/queries/share/useGetShareDetail.ts +++ b/src/hooks/queries/share/useGetShareDetail.ts @@ -6,9 +6,9 @@ const useGetShareDetail = ({ id }: { id: string | string[] | undefined }) => { if (typeof id !== 'string') { return null; } - const { data } = useBaseQuery(queryKeys.SHARE_DETAIL(), `/shares/${id}`); + const { data, refetch } = useBaseQuery(queryKeys.SHARE_DETAIL(), `/shares/${id}`); - return data?.data; + return { data, refetch }; }; export default useGetShareDetail; diff --git a/src/hooks/queries/share/usePutShareStatus.ts b/src/hooks/queries/share/usePutShareStatus.ts new file mode 100644 index 0000000..e84f9c9 --- /dev/null +++ b/src/hooks/queries/share/usePutShareStatus.ts @@ -0,0 +1,26 @@ +import type { ShareStatusType } from '@/types/friendship'; +import { queryClient } from '@/pages/_app'; +import { queryKeys } from '../queryKeys'; +import { useBaseMutation } from '../useBaseMutation'; + +const usePutShareStatus = ({ + id, + status, + onSuccessParam, +}: { + id: number; + status: ShareStatusType; + onSuccessParam: () => void; +}) => { + const onSuccess = () => { + onSuccessParam(); + void queryClient.invalidateQueries(); + }; + return useBaseMutation( + queryKeys.MODIFY_SHARE_STATUS(), + `/shares/${id}/status?updateShareStatusRequest=${status}`, + onSuccess, + 'PUT', + ); +}; +export default usePutShareStatus; diff --git a/src/pages/share/[id].tsx b/src/pages/share/[id].tsx index 95cbbd4..e8a96b6 100644 --- a/src/pages/share/[id].tsx +++ b/src/pages/share/[id].tsx @@ -1,39 +1,38 @@ import { ClockIcon, DateIcon, LocationIcon } from '@/assets/icons'; +import { Header, ShareDetailAuthorBottomWrapper, ShareDetailFriendBottomWrapper } from '@/components/organisms'; +import { Lottie, ShareStatusBadge } from '@/components/atoms'; import { ShareInfoRowItem, VerticalLabelValue } from '@/components/molecules'; -import { Header, ShareDetailAuthorBottomWrapper } from '@/components/organisms'; -import { useGetShareDetail } from '@/hooks/queries/share'; -import { type SortLabel } from '@/types/common'; -import dayjs from 'dayjs'; -import { type NextPage } from 'next'; + import Image from 'next/image'; +import type { NextPage } from 'next'; +import type { ProfileEnum } from '@/types/common'; +import React from 'react'; +import type { ShareStatusType } from '@/types/friendship'; +import dayjs from 'dayjs'; +import { returnProfileImg } from '@/utils/returnProfileImg'; +import { useGetShareDetail } from '@/hooks/queries/share'; import { useRouter } from 'next/router'; -import React, { useEffect, useState } from 'react'; - -const MOCK_DATA_IS_AUTHOR: boolean = true; - -const MOCK_DATA_SHARE_STATUS = { label: '나눔 신청', value: 'enroll' }; const ShareDetailPage: NextPage = () => { const router = useRouter(); const { id } = router.query; - const [curStatus, setCurStatus] = useState(MOCK_DATA_SHARE_STATUS); - const data = useGetShareDetail({ id }); + const shareDetail = useGetShareDetail({ id }); + + if (!shareDetail?.data) { + return ; + } - useEffect(() => { - if (window) { - console.log(window.innerWidth); - } - }, []); + const { data, refetch } = shareDetail; return ( <>
- {data?.thumbnailImage ? ( + {data.data?.thumbnailImage ? ( detailImage { )}
-

{MOCK_DATA_IS_AUTHOR ? '나의 나눔' : data?.userName}

-

{data?.title}

+
+ profileImage +

+ {data.data?.isCreatedByCurrentLoginUser ? '나의 나눔' : data.data?.nickname} +

+ +
+

{data.data?.title}

- +
- +
0 ? dayjs(data?.limitDate).diff(dayjs(), 'day') : 0}`} + value={`D-${dayjs(data.data?.limitDate).diff(dayjs(), 'day') > 0 ? dayjs(data.data?.limitDate).diff(dayjs(), 'day') : 0}`} />
- - - + + +

상세설명

-

{data?.content}

+

{data.data?.content}

- {MOCK_DATA_IS_AUTHOR ? ( - + {data.data?.isCreatedByCurrentLoginUser ? ( + ) : ( -
- -
+ )} ); diff --git a/src/types/share/index.d.ts b/src/types/share/index.d.ts index 62961b6..393355f 100644 --- a/src/types/share/index.d.ts +++ b/src/types/share/index.d.ts @@ -1,4 +1,5 @@ import type { ProfileEnum } from '../common'; +import type { ShareStatusType } from '../friendship'; interface ShareData { shareId: number; @@ -10,14 +11,15 @@ interface ShareData { limitDate: string; limitPerson: number; location: string; - status: string; + status: ShareStatusType; thumbnailImage: string; isApplied: boolean; } interface ShareDetailData extends ShareData { - userName: string; + nickname: string; profileImage: ProfileEnum; + isCreatedByCurrentLoginUser: boolean; } interface ShareApplicantData {