diff --git a/src/api/myProjectList.api.ts b/src/api/myProjectList.api.ts index cd65a19a..415a989a 100644 --- a/src/api/myProjectList.api.ts +++ b/src/api/myProjectList.api.ts @@ -5,3 +5,10 @@ export const getMyProjectLists = async () => { const response = await httpClient.get(`/project/my`); return response.data; }; + +export const patchSendResult = async (projectId: number) => { + const response = await httpClient.patch( + `/project/${projectId}/is-done` + ); + return response.data; +}; diff --git a/src/components/common/Button/Button.styled.ts b/src/components/common/Button/Button.styled.ts index d48cbf22..09a9250c 100644 --- a/src/components/common/Button/Button.styled.ts +++ b/src/components/common/Button/Button.styled.ts @@ -7,6 +7,15 @@ export const CommonButton = styled.button>` color: ${({ theme, schema }) => theme.buttonScheme[schema].color}; background-color: ${({ theme, schema }) => theme.buttonScheme[schema].bg}; border-radius: ${({ theme, radius }) => theme.borderRadius[radius]}; - pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')}; - cursor: ${({ disabled }) => (disabled ? 'none' : 'pointer')}; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; + + display: flex; + justify-content: center; + align-items: center; + + &:disabled { + cursor: not-allowed; + opacity: 0.9; + background-color: ${({ theme }) => theme.color.grey}; + } `; diff --git a/src/components/common/Button/Button.tsx b/src/components/common/Button/Button.tsx index a67e73f3..95e91c2c 100644 --- a/src/components/common/Button/Button.tsx +++ b/src/components/common/Button/Button.tsx @@ -1,4 +1,4 @@ -import { ButtonHTMLAttributes, ReactNode } from 'react'; +import { ButtonHTMLAttributes, PropsWithChildren } from 'react'; import * as S from './Button.styled'; import { BorderRadiusSize, @@ -7,7 +7,6 @@ import { } from '../../../style/theme'; export interface ButtonProps extends ButtonHTMLAttributes { - children: ReactNode; size: ButtonSize; schema: ButtonSchema; radius: BorderRadiusSize; @@ -21,7 +20,7 @@ function Button({ radius, disabled, ...props -}: ButtonProps) { +}: PropsWithChildren) { return ( theme.color.red}; + flex-shrink: 0; `; diff --git a/src/components/manageProjects/passNonPassList/DeleteButton.styled.ts b/src/components/manageProjects/passNonPassList/DeleteButton.styled.ts new file mode 100644 index 00000000..77027c7b --- /dev/null +++ b/src/components/manageProjects/passNonPassList/DeleteButton.styled.ts @@ -0,0 +1,14 @@ +import styled from 'styled-components'; + +export const DeleteButton = styled.button` + svg { + color: #e69191; + width: 1.2rem; + height: 1.2rem; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.9; + } +`; diff --git a/src/components/manageProjects/passNonPassList/DeleteButton.tsx b/src/components/manageProjects/passNonPassList/DeleteButton.tsx new file mode 100644 index 00000000..b1f6bbb8 --- /dev/null +++ b/src/components/manageProjects/passNonPassList/DeleteButton.tsx @@ -0,0 +1,17 @@ +import { ButtonHTMLAttributes } from 'react'; +import * as S from './DeleteButton.styled'; +import { XCircleIcon } from '@heroicons/react/24/outline'; +interface DeleteButtonProps extends ButtonHTMLAttributes { + onClick: () => void; + disabled: boolean; +} + +function DeleteButton({ onClick, disabled }: DeleteButtonProps) { + return ( + + + + ); +} + +export default DeleteButton; diff --git a/src/components/manageProjects/passNonPassList/PassNonPassItem.styled.ts b/src/components/manageProjects/passNonPassList/PassNonPassItem.styled.ts index 13826de6..66fbfacc 100644 --- a/src/components/manageProjects/passNonPassList/PassNonPassItem.styled.ts +++ b/src/components/manageProjects/passNonPassList/PassNonPassItem.styled.ts @@ -12,17 +12,9 @@ export const ItemWrapper = styled.li` background-color: ${({ theme }) => theme.color.navy}; color: ${({ theme }) => theme.color.white}; } - - svg { - color: #e69191; - width: 1.2rem; - height: 1.2rem; - } `; export const NickName = styled.p` font-size: ${({ theme }) => theme.heading.small.fontSize}; font-weight: 400; `; - -export const DeleteButton = styled.button``; diff --git a/src/components/manageProjects/passNonPassList/PassNonPassItem.tsx b/src/components/manageProjects/passNonPassList/PassNonPassItem.tsx index 61089540..819c890f 100644 --- a/src/components/manageProjects/passNonPassList/PassNonPassItem.tsx +++ b/src/components/manageProjects/passNonPassList/PassNonPassItem.tsx @@ -1,18 +1,27 @@ +import { useMutationParams } from '../../../hooks/usePassNonPassMutation'; import { ApplicantInfo } from '../../../models/applicant'; +import { ProjectDetailExtended } from '../../../models/projectDetail'; +import DeleteButton from './DeleteButton'; import * as S from './PassNonPassItem.styled'; -import { XCircleIcon } from '@heroicons/react/24/outline'; interface PassNonPassItemProps { userInfo: ApplicantInfo; + projectData: ProjectDetailExtended; + onClick: ({ status, userId }: useMutationParams) => void; } -function PassNonPassItem({ userInfo }: PassNonPassItemProps) { +function PassNonPassItem({ + userInfo, + onClick, + projectData, +}: PassNonPassItemProps) { return ( {userInfo.User.nickname} - - - + onClick({ status: 'WAITING', userId: userInfo.userId })} + /> ); } diff --git a/src/components/manageProjects/passNonPassList/PassNonPassList.tsx b/src/components/manageProjects/passNonPassList/PassNonPassList.tsx index c6bbdf48..6953e8f7 100644 --- a/src/components/manageProjects/passNonPassList/PassNonPassList.tsx +++ b/src/components/manageProjects/passNonPassList/PassNonPassList.tsx @@ -1,16 +1,29 @@ +import { useMutationParams } from '../../../hooks/usePassNonPassMutation'; import { ApplicantInfo } from '../../../models/applicant'; +import { ProjectDetailExtended } from '../../../models/projectDetail'; import PassNonPassItem from './PassNonPassItem'; import * as S from './PassNonPassList.styled'; interface PassNonPassListProps { passNonPassListData: ApplicantInfo[]; + projectData: ProjectDetailExtended; + onClick: ({ status, userId }: useMutationParams) => void; } -function PassNonPassList({ passNonPassListData }: PassNonPassListProps) { +function PassNonPassList({ + passNonPassListData, + projectData, + onClick, +}: PassNonPassListProps) { return ( {passNonPassListData.map((data) => ( - + ))} ); diff --git a/src/components/manageProjects/passNonPassList/SendResultButton.styled.ts b/src/components/manageProjects/passNonPassList/SendResultButton.styled.ts new file mode 100644 index 00000000..59ce6cf6 --- /dev/null +++ b/src/components/manageProjects/passNonPassList/SendResultButton.styled.ts @@ -0,0 +1,24 @@ +import styled from 'styled-components'; +import Button from '../../common/Button/Button'; + +export const Wrapper = styled.div` + width: 100%; + display: flex; + justify-content: end; + align-items: center; + position: absolute; + top: 7rem; + right: 2.5rem; +`; + +export const SendEmailButton = styled(Button)` + width: 9rem; + + svg { + width: 1.5rem; + margin-left: 0.5rem; + } + &:hover { + opacity: 0.8; + } +`; diff --git a/src/components/manageProjects/passNonPassList/SendResultButton.tsx b/src/components/manageProjects/passNonPassList/SendResultButton.tsx new file mode 100644 index 00000000..873fee43 --- /dev/null +++ b/src/components/manageProjects/passNonPassList/SendResultButton.tsx @@ -0,0 +1,26 @@ +import { ButtonHTMLAttributes } from 'react'; +import * as S from './SendResultButton.styled'; +import { EnvelopeIcon } from '@heroicons/react/24/outline'; +export interface SendResultButtonProps + extends ButtonHTMLAttributes { + onSubmit: () => void; + disabled?: boolean; +} + +function SendResultButton({ onSubmit, disabled }: SendResultButtonProps) { + return ( + + + 결과 전송 + + + ); +} + +export default SendResultButton; diff --git a/src/constants/modalMessage.ts b/src/constants/modalMessage.ts index af342656..36a8466e 100644 --- a/src/constants/modalMessage.ts +++ b/src/constants/modalMessage.ts @@ -1,7 +1,10 @@ export const MODAL_MESSAGE = { pass: '지원자를 합격 리스트에 추가했습니다.', nonPass: '지원자를 불합격 리스트에 추가했습니다.', + waiting: '지원자를 대기리스트에 추가했습니다', equalStatus: '이미 동일한 리스트에 추가하신 상태입니다.', + sendResult: '지원자들에게 이메일로 결과를 전송했어요!', + needAuth: '권한이 없어요!', signUpSuccess: '회원가입 완료되었습니다.', signUpFail: '회원가입 실패하였습니다.', changePasswordSuccess: '비밀번호가 성공적으로 재설정 되었습니다.', diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 5110fd63..5d99d7b1 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -15,5 +15,4 @@ export const ROUTES = { userpage: '/user', userJoinedProject: 'join-projects', modifyProject: '/project-modify', - notFound: '/not-found', } as const; diff --git a/src/hooks/queries/keys.ts b/src/hooks/queries/keys.ts index fb9d9247..0dfc420b 100644 --- a/src/hooks/queries/keys.ts +++ b/src/hooks/queries/keys.ts @@ -1,5 +1,6 @@ -export const managedProjectsKey = { - mine: ['myManagedProjects'], +export const managedProjectKey = { + managedProjectList: ['myManagedProjectList'], + detail: ['projectDataAll'], } as const; export const applicantKey = { diff --git a/src/hooks/useJoinProject.ts b/src/hooks/useJoinProject.ts index 19cfc4c6..a0f8614e 100644 --- a/src/hooks/useJoinProject.ts +++ b/src/hooks/useJoinProject.ts @@ -1,10 +1,11 @@ import { useQuery } from '@tanstack/react-query'; import { getProjectData } from '../api/joinProject.api'; +import { managedProjectKey } from './queries/keys'; -const useGetProjectData = (id: number) => { +const useGetProjectData = (projectId: number) => { const { data, isLoading, isFetching } = useQuery({ - queryKey: ['projectDataAll', id], - queryFn: async () => await getProjectData(id), + queryKey: [managedProjectKey.detail, projectId], + queryFn: async () => await getProjectData(projectId), staleTime: 1000 * 60 * 5, }); diff --git a/src/hooks/useManagedProjects.ts b/src/hooks/useManagedProjects.ts index f2e1aff1..27be96bf 100644 --- a/src/hooks/useManagedProjects.ts +++ b/src/hooks/useManagedProjects.ts @@ -1,11 +1,11 @@ import { ManagedProject } from '../models/manageMyProject'; import { getMyProjectLists } from '../api/myProjectList.api'; import { useQuery } from '@tanstack/react-query'; -import { managedProjectsKey } from './queries/keys'; +import { managedProjectKey } from './queries/keys'; export const useManagedProjects = () => { const { data, isLoading } = useQuery({ - queryKey: managedProjectsKey.mine, + queryKey: managedProjectKey.managedProjectList, queryFn: () => getMyProjectLists(), staleTime: 1 * 60 * 1000, }); diff --git a/src/hooks/usePassNonPassList.ts b/src/hooks/usePassNonPassList.ts index 6ef277ca..3d032187 100644 --- a/src/hooks/usePassNonPassList.ts +++ b/src/hooks/usePassNonPassList.ts @@ -1,13 +1,13 @@ -import { useQuery } from '@tanstack/react-query'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { applicantKey } from './queries/keys'; import { getPassNonPassList } from '../api/applicant.api'; export const usePassNonPassList = (projectId: number) => { - const { data } = useQuery({ - queryKey: applicantKey.passNonPass, + const { data, isLoading } = useSuspenseQuery({ + queryKey: [applicantKey.passNonPass, projectId], queryFn: () => getPassNonPassList(projectId), staleTime: 1 * 60 * 1000, }); - return { passNonPassListData: data }; + return { passNonPassListData: data, isLoading }; }; diff --git a/src/hooks/usePassNonPassMutation.ts b/src/hooks/usePassNonPassMutation.ts index 3692beab..cd3d0f42 100644 --- a/src/hooks/usePassNonPassMutation.ts +++ b/src/hooks/usePassNonPassMutation.ts @@ -4,8 +4,8 @@ import { AxiosError } from 'axios'; import { applicantKey } from './queries/keys'; import { MODAL_MESSAGE } from '../constants/modalMessage'; -interface useMutationParams { - isPass: boolean; +export interface useMutationParams { + status: 'ACCEPTED' | 'REJECTED' | 'WAITING'; userId: number; } @@ -16,19 +16,33 @@ export const usePassNonPassMutation = ( const queryClient = useQueryClient(); const passNonPassMutation = useMutation({ - mutationFn: async ({ isPass, userId }: useMutationParams) => { - const data = { status: isPass ? 'ACCEPTED' : 'REJECTED' }; + mutationFn: async ({ status, userId }: useMutationParams) => { + const data = { status: status }; await patchPassNonPassStatus(data, projectId, userId); }, - onSuccess: (_, { isPass }) => { + onSuccess: (_, { status }) => { queryClient.invalidateQueries({ queryKey: [applicantKey.all, projectId], }); - const successMessage = isPass - ? MODAL_MESSAGE.pass - : MODAL_MESSAGE.nonPass; + queryClient.invalidateQueries({ + queryKey: [applicantKey.passNonPass, projectId], + }); + + let successMessage; + switch (status) { + case 'ACCEPTED': + successMessage = MODAL_MESSAGE.pass; + break; + case 'REJECTED': + successMessage = MODAL_MESSAGE.nonPass; + break; + case 'WAITING': + successMessage = MODAL_MESSAGE.waiting; + break; + } + openModal(successMessage); }, @@ -38,8 +52,8 @@ export const usePassNonPassMutation = ( }, }); - const handlePassNonPassStatus = (isPass: boolean, userId: number) => { - passNonPassMutation.mutate({ isPass, userId }); + const handlePassNonPassStatus = ({ status, userId }: useMutationParams) => { + passNonPassMutation.mutate({ status, userId }); }; return { handlePassNonPassStatus }; diff --git a/src/hooks/useSendResultMutation.ts b/src/hooks/useSendResultMutation.ts new file mode 100644 index 00000000..846186a9 --- /dev/null +++ b/src/hooks/useSendResultMutation.ts @@ -0,0 +1,36 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { patchSendResult } from '../api/myProjectList.api'; +import { MODAL_MESSAGE } from '../constants/modalMessage'; +import { managedProjectKey } from './queries/keys'; + +export const useSendResultMutation = ( + projectId: number, + openModal: (message: string) => void +) => { + const queryClient = useQueryClient(); + + const sendResultMutaition = useMutation({ + mutationFn: async () => { + await patchSendResult(projectId); + }, + + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [managedProjectKey.detail, projectId], + }); + openModal(MODAL_MESSAGE.sendResult); + }, + + onError: (error) => { + console.error(error); + openModal(MODAL_MESSAGE.needAuth); + }, + }); + + const handleSendResult = () => { + sendResultMutaition.mutate(); + }; + + return { handleSendResult }; +}; diff --git a/src/mock/browser.ts b/src/mock/browser.ts index 6febaff4..cac154c2 100644 --- a/src/mock/browser.ts +++ b/src/mock/browser.ts @@ -1,5 +1,5 @@ import { setupWorker } from 'msw/browser'; -import { myProjectList } from './manageProjectList'; +import { myProjectList, sendResult } from './manageProjectList'; import { applicantInfo, applicantList, @@ -31,6 +31,7 @@ export const handlers = [ fetchPositionTag, fetchSkillTag, passNonPassList, + sendResult, myProjectList, applicantList, projectDetail, diff --git a/src/mock/manageProjectList.ts b/src/mock/manageProjectList.ts index 14065d83..64124788 100644 --- a/src/mock/manageProjectList.ts +++ b/src/mock/manageProjectList.ts @@ -9,3 +9,15 @@ export const myProjectList = http.get( }); } ); + +export const sendResult = http.patch( + `${import.meta.env.VITE_API_BASE_URL}/project/:projectId/is-done`, + () => { + return HttpResponse.json( + { message: '지원자들에게 결과를 전송했어요' }, + { + status: 200, + } + ); + } +); diff --git a/src/pages/error/Error.tsx b/src/pages/error/Error.tsx deleted file mode 100644 index a01c1001..00000000 --- a/src/pages/error/Error.tsx +++ /dev/null @@ -1,5 +0,0 @@ -function Error() { - return
Error
; -} - -export default Error; diff --git a/src/pages/manage/myProjectParticipantsPass/MyProjectVolunteersPass.tsx b/src/pages/manage/myProjectParticipantsPass/MyProjectVolunteersPass.tsx index 261c0171..ae04c421 100644 --- a/src/pages/manage/myProjectParticipantsPass/MyProjectVolunteersPass.tsx +++ b/src/pages/manage/myProjectParticipantsPass/MyProjectVolunteersPass.tsx @@ -9,39 +9,75 @@ import useGetProjectData from '../../../hooks/useJoinProject'; import ProjectHeader from '../../../components/manageProjects/ProjectHeader'; import PassNonPassList from '../../../components/manageProjects/passNonPassList/PassNonPassList'; import NoContent from '../../../components/common/noContent/NoContent'; +import SendResultButton from '../../../components/manageProjects/passNonPassList/SendResultButton'; +import { useSendResultMutation } from '../../../hooks/useSendResultMutation'; +import { useModal } from '../../../hooks/useModal'; +import Modal from '../../../components/common/modal/Modal'; +import { usePassNonPassMutation } from '../../../hooks/usePassNonPassMutation'; +import { Suspense, useMemo } from 'react'; +import LoadingSpinner from '../../../components/common/loadingSpinner/LoadingSpinner'; + const MyProjectVolunteersPass = () => { const { projectId } = useParams(); const { data: projectData } = useGetProjectData(Number(projectId)); const { passNonPassListData } = usePassNonPassList(Number(projectId)); + const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); + const { handleSendResult } = useSendResultMutation( + Number(projectId), + handleModalOpen + ); + const { handlePassNonPassStatus } = usePassNonPassMutation( + Number(projectId), + handleModalOpen + ); + const sidebarMenuItem = useMemo( + () => applicantsMenuItems(Number(projectId)), + [projectId] + ); return ( - - - {projectData && } - {passNonPassListData?.accepted.length > 0 || - passNonPassListData?.rejected.length > 0 ? ( - - - 합격자 리스트 - - - - 불 합격자 리스트 - - - - ) : ( - - )} - + + }> + + {projectData && } + + {passNonPassListData?.accepted.length > 0 || + passNonPassListData?.rejected.length > 0 ? ( + + + 합격자 리스트 + {projectData && ( + + )} + + + 불 합격자 리스트 + {projectData && ( + + )} + + + ) : ( + + )} + + + + + {message} + ); }; diff --git a/src/pages/manage/myProjectVolunteer/MyProjectVolunteer.tsx b/src/pages/manage/myProjectVolunteer/MyProjectVolunteer.tsx index 0319336a..efd38978 100644 --- a/src/pages/manage/myProjectVolunteer/MyProjectVolunteer.tsx +++ b/src/pages/manage/myProjectVolunteer/MyProjectVolunteer.tsx @@ -66,7 +66,10 @@ const MyProjectVolunteer = () => { - handlePassNonPassStatus(true, selectedApplicant) + handlePassNonPassStatus({ + status: 'ACCEPTED', + userId: selectedApplicant, + }) } disabled={projectData?.isDone} > @@ -75,7 +78,10 @@ const MyProjectVolunteer = () => { - handlePassNonPassStatus(false, selectedApplicant) + handlePassNonPassStatus({ + status: 'REJECTED', + userId: selectedApplicant, + }) } disabled={projectData?.isDone} > diff --git a/src/components/common/page/notFoundPage/NotFoundPage.styled.ts b/src/pages/notFoundPage/NotFoundPage.styled.ts similarity index 91% rename from src/components/common/page/notFoundPage/NotFoundPage.styled.ts rename to src/pages/notFoundPage/NotFoundPage.styled.ts index 27844ab5..9d06b9ff 100644 --- a/src/components/common/page/notFoundPage/NotFoundPage.styled.ts +++ b/src/pages/notFoundPage/NotFoundPage.styled.ts @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import notFoundImg from '../../../../assets/notFoundImg.svg'; +import notFoundImg from '../../assets/notFoundImg.svg'; export const Container = styled.div` width: 100vw; diff --git a/src/components/common/page/notFoundPage/NotFoundPage.tsx b/src/pages/notFoundPage/NotFoundPage.tsx similarity index 100% rename from src/components/common/page/notFoundPage/NotFoundPage.tsx rename to src/pages/notFoundPage/NotFoundPage.tsx diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index 4cd6a827..087fb90b 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -6,10 +6,10 @@ import { import { lazy, Suspense } from 'react'; import LoadingSpinner from '../components/common/loadingSpinner/LoadingSpinner'; -import Error from '../pages/error/Error'; import { ROUTES } from '../constants/routes'; import useAuthStore from '../store/authStore'; import ProtectRoute from '../components/common/ProtectRoute'; +import NotFoundPage from '../pages/notFoundPage/NotFoundPage'; const Login = lazy(() => import('../pages/login/Login')); const Register = lazy(() => import('../pages/register/Register')); const ChangePassword = lazy( @@ -222,7 +222,7 @@ const AppRoutes = () => { const newRouteList = routeList.map((item) => { return { ...item, - errorElement: , + errorElement: , }; });