import { useState, useRef, useEffect, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react'; import { getTodayKST, dayjs } from '@/utils'; import { MIN_YEAR, WEEKDAYS, MONTH_NAMES } from '@/constants'; const MONTHS = MONTH_NAMES; /** * 달력 컴포넌트 * @param {Date} currentDate - 현재 표시 중인 년/월 * @param {function} onDateChange - 년/월 변경 핸들러 * @param {string} selectedDate - 선택된 날짜 (YYYY-MM-DD) * @param {function} onSelectDate - 날짜 선택 핸들러 * @param {Array} schedules - 일정 목록 (점 표시용) * @param {function} getCategoryColor - 카테고리 색상 가져오기 * @param {boolean} disabled - 비활성화 여부 */ function Calendar({ currentDate, onDateChange, selectedDate, onSelectDate, schedules = [], getCategoryColor, disabled = false, }) { const [showYearMonthPicker, setShowYearMonthPicker] = useState(false); const [slideDirection, setSlideDirection] = useState(0); const [yearRangeStart, setYearRangeStart] = useState(MIN_YEAR); const pickerRef = useRef(null); const year = currentDate.getFullYear(); const month = currentDate.getMonth(); // 외부 클릭 시 팝업 닫기 useEffect(() => { const handleClickOutside = (event) => { if (pickerRef.current && !pickerRef.current.contains(event.target)) { setShowYearMonthPicker(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); // 달력 계산 const getDaysInMonth = (y, m) => new Date(y, m + 1, 0).getDate(); const getFirstDayOfMonth = (y, m) => new Date(y, m, 1).getDay(); const daysInMonth = getDaysInMonth(year, month); const firstDay = getFirstDayOfMonth(year, month); // 일정 날짜별 맵 (O(1) 조회용) const scheduleDateMap = useMemo(() => { const map = new Map(); schedules.forEach((s) => { const dateStr = s.date ? s.date.split('T')[0] : ''; if (!map.has(dateStr)) { map.set(dateStr, []); } map.get(dateStr).push(s); }); return map; }, [schedules]); // 2017년 1월 이전으로 이동 불가 const canGoPrevMonth = !(year === MIN_YEAR && month === 0); const prevMonth = () => { if (!canGoPrevMonth) return; setSlideDirection(-1); const newDate = new Date(year, month - 1, 1); onDateChange(newDate); // 이번달이면 오늘, 다른 달이면 1일 선택 const today = new Date(); if (newDate.getFullYear() === today.getFullYear() && newDate.getMonth() === today.getMonth()) { onSelectDate(getTodayKST()); } else { onSelectDate(`${newDate.getFullYear()}-${String(newDate.getMonth() + 1).padStart(2, '0')}-01`); } }; const nextMonth = () => { setSlideDirection(1); const newDate = new Date(year, month + 1, 1); onDateChange(newDate); const today = new Date(); if (newDate.getFullYear() === today.getFullYear() && newDate.getMonth() === today.getMonth()) { onSelectDate(getTodayKST()); } else { onSelectDate(`${newDate.getFullYear()}-${String(newDate.getMonth() + 1).padStart(2, '0')}-01`); } }; const selectYear = (newYear) => { onDateChange(new Date(newYear, month, 1)); }; const selectMonth = (newMonth) => { const newDate = new Date(year, newMonth, 1); onDateChange(newDate); const today = new Date(); if (newDate.getFullYear() === today.getFullYear() && newDate.getMonth() === today.getMonth()) { onSelectDate(getTodayKST()); } else { onSelectDate(`${year}-${String(newMonth + 1).padStart(2, '0')}-01`); } setShowYearMonthPicker(false); }; const selectDate = (day) => { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; onSelectDate(dateStr); }; // 연도 범위 const yearRange = Array.from({ length: 12 }, (_, i) => yearRangeStart + i); const canGoPrevYearRange = yearRangeStart > MIN_YEAR; const prevYearRange = () => canGoPrevYearRange && setYearRangeStart((prev) => Math.max(MIN_YEAR, prev - 12)); const nextYearRange = () => setYearRangeStart((prev) => prev + 12); const currentYear = new Date().getFullYear(); const isCurrentYear = (y) => y === currentYear; const isCurrentMonth = (m) => { const now = new Date(); return year === now.getFullYear() && m === now.getMonth(); }; return (
{/* 헤더 */}
{/* 년/월 선택 팝업 */} {showYearMonthPicker && (
{yearRange[0]} - {yearRange[yearRange.length - 1]}
{/* 년도 선택 */}
년도
{yearRange.map((y) => ( ))}
{/* 월 선택 */}
{MONTHS.map((m, i) => ( ))}
)}
{/* 요일 헤더 + 날짜 그리드 */}
{WEEKDAYS.map((day, i) => (
{day}
))}
{/* 전달 날짜 */} {Array.from({ length: firstDay }).map((_, i) => { const prevMonthDays = getDaysInMonth(year, month - 1); const day = prevMonthDays - firstDay + i + 1; return (
{day}
); })} {/* 현재 달 날짜 */} {Array.from({ length: daysInMonth }).map((_, i) => { const day = i + 1; const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const isSelected = selectedDate === dateStr; const dayOfWeek = (firstDay + i) % 7; const isToday = new Date().toDateString() === new Date(year, month, day).toDateString(); const daySchedules = (scheduleDateMap.get(dateStr) || []).slice(0, 3); return ( ); })} {/* 다음달 날짜 */} {(() => { const totalCells = firstDay + daysInMonth; const remainder = totalCells % 7; const nextDays = remainder === 0 ? 0 : 7 - remainder; return Array.from({ length: nextDays }).map((_, i) => (
{i + 1}
)); })()}
{/* 범례 */}
일정 있음
); } export default Calendar;