Skip to content

Commit

Permalink
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/#80-local-login
  • Loading branch information
Yoonyesol committed Aug 26, 2024
2 parents 98ccbf2 + 28be56e commit b0ecc30
Show file tree
Hide file tree
Showing 12 changed files with 337 additions and 30 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
56 changes: 41 additions & 15 deletions src/components/common/CustomMarkdown.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -14,29 +17,29 @@ const component: Partial<Components> = {
return (
<>
<h1 className="text-3xl">{children}</h1>
<hr className="my-3" />
<hr className="my-5" />
</>
);
},
h2(props) {
const { children } = props;
return <h2 className="mb-3 text-2xl">{children}</h2>;
return <h2 className="mb-5 text-2xl">{children}</h2>;
},
h3(props) {
const { children } = props;
return <h3 className="mb-3 text-xl">{children}</h3>;
return <h3 className="mb-5 text-xl">{children}</h3>;
},
h4(props) {
const { children } = props;
return <h3 className="mb-3 text-lg">{children}</h3>;
return <h3 className="mb-5 text-lg">{children}</h3>;
},
h5(props) {
const { children } = props;
return <h3 className="mb-3 text-base">{children}</h3>;
return <h3 className="mb-5 text-base">{children}</h3>;
},
h6(props) {
const { children } = props;
return <h3 className="mb-3 text-sm">{children}</h3>;
return <h3 className="mb-5 text-sm">{children}</h3>;
},
hr() {
return <hr className="my-5" />;
Expand All @@ -51,15 +54,15 @@ const component: Partial<Components> = {
},
img(props) {
const { src, alt } = props;
return <img src={src} alt={alt} className="m-auto" />;
return <img src={src} alt={alt} className="m-auto my-5" />;
},
blockquote(props) {
const { children } = props;
return <blockquote className="border-l-[3px] border-[#20C997] bg-[#F8F9FA] p-4">{children}</blockquote>;
return <blockquote className="my-5 border-l-[3px] border-[#20C997] bg-[#F8F9FA] p-8">{children}</blockquote>;
},
table(props) {
const { children } = props;
return <table className="border-collapse overflow-hidden rounded-md shadow-md">{children}</table>;
return <table className="my-5 border-collapse overflow-hidden rounded-md shadow-md">{children}</table>;
},
th(props) {
const { children, style } = props;
Expand Down Expand Up @@ -118,17 +121,40 @@ const component: Partial<Components> = {
return <section className={className}>{children}</section>;
},
code(props) {
const { children, className } = props;
return <code className={`${className} rounded-sm border-none bg-[#E9ECEF] px-2`}>{children}</code>;
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 <code className={`${className} rounded-sm border-none bg-[#E9ECEF] px-2`}>{children}</code>;
}
return (
<SyntaxHighlighter style={hybrid} language={mappedLanguage} className="my-5">
{children as string}
</SyntaxHighlighter>
);
},
};

function getChangedMarkdownForLineBreak(markdown: string) {
return markdown
.split('\n')
.map((sentence) => (sentence === '' ? '\n<br />\n' : sentence))
.join('\n');
}

export default function CustomMarkdown({ markdown }: CustomMarkdownProps) {
const changedMarkdown = useMemo(() => getChangedMarkdownForLineBreak(markdown), [markdown]);

return (
<section className="rounded-md border border-input p-10 text-sm">
<Markdown components={component} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
{markdown}
</Markdown>
{markdown.trim().length === 0 ? (
<div className="text-xs text-gray-400/90">입력된 내용이 없습니다.</div>
) : (
<Markdown components={component} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
{changedMarkdown}
</Markdown>
)}
</section>
);
}
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
Loading

0 comments on commit b0ecc30

Please sign in to comment.