Skip to content

Commit

Permalink
[#516] DatePicker 컴포넌트 (#520)
Browse files Browse the repository at this point in the history
* chore: select-icon 수정

* feat: calendar-picker-indicator 속성 정의

* feat: select 아이콘 추가

* feat: date util함수 추가

* feat: DatePicker 컴포넌트 작성

* feat: DatePicker 스토리북 작성
  • Loading branch information
hanyugeon authored and gxxrxn committed Jun 17, 2024
1 parent e6f8d87 commit 63af14d
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 1 deletion.
1 change: 1 addition & 0 deletions public/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export { default as IconDelete } from './delete.svg';
export { default as IconMembers } from './members.svg';
export { default as IconPlus } from './plus.svg';
export { default as IconBookPlus } from './book-plus.svg';
export { default as IconSelect } from './select-icon.svg';

// 카카오
export { default as IconKakao } from './kakao.svg';
2 changes: 1 addition & 1 deletion public/icons/select-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
109 changes: 109 additions & 0 deletions src/stories/base/DatePicker.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { getTodayDate } from '@/utils/date';
import Button from '@/v1/base/Button';
import DatePicker from '@/v1/base/DatePicker';
import ErrorMessage from '@/v1/base/ErrorMessage';
import { Meta, StoryObj } from '@storybook/react';
import { SubmitHandler, useForm } from 'react-hook-form';

const meta: Meta<typeof DatePicker> = {
title: 'Base/DatePicker',
component: DatePicker,
tags: ['autodocs'],
};

export default meta;

type Story = StoryObj<typeof DatePicker>;

export const Default: Story = {
args: {
defaultValue: '2023-06-16',
},
render: args => <DatePicker {...args} />,
};

export const Disabled: Story = {
args: {
disabled: true,
defaultValue: '2023-06-16',
},
render: args => <DatePicker {...args} />,
};

export const UseWithForm: Story = {
render: () => <DatePickerWithForm />,
};

type DefaultValues = {
startDate: string;
endDate: string;
hello: number;
hi: string;
};

const DatePickerWithForm = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<DefaultValues>({
mode: 'all',
defaultValues: {
startDate: getTodayDate(),
endDate: '2030-12-25',
},
});

const handleSubmitForm: SubmitHandler<DefaultValues> = ({
startDate,
endDate,
}) => {
alert(`startDate: ${startDate} endDate: ${endDate}`);
};

return (
<form
onSubmit={handleSubmit(handleSubmitForm)}
className="flex flex-col gap-[3.2rem]"
>
<div className="flex flex-col gap-[0.5rem]">
<DatePicker
disabled={true}
min={getTodayDate()}
{...register('startDate', {
required: { value: true, message: '종료일을 입력해주세요' },
min: {
value: getTodayDate(),
message: '종료일은 시작일보다 늦어야 해요',
},
})}
/>
{errors.startDate && (
<ErrorMessage>{errors.startDate.message}</ErrorMessage>
)}
</div>
<div className="flex flex-col gap-[0.5rem]">
<DatePicker
min={getTodayDate()}
{...register('endDate', {
required: { value: true, message: '종료일을 입력해주세요.' },
min: {
value: getTodayDate(),
message: '종료일은 시작일보다 늦어야 해요.',
},
})}
/>
{errors.endDate && (
<ErrorMessage>{errors.endDate.message}</ErrorMessage>
)}
</div>
<Button
size="full"
type="submit"
onClick={handleSubmit(handleSubmitForm)}
>
Submit
</Button>
</form>
);
};
11 changes: 11 additions & 0 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@
max-width: 43rem;
margin: 0 auto !important;
}
input[type='date']::-webkit-calendar-picker-indicator {
background-image: none;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
cursor: pointer;
}
}

@layer utilities {
Expand Down
22 changes: 22 additions & 0 deletions src/utils/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,25 @@ export const getDdayCount = (target: Date) =>
export const isExpired = (end: string) => {
return getDdayCount(new Date(end)) < 0;
};

export const formatDateInputValue = (target: string) => {
const date = new Date(target);

const year = date.getFullYear();
const month = ('0' + (date.getMonth() + 1)).slice(-2);
const day = ('0' + date.getDate()).slice(-2);

const daysOfWeek = ['일', '월', '화', '수', '목', '금', '토'];
const dayOfWeek = daysOfWeek[date.getDay()];

const formattedDate = `${year}.${month}.${day} (${dayOfWeek})`;

return formattedDate;
};

export const getTodayDate = () => {
const offset = new Date().getTimezoneOffset() * 60000;
const todayDate = new Date(Date.now() - offset);

return todayDate.toISOString().slice(0, 10);
};
84 changes: 84 additions & 0 deletions src/v1/base/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
ChangeEventHandler,
forwardRef,
InputHTMLAttributes,
Ref,
useEffect,
useState,
} from 'react';

import { formatDateInputValue } from '@/utils/date';

import { IconSelect } from '@public/icons';

interface DatePickerProps
extends Omit<
InputHTMLAttributes<HTMLInputElement>,
'value' | 'id' | 'className'
> {
defaultValue?: string;
}

const DatePicker = (
{
name,
onChange,
disabled = false,
defaultValue = '',
...props
}: DatePickerProps,
ref: Ref<HTMLInputElement>
) => {
const [currentDate, setCurrentDate] = useState(defaultValue);

const disabledClasses = disabled
? 'text-black-500 cursor-not-allowed [&_svg]:fill-black-500'
: 'text-black-900 cursor-pointer [&_svg]:fill-black-900';

const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
setCurrentDate(event.target.value);
onChange && onChange(event);
};

useEffect(() => {
if (defaultValue) return;

const $date = document.querySelector(`input#${name}`) as HTMLInputElement;

if (!$date) return;
setCurrentDate($date.value);
}, [defaultValue, name]);

return (
<label
className={`relative flex h-[3rem] max-w-[14rem] items-center justify-between bg-transparent ${disabledClasses}`}
htmlFor={name}
>
<div className="flex h-full min-w-0 flex-grow items-center">
<input
id={name}
name={name}
ref={ref}
type="date"
className="h-full w-0"
disabled={disabled}
defaultValue={currentDate}
onChange={handleInputChange}
{...props}
/>
<p
className={`truncate text-sm ${
currentDate ? 'text-inherit' : 'text-placeholder'
}`}
>
{currentDate
? formatDateInputValue(currentDate)
: '날짜를 선택해주세요'}
</p>
</div>
<IconSelect className={`h-[1.2rem] w-[1.2rem] flex-shrink-0`} />
</label>
);
};

export default forwardRef(DatePicker);

0 comments on commit 63af14d

Please sign in to comment.