Skip to content

Commit

Permalink
Feat: #63 할일 일정명과 기간 설정 로직 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
Seok93 committed Aug 6, 2024
1 parent b0a8154 commit 0d17158
Show file tree
Hide file tree
Showing 12 changed files with 200 additions and 29 deletions.
6 changes: 6 additions & 0 deletions src/assets/calendar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/components/common/DuplicationCheckInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function DuplicationCheckInput({
<input
type="text"
id={id}
className="h-25 w-200 rounded-md border border-input pl-10 pr-25 text-regular placeholder:text-xs"
className="h-25 w-full min-w-200 rounded-md border border-input pl-10 pr-25 text-regular placeholder:text-xs"
placeholder={placeholder}
{...register}
/>
Expand Down
6 changes: 4 additions & 2 deletions src/components/modal/task/CreateModalTask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import ModalPortal from '@components/modal/ModalPortal';
import ModalTaskForm from '@components/modal/task/ModalTaskForm';
import ModaFormButton from '@components/modal/ModaFormButton';
import { TaskForm } from '@/types/TaskType';
import { Project } from '@/types/ProjectType';

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

export default function CreateModalTask({ onClose: handleClose }: CreateModalTaskProps) {
export default function CreateModalTask({ project, onClose: handleClose }: CreateModalTaskProps) {
// ToDo: 상태 생성을 위한 네트워크 로직 추가
const handleSubmit: SubmitHandler<TaskForm> = async (data) => {
console.log('생성 폼 제출');
Expand All @@ -19,7 +21,7 @@ export default function CreateModalTask({ onClose: handleClose }: CreateModalTas
return (
<ModalPortal>
<ModalLayout onClose={handleClose}>
<ModalTaskForm formId="createTaskForm" onSubmit={handleSubmit} />
<ModalTaskForm formId="createTaskForm" project={project} onSubmit={handleSubmit} />
<ModaFormButton formId="createTaskForm" isCreate onClose={handleClose} />
</ModalLayout>
</ModalPortal>
Expand Down
76 changes: 63 additions & 13 deletions src/components/modal/task/ModalTaskForm.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
import { useState } from 'react';
import { DateTime } from 'luxon';
import { IoSearch } from 'react-icons/io5';
import { SubmitHandler, useForm } from 'react-hook-form';
import { TASK_VALIDATION_RULES } from '@constants/formValidationRules';
import DuplicationCheckInput from '@components/common/DuplicationCheckInput';
import { IoSearch } from 'react-icons/io5';
import useTaskQuery from '@hooks/query/useTaskQuery';
import { Task, TaskForm } from '@/types/TaskType';
import { Project } from '@/types/ProjectType';

type ModalTaskFormProps = {
formId: string;
project: Project;
taskId?: Task['taskId'];
onSubmit: SubmitHandler<TaskForm>;
};

export default function ModalTaskForm({ formId, taskId, onSubmit }: ModalTaskFormProps) {
export default function ModalTaskForm({ formId, project, taskId, onSubmit }: ModalTaskFormProps) {
const { startDate: projectStartDate, endDate: projectEndDate } = project;
const [hasDeadline, setHasDeadline] = useState(false);
const { taskNameList } = useTaskQuery(project.projectId);
const {
register,
watch,
setValue,
getValues,
clearErrors,
handleSubmit,
formState: { errors },
} = useForm<TaskForm>({
Expand All @@ -23,36 +33,76 @@ export default function ModalTaskForm({ formId, taskId, onSubmit }: ModalTaskFor
name: '',
content: '',
startDate: DateTime.fromJSDate(new Date()).toFormat('yyyy-LL-dd'),
endDate: undefined,
endDate: DateTime.fromJSDate(new Date()).toFormat('yyyy-LL-dd'),
},
});

const handleDeadlineToggle = () => {
setValue('endDate', getValues('startDate'));
clearErrors('endDate');
setHasDeadline((prev) => !prev);
};

return (
<form id={formId} className="mb-20 flex grow flex-col justify-center" onSubmit={handleSubmit(onSubmit)}>
{/* ToDo: 할일명 목록 추출하여 전달하기 */}
<form
id={formId}
className="mb-20 flex w-4/5 max-w-375 grow flex-col justify-center"
onSubmit={handleSubmit(onSubmit)}
>
<DuplicationCheckInput
id="name"
label="일정"
value={watch('name')}
placeholder="일정명을 입력해주세요."
errors={errors.name?.message}
register={register('name', TASK_VALIDATION_RULES.TASK_NAME(['test']))}
register={register('name', TASK_VALIDATION_RULES.TASK_NAME(taskNameList))}
/>

<div>
<div className="flex items-center justify-center gap-10">
<label htmlFor="startDate" className="grow">
<label htmlFor="startDate" className="w-1/2">
<h3 className="text-large">시작일</h3>
<input type="date" id="startDate" {...register('startDate')} />
<input
type="date"
id="startDate"
{...register('startDate', TASK_VALIDATION_RULES.START_DATE(projectStartDate, projectEndDate))}
/>
<div className={`my-5 h-10 grow text-xs text-error ${errors.startDate ? 'visible' : 'invisible'}`}>
{errors.startDate?.message}
</div>
</label>
<label htmlFor="endDate" className="grow">
<h3 className="flex items-center text-large">종료일</h3>
<input type="date" id="endDate" {...register('endDate')} />
<div className={`my-5 h-10 grow text-xs text-error ${errors.startDate ? 'visible' : 'invisible'}`}>
{errors.startDate?.message}
<label htmlFor="endDate" className="w-1/2">
<h3 className="flex items-center text-large">
종료일
<label htmlFor="deadline" className="relative ml-2 inline-block h-10 w-20">
<input
type="checkbox"
id="deadline"
className="peer h-0 w-0 border-main opacity-0"
checked={hasDeadline}
onChange={handleDeadlineToggle}
/>
{/* prettier-ignore */}
<span className="
absolute bottom-0 left-0 right-0 top-0 cursor-pointer rounded-full bg-disable transition duration-300
before:content-[''] before:absolute before:left-2 before:top-1/2 before:-translate-y-1/2 before:size-7
before:rounded-full before:bg-white before:transition before:duration-300
peer-checked:bg-main peer-checked:before:translate-x-9
"/>
</label>
</h3>
<input
type="date"
id="endDate"
className={`${hasDeadline ? '' : '!bg-disable'}`}
disabled={!hasDeadline}
{...register(
'endDate',
TASK_VALIDATION_RULES.END_DATE(hasDeadline, projectStartDate, projectEndDate, watch('startDate')),
)}
/>
<div className={`my-5 h-10 grow text-xs text-error ${errors.endDate ? 'visible' : 'invisible'}`}>
{errors.endDate?.message}
</div>
</label>
</div>
Expand Down
6 changes: 4 additions & 2 deletions src/components/modal/task/UpdateModalTask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import ModalPortal from '@components/modal/ModalPortal';
import ModalTaskForm from '@components/modal/task/ModalTaskForm';
import ModaFormButton from '@components/modal/ModaFormButton';
import { Task, TaskForm } from '@/types/TaskType';
import { Project } from '@/types/ProjectType';

type UpdateModalTaskProps = {
project: Project;
taskId: Task['taskId'];
onClose: () => void;
};

export default function UpdateModalTask({ taskId, onClose: handleClose }: UpdateModalTaskProps) {
export default function UpdateModalTask({ project, taskId, onClose: handleClose }: UpdateModalTaskProps) {
// ToDo: 상태 생성을 위한 네트워크 로직 추가
const handleSubmit: SubmitHandler<TaskForm> = async (data) => {
console.log('생성 폼 제출');
Expand All @@ -21,7 +23,7 @@ export default function UpdateModalTask({ taskId, onClose: handleClose }: Update
<ModalPortal>
<ModalLayout onClose={handleClose}>
{/* ToDo: Task 수정 모달 작성시 수정할 것 */}
<ModalTaskForm formId="updateTaskForm" taskId={taskId} onSubmit={handleSubmit} />
<ModalTaskForm formId="updateTaskForm" taskId={taskId} project={project} onSubmit={handleSubmit} />
<ModaFormButton formId="updateTaskForm" isCreate={false} onClose={handleClose} />
</ModalLayout>
</ModalPortal>
Expand Down
66 changes: 62 additions & 4 deletions src/constants/formValidationRules.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
import Validator from '@utils/Validator';
import { EMAIL_REGEX, PASSWORD_REGEX, PHONE_REGEX } from '@constants/regex';
import { deepFreeze } from '@utils/deepFreeze';
import { EMAIL_REGEX, PASSWORD_REGEX, PHONE_REGEX } from './regex';
import { Project } from '@/types/ProjectType';
import { Task } from '@/types/TaskType';

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

// ToDo: 리팩토링을 해야하나? 나중에 시간날 때 생각해보기
function getTaskDateValidation(
projectStartDate: Project['startDate'],
projectEndDate: Project['endDate'],
taskStartDate?: Task['startDate'],
) {
const validation: ValidateOption = {};

if (taskStartDate) {
validation.isEarlierDate = (taskEndDate: string) =>
!Validator.isEarlierStartDate(taskStartDate, taskEndDate) ? '시작일 이후로 설정해주세요.' : true;
}

if (projectStartDate && projectEndDate) {
validation.isWithinDateRange = (taskDate: string) =>
!Validator.isWithinDateRange(projectStartDate, projectEndDate, taskDate)
? '프로젝트 기간 내로 설정해주세요.'
: true;
}

return validation;
}

// ToDo: Form 별로 Validation 분리하기
export const STATUS_VALIDATION_RULES = deepFreeze({
STATUS_NAME: (nameList: string[]) => ({
required: '상태명을 입력해주세요.',
Expand All @@ -14,14 +42,16 @@ export const STATUS_VALIDATION_RULES = deepFreeze({
message: '상태명을 2자리 이상 20자리 이하로 입력해주세요.',
},
validate: {
isEmpty: (value: string) => !Validator.isEmptyString(value) || '상태명을 제대로 입력해주세요.',
duplicatedName: (value: string) => !Validator.isDuplicatedName(nameList, value) || '이미 사용중인 상태명입니다.',
isNotEmpty: (value: string) => (Validator.isEmptyString(value) ? '상태명을 제대로 입력해주세요.' : true),
isNotDuplicatedName: (value: string) =>
Validator.isDuplicatedName(nameList, value) ? '이미 사용중인 상태명입니다.' : true,
},
}),
COLOR: (colorList: string[]) => ({
required: '색상을 선택해주세요.',
validate: {
duplicatedName: (value: string) => !Validator.isDuplicatedName(colorList, value) || '이미 사용중인 색상입니다.',
isNotDuplicatedName: (value: string) =>
Validator.isDuplicatedName(colorList, value) ? '이미 사용중인 색상입니다.' : true,
},
}),
EMAIL: () => ({
Expand Down Expand Up @@ -65,3 +95,31 @@ export const STATUS_VALIDATION_RULES = deepFreeze({
required: '비밀번호를 한 번 더 입력해 주세요.',
}),
});

export const TASK_VALIDATION_RULES = deepFreeze({
TASK_NAME: (nameList: string[]) => ({
required: '일정명을 입력해주세요.',
maxLength: {
value: 128,
message: '일정명은 128자리 이하로 입력해주세요.',
},
validate: {
isNotEmpty: (name: string) => (Validator.isEmptyString(name) ? '일정명을 제대로 입력해주세요.' : true),
isNotDuplicatedName: (value: string) =>
Validator.isDuplicatedName(nameList, value) ? '이미 사용중인 일정명입니다.' : true,
},
}),
START_DATE: (projectStartDate: Project['startDate'], projectEndDate: Project['endDate']) => ({
required: '시작일을 선택해주세요.',
validate: getTaskDateValidation(projectStartDate, projectEndDate),
}),
END_DATE: (
hasDeadline: boolean,
projectStartDate: Project['startDate'],
projectEndDate: Project['endDate'],
taskStartDate: Task['startDate'],
) => ({
required: hasDeadline && '종료일을 선택해주세요.',
validate: getTaskDateValidation(projectStartDate, projectEndDate, taskStartDate),
}),
});
25 changes: 25 additions & 0 deletions src/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,31 @@
cursor: pointer;
background: #e5e5e5;
}

/* ========= Input Date Custom ========= */
input[type='date']::-webkit-inner-spin-button {
display: none;
appearance: none;
}
input[type='date']::-webkit-calendar-picker-indicator {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: transparent;
color: transparent;
cursor: pointer;
}
input[type='date'] {
position: relative;
width: 100%;
height: 2.5rem;
border: 1px solid #b1b1b1;
border-radius: 0.375rem;
padding: 0 1rem 0 2.2rem;
background: url('./assets/calendar.svg') no-repeat left 0.5rem bottom 50%;
}
}

@layer components {
Expand Down
20 changes: 20 additions & 0 deletions src/hooks/query/useTaskQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { TASK_DUMMY } from '@mocks/mockData';
import { TaskListWithStatus } from '@/types/TaskType';
import { Project } from '@/types/ProjectType';

function getTaskNameList(taskList: TaskListWithStatus[]) {
return taskList
.map((statusTask) => statusTask.tasks)
.flat()
.map((task) => task.name);
}

// Todo: Task Query CRUD로직 작성하기
// QueryKey: project, projectId, tasks
export default function useTaskQuery(projectId: Project['projectId']) {
const taskList = TASK_DUMMY;

const taskNameList = taskList ? getTaskNameList(taskList) : [];

return { taskList, taskNameList };
}
4 changes: 3 additions & 1 deletion src/layouts/page/ProjectLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default function ProjectLayout() {
</ListSidebar>
<section className="flex w-2/3 flex-col border border-list bg-contents-box">
<header className="flex h-30 items-center justify-between border-b p-10">
{/* ToDo: LabelTitle 공통 컴포넌트로 추출할 것 */}
<div>
<small className="mr-5 font-bold text-category">project</small>
<span className="text-emphasis">{project?.name}</span>
Expand All @@ -40,6 +41,7 @@ export default function ProjectLayout() {
<div className="sticky top-0 z-10 mb-10 flex items-center justify-between border-b bg-contents-box pt-10">
<ul className="*:mr-15">
<li className="inline">
{/* ToDo: nav 옵션사항을 정리하여 map으로 정리할 것 */}
<NavLink to="calendar" className={({ isActive }) => (isActive ? 'text-main' : 'text-emphasis')}>
Calendar
</NavLink>
Expand All @@ -63,7 +65,7 @@ export default function ProjectLayout() {
</div>
</section>
</section>
{showTaskModal && <CreateModalTask onClose={closeTaskModal} />}
{showTaskModal && <CreateModalTask project={project} onClose={closeTaskModal} />}
{showStatusModal && <CreateModalProjectStatus onClose={closeStatusModal} />}
</>
);
Expand Down
2 changes: 1 addition & 1 deletion src/pages/project/CalendarPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export default function CalendarPage() {
onSelectSlot={handleSelectSlot}
onSelectEvent={handleSelectEvent}
/>
{showModal && <UpdateModalTask taskId={selectedTask!.taskId} onClose={closeModal} />}
{showModal && <UpdateModalTask taskId={selectedTask!.taskId} project={project} onClose={closeModal} />}
</div>
);
}
2 changes: 1 addition & 1 deletion src/types/TaskType.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export type TaskForm = {
name: string;
content: string;
startDate: string;
endDate?: string;
endDate: string;
};

export type TaskWithStatus = RenameKeys<ProjectStatus, StatusKeyMapping> & Task;
Expand Down
14 changes: 10 additions & 4 deletions src/utils/Validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ export default class Validator {
return nameList.includes(name);
}

public static isWithinDateRange(start: Date, end: Date, target: Date) {
const startDate = DateTime.fromJSDate(start);
const endDate = DateTime.fromJSDate(end);
const targetDate = DateTime.fromJSDate(target);
public static isWithinDateRange(start: Date | string, end: Date | string, target: Date | string) {
const startDate = DateTime.fromJSDate(new Date(start));
const endDate = DateTime.fromJSDate(new Date(end));
const targetDate = DateTime.fromJSDate(new Date(target));
return targetDate >= startDate && targetDate < endDate;
}

public static isEarlierStartDate(start: Date | string, end: Date | string) {
const startDate = DateTime.fromJSDate(new Date(start));
const endDate = DateTime.fromJSDate(new Date(end));
return startDate <= endDate;
}
}

0 comments on commit 0d17158

Please sign in to comment.