From fbac08ff5f0cd78439e9d5f269689419c0b4e3b0 Mon Sep 17 00:00:00 2001 From: Dahyeon Date: Thu, 7 Aug 2025 04:45:16 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20#85=20=EB=AC=B4=ED=95=9C=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=ED=9B=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useInfiniteScroll.ts | 69 ++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/hooks/useInfiniteScroll.ts diff --git a/src/hooks/useInfiniteScroll.ts b/src/hooks/useInfiniteScroll.ts new file mode 100644 index 0000000..318c5b5 --- /dev/null +++ b/src/hooks/useInfiniteScroll.ts @@ -0,0 +1,69 @@ +import { useEffect, useRef, useState, useCallback } from 'react'; + +interface UseInfiniteScrollProps { + fetchFunction: ( + page: number, + size: number, + ) => Promise<{ + content: T[]; + hasNext: boolean; + page: number; + size: number; + }>; + pageSize?: number; +} + +export default function useInfiniteScroll({ + fetchFunction, + pageSize = 10, +}: UseInfiniteScrollProps) { + const [data, setData] = useState([]); + const [page, setPage] = useState(0); + const [hasNext, setHasNext] = useState(true); + const [isLoading, setIsLoading] = useState(false); + + const observerRef = useRef(null); + + const loadMore = useCallback(async () => { + if (isLoading || !hasNext) return; + + setIsLoading(true); + try { + const res = await fetchFunction(page, pageSize); + setData((prev) => [...prev, ...res.content]); + setHasNext(res.hasNext); + setPage(res.page + 1); + } catch (err) { + console.error('무한 스크롤 에러:', err); + } finally { + setIsLoading(false); + } + }, [fetchFunction, page, pageSize, hasNext, isLoading]); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasNext && !isLoading) { + loadMore(); + } + }, + { + rootMargin: '100px', + threshold: 1.0, + }, + ); + + const currentRef = observerRef.current; + if (currentRef) observer.observe(currentRef); + + return () => { + if (currentRef) observer.unobserve(currentRef); + }; + }, [loadMore, hasNext, isLoading]); + + return { + data, + isLoading, + observerRef, // 컴포넌트 하단에 ref로 연결 + }; +} From 75b4b78e88cd2501171d11a43489d576c7312831 Mon Sep 17 00:00:00 2001 From: Dahyeon Date: Thu, 7 Aug 2025 13:41:22 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20#85=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=20=EB=82=B4=EB=B6=80=EC=97=90=EC=84=9C=20=EB=AC=B4=ED=95=9C?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/theater/theater.api.ts | 17 ++-- src/components/common/Theater/TheaterList.tsx | 4 +- src/hooks/queries/useTheatersQuery.ts | 4 +- src/hooks/useInfiniteScroll.ts | 60 +++++++++------ .../onboarding/OnboardingTheaterPage.tsx | 77 +++++++++++++++++-- src/types/api-response.ts | 9 +++ 6 files changed, 132 insertions(+), 39 deletions(-) diff --git a/src/api/theater/theater.api.ts b/src/api/theater/theater.api.ts index 571a6f5..b44a02d 100644 --- a/src/api/theater/theater.api.ts +++ b/src/api/theater/theater.api.ts @@ -1,5 +1,5 @@ import api from '@/api/api'; -import type { ApiResponse } from '@/types/api-response'; +import type { ApiResponse, PaginationData } from '@/types/api-response'; import type { CinemaFormat } from '@/types/onboarding'; import type { Theater } from '@/types/theater'; @@ -14,8 +14,8 @@ const getTheaters = async ({ type, page = 1, size = 10, -}: GetTheatersParams): Promise => { - const res = await api.get>('/theaters', { +}: GetTheatersParams): Promise> => { + const res = await api.get>>('/theaters', { params: { auditoriumType: type, page, @@ -23,13 +23,20 @@ const getTheaters = async ({ }, }); - const content = res.data.data?.content; + console.log('🚀 getTheaters API response', res); + + const { content, hasNext, page: currentPage, size: pageSize } = res.data.data; if (!content) { throw new Error('영화관 목록에 응답이 없습니다.'); } - return content; + return { + content, + hasNext, + page: currentPage, + size: pageSize, + }; }; // 좌석 배치도 조회 diff --git a/src/components/common/Theater/TheaterList.tsx b/src/components/common/Theater/TheaterList.tsx index 87eed38..6734c6f 100644 --- a/src/components/common/Theater/TheaterList.tsx +++ b/src/components/common/Theater/TheaterList.tsx @@ -7,9 +7,10 @@ interface Props { selected: string[]; onSelect: (auditoriumId: string) => void; onAuditoriumClick?: (auditoriumId: string) => void; // 클릭 prop 추가 + observerRef?: React.RefObject; } -const TheaterList = ({ data, selected, onSelect, onAuditoriumClick }: Props) => { +const TheaterList = ({ data, selected, onSelect, onAuditoriumClick, observerRef }: Props) => { const [expandedTheater, setExpandedTheater] = useState(null); // theaterName 기준으로 그룹화 @@ -68,6 +69,7 @@ const TheaterList = ({ data, selected, onSelect, onAuditoriumClick }: Props) => ); })} + {/*
*/}
)} diff --git a/src/hooks/queries/useTheatersQuery.ts b/src/hooks/queries/useTheatersQuery.ts index 47f22d5..67a8196 100644 --- a/src/hooks/queries/useTheatersQuery.ts +++ b/src/hooks/queries/useTheatersQuery.ts @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query'; -import { getTheaters } from '@/api/theater/theater.api'; +// import { getTheaters } from '@/api/theater/theater.api'; import type { GetTheatersParams } from '@/api/theater/theater.api'; import type { Theater } from '@/types/theater'; import type { ApiError } from '@/types/api-response'; @@ -8,7 +8,7 @@ import { useAfterQuery } from '../useAfterQuery'; export const useTheatersQuery = ({ type, page = 1, size = 10 }: GetTheatersParams) => { const query = useQuery({ queryKey: ['theaters', type, page, size], - queryFn: () => getTheaters({ type, page, size }), + // queryFn: () => getTheaters({ type, page, size }), enabled: !!type, // type이 없으면 쿼리 실행 X }); diff --git a/src/hooks/useInfiniteScroll.ts b/src/hooks/useInfiniteScroll.ts index 318c5b5..daee6bb 100644 --- a/src/hooks/useInfiniteScroll.ts +++ b/src/hooks/useInfiniteScroll.ts @@ -1,15 +1,14 @@ import { useEffect, useRef, useState, useCallback } from 'react'; +interface FetchResponse { + content: T[]; + hasNext: boolean; + page: number; + size: number; +} + interface UseInfiniteScrollProps { - fetchFunction: ( - page: number, - size: number, - ) => Promise<{ - content: T[]; - hasNext: boolean; - page: number; - size: number; - }>; + fetchFunction: (page: number, size: number) => Promise>; pageSize?: number; } @@ -18,52 +17,67 @@ export default function useInfiniteScroll({ pageSize = 10, }: UseInfiniteScrollProps) { const [data, setData] = useState([]); - const [page, setPage] = useState(0); - const [hasNext, setHasNext] = useState(true); const [isLoading, setIsLoading] = useState(false); const observerRef = useRef(null); + const pageRef = useRef(1); + const hasNextRef = useRef(true); + const isLoadingRef = useRef(false); const loadMore = useCallback(async () => { - if (isLoading || !hasNext) return; + if (isLoadingRef.current || !hasNextRef.current) return; + isLoadingRef.current = true; setIsLoading(true); + try { - const res = await fetchFunction(page, pageSize); + const res = await fetchFunction(pageRef.current, pageSize); setData((prev) => [...prev, ...res.content]); - setHasNext(res.hasNext); - setPage(res.page + 1); + + console.log('📦 API 응답:', res); + + hasNextRef.current = res.hasNext; + pageRef.current += 1; } catch (err) { console.error('무한 스크롤 에러:', err); } finally { + isLoadingRef.current = false; setIsLoading(false); } - }, [fetchFunction, page, pageSize, hasNext, isLoading]); + }, [fetchFunction, pageSize]); useEffect(() => { const observer = new IntersectionObserver( (entries) => { - if (entries[0].isIntersecting && hasNext && !isLoading) { + if (entries[0].isIntersecting) { loadMore(); } }, { rootMargin: '100px', - threshold: 1.0, + threshold: 0.3, }, ); - const currentRef = observerRef.current; - if (currentRef) observer.observe(currentRef); + const current = observerRef.current; + if (current) observer.observe(current); return () => { - if (currentRef) observer.unobserve(currentRef); + if (current) observer.unobserve(current); }; - }, [loadMore, hasNext, isLoading]); + }, [loadMore]); + + const reset = () => { + pageRef.current = 0; + hasNextRef.current = true; + isLoadingRef.current = false; + setData([]); + }; return { data, isLoading, - observerRef, // 컴포넌트 하단에 ref로 연결 + observerRef, // 컴포넌트 하단 요소에 연결 + reset, // 필요 시 외부에서 초기화 가능 }; } diff --git a/src/pages/onboarding/OnboardingTheaterPage.tsx b/src/pages/onboarding/OnboardingTheaterPage.tsx index eab3d80..54c81bf 100644 --- a/src/pages/onboarding/OnboardingTheaterPage.tsx +++ b/src/pages/onboarding/OnboardingTheaterPage.tsx @@ -1,10 +1,11 @@ -import { useState } from 'react'; +import { useEffect, useRef, useState, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { useOnboardingStore } from '@/store/useOnboardingStore'; import { Button, ToggleTab, Header, ProgressBar, TheaterList } from '@/components'; import type { CinemaFormat } from '@/types/onboarding'; -import { useTheatersQuery } from '@/hooks/queries/useTheatersQuery'; import { useRegisterMutation } from '@/hooks/mutations/useRegisterMutation'; +import { getTheaters } from '@/api/theater/theater.api'; +import type { Theater } from '@/types/theater'; const OnboardingTheaterPage = () => { const navigate = useNavigate(); @@ -12,15 +13,75 @@ const OnboardingTheaterPage = () => { useOnboardingStore(); const [selectedTab, setSelectedTab] = useState('IMAX'); + const [theaters, setTheaters] = useState([]); + const [page, setPage] = useState(1); + const [hasNext, setHasNext] = useState(true); + const [isLoading, setIsLoading] = useState(false); + + const observerRef = useRef(null); - const { data: theaters } = useTheatersQuery({ type: selectedTab, page: 1, size: 10 }); const { mutate } = useRegisterMutation(); + const loadMore = useCallback(async () => { + if (isLoading || !hasNext) return; + + setIsLoading(true); + try { + const res = await getTheaters({ type: selectedTab, page, size: 10 }); + setTheaters((prev) => [...prev, ...res.content]); + setHasNext(res.hasNext); + setPage((prev) => prev + 1); + } catch (err) { + console.error('영화관 목록 로딩 실패:', err); + } finally { + setIsLoading(false); + } + }, [page, selectedTab, hasNext, isLoading]); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting && !isLoading && hasNext) { + loadMore(); + } + }, + { + rootMargin: '100px', + threshold: 0.7, + }, + ); + + if (observerRef.current) observer.observe(observerRef.current); + + return () => { + if (observerRef.current) observer.unobserve(observerRef.current); + }; + }, [loadMore, isLoading, hasNext]); + + // 탭 변경 시 초기화 + useEffect(() => { + const reset = async () => { + setPage(0); + setTheaters([]); + setHasNext(true); + try { + const res = await getTheaters({ type: selectedTab, page: 1, size: 10 }); + setTheaters(res.content); + setHasNext(res.hasNext); + setPage(1); + } catch (err) { + console.error('초기 로딩 실패:', err); + } + }; + + reset(); + setCinemaFormat(selectedTab); + }, [selectedTab]); + const handleToggleTab = (tab: string) => { const format = tab as CinemaFormat; if (format === selectedTab) return; setSelectedTab(format); - setCinemaFormat(format); }; const toggleTheater = (auditoriumId: string) => { @@ -37,7 +98,6 @@ const OnboardingTheaterPage = () => { if (selectedCinemas.length === 0) return; const tempUserKey = localStorage.getItem('tempKey'); - console.log('데이터: ', nickname, selectedGenres, selectedCinemas); if (!tempUserKey) { console.error('임시 유저 키가 없습니다.'); return; @@ -68,11 +128,10 @@ const OnboardingTheaterPage = () => {
{/* 상단 헤더 */}
- {/* 진행도 바 */} +
- {/* 콘텐츠 영역 */}

