diff --git a/src/components/ui/calendar/Calendar.tsx b/src/components/ui/calendar/Calendar.tsx new file mode 100644 index 0000000..bba199c --- /dev/null +++ b/src/components/ui/calendar/Calendar.tsx @@ -0,0 +1,95 @@ +import { + CalendarHeader, + DayViewMode, + MonthViewMode, + YearViewMode, +} from '@/components/ui/calendar/'; +import { CalendarProps, SelectMode } from '@/types/calendar'; +import { useState } from 'react'; + +export default function Calendar({ value, onSelect }: CalendarProps) { + const [currentDay, setCurrentDay] = useState(value ?? new Date()); + const [currentMonth, setCurrentMonth] = useState(value ?? new Date()); + const [selectMode, setSelectMode] = useState('day'); + + const TODAY = new Date(); + TODAY.setHours(0, 0, 0, 0); + + const handleSelect = (date: Date) => { + const selectedDate = new Date(date); + selectedDate.setHours(0, 0, 0, 0); + + if (selectedDate < TODAY) { + return; + } + setCurrentDay(date); + setCurrentMonth(new Date(date.getFullYear(), date.getMonth(), 1)); + onSelect?.(date); + }; + + // day 한 달 단위, year 10년 단위, month 1년 단위 + const onChange = (offset: number) => { + if (selectMode === 'day') { + setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + offset, 1)); + } else if (selectMode === 'year') { + setCurrentMonth( + new Date(currentMonth.getFullYear() + offset * 10, currentMonth.getMonth(), 1) + ); + } else { + setCurrentMonth(new Date(currentMonth.getFullYear() + offset, currentMonth.getMonth(), 1)); + } + }; + + // 오늘로 이동 + const handleToday = () => { + setCurrentMonth(TODAY); + setCurrentDay(TODAY); + setSelectMode('day'); + onSelect?.(TODAY); + }; + + // 모드 전환 + const onToggleMode = () => { + setSelectMode(prev => (prev === 'day' ? 'month' : prev === 'month' ? 'year' : 'day')); + }; + + return ( +
+ + + {selectMode === 'day' && ( + + )} + + {selectMode === 'month' && ( + { + setCurrentMonth(new Date(currentMonth.getFullYear(), month, 1)); + setSelectMode('day'); + }} + /> + )} + + {selectMode === 'year' && ( + { + setCurrentMonth(new Date(year, currentMonth.getMonth(), 1)); + setSelectMode('month'); + }} + /> + )} + +
+ +
+
+ ); +} diff --git a/src/components/ui/calendar/CalendarHeader.tsx b/src/components/ui/calendar/CalendarHeader.tsx new file mode 100644 index 0000000..34ac05c --- /dev/null +++ b/src/components/ui/calendar/CalendarHeader.tsx @@ -0,0 +1,48 @@ +import { Icon } from '@/components/ui/icon'; +import { CalendarHeaderProps } from '@/types/calendar'; +import { clsx } from 'clsx'; +import { useMemo } from 'react'; + +export default function CalendarHeader({ + selectMode, + currentMonth, + onToggleMode, + onChange, +}: CalendarHeaderProps) { + const CALENDAR_ARROW_CLASS = clsx('rounded px-1 pt-1 hover:bg-gray-100'); + + const headerLabel = useMemo(() => { + switch (selectMode) { + case 'day': + return currentMonth.toLocaleDateString('ko-KR', { + year: 'numeric', + month: 'long', + }); + case 'month': + return `${currentMonth.getFullYear()}년`; + case 'year': { + const startYear = Math.floor(currentMonth.getFullYear() / 10) * 10; + const endYear = startYear + 9; + return `${startYear} - ${endYear}`; + } + default: + return ' '; + } + }, [selectMode, currentMonth]); + + return ( +
+ + + + + +
+ ); +} diff --git a/src/components/ui/calendar/DayViewMode.tsx b/src/components/ui/calendar/DayViewMode.tsx new file mode 100644 index 0000000..760558a --- /dev/null +++ b/src/components/ui/calendar/DayViewMode.tsx @@ -0,0 +1,68 @@ +import { cn } from '@/lib/utils/cn'; +import { fillCalendarDays } from '@/lib/utils/fillCalendarDays'; +import { DayViewProps } from '@/types/calendar'; +import { clsx } from 'clsx'; + +export default function DayViewMode({ currentMonth, currentDay, onSelect }: DayViewProps) { + const DAYS = fillCalendarDays(currentMonth.getFullYear(), currentMonth.getMonth()); + const WEEKDAYS = ['일', '월', '화', '수', '목', '금', '토']; + + const TODAY = new Date(); + TODAY.setHours(0, 0, 0, 0); + + const DAY_CALENDAR_CLASS = clsx('text-md grid grid-cols-7 text-center'); + + return ( + <> +
+ {WEEKDAYS.map((day, i) => { + const headerClass = i === 0 ? 'text-red-400' : i === 6 ? 'text-blue-200' : ''; + + return ( +
+ {day} +
+ ); + })} +
+ +
+ {DAYS.map((dayObj, i) => { + const { date, isCurrentMonth } = dayObj; + + const isDisabled = date < TODAY; + const isSelected = date.toDateString() === currentDay.toDateString(); + const dayOfWeek = date.getDay(); + + const DAY_CELL_CLASS = isDisabled + ? 'text-gray-500' + : isCurrentMonth && dayOfWeek === 0 + ? 'text-red-400' + : isCurrentMonth && dayOfWeek === 6 + ? 'text-blue-200' + : ''; + + return ( + + ); + })} +
+ + ); +} diff --git a/src/components/ui/calendar/MonthViewMode.tsx b/src/components/ui/calendar/MonthViewMode.tsx new file mode 100644 index 0000000..23a3393 --- /dev/null +++ b/src/components/ui/calendar/MonthViewMode.tsx @@ -0,0 +1,17 @@ +import { MonthViewProps } from '@/types/calendar'; + +export default function MonthViewMode({ onSelect: onSelectMonth }: MonthViewProps) { + return ( +
+ {Array.from({ length: 12 }).map((_, i) => ( + + ))} +
+ ); +} diff --git a/src/components/ui/calendar/TimeSelector.styles.tsx b/src/components/ui/calendar/TimeSelector.styles.tsx new file mode 100644 index 0000000..f0ed0df --- /dev/null +++ b/src/components/ui/calendar/TimeSelector.styles.tsx @@ -0,0 +1,8 @@ +import React from 'react'; + +const TIME_SCROLL_CLASS = + 'flex max-h-[104px] flex-col gap-4 overflow-y-auto rounded border p-2 text-lg'; + +export const ScrollList: React.FC<{ children: React.ReactNode }> = ({ children }) => ( +
{children}
+); diff --git a/src/components/ui/calendar/TimeSelector.tsx b/src/components/ui/calendar/TimeSelector.tsx new file mode 100644 index 0000000..2f8b218 --- /dev/null +++ b/src/components/ui/calendar/TimeSelector.tsx @@ -0,0 +1,96 @@ +import { cn } from '@/lib/utils/cn'; +import { Period, TimeSelectorProps } from '@/types/calendar'; +import { useState } from 'react'; +import { ScrollList } from './TimeSelector.styles'; + +export default function TimeSelector({ onSelect, period, hours, minutes }: TimeSelectorProps) { + const [currentPeriod, setCurrentPeriod] = useState(period); + const [currentHour, setCurrentHour] = useState(hours); + const [currentMinute, setCurrentMinute] = useState(minutes); + + // 01 ~ 12 + const hoursList = Array.from({ length: 12 }, (_, i) => String(i + 1).padStart(2, '0')); + // 00 ~ 59 + const minutesList = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0')); + + const notifySelect = (p: Period, h: string, m: string) => { + onSelect?.(`${p} ${h}:${m}`); + }; + + const handleSelect = (type: 'period' | 'hour' | 'minute', value: string) => { + const updates = { + period: () => setCurrentPeriod(value as Period), + hour: () => setCurrentHour(value), + minute: () => setCurrentMinute(value), + }; + + updates[type](); + + notifySelect( + type === 'period' ? (value as Period) : currentPeriod, + type === 'hour' ? value : currentHour, + type === 'minute' ? value : currentMinute + ); + }; + + const TIME_SELECTOR_WRAPPER_CLASS = + 'mt-3 flex w-80 items-center justify-center gap-6 rounded-lg border bg-white p-4'; + + const BASE_PERIOD_CLASS = 'rounded-lg px-4 py-2 font-semibold transition'; + const BASE_TIME_CLASS = 'rounded px-3 py-1 transition'; + + const selectPeriodClass = (p: Period) => + cn( + BASE_PERIOD_CLASS, + currentPeriod === p ? 'bg-blue-200 text-white' : 'bg-gray-100 hover:bg-gray-200' + ); + + const selectTimeClass = (value: string, currentValue: string) => + cn(BASE_TIME_CLASS, currentValue === value ? 'bg-blue-200 text-white' : 'hover:bg-gray-100'); + + return ( +
+
+ {['오전', '오후'].map(p => ( + + ))} +
+ +
+
+ + {hoursList.map(h => ( + + ))} + +
+ +
+ + {minutesList.map(m => ( + + ))} + +
+
+
+ ); +} diff --git a/src/components/ui/calendar/YearViewMode.tsx b/src/components/ui/calendar/YearViewMode.tsx new file mode 100644 index 0000000..942afca --- /dev/null +++ b/src/components/ui/calendar/YearViewMode.tsx @@ -0,0 +1,22 @@ +import { YearViewProps } from '@/types/calendar'; + +export default function YearViewMode({ currentMonth, onSelect }: YearViewProps) { + const START_YEAR = Math.floor(currentMonth.getFullYear() / 10) * 10; + + return ( +
+ {Array.from({ length: 10 }).map((_, i) => { + const YEAR = START_YEAR + i; + return ( + + ); + })} +
+ ); +} diff --git a/src/components/ui/calendar/index.ts b/src/components/ui/calendar/index.ts new file mode 100644 index 0000000..9bc02fa --- /dev/null +++ b/src/components/ui/calendar/index.ts @@ -0,0 +1,6 @@ +export { default as Calendar } from '@/components/ui/calendar/Calendar'; +export { default as CalendarHeader } from '@/components/ui/calendar/CalendarHeader'; +export { default as DayViewMode } from '@/components/ui/calendar/DayViewMode'; +export { default as MonthViewMode } from '@/components/ui/calendar/MonthViewMode'; +export { default as TimeSelector } from '@/components/ui/calendar/TimeSelector'; +export { default as YearViewMode } from '@/components/ui/calendar/YearViewMode'; diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index dd1d4ee..aa21aaa 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -1,7 +1,15 @@ +export { + Calendar, + CalendarHeader, + DayViewMode, + MonthViewMode, + TimeSelector, + YearViewMode, +} from '@/components/ui/calendar'; +export { DateInput, Input, TimeInput } from '@/components/ui/input'; export { Table } from '@/components/ui/table'; export { Button } from './button'; export { Dropdown } from './dropdown'; export { Icon } from './icon'; -export { Input } from './input'; export { Modal, Notification } from './modal'; export { Post } from './post'; diff --git a/src/components/ui/input/DateInput.tsx b/src/components/ui/input/DateInput.tsx new file mode 100644 index 0000000..4c1ddd3 --- /dev/null +++ b/src/components/ui/input/DateInput.tsx @@ -0,0 +1,102 @@ +import { Calendar } from '@/components/ui/calendar'; +import useClickOutside from '@/hooks/useClickOutside'; +import useToggle from '@/hooks/useToggle'; +import { formatDate, formatWithDots } from '@/lib/utils/dateFormatter'; +import { useCallback, useRef, useState } from 'react'; +import Input from './input'; + +export default function DateInput() { + const { value: open, toggle, setClose } = useToggle(false); + const [selectedDate, setSelectedDate] = useState(null); + const [inputValue, setInputValue] = useState(''); // typing 사용 + + const wrapperRef = useRef(null); + + useClickOutside(wrapperRef, () => { + if (open) setClose(); + }); + + // 날짜 업데이트 중앙 관리 + const updateDate = useCallback((date: Date) => { + setSelectedDate(date); + setInputValue(formatDate(date)); + }, []); + + // 날짜 선택 + const handleDateSelect = useCallback( + (date: Date) => { + updateDate(date); + }, + [updateDate] + ); + + // typing + const handleDateInputChange = (e: React.ChangeEvent) => { + const newTypedNumbers = e.target.value.replace(/[^0-9]/g, ''); + const typedLength = newTypedNumbers.length; + + if (typedLength > 8) return; + + const year = parseInt(newTypedNumbers.slice(0, 4)); + const month = parseInt(newTypedNumbers.slice(4, 6)); + const day = parseInt(newTypedNumbers.slice(6, 8)); + + if (typedLength === 8) { + const inputDate = new Date(year, month - 1, day); + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (inputDate < today) return; + } + + const maxDaysList = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + const maxDay = month === 2 && isLeapYear ? 29 : maxDaysList[month - 1]; + + if (typedLength >= 8) { + if (day > 0 && day <= maxDay) { + setInputValue(formatWithDots(newTypedNumbers)); + } else { + setInputValue(formatWithDots(newTypedNumbers.slice(0, 7))); + } + return; + } + + if (typedLength >= 7) { + if (month === 2 && parseInt(formatWithDots(newTypedNumbers[6])) > 2) { + setInputValue(formatWithDots(newTypedNumbers.slice(0, 6))); + return; + } + } + + if (typedLength >= 6) { + if (month > 0 && month < 13) { + setInputValue(formatWithDots(newTypedNumbers)); + } else { + setInputValue(formatWithDots(newTypedNumbers.slice(0, 5))); + } + return; + } + + setInputValue(formatWithDots(newTypedNumbers)); + }; + + return ( +
+ + + {open && ( +
+ +
+ )} +
+ ); +} diff --git a/src/components/ui/input/TimeInput.tsx b/src/components/ui/input/TimeInput.tsx new file mode 100644 index 0000000..57ff760 --- /dev/null +++ b/src/components/ui/input/TimeInput.tsx @@ -0,0 +1,110 @@ +import TimeSelector from '@/components/ui/calendar/TimeSelector'; +import useClickOutside from '@/hooks/useClickOutside'; +import useToggle from '@/hooks/useToggle'; +import { formatTime } from '@/lib/utils/dateFormatter'; +import { Period } from '@/types/calendar'; +import { useCallback, useRef, useState } from 'react'; +import Input from './input'; + +export default function TimeInput() { + const { value: open, toggle, setClose } = useToggle(false); + const [period, setPeriod] = useState('오전'); + const [selectedTime, setSelectedTime] = useState(null); + const [inputValue, setInputValue] = useState(''); // typing 사용 + + const wrapperRef = useRef(null); + + useClickOutside(wrapperRef, () => { + if (open) setClose(); + }); + + // 시간 업데이트 중앙 관리 + const updateTime = useCallback((date: Date, selectedPeriod: Period) => { + setPeriod(selectedPeriod); + setSelectedTime(date); + setInputValue(formatTime(date)); + }, []); + + // 시간 선택 + const handleTimeSelect = useCallback( + (value: string) => { + const parts = value.split(' '); + const periodValue = parts.length === 2 ? (parts[0] as Period) : period; + const timePart = parts.length === 2 ? parts[1] : parts[0]; + + const [hours, minutes] = timePart.split(':').map(Number); + if (isNaN(hours) || isNaN(minutes)) return; + + const baseDate = selectedTime ?? new Date(); + const newDate = new Date(baseDate); + newDate.setHours(hours, minutes); + + updateTime(newDate, periodValue); + }, + [selectedTime, updateTime, period] + ); + + // typing + const handleTimeInputChange = (e: React.ChangeEvent) => { + const newTypedNumbers = e.target.value.replace(/[^0-9]/g, ''); + const typedLength = newTypedNumbers.length; + + setInputValue(newTypedNumbers); + + if (typedLength > 4) { + const hours = parseInt(newTypedNumbers.slice(0, typedLength - 2)); + + if (isNaN(hours) || hours < 1 || hours > 12) { + setInputValue(newTypedNumbers.slice(-1)); + return; + } + } + + if (typedLength < 3) return; + + const hoursTyped = newTypedNumbers.slice(0, typedLength - 2); + const minutesTyped = newTypedNumbers.slice(-2); + + const h = parseInt(hoursTyped); + const m = parseInt(minutesTyped); + + if (!isNaN(h) && !isNaN(m)) { + if (!(h >= 1 && h <= 12 && m >= 0 && m < 60)) return; + + const periodValue: Period = h > 12 ? '오후' : '오전'; + + const baseDate = selectedTime ?? new Date(); + const newDate = new Date(baseDate); + newDate.setHours(h, m); + + updateTime(newDate, periodValue); + } + }; + + const hours = selectedTime ? String(selectedTime.getHours() % 12 || 12).padStart(2, '0') : '12'; + const minutes = selectedTime ? String(selectedTime.getMinutes()).padStart(2, '0') : '00'; + + return ( +
+ + + {open && ( +
+ +
+ )} +
+ ); +} diff --git a/src/components/ui/input/dateinput.stories.tsx b/src/components/ui/input/dateinput.stories.tsx new file mode 100644 index 0000000..57f970d --- /dev/null +++ b/src/components/ui/input/dateinput.stories.tsx @@ -0,0 +1,21 @@ +import { Meta, StoryObj } from '@storybook/nextjs'; +import DateInput from './DateInput'; +import Input from './input'; + +const meta: Meta = { + title: 'Form/Input', + component: DateInput, + tags: ['autodocs'], +}; +export default meta; + +type Story = StoryObj; + +// 날짜 입력 인풋 // +export const Date: Story = { + render: () => ( +
+ +
+ ), +}; diff --git a/src/components/ui/input/index.ts b/src/components/ui/input/index.ts index 3f24eca..2f5eae9 100644 --- a/src/components/ui/input/index.ts +++ b/src/components/ui/input/index.ts @@ -1 +1,3 @@ -export { default as Input } from './input'; +export { default as DateInput } from '@/components/ui/input/DateInput'; +export { default as Input } from '@/components/ui/input/input'; +export { default as TimeInput } from '@/components/ui/input/TimeInput'; diff --git a/src/components/ui/input/input.tsx b/src/components/ui/input/input.tsx index c7cb212..c50667d 100644 --- a/src/components/ui/input/input.tsx +++ b/src/components/ui/input/input.tsx @@ -2,11 +2,11 @@ import { cn } from '@/lib/utils/cn'; import { InputHTMLAttributes, ReactNode } from 'react'; type Props = { - label?: string; // 라벨 텍스트 - requiredMark?: boolean; // 라벨 옆 * 표시 - error?: string; // 에러 문구(있으면 빨간 테두리/문구) - suffix?: ReactNode; // 우측 단위/아이콘(예: '원') - className?: string; // 외부 커스텀 클래스 + label?: string; // 라벨 텍스트 + requiredMark?: boolean; // 라벨 옆 * 표시 + error?: string; // 에러 문구(있으면 빨간 테두리/문구) + suffix?: ReactNode; // 우측 단위/아이콘(예: '원') + className?: string; // 외부 커스텀 클래스 } & InputHTMLAttributes; export default function Input({ @@ -30,18 +30,18 @@ export default function Input({ )} {/* Field */} -
+
= { + title: 'Form/Input', + component: TimeInput, + tags: ['autodocs'], +}; +export default meta; + +type Story = StoryObj; + +// 시간 입력 인풋 // +export const Time: Story = { + render: () => ( +
+ +
+ ), +}; diff --git a/src/components/ui/modal/notification/NotificationMessage.tsx b/src/components/ui/modal/notification/NotificationMessage.tsx index 6c84d94..a2283a8 100644 --- a/src/components/ui/modal/notification/NotificationMessage.tsx +++ b/src/components/ui/modal/notification/NotificationMessage.tsx @@ -1,4 +1,4 @@ -import { getTime } from '@/lib/utils/getTime'; +import { getTime } from '@/lib/utils/dateFormatter'; import { timeAgo } from '@/lib/utils/timeAgo'; import { clsx } from 'clsx'; import { Alert } from './Notification'; diff --git a/src/components/ui/post/post.tsx b/src/components/ui/post/post.tsx index a54366c..a32ac45 100644 --- a/src/components/ui/post/post.tsx +++ b/src/components/ui/post/post.tsx @@ -1,8 +1,8 @@ import { Icon } from '@/components/ui/icon'; import { calcPayIncreasePercent } from '@/lib/utils/calcPayIncrease'; import { cn } from '@/lib/utils/cn'; +import { getTime } from '@/lib/utils/dateFormatter'; import { formatNumber } from '@/lib/utils/formatNumber'; -import { getTime } from '@/lib/utils/getTime'; import type { PostCard } from '@/types/notice'; import Image from 'next/image'; import Link from 'next/link'; diff --git a/src/components/ui/table/TableRow.tsx b/src/components/ui/table/TableRow.tsx index 07af9c8..3665c13 100644 --- a/src/components/ui/table/TableRow.tsx +++ b/src/components/ui/table/TableRow.tsx @@ -1,5 +1,5 @@ import { TableRowProps } from '@/components/ui/table/TableRowProps'; -import { getTime } from '@/lib/utils/getTime'; +import { getTime } from '@/lib/utils/dateFormatter'; interface TableTypeVariant { rowData: TableRowProps; diff --git a/src/lib/utils/getTime.ts b/src/lib/utils/dateFormatter.ts similarity index 56% rename from src/lib/utils/getTime.ts rename to src/lib/utils/dateFormatter.ts index b8abf35..707700c 100644 --- a/src/lib/utils/getTime.ts +++ b/src/lib/utils/dateFormatter.ts @@ -1,4 +1,4 @@ -function formatDate(date: Date): string { +export function formatDate(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); @@ -6,13 +6,17 @@ function formatDate(date: Date): string { return `${year}.${month}.${day}`; } -function formatTime(date: Date): string { +export function formatTime(date: Date): string { const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${hours}:${minutes}`; } +export function formatDateTime(date: Date | null): string { + return date ? `${formatDate(date)} ${formatTime(date)}` : ''; +} + export function getTime(startsAt: string, workhour: number) { const startDate = new Date(startsAt); const endDate = new Date(startsAt); @@ -26,3 +30,14 @@ export function getTime(startsAt: string, workhour: number) { duration: `${workhour}시간`, }; } + +export function formatWithDots(numbers: string) { + const year = numbers.slice(0, 4); + const month = numbers.slice(4, 6); + const day = numbers.slice(6, 8); + + if (month && day) return `${year}.${month}.${day}`; + if (month) return `${year}.${month}`; + if (year) return `${year}`; + return numbers; +} diff --git a/src/lib/utils/fillCalendarDays.ts b/src/lib/utils/fillCalendarDays.ts new file mode 100644 index 0000000..ac88e7f --- /dev/null +++ b/src/lib/utils/fillCalendarDays.ts @@ -0,0 +1,36 @@ +import { CalendarDay } from '@/types/calendar'; + +export const fillCalendarDays = (year: number, month: number) => { + const days: CalendarDay[] = []; + + const firstDayOfMonth = new Date(year, month, 1); + const fisrtDay = firstDayOfMonth.getDay(); // 첫 날 요일 + const prevLastDate = new Date(year, month, 0); // 마지막 날짜 (month: 0 -> 12/31, 1 -> 1/31...) + const prevMonthLast = prevLastDate.getDate(); // 마지막달 일자 + + // 이전 달 날짜 채우기(일요일 시작 달력 기준) + // ex) 만약 firstDay = 2(화요일) / prevMonthLast = 30 + // => 지난 달 날짜 2번(일, 월) + // 30 - 2 + 1 + i -> 29, 30 채워 넣음 + for (let i = 0; i < fisrtDay; i++) { + days.push({ + date: new Date(year, month - 1, prevMonthLast - fisrtDay + 1 + i), + isCurrentMonth: false, + }); + } + + // 이번 달 날짜 채우기 + const lastDate = new Date(year, month + 1, 0).getDate(); + for (let i = 1; i <= lastDate; i++) { + days.push({ date: new Date(year, month, i), isCurrentMonth: true }); + } + + // 다음 달 날짜 채우기 + const totalCells = Math.ceil(days.length / 7) * 7; + const nextMonthDayCount = totalCells - days.length; + for (let i = 1; i <= nextMonthDayCount; i++) { + days.push({ date: new Date(year, month + 1, i), isCurrentMonth: false }); + } + + return days; +}; diff --git a/src/types/calendar.ts b/src/types/calendar.ts new file mode 100644 index 0000000..b103ee7 --- /dev/null +++ b/src/types/calendar.ts @@ -0,0 +1,42 @@ +// Calendar 관련 + +export type SelectMode = 'day' | 'month' | 'year'; + +export interface CalendarProps { + value?: Date; + onSelect?: (date: Date) => void; +} + +export interface BaseCalendarProps { + currentMonth: Date; +} + +export interface CalendarHeaderProps extends BaseCalendarProps { + selectMode: SelectMode; + onToggleMode: () => void; + onChange: (offset: number) => void; +} + +export interface CalendarViewProps { + onSelect: (value: T) => void; +} + +export type DayViewProps = CalendarViewProps & { currentMonth: Date; currentDay: Date }; +export type MonthViewProps = CalendarViewProps; +export type YearViewProps = CalendarViewProps & { currentMonth: Date }; + +export type CalendarDay = { + date: Date; + isCurrentMonth: boolean; +}; + +// Time Selector 관련 +export type Period = '오전' | '오후'; + +export interface TimeSelectorProps { + value?: string; + period: Period; + hours: string; + minutes: string; + onSelect?: (value: string) => void; +}