Skip to content

Commit

Permalink
Feat: #24 상태 추가 모달 UI 작업과 Form Validation 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
Seok93 committed Jun 24, 2024
1 parent 882da0a commit 4200f76
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 0 deletions.
143 changes: 143 additions & 0 deletions src/components/modal/ModalTodoStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { GiCheckMark } from 'react-icons/gi';
import { IoIosClose } from 'react-icons/io';
import { RiProhibited2Fill, RiProhibited2Line } from 'react-icons/ri';
import { SubmitHandler, useForm } from 'react-hook-form';
import { STATUS_VALIDATION_RULES } from '@constants/formValidationRules';
import type { TodoStatus, TodoStatusFormValues } from '@/types/TodoStatusType';

type TodoProps = {
todoStatus: TodoStatus[];
onClose: () => void;
};

const DEFAULT_TODO_COLORS = Object.freeze({
RED: '#c83c00',
YELLOW: '#dab700',
GREEN: '#237700',
BLUE: '#00c2ff',
ORANGE: '#ff7a00',
PURPLE: '#db00ff',
PINK: '#ff0099',
YELLO_GREEN: '#8fff00',
});

// 색상 정보 취득
function getTodoColors(todoStatus: TodoStatus[]) {
const colorMap = new Map();

Object.values(DEFAULT_TODO_COLORS).forEach((color) =>
colorMap.set(color, { color, isDefault: true, isUsable: true }),
);

todoStatus.forEach(({ color }) => {
const colorStatusInfo = colorMap.has(color)
? { ...colorMap.get(color), isUsable: false }
: { color, isDefault: false, isUsable: false };
colorMap.set(color, colorStatusInfo);
});

return [...colorMap.values()];
}

export default function ModalTodoStatus({ todoStatus, onClose }: TodoProps) {
const {
register,
watch,
handleSubmit,
formState: { errors },
} = useForm<TodoStatusFormValues>({
mode: 'onChange',
defaultValues: {
name: '',
color: '',
},
});
const statusName = watch('name');
const selectedColor = watch('color');

// ToDo: useMemo, useCallback 고려해보기
const colorList = getTodoColors(todoStatus);
const nameList = todoStatus.map((status) => status.name);
const colorNameList = todoStatus.map((status) => status.color);

const handleClickDelete = (color: string) => {
// ToDo: 색상 삭제시 등록된 할일 목록이 있는지 확인하는 로직 추가
// ToDo: 색상 삭제를 위한 네트워크 로직 추가
console.log(`${color} 삭제`);
};

const onSubmit: SubmitHandler<TodoStatusFormValues> = async (data) => {
// ToDo: 색상 생성을 위한 네트워크 로직 추가
console.log(data);
};

return (
<div className="flex h-full flex-col items-center justify-center">
<form id="statusForm" className="mb-10 flex grow flex-col justify-center" onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="name" className="mb-10">
<h3 className="text-large">상태명</h3>
<div className="relative">
<input
type="text"
id="name"
className="h-25 w-200 rounded-md border border-input pl-10 pr-25 text-regular placeholder:text-xs"
placeholder="상태명을 입력하세요."
{...register('name', STATUS_VALIDATION_RULES.STATUS_NAME(nameList))}
/>
{statusName && (
<div className="absolute right-10 top-1/2 -translate-y-1/2">
{errors.name ? (
<RiProhibited2Line className="size-10 text-error" />
) : (
<GiCheckMark className="size-10 text-main" />
)}
</div>
)}
</div>
{errors.name && <div className="mt-5 text-xs text-error">{errors.name.message}</div>}
</label>
<h3 className="text-large">색상</h3>
<section className="grid grid-cols-8 gap-4">
{colorList.map(({ color, isUsable, isDefault }, index) => (
<div className="group relative m-auto" key={index}>
<label
htmlFor={color}
style={{ backgroundColor: color }}
className={`realative inline-block size-20 cursor-pointer rounded-full ${selectedColor === color ? 'border-4 border-selected' : ''}`}
>
<input
type="radio"
id={color}
value={color}
className="hidden"
disabled={!isUsable}
{...register('color', STATUS_VALIDATION_RULES.COLOR(colorNameList))}
/>
{!isUsable && <RiProhibited2Fill className="size-20 text-white" />}
</label>
{!isDefault && (
<button
type="button"
aria-label="delete-color"
className="invisible absolute right-0 top-0 cursor-pointer rounded-full border border-white bg-close hover:brightness-125 group-hover:visible"
onClick={() => handleClickDelete(color)}
>
<IoIosClose className="size-8 text-white" />
</button>
)}
</div>
))}
</section>
{errors.color && <div className="mt-5 text-xs text-error">{errors.color.message}</div>}
</form>
<div className="h-20">
<button type="submit" form="statusForm" className="mr-10 h-20 rounded-md bg-main px-10 text-white">
등록
</button>
<button type="button" className="h-20 rounded-md bg-button px-10" onClick={onClose}>
닫기
</button>
</div>
</div>
);
}
Empty file removed src/constants/.gitkeep
Empty file.
26 changes: 26 additions & 0 deletions src/constants/formValidationRules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Validator from '@utils/Validator';
import { deepFreeze } from '@utils/deepFreeze';

export const STATUS_VALIDATION_RULES = deepFreeze({
STATUS_NAME: (nameList: string[]) => ({
required: '상태명을 입력해주세요.',
minLength: {
value: 2,
message: '상태명을 2자리 이상 20자리 이하로 입력해주세요.',
},
maxLength: {
value: 20,
message: '상태명을 2자리 이상 20자리 이하로 입력해주세요.',
},
validate: {
isEmpty: (value: string) => !Validator.isEmptyString(value) || '상태명을 제대로 입력해주세요.',
duplicatedName: (value: string) => !Validator.isDuplicatedName(nameList, value) || '이미 사용중인 상태명입니다.',
},
}),
COLOR: (colorList: string[]) => ({
required: '색상을 선택해주세요.',
validate: {
duplicatedName: (value: string) => !Validator.isDuplicatedName(colorList, value) || '이미 사용중인 색상입니다.',
},
}),
});
11 changes: 11 additions & 0 deletions src/types/TodoStatusType.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// ToDo: API 설계 완료시 데이터 타입 변경할 것
export type TodoStatus = {
statusId: number;
name: string;
color: string;
};

export type TodoStatusFormValues = {
name: string;
color: string;
};

0 comments on commit 4200f76

Please sign in to comment.