fromis_9/frontend-temp/src/components/pc/admin/DatePicker.jsx

283 lines
11 KiB
React
Raw Normal View History

/**
* DatePicker 컴포넌트
* // 선택이 가능한 드롭다운 형태의 날짜 선택기
*/
import { useState, useEffect, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Calendar, ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react';
function DatePicker({ value, onChange, placeholder = '날짜 선택', showDayOfWeek = false }) {
const [isOpen, setIsOpen] = useState(false);
const [viewMode, setViewMode] = useState('days');
const [viewDate, setViewDate] = useState(() => {
if (value) return new Date(value);
return new Date();
});
const ref = useRef(null);
useEffect(() => {
const handleClickOutside = (e) => {
if (ref.current && !ref.current.contains(e.target)) {
setIsOpen(false);
setViewMode('days');
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const year = viewDate.getFullYear();
const month = viewDate.getMonth();
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const days = [];
for (let i = 0; i < firstDay; i++) {
days.push(null);
}
for (let i = 1; i <= daysInMonth; i++) {
days.push(i);
}
const MIN_YEAR = 2025;
const startYear = Math.max(MIN_YEAR, Math.floor(year / 12) * 12 - 1);
const years = Array.from({ length: 12 }, (_, i) => startYear + i);
const canGoPrevYearRange = startYear > MIN_YEAR;
const prevMonth = () => setViewDate(new Date(year, month - 1, 1));
const nextMonth = () => setViewDate(new Date(year, month + 1, 1));
const prevYearRange = () =>
canGoPrevYearRange && setViewDate(new Date(Math.max(MIN_YEAR, year - 12), month, 1));
const nextYearRange = () => setViewDate(new Date(year + 12, month, 1));
const selectDate = (day) => {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
onChange(dateStr);
setIsOpen(false);
setViewMode('days');
};
const selectYear = (y) => {
setViewDate(new Date(y, month, 1));
};
const selectMonth = (m) => {
setViewDate(new Date(year, m, 1));
setViewMode('days');
};
// 날짜 표시 포맷 (요일 포함 옵션)
const formatDisplayDate = (dateStr) => {
if (!dateStr) return '';
const [y, m, d] = dateStr.split('-');
if (showDayOfWeek) {
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
const date = new Date(parseInt(y), parseInt(m) - 1, parseInt(d));
const dayOfWeek = dayNames[date.getDay()];
return `${y}${parseInt(m)}${parseInt(d)}일 (${dayOfWeek})`;
}
return `${y}${parseInt(m)}${parseInt(d)}`;
};
const isSelected = (day) => {
if (!value || !day) return false;
const [y, m, d] = value.split('-');
return parseInt(y) === year && parseInt(m) === month + 1 && parseInt(d) === day;
};
const isToday = (day) => {
if (!day) return false;
const today = new Date();
return today.getFullYear() === year && today.getMonth() === month && today.getDate() === day;
};
const isCurrentYear = (y) => new Date().getFullYear() === y;
const isCurrentMonth = (m) => {
const today = new Date();
return today.getFullYear() === year && today.getMonth() === m;
};
const monthNames = [
'1월',
'2월',
'3월',
'4월',
'5월',
'6월',
'7월',
'8월',
'9월',
'10월',
'11월',
'12월',
];
return (
<div ref={ref} className="relative">
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="w-full px-4 py-3 border border-gray-200 rounded-xl bg-white flex items-center justify-between hover:border-gray-300 transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
>
<span className={value ? 'text-gray-900' : 'text-gray-400'}>
{value ? formatDisplayDate(value) : placeholder}
</span>
<Calendar size={18} className="text-gray-400" />
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.15 }}
className="absolute z-50 mt-2 bg-white border border-gray-200 rounded-xl shadow-lg p-4 w-80"
>
<div className="flex items-center justify-between mb-4">
<button
type="button"
onClick={viewMode === 'years' ? prevYearRange : prevMonth}
disabled={viewMode === 'years' && !canGoPrevYearRange}
className={`p-1.5 rounded-lg transition-colors ${viewMode === 'years' && !canGoPrevYearRange ? 'opacity-30' : 'hover:bg-gray-100'}`}
>
<ChevronLeft size={20} className="text-gray-600" />
</button>
<button
type="button"
onClick={() => setViewMode(viewMode === 'days' ? 'years' : 'days')}
className="font-medium text-gray-900 hover:text-primary transition-colors flex items-center gap-1"
>
{viewMode === 'years'
? `${years[0]} - ${years[years.length - 1]}`
: `${year}${month + 1}`}
<ChevronDown
size={16}
className={`transition-transform ${viewMode !== 'days' ? 'rotate-180' : ''}`}
/>
</button>
<button
type="button"
onClick={viewMode === 'years' ? nextYearRange : nextMonth}
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
>
<ChevronRight size={20} className="text-gray-600" />
</button>
</div>
<AnimatePresence mode="wait">
{viewMode === 'years' && (
<motion.div
key="years"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<div className="text-center text-sm text-gray-500 mb-3">년도</div>
<div className="grid grid-cols-4 gap-2 mb-4">
{years.map((y) => (
<button
key={y}
type="button"
onClick={() => selectYear(y)}
className={`py-2 rounded-lg text-sm transition-colors ${year === y ? 'bg-primary text-white' : 'hover:bg-gray-100 text-gray-700'} ${isCurrentYear(y) && year !== y ? 'text-primary font-medium' : ''}`}
>
{y}
</button>
))}
</div>
<div className="text-center text-sm text-gray-500 mb-3"></div>
<div className="grid grid-cols-4 gap-2">
{monthNames.map((m, i) => (
<button
key={m}
type="button"
onClick={() => selectMonth(i)}
className={`py-2 rounded-lg text-sm transition-colors ${month === i ? 'bg-primary text-white' : 'hover:bg-gray-100 text-gray-700'} ${isCurrentMonth(i) && month !== i ? 'text-primary font-medium' : ''}`}
>
{m}
</button>
))}
</div>
</motion.div>
)}
{viewMode === 'months' && (
<motion.div
key="months"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<div className="text-center text-sm text-gray-500 mb-3"> 선택</div>
<div className="grid grid-cols-4 gap-2">
{monthNames.map((m, i) => (
<button
key={m}
type="button"
onClick={() => selectMonth(i)}
className={`py-2.5 rounded-lg text-sm transition-colors ${month === i ? 'bg-primary text-white' : 'hover:bg-gray-100 text-gray-700'} ${isCurrentMonth(i) && month !== i ? 'text-primary font-medium' : ''}`}
>
{m}
</button>
))}
</div>
</motion.div>
)}
{viewMode === 'days' && (
<motion.div
key="days"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<div className="grid grid-cols-7 gap-1 mb-2">
{['일', '월', '화', '수', '목', '금', '토'].map((d, i) => (
<div
key={d}
className={`text-center text-xs font-medium py-1 ${i === 0 ? 'text-red-400' : i === 6 ? 'text-blue-400' : 'text-gray-400'}`}
>
{d}
</div>
))}
</div>
<div className="grid grid-cols-7 gap-1">
{days.map((day, i) => {
const dayOfWeek = i % 7;
return (
<button
key={i}
type="button"
disabled={!day}
onClick={() => day && selectDate(day)}
className={`aspect-square rounded-full text-sm font-medium flex items-center justify-center transition-all
${!day ? '' : 'hover:bg-gray-100'}
${isSelected(day) ? 'bg-primary text-white hover:bg-primary' : ''}
${isToday(day) && !isSelected(day) ? 'text-primary font-bold' : ''}
${day && !isSelected(day) && !isToday(day) && dayOfWeek === 0 ? 'text-red-500' : ''}
${day && !isSelected(day) && !isToday(day) && dayOfWeek === 6 ? 'text-blue-500' : ''}
${day && !isSelected(day) && !isToday(day) && dayOfWeek > 0 && dayOfWeek < 6 ? 'text-gray-700' : ''}
`}
>
{day}
</button>
);
})}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
export default DatePicker;