Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: #170 일정 삭제 기능 구현 #177

Merged
merged 10 commits into from
Sep 30, 2024
20 changes: 20 additions & 0 deletions src/components/modal/ModalButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
type ModalButtonProps = React.PropsWithChildren<{
formId?: string;
backgroundColor: 'bg-main' | 'bg-delete' | 'bg-sub' | 'bg-button' | 'bg-kakao';
onClick?: () => void;
}>;

export default function ModalButton({ formId, backgroundColor, onClick, children }: ModalButtonProps) {
const handleClick = () => onClick && onClick();

return (
<button
type={formId ? 'submit' : 'button'}
form={formId}
className={`h-full w-full rounded-md px-10 text-white outline-none ${backgroundColor} hover:brightness-90`}
onClick={handleClick}
>
{children}
</button>
);
}
22 changes: 17 additions & 5 deletions src/components/modal/task/DetailModalTask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { useMemo } from 'react';
import { LuDownload } from 'react-icons/lu';
import ModalPortal from '@components/modal/ModalPortal';
import ModalLayout from '@layouts/ModalLayout';
import ModalButton from '@components/modal/ModalButton';
import Spinner from '@components/common/Spinner';
import RoleIcon from '@components/common/RoleIcon';
import CustomMarkdown from '@components/common/CustomMarkdown';
import { useReadAssignees, useReadTaskFiles } from '@hooks/query/useTaskQuery';
import { useReadStatuses } from '@hooks/query/useStatusQuery';
import { useDeleteTask, useReadAssignees, useReadTaskFiles } from '@hooks/query/useTaskQuery';

import type { Task } from '@/types/TaskType';
import type { Project } from '@/types/ProjectType';
Expand All @@ -18,7 +19,7 @@ type ViewModalTaskProps = {
};

