From b3ed819fd60062f139fe532d1ae40b91834a9285 Mon Sep 17 00:00:00 2001 From: Suk Woo Date: Mon, 26 Aug 2024 12:50:14 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Feat:=20#63=20=EB=A7=88=ED=81=AC=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20=EC=BD=94=EB=93=9C=20=EB=B8=94=EB=A1=9D=20=ED=95=98?= =?UTF-8?q?=EC=9D=B4=EB=9D=BC=EC=9D=B4=ED=8C=85=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Config: #63 ESLint 설정 추가 (no-cond-assign: off) * Config: #63 @types/react-syntax-highlighter 추가 * Feat: #63 코드 블록 하이라이팅 기능 추가 * Fix: #63 인라인 코드와 코드 블록 분기 처리 수정 * Feat: #63 markdown 개행 처리를 위한 기능 추가 --- .eslintrc.cjs | 1 + package.json | 1 + src/components/common/CustomMarkdown.tsx | 56 +++++-- src/constants/language.ts | 203 +++++++++++++++++++++++ yarn.lock | 7 + 5 files changed, 253 insertions(+), 15 deletions(-) create mode 100644 src/constants/language.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b3474b60..5b41f91e 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -25,6 +25,7 @@ module.exports = { 'no-param-reassign': 'warn', 'no-return-assign': 'warn', 'no-unused-vars': 'warn', + 'no-cond-assign': 'off', '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/constants/language.ts b/src/constants/language.ts new file mode 100644 index 00000000..60890ce9 --- /dev/null +++ b/src/constants/language.ts @@ -0,0 +1,203 @@ +export const languageMap: { [key: string]: string } = { + oneC: '1c', + '1c': '1c', + abnf: 'abnf', + accesslog: 'accesslog', + actionscript: 'actionscript', + ada: 'ada', + angelscript: 'angelscript', + apache: 'apache', + applescript: 'applescript', + arcade: 'arcade', + arduino: 'arduino', + armasm: 'armasm', + asciidoc: 'asciidoc', + aspectj: 'aspectj', + autohotkey: 'autohotkey', + autoit: 'autoit', + avrasm: 'avrasm', + awk: 'awk', + axapta: 'axapta', + bash: 'bash', + basic: 'basic', + bnf: 'bnf', + brainfuck: 'brainfuck', + cLike: 'c-like', + 'c-like': 'c-like', + c: 'c', + cal: 'cal', + capnproto: 'capnproto', + ceylon: 'ceylon', + clean: 'clean', + clojureRepl: 'clojure-repl', + 'clojure-repl': 'clojure-repl', + clojure: 'clojure', + cmake: 'cmake', + coffeescript: 'coffeescript', + coq: 'coq', + cos: 'cos', + cpp: 'cpp', + crmsh: 'crmsh', + crystal: 'crystal', + csharp: 'csharp', + csp: 'csp', + css: 'css', + d: 'd', + dart: 'dart', + delphi: 'delphi', + diff: 'diff', + django: 'django', + dns: 'dns', + dockerfile: 'dockerfile', + dos: 'dos', + dsconfig: 'dsconfig', + dts: 'dts', + dust: 'dust', + ebnf: 'ebnf', + elixir: 'elixir', + elm: 'elm', + erb: 'erb', + erlangRepl: 'erlang-repl', + 'erlang-repl': 'erlang-repl', + erlang: 'erlang', + excel: 'excel', + fix: 'fix', + flix: 'flix', + fortran: 'fortran', + fsharp: 'fsharp', + gams: 'gams', + gauss: 'gauss', + gcode: 'gcode', + gherkin: 'gherkin', + glsl: 'glsl', + gml: 'gml', + go: 'go', + golo: 'golo', + gradle: 'gradle', + groovy: 'groovy', + haml: 'haml', + handlebars: 'handlebars', + haskell: 'haskell', + haxe: 'haxe', + hsp: 'hsp', + htmlbars: 'htmlbars', + http: 'http', + hy: 'hy', + inform7: 'inform7', + ini: 'ini', + irpf90: 'irpf90', + isbl: 'isbl', + java: 'java', + js: 'javascript', + javascript: 'javascript', + jbossCli: 'jboss-cli', + 'jboss-cli': 'jboss-cli', + json: 'json', + juliaRepl: 'julia-repl', + 'julia-repl': 'julia-repl', + julia: 'julia', + kotlin: 'kotlin', + lasso: 'lasso', + latex: 'latex', + ldif: 'ldif', + leaf: 'leaf', + less: 'less', + lisp: 'lisp', + livecodeserver: 'livecodeserver', + livescript: 'livescript', + llvm: 'llvm', + lsl: 'lsl', + lua: 'lua', + makefile: 'makefile', + markdown: 'markdown', + mathematica: 'mathematica', + matlab: 'matlab', + maxima: 'maxima', + mel: 'mel', + mercury: 'mercury', + mipsasm: 'mipsasm', + mizar: 'mizar', + mojolicious: 'mojolicious', + monkey: 'monkey', + moonscript: 'moonscript', + n1ql: 'n1ql', + nginx: 'nginx', + nim: 'nim', + nix: 'nix', + nodeRepl: 'node-repl', + 'node-repl': 'node-repl', + nsis: 'nsis', + objectivec: 'objectivec', + ocaml: 'ocaml', + openscad: 'openscad', + oxygene: 'oxygene', + parser3: 'parser3', + perl: 'perl', + pf: 'pf', + pgsql: 'pgsql', + phpTemplate: 'php-template', + 'php-template': 'php-template', + php: 'php', + plaintext: 'plaintext', + pony: 'pony', + powershell: 'powershell', + processing: 'processing', + profile: 'profile', + prolog: 'prolog', + properties: 'properties', + protobuf: 'protobuf', + puppet: 'puppet', + purebasic: 'purebasic', + pythonRepl: 'python-repl', + 'python-repl': 'python-repl', + python: 'python', + q: 'q', + qml: 'qml', + r: 'r', + reasonml: 'reasonml', + rib: 'rib', + roboconf: 'roboconf', + routeros: 'routeros', + rsl: 'rsl', + ruby: 'ruby', + ruleslanguage: 'ruleslanguage', + rust: 'rust', + sas: 'sas', + scala: 'scala', + scheme: 'scheme', + scilab: 'scilab', + scss: 'scss', + shell: 'shell', + smali: 'smali', + smalltalk: 'smalltalk', + sml: 'sml', + sqf: 'sqf', + sql: 'sql', + stan: 'stan', + stata: 'stata', + step21: 'step21', + stylus: 'stylus', + subunit: 'subunit', + swift: 'swift', + taggerscript: 'taggerscript', + tap: 'tap', + tcl: 'tcl', + thrift: 'thrift', + tp: 'tp', + twig: 'twig', + typescript: 'typescript', + vala: 'vala', + vbnet: 'vbnet', + vbscriptHtml: 'vbscript-html', + 'vbscript-html': 'vbscript-html', + vbscript: 'vbscript', + verilog: 'verilog', + vhdl: 'vhdl', + vim: 'vim', + x86asm: 'x86asm', + xl: 'xl', + xml: 'xml', + xquery: 'xquery', + yaml: 'yaml', + zephir: 'zephir', +}; diff --git a/yarn.lock b/yarn.lock index 15d1e8d7..81c2b3fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -765,6 +765,13 @@ dependencies: "@types/react" "*" +"@types/react-syntax-highlighter@^15.5.13": + version "15.5.13" + resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz#c5baf62a3219b3bf28d39cfea55d0a49a263d1f2" + integrity sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@>=16.9.11", "@types/react@^18.3.2": version "18.3.2" resolved "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz" From 28be56e19b0c0df4b80e3015d20f385614d5c16b Mon Sep 17 00:00:00 2001 From: Suk Woo Date: Mon, 26 Aug 2024 21:04:28 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Feat:=20#63=20=ED=95=A0=EC=9D=BC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EB=AA=A8=EB=8B=AC=20DnD=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=B2=A8=EB=B6=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename: #63 userSettings → settings 파일 이름 변경 * Formatting: #63 파일 이름 변경에 따른 변수명 변경 * Config: #63 ESLint 환경 설정 추가 * Fix: #63 ESLint 설정에 포함된 공백문자 제거 * Feat: #63 할일 모달 파일 첨부 기능 추가 * UI: #63 삭제 버튼 색상 변경 --- .eslintrc.cjs | 1 + src/components/modal/task/ModalTaskForm.tsx | 75 +++++++++++++++++-- .../user/auth-form/LinkContainer.tsx | 2 +- .../user/auth-form/ProfileImageContainer.tsx | 2 +- src/constants/formValidationRules.ts | 8 +- src/constants/regex.ts | 2 +- .../{userSettings.ts => settings.ts} | 7 +- src/utils/reduceImageSize.ts | 2 +- 8 files changed, 84 insertions(+), 15 deletions(-) rename src/constants/{userSettings.ts => settings.ts} (65%) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 5b41f91e..fc5966c4 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -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', 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