자주 가는 영화관을 선택해주세요

최대 2개까지 선택할 수 있어요.

@@ -89,7 +148,9 @@ const OnboardingTheaterPage = () => {
- + + + {hasNext &&
}
diff --git a/src/types/api-response.ts b/src/types/api-response.ts index a5d7a91..c733b0c 100644 --- a/src/types/api-response.ts +++ b/src/types/api-response.ts @@ -18,3 +18,12 @@ export interface ApiError { code: number | string; error: FieldError[] | null; } + +export interface PaginationData { + content: T[]; + hasNext: boolean; + page: number; + size: number; +} + +export type InfiniteApiResponse = ApiResponse>; From a3c0bc26a878d737a5274b964a4ee87a3569e257 Mon Sep 17 00:00:00 2001 From: Dahyeon Date: Thu, 7 Aug 2025 13:57:04 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20#85=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=83=81=EC=98=81=EA=B4=80=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/Theater/TheaterList.tsx | 55 +++++++++++-------- .../onboarding/OnboardingTheaterPage.tsx | 2 +- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/components/common/Theater/TheaterList.tsx b/src/components/common/Theater/TheaterList.tsx index 6734c6f..dc7bed7 100644 --- a/src/components/common/Theater/TheaterList.tsx +++ b/src/components/common/Theater/TheaterList.tsx @@ -6,14 +6,14 @@ interface Props { data: Theater[]; selected: string[]; onSelect: (auditoriumId: string) => void; - onAuditoriumClick?: (auditoriumId: string) => void; // 클릭 prop 추가 + onAuditoriumClick?: (auditoriumId: string) => void; observerRef?: React.RefObject; } -const TheaterList = ({ data, selected, onSelect, onAuditoriumClick, observerRef }: Props) => { +const TheaterList = ({ data, selected, onSelect, onAuditoriumClick }: Props) => { const [expandedTheater, setExpandedTheater] = useState(null); - // theaterName 기준으로 그룹화 + // 그룹화: theaterName 기준 const grouped = data.reduce>((acc, cur) => { if (!acc[cur.theaterName]) acc[cur.theaterName] = []; acc[cur.theaterName].push(cur); @@ -48,28 +48,35 @@ const TheaterList = ({ data, selected, onSelect, onAuditoriumClick, observerRef {/* 하위 관 목록 */} {isExpanded && (
- {auditoriums.map((auditorium) => { - const isSelected = isAuditoriumSelected(auditorium.auditoriumId); - - return ( - + {(() => { + // auditoriumId 기준 중복 제거 + const uniqueAuditoriums = Array.from( + new Map(auditoriums.map((a) => [a.auditoriumId, a])).values(), ); - })} - {/*
*/} + + return uniqueAuditoriums.map((auditorium) => { + const isSelected = isAuditoriumSelected(auditorium.auditoriumId); + + return ( + + ); + }); + })()}
)}
diff --git a/src/pages/onboarding/OnboardingTheaterPage.tsx b/src/pages/onboarding/OnboardingTheaterPage.tsx index 54c81bf..3c2a7dc 100644 --- a/src/pages/onboarding/OnboardingTheaterPage.tsx +++ b/src/pages/onboarding/OnboardingTheaterPage.tsx @@ -61,7 +61,7 @@ const OnboardingTheaterPage = () => { // 탭 변경 시 초기화 useEffect(() => { const reset = async () => { - setPage(0); + setPage(1); setTheaters([]); setHasNext(true); try { From b4af1940b440c570850c6a2492223dec70211368 Mon Sep 17 00:00:00 2001 From: Dahyeon Date: Thu, 7 Aug 2025 13:57:40 +0900 Subject: [PATCH 4/5] =?UTF-8?q?chore:=20#85=20=EC=BD=98=EC=86=94=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/theater/theater.api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/theater/theater.api.ts b/src/api/theater/theater.api.ts index b44a02d..194fef3 100644 --- a/src/api/theater/theater.api.ts +++ b/src/api/theater/theater.api.ts @@ -23,7 +23,7 @@ const getTheaters = async ({ }, }); - console.log('🚀 getTheaters API response', res); + console.log('getTheaters API 응답', res.data.data); const { content, hasNext, page: currentPage, size: pageSize } = res.data.data; From 67ce5ad46415a90017518855e70fd416e61429f7 Mon Sep 17 00:00:00 2001 From: Dahyeon Date: Thu, 7 Aug 2025 14:14:50 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20#85=20=ED=99=88,=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=EC=9E=91=EC=84=B1=20=EC=98=81=ED=99=94=EA=B4=80=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=AC=B4?= =?UTF-8?q?=ED=95=9C=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/home/TheatersList.tsx | 76 ++++++++++++++++++++++++++++--- src/pages/review/CinemaSelect.tsx | 71 +++++++++++++++++++++++++++-- 2 files changed, 135 insertions(+), 12 deletions(-) diff --git a/src/pages/home/TheatersList.tsx b/src/pages/home/TheatersList.tsx index be5560c..e4c2372 100644 --- a/src/pages/home/TheatersList.tsx +++ b/src/pages/home/TheatersList.tsx @@ -1,8 +1,9 @@ -import { useState } from 'react'; -import { ToggleTab, Header, TheaterList } from '@/components'; +import { useEffect, useRef, useState, useCallback } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; +import { ToggleTab, Header, TheaterList } from '@/components'; import type { CinemaFormat } from '@/types/onboarding'; -import { useTheatersQuery } from '@/hooks/queries/useTheatersQuery'; +import { getTheaters } from '@/api/theater/theater.api'; +import type { Theater } from '@/types/theater'; export default function TheaterListPage() { const navigate = useNavigate(); @@ -13,8 +14,67 @@ export default function TheaterListPage() { const [selectedTab, setSelectedTab] = useState(initialTab); const [selectedAuditorium, setSelectedAuditorium] = useState(null); + const [theaters, setTheaters] = useState([]); + const [page, setPage] = useState(1); + const [hasNext, setHasNext] = useState(true); + const [isLoading, setIsLoading] = useState(false); + + const observerRef = useRef(null); + + const loadMore = useCallback(async () => { + if (isLoading || !hasNext) return; + + setIsLoading(true); + try { + const res = await getTheaters({ type: selectedTab, page, size: 10 }); + setTheaters((prev) => [...prev, ...res.content]); + setHasNext(res.hasNext); + setPage((prev) => prev + 1); + } catch (err) { + console.error('영화관 목록 로딩 실패:', err); + } finally { + setIsLoading(false); + } + }, [isLoading, hasNext, page, selectedTab]); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting && !isLoading && hasNext) { + loadMore(); + } + }, + { + rootMargin: '100px', + threshold: 0.7, + }, + ); - const { data: theaters } = useTheatersQuery({ type: selectedTab, page: 1, size: 10 }); + if (observerRef.current) observer.observe(observerRef.current); + + return () => { + if (observerRef.current) observer.unobserve(observerRef.current); + }; + }, [loadMore, isLoading, hasNext]); + + // 탭 변경 시 초기화 + useEffect(() => { + const reset = async () => { + setPage(1); + setTheaters([]); + setHasNext(true); + try { + const res = await getTheaters({ type: selectedTab, page: 1, size: 10 }); + setTheaters(res.content); + setHasNext(res.hasNext); + setPage(2); + } catch (err) { + console.error('초기 로딩 실패:', err); + } + }; + + reset(); + }, [selectedTab]); const handleTabChange = (tab: string) => { setSelectedTab(tab as CinemaFormat); @@ -26,6 +86,7 @@ export default function TheaterListPage() {
navigate('/home')} className="bg-gray-900"> 영화관 리스트
+ {/* 탭 */}
@@ -39,17 +100,18 @@ export default function TheaterListPage() { />
-
- {/* 리스트 */} + {/* 리스트 */} +
setSelectedAuditorium(id)} onAuditoriumClick={(auditoriumId) => { navigate(`/theaters/${auditoriumId}`); }} /> + {hasNext &&
}
); diff --git a/src/pages/review/CinemaSelect.tsx b/src/pages/review/CinemaSelect.tsx index 1fb0763..ac20517 100644 --- a/src/pages/review/CinemaSelect.tsx +++ b/src/pages/review/CinemaSelect.tsx @@ -1,9 +1,10 @@ import { useNavigate } from 'react-router-dom'; import { ToggleTab, ReviewStepLayout, TheaterList } from '@/components'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { useReviewStore } from '@/store'; -import { useTheatersQuery } from '@/hooks/queries/useTheatersQuery'; +import { getTheaters } from '@/api/theater/theater.api'; import type { CinemaFormat } from '@/types/onboarding'; +import type { Theater } from '@/types/theater'; export default function CinemaSelect() { const { isInitialized } = useReviewStore(); @@ -11,8 +12,12 @@ export default function CinemaSelect() { const [selectedTab, setSelectedTab] = useState('IMAX'); const [selectedAuditorium, setSelectedAuditorium] = useState(null); + const [theaters, setTheaters] = useState([]); + const [page, setPage] = useState(1); + const [hasNext, setHasNext] = useState(true); + const [isLoading, setIsLoading] = useState(false); - const { data: theaters } = useTheatersQuery({ type: selectedTab, page: 1, size: 10 }); + const observerRef = useRef(null); useEffect(() => { if (!isInitialized) { @@ -20,13 +25,68 @@ export default function CinemaSelect() { } }, [isInitialized, navigate]); + const loadMore = useCallback(async () => { + if (isLoading || !hasNext) return; + + setIsLoading(true); + try { + const res = await getTheaters({ type: selectedTab, page, size: 10 }); + setTheaters((prev) => [...prev, ...res.content]); + setHasNext(res.hasNext); + setPage((prev) => prev + 1); + } catch (err) { + console.error('영화관 목록 로딩 실패:', err); + } finally { + setIsLoading(false); + } + }, [isLoading, hasNext, page, selectedTab]); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting && !isLoading && hasNext) { + loadMore(); + } + }, + { + rootMargin: '100px', + threshold: 0.7, + }, + ); + + if (observerRef.current) observer.observe(observerRef.current); + + return () => { + if (observerRef.current) observer.unobserve(observerRef.current); + }; + }, [loadMore, isLoading, hasNext]); + + // 탭 변경 시 초기화 + useEffect(() => { + const reset = async () => { + setPage(1); + setTheaters([]); + setHasNext(true); + try { + const res = await getTheaters({ type: selectedTab, page: 1, size: 10 }); + setTheaters(res.content); + setHasNext(res.hasNext); + setPage(2); + } catch (err) { + console.error('초기 로딩 실패:', err); + } + }; + + reset(); + }, [selectedTab]); + const handleTabChange = (tab: string) => { setSelectedTab(tab as CinemaFormat); setSelectedAuditorium(null); }; const handleNext = () => { - const selected = theaters?.find((d) => d.auditoriumId === selectedAuditorium); + const selected = theaters.find((d) => d.auditoriumId === selectedAuditorium); if (!selected) return; navigate('/review/info', { @@ -57,10 +117,11 @@ export default function CinemaSelect() { {/* 영화관 목록 */} setSelectedAuditorium(id)} /> + {hasNext &&
} ); }