Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 39 additions & 111 deletions src/features/event/ui/EventList.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { useRef, useEffect, useState } from 'react';
import { VirtuosoGrid } from 'react-virtuoso';
import { useNavigate } from 'react-router-dom';
import { useInfiniteScroll } from '../../../shared/hooks/useInfiniteScroll';
import { getAllEventsInfinite, getCategoryEventsInfinite } from '../../../entities/event/api/event';
import EventCard from '../../../shared/ui/EventCard';
import { BaseEvent, CategoryType, TagType } from '../../../shared/types/baseEventType';
import { useNavigate } from 'react-router-dom';
import { useVirtualizer } from '@tanstack/react-virtual';

import { getAllEventsInfinite, getCategoryEventsInfinite } from '../../../entities/event/api/event';
interface EventListProps extends BaseEvent {
id: number;
hostChannelName: string;
Expand All @@ -27,10 +25,6 @@ const categoryToKorean: Record<CategoryType, string> = {
const EventList = ({ category, tag }: EventListComponentProps) => {
const navigate = useNavigate();

const MOBILE_CARD_HEIGHT = 250;
const DESKTOP_CARD_HEIGHT = 350;


const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteScroll<EventListProps>({
queryKey: ['events', 'infinite', category ?? '', tag ?? ''],
queryFn: params => {
Expand All @@ -44,113 +38,47 @@ const EventList = ({ category, tag }: EventListComponentProps) => {
});

const flatEvents = data?.pages.flatMap(page => page.items) ?? [];
const parentRef = useRef<HTMLDivElement>(null);
const firstRowRef = useRef<HTMLDivElement>(null);

// 반응형
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const isMobile = windowWidth < 768;

const [rowHeight, setRowHeight] = useState(isMobile ? MOBILE_CARD_HEIGHT : DESKTOP_CARD_HEIGHT);

useEffect(() => {
if (!firstRowRef.current) return;
requestAnimationFrame(() => {
const measuredHeight = firstRowRef.current!.offsetHeight;
if (measuredHeight && measuredHeight !== rowHeight) {
setRowHeight(measuredHeight);
}
});
}, [flatEvents, windowWidth]);

const rowCount = Math.ceil(flatEvents.length / 2);
const rowVirtualizer = useVirtualizer({
count: rowCount,
getScrollElement: () => parentRef.current,
estimateSize: () => isMobile ? MOBILE_CARD_HEIGHT : DESKTOP_CARD_HEIGHT,
measureElement: el => el.getBoundingClientRect().height,
overscan: 5,
});

useEffect(() => {
const virtualItems = rowVirtualizer.getVirtualItems();
if (virtualItems.length === 0) return;

const lastVirtualItem = virtualItems[virtualItems.length - 1];
if (lastVirtualItem.index >= rowCount - 1 && hasNextPage && !isFetching) {
fetchNextPage();
}
}, [rowVirtualizer.getVirtualItems(), rowCount, hasNextPage, isFetching, fetchNextPage]);
if (flatEvents.length === 0) {
return (
<div className="sm:text-12 md:text-14 lg:text-16 py-8 text-placeholderText ">
{tag ? (
<div>열린 이벤트가 없습니다.</div>
) : category ? (
<div>열린 {categoryToKorean[category]} 이벤트가 없습니다.</div>
) : null}
</div>
);
}

return (
<>
{flatEvents.length === 0 ? (
<div className="sm:text-12 md:text-14 lg:text-16 py-8 text-placeholderText ">
{tag ? (
<div>열린 이벤트가 없습니다.</div>
) : category ? (
<div>열린 {categoryToKorean[category]} 이벤트가 없습니다.</div>
) : null}
</div>
) : (
<div
ref={parentRef}
className="relative w-[90%] mx-auto h-[80vh] md:h-[85vh] lg:h-[90vh] overflow-auto"
role="region"
aria-label="이벤트 목록"
tabIndex={0}
>
<div
style={{ height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative' }}
className="relative"
>
{rowVirtualizer.getVirtualItems().map(virtualRow => {
const firstIndex = virtualRow.index * 2;
const secondIndex = firstIndex + 1;

const items = [flatEvents[firstIndex], flatEvents[secondIndex]].filter(Boolean);
<VirtuosoGrid
style={{ height: '80vh', width: '90%', margin: '0 auto' }} // 높이 명시 필수!
useWindowScroll={true} // 전체 창 스크롤 아니라면 false 권장
data={flatEvents}
endReached={() => {
if (hasNextPage && !isFetching) fetchNextPage();
}}
overscan={200}
listClassName="flex flex-wrap justify-between"
itemClassName="w-[48%] mb-4 cursor-pointer"
itemContent={(_index, event) => (
<EventCard
key={event.id}
id={event.id}
img={event.bannerImageUrl}
eventTitle={event.title}
eventDate={event.startDate}
location={event.address}
host={event.hostChannelName}
hashtags={event.hashtags}
dDay={event.remainDays}
onClick={() => navigate(`/event-details/${event.id}`)}
/>
)}
/>
Comment on lines +56 to +80
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

VirtuosoGrid 구현에서 스크롤 동작을 검토해주세요.

useWindowScroll={true} 설정으로 인해 전체 페이지 스크롤을 사용하게 되는데, 이것이 의도된 동작인지 확인이 필요합니다.

개선 제안:

  1. 컨테이너 기반 스크롤을 원한다면 useWindowScroll={false}로 변경
  2. overscan={200}은 다소 높은 값으로, 성능에 영향을 줄 수 있습니다. 기본값(50-100) 사용 권장
-        useWindowScroll={true}  // 전체 창 스크롤 아니라면 false 권장
+        useWindowScroll={false}
-        overscan={200}
+        overscan={100}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<VirtuosoGrid
style={{ height: '80vh', width: '90%', margin: '0 auto' }} // 높이 명시 필수!
useWindowScroll={true} // 전체 창 스크롤 아니라면 false 권장
data={flatEvents}
endReached={() => {
if (hasNextPage && !isFetching) fetchNextPage();
}}
overscan={200}
listClassName="flex flex-wrap justify-between"
itemClassName="w-[48%] mb-4 cursor-pointer"
itemContent={(_index, event) => (
<EventCard
key={event.id}
id={event.id}
img={event.bannerImageUrl}
eventTitle={event.title}
eventDate={event.startDate}
location={event.address}
host={event.hostChannelName}
hashtags={event.hashtags}
dDay={event.remainDays}
onClick={() => navigate(`/event-details/${event.id}`)}
/>
)}
/>
<VirtuosoGrid
style={{ height: '80vh', width: '90%', margin: '0 auto' }} // 높이 명시 필수!
useWindowScroll={false}
data={flatEvents}
endReached={() => {
if (hasNextPage && !isFetching) fetchNextPage();
}}
overscan={100}
listClassName="flex flex-wrap justify-between"
itemClassName="w-[48%] mb-4 cursor-pointer"
itemContent={(_index, event) => (
<EventCard
key={event.id}
id={event.id}
img={event.bannerImageUrl}
eventTitle={event.title}
eventDate={event.startDate}
location={event.address}
host={event.hostChannelName}
hashtags={event.hashtags}
dDay={event.remainDays}
onClick={() => navigate(`/event-details/${event.id}`)}
/>
)}
/>
🤖 Prompt for AI Agents
In src/features/event/ui/EventList.tsx between lines 56 and 80, the VirtuosoGrid
component uses useWindowScroll={true}, which enables full page scrolling; verify
if this is intended. If container-based scrolling is preferred, change
useWindowScroll to false. Also, reduce overscan from 200 to a value between 50
and 100 to improve performance by limiting offscreen item rendering.


return (
<div
key={virtualRow.index}
ref={virtualRow.index === 0 ? firstRowRef : null}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
className="grid grid-cols-2 gap-4"
>
{items.map(event => (
<div
key={event.id}
onClick={() => navigate(`/event-details/${event.id}`)}
>
<EventCard
id={event.id}
img={event.bannerImageUrl}
eventTitle={event.title}
eventDate={event.startDate}
location={event.address}
host={event.hostChannelName}
hashtags={event.hashtags}
dDay={event.remainDays}
/>
</div>
))}
</div>
);
})}
</div>
</div>
)}
{isFetching && <div className="text-center py-4">Loading...</div>}
</>
);
Expand Down