Skip to content

Commit

Permalink
Feat: #167 일정 수정 모달의 일정 파일 삭제 기능 구현 (#176)
Browse files Browse the repository at this point in the history
* Feat: #167 일정 파일 삭제 API를 위한 React Query 기능 추가

* Feat: #167 일정 파일 삭제 API를 위한 MSW 처리 추가

* Feat: #167 일정 파일 삭제 기능 구현

* Feat: #167 일정 수정 모달에서 각 양식별로 Spinner가 생기도록 수정

* Fix: #167 조건문에 지정한 변수 수정 (taskFileIndex → fileIndex)
  • Loading branch information
Seok93 authored Sep 30, 2024
1 parent 7d71ff4 commit cdd6481
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 66 deletions.
2 changes: 1 addition & 1 deletion src/components/common/FileDropZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export default function FileDropZone({
className="text-close"
onClick={(e: React.MouseEvent<HTMLOrSVGElement>) => {
e.preventDefault();
handleFileDeleteClick(id);
handleFileDeleteClick(fileId.toString());
}}
/>
</li>
Expand Down
131 changes: 71 additions & 60 deletions src/components/modal/task/UpdateModalTask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useReadStatuses } from '@hooks/query/useStatusQuery';
import {
useAddAssignee,
useDeleteAssignee,
useDeleteTaskFile,
useReadAssignees,
useReadStatusTasks,
useReadTaskFiles,
Expand Down Expand Up @@ -59,6 +60,7 @@ export default function UpdateModalTask({ project, taskId, onClose: handleClose
const { mutate: updateTaskInfoMutate } = useUpdateTaskInfo(projectId, taskId);
const { mutate: addAssigneeMutate } = useAddAssignee(projectId, taskId);
const { mutate: deleteAssigneeMutate } = useDeleteAssignee(projectId, taskId);
const { mutate: deleteTaskFileMutate } = useDeleteTaskFile(projectId, taskId);

const methods = useForm<TaskUpdateForm>({ mode: 'onChange' });
const {
Expand Down Expand Up @@ -123,8 +125,7 @@ export default function UpdateModalTask({ project, taskId, onClose: handleClose
updateFiles(files);
};

// ToDo: 일정 파일 삭제 API 작업시 추가할 것
const handleFileDeleteClick = (fileId: string) => {};
const handleFileDeleteClick = (fileId: string) => deleteTaskFileMutate(Number(fileId));

if (isStatusLoading || isTaskLoading || isProjectUserRoleLoading || isTaskFileLoading || isAssigneeLoading) {
return <Spinner />;
Expand All @@ -137,67 +138,77 @@ export default function UpdateModalTask({ project, taskId, onClose: handleClose
return (
<ModalPortal>
<ModalLayout onClose={handleClose}>
<FormProvider {...methods}>
<form
id="updateTaskForm"
className="flex w-4/5 grow flex-col justify-center"
onSubmit={handleSubmit(handleFormSubmit)}
>
<StatusRadio statusFieldName="statusId" statusList={statusList} />

<DuplicationCheckInput
id="name"
label="일정"
value={watch('name')}
placeholder="일정명을 입력해주세요."
errors={errors.name?.message}
register={register('name', TASK_VALIDATION_RULES.TASK_NAME(taskNameList))}
/>

<PeriodDateInput
startDateLabel="시작일"
endDateLabel="종료일"
startDateId="startDate"
endDateId="endDate"
startDate={startDate}
endDate={endDate}
startDateFieldName="startDate"
endDateFieldName="endDate"
{isStatusLoading || isTaskLoading ? (
<Spinner />
) : (
<>
<FormProvider {...methods}>
<form
id="updateTaskForm"
className="flex w-4/5 grow flex-col justify-center"
onSubmit={handleSubmit(handleFormSubmit)}
>
<StatusRadio statusFieldName="statusId" statusList={statusList} />

<DuplicationCheckInput
id="name"
label="일정"
value={watch('name')}
placeholder="일정명을 입력해주세요."
errors={errors.name?.message}
register={register('name', TASK_VALIDATION_RULES.TASK_NAME(taskNameList))}
/>

<PeriodDateInput
startDateLabel="시작일"
endDateLabel="종료일"
startDateId="startDate"
endDateId="endDate"
startDate={startDate}
endDate={endDate}
startDateFieldName="startDate"
endDateFieldName="endDate"
/>

<MarkdownEditor id="content" label="내용" contentFieldName="content" />
</form>
</FormProvider>
<ModalFormButton formId="updateTaskForm" isCreate={false} onClose={handleClose} />
</>
)}
<hr className="my-20" />
{isProjectUserRoleLoading || isAssigneeLoading ? (
<Spinner />
) : (
<section>
<SearchUserInput
id="search"
label="수행자"
keyword={keyword}
searchId={projectId}
loading={loading}
userList={userList}
searchCallbackInfo={searchCallbackInfo}
onKeywordChange={handleKeywordChange}
onUserClick={handleUserClick}
/>

<MarkdownEditor id="content" label="내용" contentFieldName="content" />
</form>
</FormProvider>
<ModalFormButton formId="updateTaskForm" isCreate={false} onClose={handleClose} />

<AssigneeList assigneeList={assigneeList} onAssigneeDeleteClick={handleAssigneeDeleteClick} />
</section>
)}
<hr className="my-20" />

<section>
<SearchUserInput
id="search"
label="수행자"
keyword={keyword}
searchId={projectId}
loading={loading}
userList={userList}
searchCallbackInfo={searchCallbackInfo}
onKeywordChange={handleKeywordChange}
onUserClick={handleUserClick}
{isTaskFileLoading ? (
<Spinner />
) : (
<FileDropZone
id="files"
label="첨부파일"
files={taskFileList}
accept={TASK_SETTINGS.FILE_ACCEPT}
onFileChange={handleFileChange}
onFileDrop={handleFileDrop}
onFileDeleteClick={handleFileDeleteClick}
/>
<AssigneeList assigneeList={assigneeList} onAssigneeDeleteClick={handleAssigneeDeleteClick} />
</section>

<hr className="my-20" />

<FileDropZone
id="files"
label="첨부파일"
files={taskFileList}
accept={TASK_SETTINGS.FILE_ACCEPT}
onFileChange={handleFileChange}
onFileDrop={handleFileDrop}
onFileDeleteClick={handleFileDeleteClick}
/>
)}
</ModalLayout>
</ModalPortal>
);
Expand Down
30 changes: 25 additions & 5 deletions src/hooks/query/useTaskQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
addAssignee,
createTask,
deleteAssignee,
deleteTaskFile,
findAssignees,
findTaskFiles,
findTaskList,
Expand All @@ -28,6 +29,7 @@ import type {
TaskUpdateForm,
TaskUploadForm,
} from '@/types/TaskType';
import { TaskFile } from '@/types/FileType';

function getTaskNameList(taskList: TaskListWithStatus[], excludedTaskName?: Task['name']) {
const taskNameList = taskList
Expand Down Expand Up @@ -126,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 @@ -196,10 +198,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 @@ -216,13 +218,31 @@ 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 });
},
});

return mutation;
}

// 일정 파일 삭제
export function useDeleteTaskFile(projectId: Project['projectId'], taskId: Task['taskId']) {
const { toastSuccess, toastError } = useToast();
const queryClient = useQueryClient();
const taskFilesQueryKey = generateTaskFilesQueryKey(projectId, taskId);

const mutation = useMutation({
mutationFn: (fileId: TaskFile['fileId']) => deleteTaskFile(projectId, taskId, fileId),
onError: () => toastError('일정 파일 삭제에 실패 했습니다. 잠시후 다시 시도해주세요.'),
onSuccess: () => {
toastSuccess('일정 파일을 삭제 했습니다.');
queryClient.invalidateQueries({ queryKey: taskFilesQueryKey });
},
});

return mutation;
}
23 changes: 23 additions & 0 deletions src/mocks/services/taskServiceHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,29 @@ const taskServiceHandler = [

return new HttpResponse(null, { status: 200 });
}),
// 일정 파일 삭제 API
http.delete(`${BASE_URL}/project/:projectId/task/:taskId/file/:fileId`, ({ request, params }) => {
const accessToken = request.headers.get('Authorization');
const { projectId, taskId, fileId } = params;

if (!accessToken) return new HttpResponse(null, { status: 401 });

// ToDo: JWT의 userId 정보를 가져와 프로젝트 권한 확인이 필요.
const task = TASK_DUMMY.find((task) => task.taskId === Number(taskId));
if (!task) return new HttpResponse(null, { status: 404 });

const taskFileIndex = TASK_FILE_DUMMY.findIndex(
(taskFile) => taskFile.fileId === Number(fileId) && taskFile.taskId === Number(taskId),
);
if (taskFileIndex === -1) return new HttpResponse(null, { status: 404 });
TASK_FILE_DUMMY.splice(taskFileIndex, 1);

const fileIndex = FILE_DUMMY.findIndex((file) => file.fileId === Number(fileId) && file.taskId === Number(taskId));
if (fileIndex === -1) return new HttpResponse(null, { status: 404 });
FILE_DUMMY.splice(fileIndex, 1);

return new HttpResponse(null, { status: 204 });
}),
// 일정 순서 변경 API
http.patch(`${BASE_URL}/project/:projectId/task/order`, async ({ request, params }) => {
const accessToken = request.headers.get('Authorization');
Expand Down
20 changes: 20 additions & 0 deletions src/services/taskService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,23 @@ export async function deleteAssignee(
) {
return authAxios.delete(`/project/${projectId}/task/${taskId}/assignee/${userId}`, axiosConfig);
}

/**
* 일정 파일 삭제 API
*
* @export
* @async
* @param {Project['projectId']} projectId - 프로젝트 ID
* @param {Task['taskId']} taskId - 일정 ID
* @param {TaskFile['fileId']} fileId - 일정 파일 ID
* @param {AxiosRequestConfig} [axiosConfig={}] - axios 요청 옵션 설정 객체
* @returns {Promise<AxiosResponse<void>>}
*/
export async function deleteTaskFile(
projectId: Project['projectId'],
taskId: Task['taskId'],
fileId: TaskFile['fileId'],
axiosConfig: AxiosRequestConfig = {},
) {
return authAxios.delete(`/project/${projectId}/task/${taskId}/file/${fileId}`, axiosConfig);
}

0 comments on commit cdd6481

Please sign in to comment.