Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'develop' of https://github.com/GU-99/grow-up-fe into fe…
Browse files Browse the repository at this point in the history
…ature/#87-team-modal-ui
ice-bear98 committed Sep 30, 2024
2 parents e09f27b + cdd6481 commit 357775c
Showing 33 changed files with 774 additions and 183 deletions.
15 changes: 12 additions & 3 deletions src/components/common/FileDropZone.tsx
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ type FileDropZoneProps = {
id: string;
label: string;
files: FileInfo[];
accept: string;
onFileDrop: (e: React.DragEvent<HTMLElement>) => void;
onFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onFileDeleteClick: (fileId: string) => void;
@@ -14,11 +15,11 @@ type FileDropZoneProps = {
const DEFAULT_BG_COLOR = 'inherit';
const FILE_DRAG_OVER_BG_COLOR = '#e1f4d9';

// ToDo: 파일 업로드 API 작업시 구조 다시 한 번 확인해보기
export default function FileDropZone({
id,
label,
files,
accept = '*',
onFileDrop,
onFileChange: handleFileChange,
onFileDeleteClick: handleFileDeleteClick,
@@ -41,7 +42,15 @@ export default function FileDropZone({
return (
<label htmlFor={id}>
<h3 className="text-large">{label}</h3>
<input type="file" id={id} className="h-0 w-0 opacity-0" multiple hidden onChange={handleFileChange} />
<input
id={id}
type="file"
accept={accept}
className="h-0 w-0 opacity-0"
onChange={handleFileChange}
multiple
hidden
/>
<div
role="button"
tabIndex={0}
@@ -59,7 +68,7 @@ export default function FileDropZone({
className="text-close"
onClick={(e: React.MouseEvent<HTMLOrSVGElement>) => {
e.preventDefault();
handleFileDeleteClick(id);
handleFileDeleteClick(fileId.toString());
}}
/>
</li>
2 changes: 1 addition & 1 deletion src/components/common/StatusRadio.tsx
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ export default function StatusRadio({ statusFieldName, statusList }: StatusRadio
<div className="flex items-center justify-start gap-4">
{statusList.map((status) => {
const { statusId, statusName, colorCode } = status;
const isChecked = Number(watch('statusId')) === statusId;
const isChecked = Number(watch(statusFieldName)) === statusId;
return (
<label
key={statusId}
47 changes: 41 additions & 6 deletions src/components/modal/task/CreateModalTask.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import { useQueryClient } from '@tanstack/react-query';
import ModalLayout from '@layouts/ModalLayout';
import ModalPortal from '@components/modal/ModalPortal';
import ModalTaskForm from '@components/modal/task/ModalTaskForm';
import ModalFormButton from '@components/modal/ModalFormButton';
import { useCreateStatusTask, useReadStatusTasks } from '@hooks/query/useTaskQuery';
import useToast from '@hooks/useToast';
import { useCreateStatusTask, useReadStatusTasks, useUploadTaskFile } from '@hooks/query/useTaskQuery';

import type { SubmitHandler } from 'react-hook-form';
import type { TaskForm } from '@/types/TaskType';
import type { Task, TaskForm } from '@/types/TaskType';
import type { Project } from '@/types/ProjectType';
import type { ProjectStatus } from '@/types/ProjectStatusType';
import type { FileUploadFailureResult, FileUploadSuccessResult } from '@/types/FileType';

type CreateModalTaskProps = {
project: Project;
onClose: () => void;
};

export default function CreateModalTask({ project, onClose: handleClose }: CreateModalTaskProps) {
const { toastError } = useToast();
const { mutate: createTaskMutate } = useCreateStatusTask(project.projectId);
const { toastSuccess, toastError } = useToast();
const { mutateAsync: createTaskInfoMutateAsync } = useCreateStatusTask(project.projectId);
const { mutateAsync: createTaskFileMutateAsync } = useUploadTaskFile(project.projectId);
const { statusTaskList } = useReadStatusTasks(project.projectId);
const queryClient = useQueryClient();

const getLastSortOrder = (statusId: ProjectStatus['statusId']) => {
const statusTask = statusTaskList.find((statusTask) => statusTask.statusId === Number(statusId));
@@ -29,10 +33,41 @@ export default function CreateModalTask({ project, onClose: handleClose }: Creat
return statusTask.tasks.length + 1;
};

// ToDo: 파일 생성 위한 네트워크 로직 추가
const taskFilesUpload = async (taskId: Task['taskId'], files: File[]) => {
const createFilePromises = files.map((file) =>
createTaskFileMutateAsync({ taskId, file }).then(
(): FileUploadSuccessResult => ({ status: 'fulfilled', file }),
(error): FileUploadFailureResult => ({ status: 'rejected', file, error }),
),
);

const results = (await Promise.allSettled(createFilePromises)) as PromiseFulfilledResult<
FileUploadSuccessResult | FileUploadFailureResult
>[];
const queryKey = ['projects', project.projectId, 'tasks'];
queryClient.invalidateQueries({ queryKey });

const fulfilledFileList: FileUploadSuccessResult[] = [];
const rejectedFileList: FileUploadFailureResult[] = [];
results
.map((result) => result.value)
.forEach((result) => {
if (result.status === 'fulfilled') fulfilledFileList.push(result);
else rejectedFileList.push(result);
});

if (fulfilledFileList.length > 0) toastSuccess(`${fulfilledFileList.length}개의 파일 업로드에 성공했습니다.`);
};

const handleSubmit: SubmitHandler<TaskForm> = async (taskFormData) => {
const { files, ...taskInfoForm } = taskFormData;
const sortOrder = getLastSortOrder(taskFormData.statusId);
createTaskMutate({ ...taskFormData, sortOrder });

// 일정 정보 등록
const { data: taskInfo } = await createTaskInfoMutateAsync({ ...taskInfoForm, sortOrder });

// 일정 파일 업로드
if (files.length > 0) await taskFilesUpload(taskInfo.taskId, files);
handleClose();
};
return (
44 changes: 35 additions & 9 deletions src/components/modal/task/ModalTaskForm.tsx
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import useAxios from '@hooks/useAxios';
import { useReadStatuses } from '@hooks/query/useStatusQuery';
import { useReadStatusTasks } from '@hooks/query/useTaskQuery';
import { useReadProjectUserRoleList } from '@hooks/query/useProjectQuery';
import Validator from '@utils/Validator';
import { convertBytesToString } from '@utils/converter';
import { findUserByProject } from '@services/projectService';

@@ -106,22 +107,46 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
setValue('assignees', assigneesIdList);
};

const updateFiles = (newFiles: FileList) => {
// ToDo: 일정 수정에서도 사용하도록 분리할 것, 어디에 분리하는게 좋으려나?
const isValidTaskFile = (file: File) => {
if (!Validator.isValidFileName(file.name)) {
toastWarn(
`${file.name} 파일은 업로드 할 수 없습니다. 파일명은 한글, 영어, 숫자, 특수기호(.-_), 공백문자만 가능합니다.`,
);
return false;
}

if (!Validator.isValidFileExtension(TASK_SETTINGS.FILE_TYPES, file.type)) {
toastWarn(`${file.name} 파일은 업로드 할 수 없습니다. 지원하지 않는 파일 타입입니다.`);
return false;
}

if (!Validator.isValidFileSize(TASK_SETTINGS.MAX_FILE_SIZE, file.size)) {
toastWarn(
`${file.name} 파일은 업로드 할 수 없습니다. ${convertBytesToString(TASK_SETTINGS.MAX_FILE_SIZE)} 이하의 파일만 업로드 가능합니다.`,
);
return false;
}

return true;
};

const updateTaskFiles = (newFiles: FileList) => {
// 최대 파일 등록 개수 확인
if (files.length + newFiles.length > TASK_SETTINGS.MAX_FILE_COUNT) {
if (!Validator.isValidFileCount(TASK_SETTINGS.MAX_FILE_COUNT, files.length + newFiles.length)) {
return toastWarn(`최대로 등록 가능한 파일수는 ${TASK_SETTINGS.MAX_FILE_COUNT}개입니다.`);
}

// 새로운 파일별 파일 크기 확인 & 고유 ID 부여
// 새로운 파일 Validation & 고유 ID 부여
const originFiles: File[] = files.map(({ file }) => file);
const customFiles: CustomFile[] = [];
for (let i = 0; i < newFiles.length; i++) {
const file = newFiles[i];
if (file.size > TASK_SETTINGS.MAX_FILE_SIZE) {
return toastWarn(`최대 ${convertBytesToString(TASK_SETTINGS.MAX_FILE_SIZE)} 이하의 파일만 업로드 가능합니다.`);

if (isValidTaskFile(file)) {
originFiles.push(file);
customFiles.push({ fileId: `${file.name}_${Date.now()}`, fileName: file.name, file });
}
originFiles.push(file);
customFiles.push({ fileId: `${file.name}_${Date.now()}`, fileName: file.name, file });
}
setValue('files', originFiles);
setFiles((prev) => [...prev, ...customFiles]);
@@ -130,13 +155,13 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { files } = e.target;
if (!files || files.length === 0) return;
updateFiles(files);
updateTaskFiles(files);
};

const handleFileDrop = (e: React.DragEvent<HTMLElement>) => {
const { files } = e.dataTransfer;
if (!files || files.length === 0) return;
updateFiles(files);
updateTaskFiles(files);
};

const handleFileDeleteClick = (fileId: string) => {
@@ -194,6 +219,7 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
id="files"
label="첨부파일"
files={files}
accept={TASK_SETTINGS.FILE_ACCEPT}
onFileChange={handleFileChange}
onFileDrop={handleFileDrop}
onFileDeleteClick={handleFileDeleteClick}
Loading

0 comments on commit 357775c

Please sign in to comment.