UNPKG

mobile-react-infinite-calendar

Version:

A mobile-optimized infinite scroll calendar component for React

149 lines (148 loc) 7.29 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** * 리팩토링된 InfiniteCalendar 컴포넌트 * - 비즈니스 로직과 UI 완전 분리 * - 복잡한 로직을 커스텀 훅으로 추출 * - 단일 책임 원칙 적용 * - 성능 최적화 및 메모리 리크 방지 */ import { memo, useRef, useMemo, useEffect } from 'react'; import { cn } from '../utils/cn'; import { CalendarHeader } from './CalendarHeader'; import { DateSelector } from './DateSelector'; import { WeekDaysHeader } from './WeekDaysHeader'; import { MonthRow } from './MonthRow'; import { useCalendarState } from '../hooks/useCalendarState'; import { useDateSelectorLogic } from '../hooks/useDateSelectorLogic'; import { useHeaderOptions } from '../hooks/useHeaderOptions'; import { useScrollManagement } from '../hooks/useScrollManagement'; import { useIntersectionObserver } from '../hooks/useIntersectionObserver'; import { useAutoHeight } from '../hooks/useAutoHeight'; import { useInitialScroll } from '../hooks/useInitialScroll'; import { useMultiRef } from '../hooks/useMultiRef'; import { injectRequiredStyles } from '../utils/styleInjector'; import { initializeLogger } from '../utils/logger'; /** * 무한 스크롤 캘린더 컴포넌트 * * @example * ```tsx * <InfiniteCalendar * events={events} * dynamicEvents={async (startDate, endDate) => await fetchEvents(startDate, endDate)} * onDayAction={(date, dayInfo) => handleDayClick(date, dayInfo)} * options={{ * debug: true, * height: 'auto', * autoHeight: { bottomOffset: 20 } * }} * /> * ``` */ const InfiniteCalendar = memo(function InfiniteCalendar({ // 데이터 events, holidays, // 동적 이벤트 dynamicEvents, dynamicEventMapping, dynamicEventTransform, onDynamicEventLoad, // 이벤트 핸들러 onDayAction, // 지역화 locale = 'ko-KR', holidayServiceKey, // UI 옵션 options = {}, // 레거시 props showTodayButton, showDatePicker, height, initialDate }) { var _a; // === 스타일 주입 (한 번만) === useEffect(() => { injectRequiredStyles(); }, []); // === 헤더 옵션 처리 === const { mergedOptions, headerOptions } = useHeaderOptions({ options, showTodayButton, showDatePicker, height, initialDate }); // === 로거 초기화 (한 번만) === useEffect(() => { initializeLogger(mergedOptions.debug); }, [mergedOptions.debug]); // === 캘린더 상태 관리 === const calendarState = useCalendarState({ events, holidays, dynamicEvents, dynamicEventMapping, dynamicEventTransform, onDynamicEventLoad, locale, holidayServiceKey, options: mergedOptions }); // === 날짜 선택기 로직 === const dateSelectorLogic = useDateSelectorLogic({ selectedYear: calendarState.selectedYear, selectedMonth: calendarState.selectedMonth, showYearDropdown: calendarState.showYearDropdown, showMonthDropdown: calendarState.showMonthDropdown, setSelectedYear: calendarState.setSelectedYear, setSelectedMonth: calendarState.setSelectedMonth, setShowYearDropdown: calendarState.setShowYearDropdown, setShowMonthDropdown: calendarState.setShowMonthDropdown, confirmDateSelection: calendarState.confirmDateSelection, isDebugEnabled: calendarState.isDebugEnabled }); // === Refs === const containerRef = useRef(null); const [scrollContainerRef, calendarRef, setScrollRefs] = useMultiRef(); // === 자동 높이 계산 === useAutoHeight({ containerRef, height: mergedOptions.height, autoHeight: mergedOptions.autoHeight, onHeightChange: calendarState.setAvailableHeight }); // === 스크롤 관리 === useScrollManagement({ scrollContainerRef, onScrollToTop: calendarState.addPrevMonth, onScrollToBottom: calendarState.addNextMonth }); // === IntersectionObserver === useIntersectionObserver({ calendarRef, monthsData: calendarState.monthsData, activeMonth: calendarState.activeMonth, onActiveMonthChange: calendarState.setActiveMonth, onCurrentMonthVisibilityChange: calendarState.setIsCurrentMonthVisible }); // === 초기 스크롤 설정 === useInitialScroll({ calendarRef, monthsData: calendarState.monthsData, availableHeight: calendarState.availableHeight, isInitialScrollSet: calendarState.isInitialScrollSet, onInitialScrollSet: calendarState.setIsInitialScrollSet }); // === 컨테이너 스타일 (메모이제이션) === const containerClassName = useMemo(() => { var _a; return cn("flex flex-col bg-white infinite-calendar-container", (_a = mergedOptions.classNames) === null || _a === void 0 ? void 0 : _a.container); }, [(_a = mergedOptions.classNames) === null || _a === void 0 ? void 0 : _a.container]); const containerStyle = useMemo(() => { const { height: optionHeight, autoHeight } = mergedOptions; const { availableHeight } = calendarState; if (typeof optionHeight === 'number') { return { height: `${optionHeight}px` }; } if (optionHeight === 'auto' || autoHeight) { return { height: availableHeight ? `${availableHeight}px` : '100%' }; } return { height: optionHeight }; }, [mergedOptions.height, mergedOptions.autoHeight, calendarState.availableHeight]); return (_jsxs("div", { ref: containerRef, className: containerClassName, style: containerStyle, children: [_jsx(CalendarHeader, { activeMonth: calendarState.activeMonth, locale: locale, headerOptions: headerOptions, classNames: mergedOptions.classNames, isCurrentMonthVisible: calendarState.isCurrentMonthVisible, onTodayClick: calendarState.handleTodayClick, onDatePickerClick: headerOptions.datePicker ? calendarState.openDateSelector : undefined }), headerOptions.datePicker && (_jsx(DateSelector, { show: calendarState.showDateSelector, selectedYear: calendarState.selectedYear, selectedMonth: calendarState.selectedMonth, showYearDropdown: calendarState.showYearDropdown, showMonthDropdown: calendarState.showMonthDropdown, locale: locale, onClose: calendarState.closeDateSelector, onConfirm: dateSelectorLogic.handleConfirmDateSelection, onYearSelect: dateSelectorLogic.handleYearSelect, onMonthSelect: dateSelectorLogic.handleMonthSelect, onToggleYearDropdown: dateSelectorLogic.handleToggleYearDropdown, onToggleMonthDropdown: dateSelectorLogic.handleToggleMonthDropdown })), _jsx(WeekDaysHeader, { locale: locale, classNames: mergedOptions.classNames, show: !!(headerOptions.show && headerOptions.weekDays) }), _jsx("div", { className: "flex-1 overflow-y-auto min-h-0 scrollbar-hide", ref: setScrollRefs, children: _jsx("div", { children: calendarState.monthsData.map((monthData, monthIndex) => (_jsx(MonthRow, { monthData: monthData, monthIndex: monthIndex, activeMonth: calendarState.activeMonth, classNames: mergedOptions.classNames, onDayClick: onDayAction }, monthData.month.toISOString()))) }) })] })); }); InfiniteCalendar.displayName = 'InfiniteCalendar'; export { InfiniteCalendar };