diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b3474b60..fc5966c4 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -25,6 +25,8 @@ module.exports = { 'no-param-reassign': 'warn', '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', diff --git a/package.json b/package.json index 46f14412..4231414a 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@types/react": "^18.3.2", "@types/react-big-calendar": "^1.8.9", "@types/react-dom": "^18.3.0", + "@types/react-syntax-highlighter": "^15.5.13", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react-swc": "^3.5.0", diff --git a/src/components/common/CustomMarkdown.tsx b/src/components/common/CustomMarkdown.tsx index 62b8563a..f9373f22 100644 --- a/src/components/common/CustomMarkdown.tsx +++ b/src/components/common/CustomMarkdown.tsx @@ -1,7 +1,10 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import Markdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; +import { LightAsync as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { hybrid } from 'react-syntax-highlighter/dist/esm/styles/hljs'; +import { languageMap } from '@constants/language'; import type { Components } from 'react-markdown'; type CustomMarkdownProps = { @@ -14,29 +17,29 @@ const component: Partial = { return ( <>

{children}

-
+
); }, h2(props) { const { children } = props; - return

{children}

; + return

{children}

; }, h3(props) { const { children } = props; - return

{children}

; + return

{children}

; }, h4(props) { const { children } = props; - return

{children}

; + return

{children}

; }, h5(props) { const { children } = props; - return

{children}

; + return

{children}

; }, h6(props) { const { children } = props; - return

{children}

; + return

{children}

; }, hr() { return
; @@ -51,15 +54,15 @@ const component: Partial = { }, img(props) { const { src, alt } = props; - return {alt}; + return {alt}; }, blockquote(props) { const { children } = props; - return
{children}
; + return
{children}
; }, table(props) { const { children } = props; - return {children}
; + return {children}
; }, th(props) { const { children, style } = props; @@ -118,17 +121,40 @@ const component: Partial = { return
{children}
; }, code(props) { - const { children, className } = props; - return {children}; + const { children, className, node } = props; + const language = className?.split('-')[1] || ''; + const mappedLanguage = languageMap[language] || 'plaintext'; + + if (!language && node?.position?.start.line === node?.position?.end.line) { + return {children}; + } + return ( + + {children as string} + + ); }, }; +function getChangedMarkdownForLineBreak(markdown: string) { + return markdown + .split('\n') + .map((sentence) => (sentence === '' ? '\n
\n' : sentence)) + .join('\n'); +} + export default function CustomMarkdown({ markdown }: CustomMarkdownProps) { + const changedMarkdown = useMemo(() => getChangedMarkdownForLineBreak(markdown), [markdown]); + return (
- - {markdown} - + {markdown.trim().length === 0 ? ( +
입력된 내용이 없습니다.
+ ) : ( + + {changedMarkdown} + + )}
); } diff --git a/src/components/modal/task/ModalTaskForm.tsx b/src/components/modal/task/ModalTaskForm.tsx index 167d8816..83309bf2 100644 --- a/src/components/modal/task/ModalTaskForm.tsx +++ b/src/components/modal/task/ModalTaskForm.tsx @@ -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'; @@ -13,6 +15,7 @@ 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'; @@ -20,6 +23,7 @@ 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; @@ -36,11 +40,12 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod const [keyword, setKeyword] = useState(''); const [workers, setWorkers] = useState([]); const [preview, setPreview] = useState(false); + const [files, setFiles] = useState([]); 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 { @@ -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) => { + const { files } = e.target; + if (!files || files.length === 0) return; + updateFiles(files); + }; + + const handleFileDrop = (e: React.DragEvent) => { + const { files } = e.dataTransfer; + if (!files || files.length === 0) return; + updateFiles(files); + }; + return (
{/* ToDo: 상태 선택 리팩토링 할 것 */} @@ -132,7 +173,7 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod