Skip to content

Commit

Permalink
Feat: #63 할일 등록 모달 DnD 파일 첨부 기능 추가 (#88)
Browse files Browse the repository at this point in the history
* Rename: #63 userSettings → settings 파일 이름 변경

* Formatting: #63 파일 이름 변경에 따른 변수명 변경

* Config: #63 ESLint 환경 설정 추가

* Fix: #63 ESLint 설정에 포함된 공백문자 제거

* Feat: #63 할일 모달 파일 첨부 기능 추가

* UI: #63 삭제 버튼 색상 변경
  • Loading branch information
Seok93 authored Aug 26, 2024
1 parent b3ed819 commit 28be56e
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 15 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = {
'no-return-assign': 'warn',
'no-unused-vars': 'warn',
'no-cond-assign': 'off',
'no-plusplus': 'warn',
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'react/require-default-props': 'off',
Expand Down
75 changes: 69 additions & 6 deletions src/components/modal/task/ModalTaskForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { DateTime } from 'luxon';
import { useForm } from 'react-hook-form';
import { IoSearch } from 'react-icons/io5';
import { GoPlusCircle } from 'react-icons/go';
import { IoMdCloseCircle } from 'react-icons/io';

import { TASK_SETTINGS } from '@constants/settings';
import { TASK_VALIDATION_RULES } from '@constants/formValidationRules';
import RoleIcon from '@components/common/RoleIcon';
import ToggleButton from '@components/common/ToggleButton';
Expand All @@ -13,13 +15,15 @@ import useToast from '@hooks/useToast';
import useAxios from '@hooks/useAxios';
import useTaskQuery from '@hooks/query/useTaskQuery';
import useStatusQuery from '@hooks/query/useStatusQuery';
import { convertBytesToString } from '@utils/converter';
import { findUserByProject } from '@services/projectService';

import type { SubmitHandler } from 'react-hook-form';
import type { UserWithRole } from '@/types/UserType';
import type { Project } from '@/types/ProjectType';
import type { Task, TaskForm } from '@/types/TaskType';

type CustomFile = { id: string; file: File };
type ModalTaskFormProps = {
formId: string;
project: Project;
Expand All @@ -36,11 +40,12 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
const [keyword, setKeyword] = useState('');
const [workers, setWorkers] = useState<UserWithRole[]>([]);
const [preview, setPreview] = useState(false);
const [files, setFiles] = useState<CustomFile[]>([]);

const { statusList } = useStatusQuery(projectId, taskId);
const { taskNameList } = useTaskQuery(projectId);
const { data, loading, clearData, fetchData } = useAxios(findUserByProject);
const { toastInfo } = useToast();
const { toastInfo, toastWarn } = useToast();

// ToDo: 상태 수정 모달 작성시 기본값 설정 방식 변경할 것
const {
Expand Down Expand Up @@ -114,13 +119,49 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
clearData();
};

const handleDeleteClick = (user: UserWithRole) => {
const handleWorkerDeleteClick = (user: UserWithRole) => {
const filteredWorker = workers.filter((worker) => worker.userId !== user.userId);
const workersIdList = filteredWorker.map((worker) => worker.userId);
setWorkers(filteredWorker);
setValue('userId', workersIdList);
};

const handleFileDeleteClick = (fileId: string) => {
const filteredFiles = files.filter((file) => file.id !== fileId);
setFiles(filteredFiles);
};

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

// 새로운 파일별 파일 크기 확인 & 고유 ID 부여
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)} 이하의 파일만 업로드 가능합니다.`);
}
customFiles.push({ id: `${file.name}_${file.size}_${Date.now()}`, file });
}

setFiles((prev) => [...prev, ...customFiles]);
};

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { files } = e.target;
if (!files || files.length === 0) return;
updateFiles(files);
};

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

return (
<form id={formId} className="mb-20 flex w-4/5 grow flex-col justify-center" onSubmit={handleSubmit(onSubmit)}>
{/* ToDo: 상태 선택 리팩토링 할 것 */}
Expand All @@ -132,7 +173,7 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
<label
key={statusId}
htmlFor={name}
className={`flex items-center rounded-lg border px-5 py-3 text-emphasis ${isChecked ? 'border-input bg-white' : 'bg-button'}`}
className={`flex cursor-pointer items-center rounded-lg border px-5 py-3 text-emphasis ${isChecked ? 'border-input bg-white' : 'bg-button'}`}
>
<input
id={name}
Expand Down Expand Up @@ -245,8 +286,8 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
<div key={user.userId} className="flex items-center space-x-4 rounded-md bg-button px-5">
<RoleIcon roleName={user.roleName} />
<div>{user.nickname}</div>
<button type="button" aria-label="delete-worker" onClick={() => handleDeleteClick(user)}>
<IoMdCloseCircle className="text-error" />
<button type="button" aria-label="delete-worker" onClick={() => handleWorkerDeleteClick(user)}>
<IoMdCloseCircle className="text-close" />
</button>
</div>
))}
Expand All @@ -273,7 +314,29 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod

<label htmlFor="files">
<h3 className="text-large">첨부파일</h3>
<input type="file" id="files" />
<input type="file" id="files" className="h-0 w-0 opacity-0" multiple hidden onChange={handleFileChange} />
<section
className="flex cursor-pointer items-center gap-4 rounded-sl border-2 border-dashed border-input p-10"
onDrop={handleFileDrop}
>
<ul className="flex grow flex-wrap gap-4">
{files.map(({ id, file }) => (
<li key={id} className="flex items-center gap-4 rounded-md bg-button px-4 py-2">
<span>{file.name}</span>
<IoMdCloseCircle
className="text-close"
onClick={(e: React.MouseEvent<HTMLOrSVGElement>) => {
e.preventDefault();
handleFileDeleteClick(id);
}}
/>
</li>
))}
</ul>
<div>
<GoPlusCircle className="size-15 text-[#5E5E5E]" />
</div>
</section>
</label>
</form>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/user/auth-form/LinkContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChangeEvent, useState } from 'react';
import { FaPlus, FaMinus } from 'react-icons/fa6';
import { useFormContext } from 'react-hook-form';
import { USER_SETTINGS } from '@constants/userSettings';
import { USER_SETTINGS } from '@constants/settings';
import useToast from '@hooks/useToast';

type LinkContainerProps = {
Expand Down
2 changes: 1 addition & 1 deletion src/components/user/auth-form/ProfileImageContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { GoPlusCircle } from 'react-icons/go';
import { FaRegTrashCan } from 'react-icons/fa6';
import { useFormContext } from 'react-hook-form';
import { convertBytesToString } from '@utils/converter';
import { USER_SETTINGS } from '@constants/userSettings';
import { USER_SETTINGS } from '@constants/settings';
import useToast from '@hooks/useToast';

type ProfileImageContainerProps = {
Expand Down
8 changes: 4 additions & 4 deletions src/constants/formValidationRules.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Validator from '@utils/Validator';
import { deepFreeze } from '@utils/deepFreeze';
import { EMAIL_REGEX, ID_REGEX, NICKNAME_REGEX, PASSWORD_REGEX } from './regex';
import { USER_SETTINGS } from './userSettings';
import { Project } from '@/types/ProjectType';
import { Task } from '@/types/TaskType';
import { EMAIL_REGEX, ID_REGEX, NICKNAME_REGEX, PASSWORD_REGEX } from '@constants/regex';
import { USER_SETTINGS } from '@constants/settings';
import type { Project } from '@/types/ProjectType';
import type { Task } from '@/types/TaskType';

type ValidateOption = { [key: string]: (value: string) => string | boolean };

Expand Down
2 changes: 1 addition & 1 deletion src/constants/regex.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { USER_SETTINGS } from './userSettings';
import { USER_SETTINGS } from '@constants/settings';

export const EMAIL_REGEX = /^[a-z0-9._%+-]+@[a-z0-9-]+\.[a-z]{2,3}(?:\.[a-z]{2,3})?$/i;
export const PHONE_REGEX = /^01([0|1|6|7|8|9])-?([0-9]{3,4})-?([0-9]{4})$/;
Expand Down
7 changes: 6 additions & 1 deletion src/constants/userSettings.ts → src/constants/settings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MB } from './units';
import { MB } from '@constants/units';

export const USER_SETTINGS = Object.freeze({
MAX_IMAGE_SIZE: 2 * MB,
Expand All @@ -11,3 +11,8 @@ export const USER_SETTINGS = Object.freeze({
MAX_NICKNAME_LENGTH: 20,
MAX_EMAIL_LENGTH: 128,
});

export const TASK_SETTINGS = Object.freeze({
MAX_FILE_SIZE: 2 * MB,
MAX_FILE_COUNT: 10,
});
2 changes: 1 addition & 1 deletion src/utils/reduceImageSize.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { USER_SETTINGS } from '@/constants/userSettings';
import { USER_SETTINGS } from '@constants/settings';

const reduceImageSize = (objUrl: string) => {
return new Promise<Blob>((resolve, reject) => {
Expand Down

0 comments on commit 28be56e

Please sign in to comment.