diff --git a/src/components/ui/calendar/Calendar.tsx b/src/components/ui/calendar/Calendar.tsx index bba199c..6044ae9 100644 --- a/src/components/ui/calendar/Calendar.tsx +++ b/src/components/ui/calendar/Calendar.tsx @@ -54,7 +54,7 @@ export default function Calendar({ value, onSelect }: CalendarProps) { }; return ( -
+
)} -
- -
+
); } diff --git a/src/components/ui/calendar/CalendarHeader.tsx b/src/components/ui/calendar/CalendarHeader.tsx index 34ac05c..782234e 100644 --- a/src/components/ui/calendar/CalendarHeader.tsx +++ b/src/components/ui/calendar/CalendarHeader.tsx @@ -31,12 +31,12 @@ export default function CalendarHeader({ }, [selectMode, currentMonth]); return ( -
+
- diff --git a/src/components/ui/calendar/DayViewMode.tsx b/src/components/ui/calendar/DayViewMode.tsx index 760558a..0abdbc8 100644 --- a/src/components/ui/calendar/DayViewMode.tsx +++ b/src/components/ui/calendar/DayViewMode.tsx @@ -14,7 +14,7 @@ export default function DayViewMode({ currentMonth, currentDay, onSelect }: DayV return ( <> -
+
{WEEKDAYS.map((day, i) => { const headerClass = i === 0 ? 'text-red-400' : i === 6 ? 'text-blue-200' : ''; @@ -36,9 +36,9 @@ export default function DayViewMode({ currentMonth, currentDay, onSelect }: DayV const DAY_CELL_CLASS = isDisabled ? 'text-gray-500' - : isCurrentMonth && dayOfWeek === 0 + : !isSelected && isCurrentMonth && dayOfWeek === 0 ? 'text-red-400' - : isCurrentMonth && dayOfWeek === 6 + : !isSelected && isCurrentMonth && dayOfWeek === 6 ? 'text-blue-200' : ''; @@ -48,7 +48,7 @@ export default function DayViewMode({ currentMonth, currentDay, onSelect }: DayV onClick={() => !isDisabled && onSelect(date)} disabled={isDisabled} className={cn( - 'rounded-lg py-1.5 transition', + 'h-[3rem] w-[3rem] rounded-lg py-1.5 transition', isSelected ? 'bg-blue-200 font-semibold text-white' : !isDisabled diff --git a/src/components/ui/calendar/MonthViewMode.tsx b/src/components/ui/calendar/MonthViewMode.tsx index 23a3393..d80109c 100644 --- a/src/components/ui/calendar/MonthViewMode.tsx +++ b/src/components/ui/calendar/MonthViewMode.tsx @@ -2,12 +2,12 @@ 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 index f0ed0df..7ccb151 100644 --- a/src/components/ui/calendar/TimeSelector.styles.tsx +++ b/src/components/ui/calendar/TimeSelector.styles.tsx @@ -1,7 +1,7 @@ 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'; + 'flex max-h-[88px] flex-col gap-4 overflow-y-auto rounded border p-2 text-xl'; 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 index 2f8b218..0e59f37 100644 --- a/src/components/ui/calendar/TimeSelector.tsx +++ b/src/components/ui/calendar/TimeSelector.tsx @@ -34,7 +34,7 @@ export default function TimeSelector({ onSelect, period, hours, minutes }: TimeS }; const TIME_SELECTOR_WRAPPER_CLASS = - 'mt-3 flex w-80 items-center justify-center gap-6 rounded-lg border bg-white p-4'; + 'mt-3 flex items-center justify-center gap-6 rounded-lg border bg-white p-3'; const BASE_PERIOD_CLASS = 'rounded-lg px-4 py-2 font-semibold transition'; const BASE_TIME_CLASS = 'rounded px-3 py-1 transition'; @@ -50,7 +50,7 @@ export default function TimeSelector({ onSelect, period, hours, minutes }: TimeS return (
-
+
{['오전', '오후'].map(p => ( diff --git a/src/components/ui/input/DateInput.tsx b/src/components/ui/input/DateInput.tsx index 4c1ddd3..9721b6d 100644 --- a/src/components/ui/input/DateInput.tsx +++ b/src/components/ui/input/DateInput.tsx @@ -2,25 +2,48 @@ 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 { DateInputProps } from '@/types/calendar'; +import { useCallback, useEffect, useRef, useState } from 'react'; import Input from './input'; -export default function DateInput() { - const { value: open, toggle, setClose } = useToggle(false); +export default function DateInput({ + id = 'date', + label = '날짜 선택', + className, + value, + onChange, + requiredMark = false, + error, +}: DateInputProps) { + const { isOpen, toggle, setClose } = useToggle(false); + const wrapperRef = useRef(null); const [selectedDate, setSelectedDate] = useState(null); const [inputValue, setInputValue] = useState(''); // typing 사용 - - const wrapperRef = useRef(null); + const [dateError, setDateError] = useState(''); useClickOutside(wrapperRef, () => { - if (open) setClose(); + if (isOpen) setClose(); }); + useEffect(() => { + if (value) { + setSelectedDate(value); + setInputValue(formatDate(value)); + } else { + setSelectedDate(null); + setInputValue(''); + } + }, [value]); + // 날짜 업데이트 중앙 관리 - const updateDate = useCallback((date: Date) => { - setSelectedDate(date); - setInputValue(formatDate(date)); - }, []); + const updateDate = useCallback( + (date: Date) => { + setSelectedDate(date); + setInputValue(formatDate(date)); + onChange?.(date); + }, + [onChange] + ); // 날짜 선택 const handleDateSelect = useCallback( @@ -31,69 +54,77 @@ export default function DateInput() { ); // typing + const maxDaysList = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + const isLeapYear = (year: number) => { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + }; + const getMaxDay = (year: number, month: number) => { + if (month === 2 && isLeapYear(year)) return 29; + return maxDaysList[month - 1]; + }; + + const validateDate = (year: number, month: number, day: number) => { + const maxDay = getMaxDay(year, month); + + if (month < 1 || month > 12 || day < 1 || day > maxDay) { + return { valid: false, error: '올바른 날짜를 입력하세요.' }; + } + + const inputDate = new Date(year, month - 1, day); + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (inputDate < today) { + return { valid: false, error: '이전 날짜는 선택할 수 없습니다.' }; + } + + return { valid: true, date: inputDate }; + }; + 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)); + setDateError(''); + setInputValue(formatWithDots(newTypedNumbers)); 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 year = parseInt(newTypedNumbers.slice(0, 4)); + const month = parseInt(newTypedNumbers.slice(4, 6)); + const day = parseInt(newTypedNumbers.slice(6, 8)); - 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]; + const { valid, error, date } = validateDate(year, month, day); - 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))); + if (!valid) { + setDateError(error ?? ''); return; } - } - - if (typedLength >= 6) { - if (month > 0 && month < 13) { - setInputValue(formatWithDots(newTypedNumbers)); - } else { - setInputValue(formatWithDots(newTypedNumbers.slice(0, 5))); + if (date) { + setDateError(''); + updateDate(date); } - return; } - - setInputValue(formatWithDots(newTypedNumbers)); }; return ( -
+
- {open && ( -
+ {isOpen && ( +
)} diff --git a/src/components/ui/input/TimeInput.tsx b/src/components/ui/input/TimeInput.tsx index 57ff760..aa637b8 100644 --- a/src/components/ui/input/TimeInput.tsx +++ b/src/components/ui/input/TimeInput.tsx @@ -85,7 +85,7 @@ export default function TimeInput() { const minutes = selectedTime ? String(selectedTime.getMinutes()).padStart(2, '0') : '00'; return ( -
+
{open && ( -
+
void; setOpen: () => void; setClose: () => void; } const useToggle = (init = false): UseToggle => { - const [value, setValue] = useState(init); - const toggle = useCallback(() => setValue(prev => !prev), []); - const setOpen = useCallback(() => setValue(true), []); - const setClose = useCallback(() => setValue(false), []); - return { value, toggle, setOpen, setClose }; + const [isOpen, setIsOpen] = useState(init); + const toggle = useCallback(() => setIsOpen(prev => !prev), []); + const setOpen = useCallback(() => setIsOpen(true), []); + const setClose = useCallback(() => setIsOpen(false), []); + return { isOpen, toggle, setOpen, setClose }; }; export default useToggle; diff --git a/src/types/calendar.ts b/src/types/calendar.ts index b103ee7..e009fc0 100644 --- a/src/types/calendar.ts +++ b/src/types/calendar.ts @@ -1,4 +1,13 @@ // Calendar 관련 +export type DateInputProps = { + id?: string; + label?: string; + className?: string; + value?: Date | null; + onChange?: (date: Date | string) => void; + requiredMark?: boolean; + error?: string; +}; export type SelectMode = 'day' | 'month' | 'year';