Skip to content

Commit

Permalink
Feat: #169 상태 삭제 기능 구현 (#180)
Browse files Browse the repository at this point in the history
* Feat: #169 상태 삭제 API를 위한 React Query 기능 추가

* Feat: #169 상태 삭제 API를 위한 MSW 기능 추가

* Feat: #169 상태 삭제 기능 구현

* Config: #169 react query client 설정 추가

* Feat: #169 예외 처리 로직 추가 & 로딩 시 화면 추가

* Chore: #169 메세지 오탈자/맞춤법 수정
  • Loading branch information
Seok93 authored Oct 2, 2024
1 parent c2d7b94 commit d336e9c
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 43 deletions.
61 changes: 41 additions & 20 deletions src/components/modal/project-status/UpdateModalProjectStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<ProjectStatusForm> = 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 (
<ModalPortal>
<ModalLayout onClose={handleClose}>
<ModalProjectStatusForm
formId={updateStatusFormId}
project={project}
statusId={statusId}
onSubmit={handleSubmit}
/>
<div className="flex min-h-25 w-4/5 gap-10">
<ModalButton formId={updateStatusFormId} backgroundColor="bg-main">
수정
</ModalButton>
<ModalButton backgroundColor="bg-delete" onClick={handleDeleteClick}>
삭제
</ModalButton>
</div>
{isTaskLoading ? (
<Spinner />
) : (
<>
<ModalProjectStatusForm
formId={updateStatusFormId}
project={project}
statusId={statusId}
onSubmit={handleSubmit}
/>
<div className="flex min-h-25 w-4/5 gap-10">
<ModalButton formId={updateStatusFormId} backgroundColor="bg-main">
수정
</ModalButton>
<ModalButton backgroundColor="bg-delete" onClick={() => handleDeleteClick(statusId)}>
삭제
</ModalButton>
</div>
</>
)}
</ModalLayout>
</ModalPortal>
);
Expand Down
1 change: 1 addition & 0 deletions src/hooks/query/queryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const queryClientOptions: QueryClientConfig = {
refetchOnMount: true,
refetchOnReconnect: true,
refetchOnWindowFocus: true,
refetchInterval: 5 * MINUTE,
retry: 3,
},
},
Expand Down
38 changes: 31 additions & 7 deletions src/hooks/query/useStatusQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
});
Expand All @@ -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({
Expand All @@ -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();
Expand All @@ -149,7 +173,7 @@ export function useUpdateStatusesOrder(projectId: Project['projectId']) {
return { previousStatusTaskList };
},
onError: (error, newStatusTaskList, context) => {
toastError('프로젝트 상태 순서 변경에 실패 하였습니다. 잠시후 다시 진행해주세요.');
toastError('프로젝트 상태 순서 변경에 실패했습니다. 잠시 후 다시 진행해 주세요.');
queryClient.setQueryData(TasksQueryKey, context?.previousStatusTaskList);
},
onSuccess: () => {
Expand Down
29 changes: 14 additions & 15 deletions src/hooks/query/useTaskQuery.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useMemo } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
generateStatusesQueryKey,
generateTaskAssigneesQueryKey,
generateTaskFilesQueryKey,
generateTasksQueryKey,
Expand Down Expand Up @@ -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 });
},
});
Expand All @@ -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;
Expand Down Expand Up @@ -129,7 +128,7 @@ export function useUpdateTasksOrder(projectId: Project['projectId']) {
return { previousStatusTaskList };
},
onError: (err, newStatusTaskList, context) => {
toastError('일정 순서 변경에 실패 했습니다. 잠시후 다시 진행해주세요.');
toastError('일정 순서 변경에실패 했습니다. 잠시 후 다시 시도해 주세요.');
queryClient.setQueryData(tasksQueryKey, context?.previousStatusTaskList);
},
});
Expand Down Expand Up @@ -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 });
},
});
Expand All @@ -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 });
Expand All @@ -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 });
},
});
Expand All @@ -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 });
},
});
Expand All @@ -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 });
},
});
Expand Down
24 changes: 23 additions & 1 deletion src/mocks/services/statusServiceHandler.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
18 changes: 18 additions & 0 deletions src/services/statusService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AxiosResponse<void>>}
*/
export async function deleteStatus(
projectId: Project['projectId'],
statusId: ProjectStatus['statusId'],
axiosConfig: AxiosRequestConfig = {},
) {
return authAxios.delete(`/project/${projectId}/status/${statusId}`, axiosConfig);
}

/**
* 상태 순서 변경 API
*
Expand Down

0 comments on commit d336e9c

Please sign in to comment.