export default function DetailModalTask({ project, task, onClose: handleClose }: ViewModalTaskProps) {
// ToDo: 다운로드 파일 목록 가져오기
const { mutate: deleteTaskMutate } = useDeleteTask(project.projectId);
const { status, isStatusLoading } = useReadStatuses(project.projectId, task.statusId);
const { assigneeList, isAssigneeLoading } = useReadAssignees(project.projectId, task.taskId);
const { taskFileList, isTaskFileLoading } = useReadTaskFiles(project.projectId, task.taskId);
Expand All @@ -29,6 +30,12 @@ export default function DetailModalTask({ project, task, onClose: handleClose }:
[startDate, endDate],
);

// ToDo: 일정 수정 버튼 클릭시 처리 추가할 것
const handleUpdateClick = () => {};

Seok93 marked this conversation as resolved.
Show resolved Hide resolved
// ToDo: 유저 권한 확인하는 로직 추가할 것
const handleDeleteClick = (taskId: Task['taskId']) => deleteTaskMutate(taskId);

Seok93 marked this conversation as resolved.
Show resolved Hide resolved
return (
<ModalPortal>
<ModalLayout onClose={handleClose}>
Expand Down Expand Up @@ -86,9 +93,14 @@ export default function DetailModalTask({ project, task, onClose: handleClose }:
</div>
)}
</section>
<button type="submit" className="h-25 w-full rounded-md bg-main px-10 text-white">
수정
</button>
<div className="flex min-h-25 gap-10">
<ModalButton backgroundColor="bg-main" onClick={handleUpdateClick}>
수정
</ModalButton>
<ModalButton backgroundColor="bg-delete" onClick={() => handleDeleteClick(task.taskId)}>
삭제
</ModalButton>
</div>
</article>
)}
</ModalLayout>
Expand Down
1 change: 1 addition & 0 deletions src/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
--color-sub: #e1f4d9;
--color-close: #be0000;
--color-error: #ff0000;
--color-delete: #ef2222;
Seok93 marked this conversation as resolved.
Show resolved Hide resolved
--color-contents-box: #fdfdfd;
--color-disable: #c2c2c2;
--color-selected: #c2c2c2;
Expand Down
26 changes: 25 additions & 1 deletion src/hooks/query/useTaskQuery.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMemo } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
generateStatusesQueryKey,
generateTaskAssigneesQueryKey,
generateTaskFilesQueryKey,
generateTasksQueryKey,
Expand All @@ -9,6 +10,7 @@ import {
addAssignee,
createTask,
deleteAssignee,
deleteTask,
deleteTaskFile,
findAssignees,
findTaskFiles,
Expand Down Expand Up @@ -40,7 +42,6 @@ function getTaskNameList(taskList: TaskListWithStatus[], excludedTaskName?: Task
return excludedTaskName ? taskNameList.filter((taskName) => taskName !== excludedTaskName) : taskNameList;
}

// Todo: Task Query D로직 작성하기
// 일정 생성
export function useCreateStatusTask(projectId: Project['projectId']) {
const { toastError, toastSuccess } = useToast();
Expand Down Expand Up @@ -189,6 +190,29 @@ export function useUpdateTaskInfo(projectId: Project['projectId'], taskId: Task[
return mutation;
}

// 일정 삭제
export function useDeleteTask(projectId: Project['projectId']) {
const { toastError, toastSuccess } = useToast();
const queryClient = useQueryClient();

const mutation = useMutation({
mutationFn: (taskId: Task['taskId']) => deleteTask(projectId, taskId),
onError: () => toastError('일정 삭제에 실패 했습니다. 잠시후 다시 시도해주세요.'),
onSuccess: (res, taskId) => {
const tasksQueryKey = generateTasksQueryKey(projectId);
const filesQueryKey = generateTaskFilesQueryKey(projectId, taskId);
const assigneesQueryKey = generateTaskAssigneesQueryKey(projectId, taskId);

toastSuccess('일정을 삭제 했습니다.');
queryClient.invalidateQueries({ queryKey: tasksQueryKey, exact: true });
queryClient.removeQueries({ queryKey: filesQueryKey, exact: true });
queryClient.removeQueries({ queryKey: assigneesQueryKey, exact: true });
},
});

return mutation;
}

// 일정 수행자 추가
export function useAddAssignee(projectId: Project['projectId'], taskId: Task['taskId']) {
const { toastError, toastSuccess } = useToast();
Expand Down
50 changes: 49 additions & 1 deletion src/mocks/services/taskServiceHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,59 @@ const taskServiceHandler = [
if (!isIncludedTask) return new HttpResponse(null, { status: 404 });

const index = TASK_DUMMY.findIndex((task) => task.taskId === Number(taskId));
if (index !== -1)
if (index !== -1) {
TASK_DUMMY[index] = { ...TASK_DUMMY[index], ...taskInfoData, statusId: Number(taskInfoData.statusId) };
}

return new HttpResponse(null, { status: 200 });
}),
// 일정 삭제 API
http.delete(`${BASE_URL}/project/:projectId/task/:taskId`, ({ request, params }) => {
const accessToken = request.headers.get('Authorization');
const { projectId, taskId } = 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 task = TASK_DUMMY.find((task) => task.taskId === Number(taskId));
if (!task) return new HttpResponse(null, { status: 404 });

const isIncludedTask = statuses.map((status) => status.statusId).includes(task.statusId);
if (!isIncludedTask) return new HttpResponse(null, { status: 404 });

Seok93 marked this conversation as resolved.
Show resolved Hide resolved
// 일정 삭제
const taskIndex = TASK_DUMMY.findIndex((task) => task.taskId === Number(taskId));
if (taskIndex !== -1) TASK_DUMMY.splice(taskIndex, 1);

// 일정의 수행자 삭제
const filteredTaskUser = TASK_USER_DUMMY.filter((taskUser) => taskUser.taskId !== Number(taskId));
if (filteredTaskUser.length !== TASK_USER_DUMMY.length) {
TASK_USER_DUMMY.length = 0;
TASK_USER_DUMMY.push(...filteredTaskUser);
}

// 일정의 파일 삭제
const filteredTaskFile = TASK_FILE_DUMMY.filter((taskFile) => taskFile.taskId !== Number(taskId));
if (filteredTaskFile.length !== TASK_FILE_DUMMY.length) {
TASK_FILE_DUMMY.length = 0;
TASK_FILE_DUMMY.push(...filteredTaskFile);
}

// MSW용 파일 정보 삭제
const filteredFile = FILE_DUMMY.filter((file) => file.taskId !== Number(taskId));
if (filteredFile.length !== FILE_DUMMY.length) {
FILE_DUMMY.length = 0;
FILE_DUMMY.push(...filteredFile);
}

return new HttpResponse(null, { status: 204 });
}),
// 일정 수행자 추가 API
http.post(`${BASE_URL}/project/:projectId/task/:taskId/assignee`, async ({ request, params }) => {
const accessToken = request.headers.get('Authorization');
Expand Down
20 changes: 19 additions & 1 deletion src/services/taskService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export async function findTaskFiles(
* @async
* @param {Project['projectId']} projectId - 프로젝트 ID
* @param {Task['taskId']} taskId - 일정 ID
* @param {TaskUpdateForm} formData - 일정 수정 정보 객체
* @param {TaskUpdateForm} formData - 일정 수정 정보 객체
* @param {AxiosRequestConfig} [axiosConfig={}] - axios 요청 옵션 설정 객체
* @returns {Promise<AxiosResponse<void>>}
*/
Expand All @@ -132,6 +132,24 @@ export async function updateTaskInfo(
return authAxios.patch(`/project/${projectId}/task/${taskId}`, formData, axiosConfig);
}

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

/**
* 일정 수행자 추가 API
*
Expand Down
1 change: 1 addition & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default {
close: 'var(--color-close)',
'contents-box': 'var(--color-contents-box)',
error: 'var(--color-error)',
delete: 'var(--color-delete)',
disable: 'var(--color-disable)',
selected: 'var(--color-selected)',
scroll: 'var(--color-scroll)',
Expand Down