diff --git a/src/components/modal/project-status/UpdateModalProjectStatus.tsx b/src/components/modal/project-status/UpdateModalProjectStatus.tsx index 9f457043..4dcb1c77 100644 --- a/src/components/modal/project-status/UpdateModalProjectStatus.tsx +++ b/src/components/modal/project-status/UpdateModalProjectStatus.tsx @@ -2,7 +2,10 @@ import ModalLayout from '@layouts/ModalLayout'; import ModalPortal from '@components/modal/ModalPortal'; import ModalButton from '@components/modal/ModalButton'; import ModalProjectStatusForm from '@components/modal/project-status/ModalProjectStatusForm'; -import { useUpdateStatus } from '@hooks/query/useStatusQuery'; +import Spinner from '@components/common/Spinner'; +import useToast from '@hooks/useToast'; +import { useReadStatusTasks } from '@hooks/query/useTaskQuery'; +import { useDeleteStatus, useUpdateStatus } from '@hooks/query/useStatusQuery'; import type { SubmitHandler } from 'react-hook-form'; import type { Project } from '@/types/ProjectType'; @@ -20,35 +23,53 @@ export default function UpdateModalProjectStatus({ onClose: handleClose, }: UpdateModalProjectStatusProps) { const updateStatusFormId = 'updateStatusForm'; - const updateMutation = useUpdateStatus(project.projectId, statusId); + + const { statusTaskList, isTaskLoading } = useReadStatusTasks(project.projectId); + const { mutate: updateStatusMutate } = useUpdateStatus(project.projectId, statusId); + const { mutate: deleteStatusMutate } = useDeleteStatus(project.projectId); + const { toastWarn } = useToast(); // ToDo: Error 처리 추가 const handleSubmit: SubmitHandler = async (data) => { - updateMutation.mutate(data); - updateMutation.reset(); + updateStatusMutate(data); handleClose(); }; - // ToDo: 상태 삭제 작업시 채워둘것 - const handleDeleteClick = () => {}; + // ToDo: 유저 권한 확인하는 로직 추가할 것 + const handleDeleteClick = (statusId: ProjectStatus['statusId']) => { + try { + const statusTasks = statusTaskList.find((statusTask) => statusTask.statusId === statusId); + if (!statusTasks) throw new Error('일치하는 프로젝트 상태가 없습니다.'); + if (statusTasks.tasks.length > 0) return toastWarn('프로젝트 상태에 일정이 등록되어 있습니다.'); + } catch (error) { + if (error instanceof Error) console.error(`${error.name}:${error.message}`); + } + deleteStatusMutate(statusId); + }; return ( - -
- - 수정 - - - 삭제 - -
+ {isTaskLoading ? ( + + ) : ( + <> + +
+ + 수정 + + handleDeleteClick(statusId)}> + 삭제 + +
+ + )}
); diff --git a/src/hooks/query/queryClient.ts b/src/hooks/query/queryClient.ts index bd8ccb67..7e5ca1a1 100644 --- a/src/hooks/query/queryClient.ts +++ b/src/hooks/query/queryClient.ts @@ -12,6 +12,7 @@ const queryClientOptions: QueryClientConfig = { refetchOnMount: true, refetchOnReconnect: true, refetchOnWindowFocus: true, + refetchInterval: 5 * MINUTE, retry: 3, }, }, diff --git a/src/hooks/query/useStatusQuery.ts b/src/hooks/query/useStatusQuery.ts index 2cbf553f..4ac0de5f 100644 --- a/src/hooks/query/useStatusQuery.ts +++ b/src/hooks/query/useStatusQuery.ts @@ -2,9 +2,8 @@ import { useMemo } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import useToast from '@hooks/useToast'; import { PROJECT_STATUS_COLORS } from '@constants/projectStatus'; -import { createStatus, getStatusList, updateStatus, updateStatusesOrder } from '@services/statusService'; +import { createStatus, deleteStatus, getStatusList, updateStatus, updateStatusesOrder } from '@services/statusService'; import { generateProjectQueryKey, generateStatusesQueryKey, generateTasksQueryKey } from '@utils/queryKeyGenergator'; - import type { Project } from '@/types/ProjectType'; import type { TaskListWithStatus } from '@/types/TaskType'; import type { ProjectStatus, ProjectStatusForm, StatusOrder, UsableColor } from '@/types/ProjectStatusType'; @@ -50,7 +49,7 @@ function getUsableStatusColorList( return [...statusColorMap.values()]; } -// ToDo: ProjectStatus 관련 Query 로직 작성하기 +// 프로젝트 상태 목록 조회 // ToDo: React Query 로직과 initialValue, nameList 등을 구하는 로직이 사실 관련이 없는 것 같음. 분리 고려하기. export function useReadStatuses(projectId: Project['projectId'], statusId?: ProjectStatus['statusId']) { const { @@ -95,14 +94,16 @@ export function useReadStatuses(projectId: Project['projectId'], statusId?: Proj }; } +// 프로젝트 상태 생성 export function useCreateStatus(projectId: Project['projectId']) { - const { toastSuccess } = useToast(); + const { toastSuccess, toastError } = useToast(); const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (formData: ProjectStatusForm) => createStatus(projectId, formData), + onError: () => toastError('프로젝트 상태 등록을 실패했습니다. 잠시 후 다시 등록해 주세요.'), onSuccess: () => { - toastSuccess('프로젝트 상태를 추가하였습니다.'); + toastSuccess('프로젝트 상태를 등록하였습니다.'); queryClient.invalidateQueries({ queryKey: generateProjectQueryKey(projectId), }); @@ -112,12 +113,14 @@ export function useCreateStatus(projectId: Project['projectId']) { return mutation; } +// 프로젝트 상태 수정 export function useUpdateStatus(projectId: Project['projectId'], statusId: ProjectStatus['statusId']) { - const { toastSuccess } = useToast(); + const { toastSuccess, toastError } = useToast(); const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (formData: ProjectStatusForm) => updateStatus(projectId, statusId, formData), + onError: () => toastError('프로젝트 상태 수정에 실패했습니다. 잠시 후 다시 시도해 주세요.'), onSuccess: () => { toastSuccess('프로젝트 상태를 수정했습니다.'); queryClient.invalidateQueries({ @@ -129,6 +132,27 @@ export function useUpdateStatus(projectId: Project['projectId'], statusId: Proje return mutation; } +// 프로젝트 상태 삭제 +export function useDeleteStatus(projectId: Project['projectId']) { + const { toastError, toastSuccess } = useToast(); + const queryClient = useQueryClient(); + const tasksQueryKeys = generateTasksQueryKey(projectId); + const statusesQueryKey = generateStatusesQueryKey(projectId); + + const mutation = useMutation({ + mutationFn: (statusId: ProjectStatus['statusId']) => deleteStatus(projectId, statusId), + onError: () => toastError('프로젝트 상태 삭제에 실패했습니다. 잠시 후 다시 시도해 주세요.'), + onSuccess: () => { + toastSuccess('프로젝트 상태를 삭제했습니다.'); + queryClient.invalidateQueries({ queryKey: tasksQueryKeys, exact: true }); + queryClient.invalidateQueries({ queryKey: statusesQueryKey, exact: true }); + }, + }); + + return mutation; +} + +// 상태 목록 순서 정렬 export function useUpdateStatusesOrder(projectId: Project['projectId']) { const { toastError } = useToast(); const queryClient = useQueryClient(); @@ -149,7 +173,7 @@ export function useUpdateStatusesOrder(projectId: Project['projectId']) { return { previousStatusTaskList }; }, onError: (error, newStatusTaskList, context) => { - toastError('프로젝트 상태 순서 변경에 실패 하였습니다. 잠시후 다시 진행해주세요.'); + toastError('프로젝트 상태 순서 변경에 실패했습니다. 잠시 후 다시 진행해 주세요.'); queryClient.setQueryData(TasksQueryKey, context?.previousStatusTaskList); }, onSuccess: () => { diff --git a/src/hooks/query/useTaskQuery.ts b/src/hooks/query/useTaskQuery.ts index f5d0eebe..e07d9bb0 100644 --- a/src/hooks/query/useTaskQuery.ts +++ b/src/hooks/query/useTaskQuery.ts @@ -1,7 +1,6 @@ import { useMemo } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { - generateStatusesQueryKey, generateTaskAssigneesQueryKey, generateTaskFilesQueryKey, generateTasksQueryKey, @@ -51,10 +50,10 @@ export function useCreateStatusTask(projectId: Project['projectId']) { const mutation = useMutation({ mutationFn: (formData: TaskCreationForm) => createTask(projectId, formData), onError: () => { - toastError('프로젝트 일정 등록 중 오류가 발생했습니다. 잠시후 다시 등록해주세요.'); + toastError('프로젝트 일정 등록 중 오류가 발생했습니다. 잠시 후 다시 등록해 주세요.'); }, onSuccess: () => { - toastSuccess('프로젝트 일정을 등록하였습니다.'); + toastSuccess('프로젝트 일정을 등록했습니다.'); queryClient.invalidateQueries({ queryKey: tasksQueryKey }); }, }); @@ -70,7 +69,7 @@ export function useUploadTaskFile(projectId: Project['projectId']) { uploadTaskFile(projectId, taskId, file, { headers: { 'Content-Type': 'multipart/form-data' }, }), - onError: (error, { file }) => toastError(`${file.name} 파일 업로드에 실패 했습니다.`), + onError: (error, { file }) => toastError(`${file.name} 파일 업로드에 실패했습니다.`), }); return mutation; @@ -129,7 +128,7 @@ export function useUpdateTasksOrder(projectId: Project['projectId']) { return { previousStatusTaskList }; }, onError: (err, newStatusTaskList, context) => { - toastError('일정 순서 변경에 실패 했습니다. 잠시후 다시 진행해주세요.'); + toastError('일정 순서 변경에실패 했습니다. 잠시 후 다시 시도해 주세요.'); queryClient.setQueryData(tasksQueryKey, context?.previousStatusTaskList); }, }); @@ -180,9 +179,9 @@ export function useUpdateTaskInfo(projectId: Project['projectId'], taskId: Task[ const mutation = useMutation({ mutationFn: (formData: TaskUpdateForm) => updateTaskInfo(projectId, taskId, formData), - onError: () => toastError('일정 정보 수정에 실패 했습니다. 잠시후 다시 시도해주세요.'), + onError: () => toastError('일정 정보 수정에 실패했습니다. 잠시 후 다시 시도해 주세요.'), onSuccess: () => { - toastSuccess('일정 정보를 수정 했습니다.'); + toastSuccess('일정 정보를 수정했습니다.'); queryClient.invalidateQueries({ queryKey: tasksQueryKey }); }, }); @@ -197,13 +196,13 @@ export function useDeleteTask(projectId: Project['projectId']) { const mutation = useMutation({ mutationFn: (taskId: Task['taskId']) => deleteTask(projectId, taskId), - onError: () => toastError('일정 삭제에 실패 했습니다. 잠시후 다시 시도해주세요.'), + onError: () => toastError('일정 삭제에 실패했습니다. 잠시 후 다시 시도해 주세요.'), onSuccess: (res, taskId) => { const tasksQueryKey = generateTasksQueryKey(projectId); const filesQueryKey = generateTaskFilesQueryKey(projectId, taskId); const assigneesQueryKey = generateTaskAssigneesQueryKey(projectId, taskId); - toastSuccess('일정을 삭제 했습니다.'); + toastSuccess('일정을 삭제했습니다.'); queryClient.invalidateQueries({ queryKey: tasksQueryKey, exact: true }); queryClient.removeQueries({ queryKey: filesQueryKey, exact: true }); queryClient.removeQueries({ queryKey: assigneesQueryKey, exact: true }); @@ -222,10 +221,10 @@ export function useAddAssignee(projectId: Project['projectId'], taskId: Task['ta const mutation = useMutation({ mutationFn: (userId: User['userId']) => addAssignee(projectId, taskId, userId), onError: () => { - toastError('수행자 추가에 실패 했습니다. 잠시후 다시 시도해주세요.'); + toastError('수행자 추가에 실패했습니다. 잠시 후 다시 시도해 주세요.'); }, onSuccess: () => { - toastSuccess('수행자를 추가 했습니다.'); + toastSuccess('수행자를 추가했습니다.'); queryClient.invalidateQueries({ queryKey: taskAssigneesQueryKey }); }, }); @@ -242,10 +241,10 @@ export function useDeleteAssignee(projectId: Project['projectId'], taskId: Task[ const mutation = useMutation({ mutationFn: (userId: User['userId']) => deleteAssignee(projectId, taskId, userId), onError: () => { - toastError('수행자 삭제에 실패 했습니다. 잠시후 다시 시도해주세요.'); + toastError('수행자 삭제에 실패했습니다. 잠시 후 다시 시도해 주세요.'); }, onSuccess: () => { - toastSuccess('수행자를 삭제 했습니다.'); + toastSuccess('수행자를 삭제했습니다.'); queryClient.invalidateQueries({ queryKey: taskAssigneesQueryKey }); }, }); @@ -261,9 +260,9 @@ export function useDeleteTaskFile(projectId: Project['projectId'], taskId: Task[ const mutation = useMutation({ mutationFn: (fileId: TaskFile['fileId']) => deleteTaskFile(projectId, taskId, fileId), - onError: () => toastError('일정 파일 삭제에 실패 했습니다. 잠시후 다시 시도해주세요.'), + onError: () => toastError('일정 파일 삭제에 실패했습니다. 잠시 후 다시 시도해 주세요.'), onSuccess: () => { - toastSuccess('일정 파일을 삭제 했습니다.'); + toastSuccess('일정 파일을 삭제했습니다.'); queryClient.invalidateQueries({ queryKey: taskFilesQueryKey }); }, }); diff --git a/src/mocks/services/statusServiceHandler.ts b/src/mocks/services/statusServiceHandler.ts index 4637ece9..667c311e 100644 --- a/src/mocks/services/statusServiceHandler.ts +++ b/src/mocks/services/statusServiceHandler.ts @@ -1,5 +1,5 @@ import { http, HttpResponse } from 'msw'; -import { STATUS_DUMMY } from '@mocks/mockData'; +import { PROJECT_DUMMY, STATUS_DUMMY } from '@mocks/mockData'; import { getStatusHash } from '@mocks/mockHash'; import type { ProjectStatusForm, StatusOrderForm } from '@/types/ProjectStatusType'; @@ -72,6 +72,28 @@ const statusServiceHandler = [ status.sortOrder = formData.sortOrder; return new HttpResponse(null, { status: 204 }); }), + // 프로젝트 상태 삭제 API + http.delete(`${BASE_URL}/project/:projectId/status/:statusId`, ({ request, params }) => { + const accessToken = request.headers.get('Authorization'); + const { projectId, statusId } = params; + + if (!accessToken) return new HttpResponse(null, { status: 401 }); + + // ToDo: JWT의 userId 정보를 가져와 프로젝트 권한 확인이 필요. + const project = PROJECT_DUMMY.find((project) => project.projectId === Number(projectId)); + if (!project) return new HttpResponse(null, { status: 404 }); + + const statuses = STATUS_DUMMY.filter((status) => status.projectId === project.projectId); + if (statuses.length === 0) return new HttpResponse(null, { status: 404 }); + + const isIncludedStatus = statuses.map((status) => status.statusId).includes(Number(statusId)); + if (!isIncludedStatus) return new HttpResponse(null, { status: 404 }); + + const statusIndex = STATUS_DUMMY.findIndex((status) => status.statusId === Number(statusId)); + if (statusIndex !== -1) STATUS_DUMMY.splice(statusIndex, 1); + + return new HttpResponse(null, { status: 204 }); + }), ]; export default statusServiceHandler; diff --git a/src/services/statusService.ts b/src/services/statusService.ts index 242cf79e..a434443b 100644 --- a/src/services/statusService.ts +++ b/src/services/statusService.ts @@ -55,6 +55,24 @@ export async function updateStatus( return authAxios.patch(`/project/${projectId}/status/${statusId}`, formData, axiosConfig); } +/** + * 프로젝트 상태 삭제 API + * + * @export + * @async + * @param {Project['projectId']} projectId - 프로젝트 ID + * @param {ProjectStatus['statusId']} statusId - 프로젝트 상태 ID + * @param {AxiosRequestConfig} [axiosConfig={}] - axios 요청 옵션 설정 객체 + * @returns {Promise>} + */ +export async function deleteStatus( + projectId: Project['projectId'], + statusId: ProjectStatus['statusId'], + axiosConfig: AxiosRequestConfig = {}, +) { + return authAxios.delete(`/project/${projectId}/status/${statusId}`, axiosConfig); +} + /** * 상태 순서 변경 API *