mobile-react-infinite-calendar
Version:
A mobile-optimized infinite scroll calendar component for React
149 lines (148 loc) • 7.29 kB
JavaScript
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 };