Skip to content
Merged
Show file tree
Hide file tree
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
14 changes: 14 additions & 0 deletions src/entities/event/hook/useEventListHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { EventList } from '../../../features/event-manage/event-list/model/eventList';
import { useInfiniteScroll } from '../../../shared/hooks/useInfiniteScroll';
import { getAllEventsInfinite } from '../api/event';

const useEventList = () => {
const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteScroll<EventList>({
queryKey: ['events', 'infinite'],
queryFn: getAllEventsInfinite,
size: 10,
filters: { tag: 'current' },
});
return { data, fetchNextPage, hasNextPage, isFetching };
};
Comment on lines +5 to +13
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

커스텀 훅의 재사용성 개선이 필요합니다.

이벤트 목록을 가져오는 훅이 잘 구현되었지만, 하드코딩된 필터 값({ tag: 'current' })과 고정된 사이즈(10)는 훅의 재사용성을 제한합니다. 다양한 상황에서 활용할 수 있도록 매개변수를 받도록 개선하는 것이 좋겠습니다.

- const useEventList = () => {
+ const useEventList = (options = { size: 10, tag: 'current' }) => {
  const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteScroll<EventList>({
    queryKey: ['events', 'infinite'],
    queryFn: getAllEventsInfinite,
-   size: 10,
-   filters: { tag: 'current' },
+   size: options.size,
+   filters: { tag: options.tag },
  });
  return { data, fetchNextPage, hasNextPage, isFetching };
};
📝 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
const useEventList = () => {
const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteScroll<EventList>({
queryKey: ['events', 'infinite'],
queryFn: getAllEventsInfinite,
size: 10,
filters: { tag: 'current' },
});
return { data, fetchNextPage, hasNextPage, isFetching };
};
const useEventList = (options = { size: 10, tag: 'current' }) => {
const { data, fetchNextPage, hasNextPage, isFetching } =
useInfiniteScroll<EventList>({
queryKey: ['events', 'infinite'],
queryFn: getAllEventsInfinite,
size: options.size,
filters: { tag: options.tag },
});
return { data, fetchNextPage, hasNextPage, isFetching };
};

export default useEventList;
7 changes: 7 additions & 0 deletions src/features/event-manage/event-list/model/eventList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { BaseEvent } from '../../../../shared/types/baseEventType';

export interface EventList extends BaseEvent {
id: number;
hostChannelName: string;
remainDays: string;
}
20 changes: 4 additions & 16 deletions src/features/event-manage/event-list/ui/EventList.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
import { useRef, useEffect } from 'react';
import { useInfiniteScroll } from '../../../../shared/hooks/useInfiniteScroll';
import { getAllEventsInfinite } from '../../../../entities/event/api/event';
import EventCard from '../../../../shared/ui/EventCard';
import { BaseEvent } from '../../../../shared/types/baseEventType';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
interface EventListProps extends BaseEvent{
id: number;
hostChannelName: string;
remainDays: string;
}
import useEventList from '../../../../entities/event/hook/useEventListHook';
import type { EventList } from '../model/eventList';

const EventList = () => {
const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteScroll<EventListProps>({
queryKey: ['events', 'infinite'],
queryFn: getAllEventsInfinite,
size: 10,
filters: { tag: 'current' },
});

const { data, hasNextPage, isFetching, fetchNextPage } = useEventList();
const observerRef = useRef<IntersectionObserver>();
const lastEventCardRef = useRef<HTMLDivElement | null>(null);

Expand Down Expand Up @@ -46,7 +34,7 @@ const EventList = () => {
<>
<div className="grid grid-cols-2 gap-4 mx-6 mt-2 md:grid-cols-2 lg:grid-cols-2">
{data?.pages.map((page, pageIndex) =>
page.items.map((event: EventListProps, eventIndex) => {
page.items.map((event: EventList, eventIndex) => {
const isLastElement = pageIndex === data.pages.length - 1 && eventIndex === page.items.length - 1;
return (
<div key={event.id} ref={isLastElement ? lastEventCardRef : null}>
Expand Down
12 changes: 6 additions & 6 deletions src/pages/home/ui/MainPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import LoginModal from '../../../widgets/main/ui/LoginModal';
import { cardButtons } from '../../../shared/types/mainCardButtonType';
import useAuthStore from '../../../app/provider/authStore';
import EventTags from '../../../features/home/ui/EventTags';
import ProfileCircle from '../../../../design-system/ui/Profile';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const MainPage = () => {
Expand All @@ -40,12 +41,11 @@ const MainPage = () => {
leftButtonClick={() => {}}
leftButtonLabel="같이가요"
rightContent={
<SecondaryButton
size="large"
color="black"
label={isLoggedIn ? `${name}님` : '로그인'}
onClick={isLoggedIn ? closeModal : openModal}
/>
isLoggedIn ? (
<ProfileCircle profile="userProfile" name={name?.slice(1, 3) || ''} className="w-11 h-11 text-15" />
) : (
<SecondaryButton size="large" color="black" label="로그인" onClick={openModal} />
)
}
/>
<AnimatePresence>{isModalOpen && <LoginModal onClose={closeModal} />}</AnimatePresence>
Expand Down
119 changes: 74 additions & 45 deletions src/pages/search/ui/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,17 @@ import firstPage from '../../../../public/assets/banners/1.png';
import secondPage from '../../../../public/assets/banners/2.png';
import thirdPage from '../../../../public/assets/banners/3.png';
import EventCard from '../../../shared/ui/EventCard';
import { trendingEventsData } from '../../../shared/types/eventCardType';
import { hostInfoData } from '../../../shared/types/hostInfoType';
import { FilterDataType } from '../../../shared/types/filterDataType';
import { FilterMockData } from '../../../shared/types/filterDataType';
import ProfileCircle from '../../../../design-system/ui/Profile';
import useEventList from '../../../entities/event/hook/useEventListHook';
import type { EventList } from '../../../features/event-manage/event-list/model/eventList';
import useHostChannelList from '../../../entities/host/hook/useHostChannelListHook';

const SearchPage = () => {
const [keyword, setKeyword] = useState('');
const [filterData, setFilterDate] = useState<FilterDataType>({
Events: [],
Host: [],
});
//@TODO:추후에 response body 보고 Type 수정
const { data, hasNextPage, isFetching, fetchNextPage } = useEventList();
const { data: hostData } = useHostChannelList();
const observerRef = useRef<IntersectionObserver>();
const lastEventCardRef = useRef<HTMLDivElement | null>(null);

const images = [
{ img: firstPage, link: 'https://example.com/page1' },
Expand All @@ -36,11 +34,23 @@ const SearchPage = () => {
];

useEffect(() => {
//@TODO:API 호출 후, response를 setFilterDate에 넣을 예정
//현재는 목업 데이터를 넣어놓음
//@TODO:API 연동하며 디바운스 구현 예정
setFilterDate(FilterMockData);
}, [keyword]);
if (!hasNextPage || isFetching) return;
if (observerRef.current) observerRef.current.disconnect();

observerRef.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage();
}
});

if (lastEventCardRef.current) observerRef.current.observe(lastEventCardRef.current);

return () => {
if (observerRef.current) {
observerRef.current.disconnect();
}
};
}, [hasNextPage, isFetching, fetchNextPage]);

const navigate = useNavigate();
const inputRef = useRef<HTMLInputElement | null>(null); // Input 요소를 참조하기 위한 훅
Expand Down Expand Up @@ -75,49 +85,68 @@ const SearchPage = () => {
{keyword ? (
<>
<div className="px-6 flex flex-col gap-8">
{/* 이벤트 검색 결과를 렌더링 하는 부분 */}
{filterData.Events?.length > 0 && (
<div>
<p className="font-bold text-lg lg:text-xl mb-3">이벤트</p>
<div className="grid grid-cols-2 gap-4">
{filterData.Events?.map((event: trendingEventsData) => (
<EventCard
key={event.id}
img={event.img}
eventTitle={event.eventTitle}
dDay={event.dDay}
host={event.host}
eventDate={event.eventDate}
location={event.location}
hashtags={event.hashtags}
/>
))}
</div>
<div>
<p className="font-bold text-lg lg:text-xl mb-3">이벤트</p>
<div className="grid grid-cols-2 gap-4">
{data?.pages.map((page, pageIndex) =>
page.items
.filter(
(event: EventList) =>
event.title.toLowerCase().includes(keyword.toLowerCase()) ||
event.address.toLowerCase().includes(keyword.toLowerCase()) ||
event.hostChannelName.toLowerCase().includes(keyword.toLowerCase())
)
.map((event: EventList, eventIndex) => {
const isLastElement = pageIndex === data.pages.length - 1 && eventIndex === page.items.length - 1;
return (
<div key={event.id} ref={isLastElement ? lastEventCardRef : null}>
<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>
)}
{isFetching && <div className="text-center py-4">Loading...</div>}
</div>

{/* 호스트 검색 결과를 렌더링 하는 부분 */}
{filterData.Host?.length > 0 && (
<div>
<p className="font-bold pb-3 text-lg lg:text-xl mb-3">호스트</p>
<div className="flex flex-wrap gap-9 px-2">
{filterData.Host?.map((host: hostInfoData) => (
<div>
<p className="font-bold pb-3 text-lg lg:text-xl mb-3">호스트</p>
<div className="flex flex-wrap gap-9 px-2 mb-10">
{hostData?.result
.filter(host => host.hostChannelName.toLowerCase().includes(keyword.toLowerCase()))
.map(host => (
<ProfileCircle
key={host.id}
profile="hostInfoProfile"
name={host.name}
id={host.id}
profile="hostInfoProfile"
name={host.hostChannelName}
onClick={() => navigate(`/menu/hostInfo/${host.id}`)}
className="w-19 h-19 md:w-20 md:h-20 text-sm md:text-16 lg:text-base"
/>
))}
</div>
</div>
Comment on lines +125 to 137
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

hostData?.result 접근 시 null-safe 체인이 빠져 런타임 오류가 발생합니다

hostData 가 아직 로딩 중이거나 에러로 인해 undefined 인 경우, hostData?.resultundefined 를 반환합니다. 그럼에도 .filter / .some 메서드를 바로 호출하기 때문에 “Cannot read properties of undefined (reading 'filter')” 와 같은 예외가 발생할 수 있습니다.
아래처럼 result 에도 optional chaining 을 추가하여 방어 코드를 적용해 주세요.

- {hostData?.result
-   .filter(host => host.hostChannelName.toLowerCase().includes(keyword.toLowerCase()))
+ {hostData?.result?.filter(
+     host => host.hostChannelName.toLowerCase().includes(keyword.toLowerCase())
+   )
- {!hostData?.result.some(host =>
+ {!hostData?.result?.some(host =>

Also applies to: 141-149

)}
</div>
</div>

{!filterData.Host?.length && !filterData.Events?.length && (
<div className="p-6 text-center font-semibold text-gray-700">검색 결과가 없습니다.</div>
)}
{!hostData?.result.some(host => host.hostChannelName.toLowerCase().includes(keyword.toLowerCase())) &&
!data?.pages.some(page =>
page.items.some(
event =>
event.title.toLowerCase().includes(keyword.toLowerCase()) ||
event.address.toLowerCase().includes(keyword.toLowerCase()) ||
event.hostChannelName.toLowerCase().includes(keyword.toLowerCase())
)
) && <div className="p-6 text-center font-semibold text-gray-700">검색 결과가 없습니다.</div>}
</>
) : (
<div className="px-6">
Expand Down