'use client'; import { useState, useCallback, useEffect } from 'react'; import { cn } from '@/components/ui/utils'; import { CalendarHeader } from './CalendarHeader'; import { DayTimeView } from './DayTimeView'; import { MonthView } from './MonthView'; import { WeekView } from './WeekView'; import { WeekTimeView } from './WeekTimeView'; import type { ScheduleCalendarProps, CalendarView } from './types'; import { getNextMonth, getPrevMonth, getNextDay, getPrevDay, getNextWeek, getPrevWeek } from './utils'; import { useCalendarScheduleStore } from '@/stores/useCalendarScheduleStore'; /** * 스케줄 달력 공통 컴포넌트 * * 주/월 뷰 전환, 일정 바 표시, 날짜별 뱃지 등을 지원하는 재사용 가능한 달력 * * @example * ```tsx * console.log('날짜 클릭:', date)} * onEventClick={(event) => console.log('이벤트 클릭:', event)} * filterSlot={} * /> * ``` */ export function ScheduleCalendar({ events = [], badges = [], currentDate: controlledDate, view: controlledView, selectedDate: controlledSelectedDate, onDateClick, onEventClick, onMonthChange, onViewChange, titleSlot, filterSlot, maxEventsPerDay = 5, weekStartsOn = 0, isLoading = false, className, availableViews, timeRange, hideNavigation, }: ScheduleCalendarProps) { // 내부 상태 (controlled/uncontrolled 지원) const [internalDate, setInternalDate] = useState(() => new Date()); const [internalView, setInternalView] = useState('month'); const [internalSelectedDate, setInternalSelectedDate] = useState(null); // 현재 사용할 값 결정 const currentDate = controlledDate ?? internalDate; const view = controlledView ?? internalView; const selectedDate = controlledSelectedDate !== undefined ? controlledSelectedDate : internalSelectedDate; // Hydration 불일치 방지 const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); // 현재 표시 중인 연도의 공휴일/일정 데이터를 스토어에 로드 useEffect(() => { const year = currentDate.getFullYear(); useCalendarScheduleStore.getState().fetchSchedules(year); }, [currentDate]); // 이전 (뷰에 따라 일/주/월 단위) const handlePrevMonth = useCallback(() => { let newDate: Date; if (view === 'day-time') { newDate = getPrevDay(currentDate); } else if (view === 'week-time' || view === 'week') { newDate = getPrevWeek(currentDate); } else { newDate = getPrevMonth(currentDate); } if (controlledDate === undefined) { setInternalDate(newDate); } onMonthChange?.(newDate); }, [currentDate, view, controlledDate, onMonthChange]); // 다음 (뷰에 따라 일/주/월 단위) const handleNextMonth = useCallback(() => { let newDate: Date; if (view === 'day-time') { newDate = getNextDay(currentDate); } else if (view === 'week-time' || view === 'week') { newDate = getNextWeek(currentDate); } else { newDate = getNextMonth(currentDate); } if (controlledDate === undefined) { setInternalDate(newDate); } onMonthChange?.(newDate); }, [currentDate, view, controlledDate, onMonthChange]); // 뷰 변경 const handleViewChange = useCallback((newView: CalendarView) => { if (controlledView === undefined) { setInternalView(newView); } onViewChange?.(newView); }, [controlledView, onViewChange]); // 날짜 클릭 const handleDateClick = useCallback((date: Date) => { if (controlledSelectedDate === undefined) { setInternalSelectedDate(date); } onDateClick?.(date); }, [controlledSelectedDate, onDateClick]); // 이벤트 클릭 const handleEventClick = useCallback((event: import('./types').ScheduleEvent) => { onEventClick?.(event); }, [onEventClick]); // SSR에서는 빈 컨테이너 렌더링 if (!mounted) { return (
); } return (
{/* 헤더 */} {/* 본문 */}
{isLoading ? (
) : view === 'day-time' ? ( ) : view === 'week-time' ? ( ) : view === 'month' ? ( ) : ( )}
); }