1- import { useRef , useEffect , useState } from 'react' ;
1+ import { VirtuosoGrid } from 'react-virtuoso' ;
2+ import { useNavigate } from 'react-router-dom' ;
23import { useInfiniteScroll } from '../../../shared/hooks/useInfiniteScroll' ;
3- import { getAllEventsInfinite , getCategoryEventsInfinite } from '../../../entities/event/api/event' ;
44import EventCard from '../../../shared/ui/EventCard' ;
55import { BaseEvent , CategoryType , TagType } from '../../../shared/types/baseEventType' ;
6- import { useNavigate } from 'react-router-dom' ;
7- import { useVirtualizer } from '@tanstack/react-virtual' ;
8-
6+ import { getAllEventsInfinite , getCategoryEventsInfinite } from '../../../entities/event/api/event' ;
97interface EventListProps extends BaseEvent {
108 id : number ;
119 hostChannelName : string ;
@@ -27,10 +25,6 @@ const categoryToKorean: Record<CategoryType, string> = {
2725const EventList = ( { category, tag } : EventListComponentProps ) => {
2826 const navigate = useNavigate ( ) ;
2927
30- const MOBILE_CARD_HEIGHT = 250 ;
31- const DESKTOP_CARD_HEIGHT = 330 ;
32-
33-
3428 const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteScroll < EventListProps > ( {
3529 queryKey : [ 'events' , 'infinite' , category ?? '' , tag ?? '' ] ,
3630 queryFn : params => {
@@ -44,113 +38,47 @@ const EventList = ({ category, tag }: EventListComponentProps) => {
4438 } ) ;
4539
4640 const flatEvents = data ?. pages . flatMap ( page => page . items ) ?? [ ] ;
47- const parentRef = useRef < HTMLDivElement > ( null ) ;
48- const firstRowRef = useRef < HTMLDivElement > ( null ) ;
49-
50- // 반응형
51- const [ windowWidth , setWindowWidth ] = useState ( window . innerWidth ) ;
52- useEffect ( ( ) => {
53- const handleResize = ( ) => setWindowWidth ( window . innerWidth ) ;
54- window . addEventListener ( 'resize' , handleResize ) ;
55- return ( ) => window . removeEventListener ( 'resize' , handleResize ) ;
56- } , [ ] ) ;
57- const isMobile = windowWidth < 768 ;
5841
59- const [ rowHeight , setRowHeight ] = useState ( isMobile ? MOBILE_CARD_HEIGHT : DESKTOP_CARD_HEIGHT ) ;
60-
61- useEffect ( ( ) => {
62- if ( ! firstRowRef . current ) return ;
63- requestAnimationFrame ( ( ) => {
64- const measuredHeight = firstRowRef . current ! . offsetHeight ;
65- if ( measuredHeight && measuredHeight !== rowHeight ) {
66- setRowHeight ( measuredHeight ) ;
67- }
68- } ) ;
69- } , [ flatEvents , windowWidth ] ) ;
70-
71- const rowCount = Math . ceil ( flatEvents . length / 2 ) ;
72- const rowVirtualizer = useVirtualizer ( {
73- count : rowCount ,
74- getScrollElement : ( ) => parentRef . current ,
75- estimateSize : ( ) => isMobile ? MOBILE_CARD_HEIGHT : DESKTOP_CARD_HEIGHT ,
76- measureElement : el => el . getBoundingClientRect ( ) . height ,
77- overscan : 5 ,
78- } ) ;
79-
80- useEffect ( ( ) => {
81- const virtualItems = rowVirtualizer . getVirtualItems ( ) ;
82- if ( virtualItems . length === 0 ) return ;
83-
84- const lastVirtualItem = virtualItems [ virtualItems . length - 1 ] ;
85- if ( lastVirtualItem . index >= rowCount - 1 && hasNextPage && ! isFetching ) {
86- fetchNextPage ( ) ;
87- }
88- } , [ rowVirtualizer . getVirtualItems ( ) , rowCount , hasNextPage , isFetching , fetchNextPage ] ) ;
42+ if ( flatEvents . length === 0 ) {
43+ return (
44+ < div className = "sm:text-12 md:text-14 lg:text-16 py-8 text-placeholderText " >
45+ { tag ? (
46+ < div > 열린 이벤트가 없습니다.</ div >
47+ ) : category ? (
48+ < div > 열린 { categoryToKorean [ category ] } 이벤트가 없습니다.</ div >
49+ ) : null }
50+ </ div >
51+ ) ;
52+ }
8953
9054 return (
9155 < >
92- { flatEvents . length === 0 ? (
93- < div className = "sm:text-12 md:text-14 lg:text-16 py-8 text-placeholderText " >
94- { tag ? (
95- < div > 열린 이벤트가 없습니다. </ div >
96- ) : category ? (
97- < div > 열린 { categoryToKorean [ category ] } 이벤트가 없습니다. </ div >
98- ) : null }
99- </ div >
100- ) : (
101- < div
102- ref = { parentRef }
103- className = "relative w-[90%] mx-auto h-[80vh] md:h-[85vh] lg:h-[90vh] overflow-auto pb-20"
104- role = "region"
105- aria-label = "이벤트 목록"
106- tabIndex = { 0 }
107- >
108- < div
109- style = { { height : ` ${ rowVirtualizer . getTotalSize ( ) } px` , position : 'relative' } }
110- className = "relative"
111- >
112- { rowVirtualizer . getVirtualItems ( ) . map ( virtualRow => {
113- const firstIndex = virtualRow . index * 2 ;
114- const secondIndex = firstIndex + 1 ;
115-
116- const items = [ flatEvents [ firstIndex ] , flatEvents [ secondIndex ] ] . filter ( Boolean ) ;
56+ < VirtuosoGrid
57+ style = { { height : '80vh' , width : '90%' , margin : '0 auto' } } // 높이 명시 필수!
58+ useWindowScroll = { true } // 전체 창 스크롤 아니라면 false 권장
59+ data = { flatEvents }
60+ endReached = { ( ) => {
61+ if ( hasNextPage && ! isFetching ) fetchNextPage ( ) ;
62+ } }
63+ overscan = { 200 }
64+ listClassName = "flex flex-wrap justify-between"
65+ itemClassName = "w-[48%] mb-4 cursor-pointer"
66+ itemContent = { ( _index , event ) => (
67+ < EventCard
68+ key = { event . id }
69+ id = { event . id }
70+ img = { event . bannerImageUrl }
71+ eventTitle = { event . title }
72+ eventDate = { event . startDate }
73+ location = { event . address }
74+ host = { event . hostChannelName }
75+ hashtags = { event . hashtags }
76+ dDay = { event . remainDays }
77+ onClick = { ( ) => navigate ( `/event-details/ ${ event . id } ` ) }
78+ />
79+ ) }
80+ />
11781
118- return (
119- < div
120- key = { virtualRow . index }
121- ref = { virtualRow . index === 0 ? firstRowRef : null }
122- style = { {
123- position : 'absolute' ,
124- top : 0 ,
125- left : 0 ,
126- width : '100%' ,
127- transform : `translateY(${ virtualRow . start } px)` ,
128- } }
129- className = "grid grid-cols-2 gap-4"
130- >
131- { items . map ( event => (
132- < div
133- key = { event . id }
134- onClick = { ( ) => navigate ( `/event-details/${ event . id } ` ) }
135- >
136- < EventCard
137- id = { event . id }
138- img = { event . bannerImageUrl }
139- eventTitle = { event . title }
140- eventDate = { event . startDate }
141- location = { event . address }
142- host = { event . hostChannelName }
143- hashtags = { event . hashtags }
144- dDay = { event . remainDays }
145- />
146- </ div >
147- ) ) }
148- </ div >
149- ) ;
150- } ) }
151- </ div >
152- </ div >
153- ) }
15482 { isFetching && < div className = "text-center py-4" > Loading...</ div > }
15583 </ >
15684 ) ;
0 commit comments