diff --git a/frontend/src/pages/mobile/public/Schedule.jsx b/frontend/src/pages/mobile/public/Schedule.jsx index 144a825..d6a52cd 100644 --- a/frontend/src/pages/mobile/public/Schedule.jsx +++ b/frontend/src/pages/mobile/public/Schedule.jsx @@ -3,6 +3,7 @@ import { motion, AnimatePresence } from 'framer-motion'; import { Clock, Tag, Link2, ChevronLeft, ChevronRight, ChevronDown, Search, X, Calendar } from 'lucide-react'; import { useInfiniteQuery } from '@tanstack/react-query'; import { useInView } from 'react-intersection-observer'; +import { useVirtualizer } from '@tanstack/react-virtual'; import { getSchedules, getCategories, searchSchedules } from '../../../api/public/schedules'; // 모바일 일정 페이지 @@ -26,7 +27,9 @@ function MobileSchedule() { setCalendarViewDate(newDate); }; - const SEARCH_LIMIT = 10; + const SEARCH_LIMIT = 20; // 페이지당 20개 + const ESTIMATED_ITEM_HEIGHT = 100; // 아이템 추정 높이 (동적 측정) + const scrollContainerRef = useRef(null); // 가상 스크롤 컨테이너 const { ref: loadMoreRef, inView } = useInView({ threshold: 0, rootMargin: '100px' }); // 검색 무한 스크롤 @@ -55,6 +58,14 @@ function MobileSchedule() { return searchData.pages.flatMap(page => page.schedules); }, [searchData]); + // 가상 스크롤 설정 (검색 모드에서만 활성화, 동적 높이 지원) + const virtualizer = useVirtualizer({ + count: isSearchMode && searchTerm ? searchResults.length : 0, + getScrollElement: () => scrollContainerRef.current, + estimateSize: () => ESTIMATED_ITEM_HEIGHT, + overscan: 5, // 버퍼 아이템 수 + }); + useEffect(() => { if (inView && hasNextPage && !isFetchingNextPage && isSearchMode && searchTerm) { fetchNextPage(); @@ -436,39 +447,65 @@ function MobileSchedule() { {/* 컨텐츠 영역 */} -
+
{isSearchMode && searchTerm ? ( - // 검색 결과 -
- {searchLoading ? ( -
-
-
- ) : searchResults.length === 0 ? ( -
- 검색 결과가 없습니다 -
- ) : ( - <> - {searchResults.map((schedule, index) => ( - - ))} -
- {isFetchingNextPage && ( -
-
+ // 검색 결과 - 가상 스크롤 + searchLoading ? ( +
+
+
+ ) : searchResults.length === 0 ? ( +
+ 검색 결과가 없습니다 +
+ ) : ( + <> +
+ {virtualizer.getVirtualItems().map((virtualItem) => { + const schedule = searchResults[virtualItem.index]; + if (!schedule) return null; + + return ( +
+
+ +
- )} -
- - )} -
+ ); + })} +
+ {/* 무한 스크롤 트리거 */} +
+ {isFetchingNextPage && ( +
+
+
+ )} +
+ + ) ) : loading ? (