Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions src/components/ui/calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default function Calendar({ value, onSelect }: CalendarProps) {
};

return (
<div className='mt-3 w-80 rounded-xl border bg-white p-4'>
<div className='relative mt-3 aspect-square rounded-xl border bg-white p-4'>
<CalendarHeader
selectMode={selectMode}
currentMonth={currentMonth}
Expand Down Expand Up @@ -85,11 +85,12 @@ export default function Calendar({ value, onSelect }: CalendarProps) {
/>
)}

<div className='mt-3 text-right'>
<button onClick={handleToday} className='text-sm text-blue-200 hover:underline'>
์˜ค๋Š˜๋กœ ์ด๋™
</button>
</div>
<button
onClick={handleToday}
className='absolute bottom-5 right-5 text-sm text-blue-200 hover:underline'
>
์˜ค๋Š˜๋กœ ์ด๋™
</button>
</div>
);
}
4 changes: 2 additions & 2 deletions src/components/ui/calendar/CalendarHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ export default function CalendarHeader({
}, [selectMode, currentMonth]);

return (
<div className='mb-3 flex items-center justify-between'>
<div className='mb-5 mt-3 flex items-center justify-between'>
<button onClick={() => onChange(-1)} className={CALENDAR_ARROW_CLASS}>
<Icon iconName='chevronLeft' iconSize='md' ariaLabel='์ด์ „์œผ๋กœ ์ด๋™' />
</button>

<button onClick={onToggleMode} className='text-lg font-semibold hover:underline'>
<button onClick={onToggleMode} className='text-xl font-semibold hover:underline'>
{headerLabel}
</button>

Expand Down
8 changes: 4 additions & 4 deletions src/components/ui/calendar/DayViewMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function DayViewMode({ currentMonth, currentDay, onSelect }: DayV

return (
<>
<div className={`${DAY_CALENDAR_CLASS} mb-3 font-medium text-gray-500`}>
<div className={`${DAY_CALENDAR_CLASS} mb-3 pt-2 font-medium text-gray-500`}>
{WEEKDAYS.map((day, i) => {
const headerClass = i === 0 ? 'text-red-400' : i === 6 ? 'text-blue-200' : '';

Expand All @@ -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'
: '';

Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/calendar/MonthViewMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { MonthViewProps } from '@/types/calendar';

export default function MonthViewMode({ onSelect: onSelectMonth }: MonthViewProps) {
return (
<div className='grid grid-cols-3 gap-2 text-center'>
<div className='grid grid-cols-4 gap-2 text-center'>
{Array.from({ length: 12 }).map((_, i) => (
<button
key={i}
onClick={() => onSelectMonth(i)}
className='rounded-lg py-2 hover:bg-blue-100'
className='aspect-square rounded-lg py-2 hover:bg-blue-100'
>
{i + 1}์›”
</button>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/calendar/TimeSelector.styles.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<div className={TIME_SCROLL_CLASS}>{children}</div>
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/calendar/TimeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -50,7 +50,7 @@ export default function TimeSelector({ onSelect, period, hours, minutes }: TimeS

return (
<div className={TIME_SELECTOR_WRAPPER_CLASS}>
<div className='flex flex-col gap-4 p-2'>
<div className='flex flex-col gap-3 p-2'>
{['์˜ค์ „', '์˜คํ›„'].map(p => (
<button
key={p}
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/calendar/YearViewMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ export default function YearViewMode({ currentMonth, onSelect }: YearViewProps)
const START_YEAR = Math.floor(currentMonth.getFullYear() / 10) * 10;

return (
<div className='grid grid-cols-3 gap-2 text-center'>
<div className='grid grid-cols-2 gap-4 text-center'>
{Array.from({ length: 10 }).map((_, i) => {
const YEAR = START_YEAR + i;
return (
<button
key={YEAR}
onClick={() => onSelect(YEAR)}
className='rounded-lg py-2 hover:bg-blue-100'
className='h-[3rem] rounded-lg py-2 hover:bg-blue-100'
>
{YEAR}
</button>
Expand Down
129 changes: 80 additions & 49 deletions src/components/ui/input/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>(null);
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [inputValue, setInputValue] = useState(''); // typing ์‚ฌ์šฉ

const wrapperRef = useRef<HTMLDivElement>(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(
Expand All @@ -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<HTMLInputElement>) => {
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 (
<div ref={wrapperRef} className='relative w-full'>
<div ref={wrapperRef} className='relative max-w-md'>
<Input
id='date'
label='๋‚ ์งœ ์„ ํƒ'
id={id}
label={label}
placeholder={`${formatDate(new Date())}`}
value={inputValue}
onClick={toggle}
onChange={handleDateInputChange}
requiredMark={requiredMark}
className={className}
error={dateError || error}
autoComplete='off'
/>

{open && (
<div className='absolute'>
{isOpen && (
<div className='z-1 absolute w-full'>
<Calendar onSelect={handleDateSelect} value={selectedDate ?? new Date()} />
</div>
)}
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/input/TimeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default function TimeInput() {
const minutes = selectedTime ? String(selectedTime.getMinutes()).padStart(2, '0') : '00';

return (
<div ref={wrapperRef} className='relative w-full'>
<div ref={wrapperRef} className='relative max-w-md'>
<Input
value={inputValue ? `${period} ${inputValue}` : ''}
label='์‹œ๊ฐ„ ์„ ํƒ'
Expand All @@ -95,7 +95,7 @@ export default function TimeInput() {
/>

{open && (
<div className='absolute'>
<div className='z-1 absolute'>
<TimeSelector
onSelect={handleTimeSelect}
period={period}
Expand Down
12 changes: 6 additions & 6 deletions src/hooks/useToggle.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { useCallback, useState } from 'react';

interface UseToggle {
value: boolean;
isOpen: boolean;
toggle: () => 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;
9 changes: 9 additions & 0 deletions src/types/calendar.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down