diff --git a/src/pages/feed/Feed.tsx b/src/pages/feed/Feed.tsx index 61983d4..1199100 100644 --- a/src/pages/feed/Feed.tsx +++ b/src/pages/feed/Feed.tsx @@ -11,6 +11,7 @@ import { getTotalFeeds } from '@/api/feeds/getTotalFeed'; import { getMyFeeds } from '@/api/feeds/getMyFeed'; import { useSocialLoginToken } from '@/hooks/useSocialLoginToken'; import { useInifinieScroll } from '@/hooks/useInifinieScroll'; +import LoadingSpinner from '@/components/common/LoadingSpinner'; import { Container, SkeletonWrapper } from './Feed.styled'; import type { PostData } from '@/types/post'; @@ -77,29 +78,7 @@ const Feed = () => { }, [activeTab]); const currentFeed = activeTab === '피드' ? totalFeed : myFeed; - - useEffect(() => { - const loadFeedsWithToken = async () => { - await waitForToken(); - - setTabLoading(true); - - try { - const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500)); - - if (activeTab === '피드') { - await Promise.all([loadTotalFeeds(), minLoadingTime]); - } else if (activeTab === '내 피드') { - await Promise.all([loadMyFeeds(), minLoadingTime]); - } - } finally { - setTabLoading(false); - setInitialLoading(false); - } - }; - - loadFeedsWithToken(); - }, [activeTab, waitForToken, loadTotalFeeds, loadMyFeeds]); + const showInitialLoading = currentFeed.isLoading && currentFeed.items.length === 0; return ( @@ -109,7 +88,7 @@ const Feed = () => { rightButtonClick={handleNoticeButton} /> - {initialLoading || tabLoading ? ( + {showInitialLoading ? ( activeTab === '내 피드' ? ( ) : ( @@ -126,7 +105,7 @@ const Feed = () => { diff --git a/src/pages/feed/FeedDetailPage.tsx b/src/pages/feed/FeedDetailPage.tsx index 83169c0..fc718d5 100644 --- a/src/pages/feed/FeedDetailPage.tsx +++ b/src/pages/feed/FeedDetailPage.tsx @@ -43,14 +43,8 @@ const FeedDetailPage = () => { try { setLoading(true); - - const feedResponse = await getFeedDetail(Number(feedId)); const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500)); - const [feedResponse, commentsResponse] = await Promise.all([ - getFeedDetail(Number(feedId)), - getComments(Number(feedId), { postType: 'FEED' }), - ]); - await minLoadingTime; + const [feedResponse] = await Promise.all([getFeedDetail(Number(feedId)), minLoadingTime]); setFeedData(feedResponse.data); setError(null); diff --git a/src/pages/feed/FollowerListPage.tsx b/src/pages/feed/FollowerListPage.tsx index 2d5e8e6..5f46506 100644 --- a/src/pages/feed/FollowerListPage.tsx +++ b/src/pages/feed/FollowerListPage.tsx @@ -19,52 +19,23 @@ const FollowerListPage = () => { const [totalCount, setTotalCount] = useState(0); - const handleBackClick = () => { - navigate(-1); - }; - - const loadUserList = useCallback( - async (cursor?: string) => { - if (loading) return; - - try { - setLoading(true); - setError(null); - let response; - - const minLoadingTime = !cursor ? new Promise(resolve => setTimeout(resolve, 500)) : null; - - if (type === 'followerlist') { - if (!userId) { - setError('사용자 ID가 없습니다.'); - return; - } - const [data] = await Promise.all([ - getFollowerList(userId, { size: 10, cursor: cursor || null }), - ]); - response = data; - } else { - const [data] = await Promise.all([ - getFollowingList({ size: 10, cursor: cursor || null }), - ]); - response = data; - } - await minLoadingTime; + const userList = useInifinieScroll({ + enabled: !!type, + reloadKey: `${type}-${userId ?? ''}`, + fetchPage: async cursor => { + const minLoadingTime = cursor ? null : new Promise(resolve => setTimeout(resolve, 500)); - let userData: FollowData[] = []; - if (type === 'followerlist') { - userData = (response.data as { followers: FollowData[] })?.followers || []; - } else { - userData = (response.data as { followings: FollowData[] })?.followings || []; + if (type === 'followerlist') { + if (!userId) { + throw new Error('사용자 ID가 없습니다.'); } - if (!response || !response.data) { - setError('API 응답이 없습니다.'); - return; - } const response = await getFollowerList(userId, { size: 10, cursor }); + if (minLoadingTime) await minLoadingTime; + const total = response.data.totalFollowerCount; if (typeof total === 'number') setTotalCount(total); + return { items: response.data.followers || [], nextCursor: response.data.nextCursor || null, @@ -73,8 +44,11 @@ const FollowerListPage = () => { } const response = await getFollowingList({ size: 10, cursor }); + if (minLoadingTime) await minLoadingTime; + const total = response.data.totalFollowingCount; if (typeof total === 'number') setTotalCount(total); + return { items: response.data.followings || [], nextCursor: response.data.nextCursor || null, @@ -89,11 +63,18 @@ const FollowerListPage = () => { window.scrollTo(0, 0); }, [type, userId]); + const showInitialSkeleton = userList.isLoading && userList.items.length === 0; + return ( - } onLeftClick={handleBackClick} title={title} /> + } + onLeftClick={() => navigate(-1)} + title={title} + /> 전체 {totalCount} - {loading && userList.length === 0 ? ( + + {showInitialSkeleton ? ( {Array.from({ length: 5 }).map((_, i) => ( @@ -116,6 +97,7 @@ const FollowerListPage = () => { isMyself={user.isMyself} /> ))} + {!userList.isLast &&
} {userList.isLoadingMore && (
diff --git a/src/pages/groupSearch/GroupSearch.tsx b/src/pages/groupSearch/GroupSearch.tsx index c96ba15..b3a7016 100644 --- a/src/pages/groupSearch/GroupSearch.tsx +++ b/src/pages/groupSearch/GroupSearch.tsx @@ -6,12 +6,11 @@ import rightChevron from '../../assets/common/right-Chevron.svg'; import { useState, useEffect, useCallback } from 'react'; import RecentSearchTabs from '@/components/search/RecentSearchTabs'; import GroupSearchResult from '@/components/search/GroupSearchResult'; -import LoadingSpinner from '@/components/common/LoadingSpinner'; import { getRecentSearch, type RecentSearchData } from '@/api/recentsearch/getRecentSearch'; import { deleteRecentSearch } from '@/api/recentsearch/deleteRecentSearch'; import { getSearchRooms } from '@/api/rooms/getSearchRooms'; import { useNavigate, useLocation } from 'react-router-dom'; -import { AllRoomsButton, LoadingMessage } from './GroupSearch.styled'; +import { AllRoomsButton } from './GroupSearch.styled'; import { useInifinieScroll } from '@/hooks/useInifinieScroll'; import { GroupCardSkeleton, RecentSearchTabsSkeleton } from '@/shared/ui/Skeleton'; import { Content } from '@/components/search/GroupSearchResult.styled'; @@ -25,7 +24,6 @@ const GroupSearch = () => { const [searchTerm, setSearchTerm] = useState(''); const [searchStatus, setSearchStatus] = useState('idle'); - const [selectedFilter, setSelectedFilter] = useState('마감임박순'); const toSortKey = useCallback( (f: string): SortKey => (f === '인기순' ? 'memberCount' : 'deadline'), @@ -33,12 +31,9 @@ const GroupSearch = () => { ); const [category, setCategory] = useState(''); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(''); - const [recentSearches, setRecentSearches] = useState([]); - const [isLoadingRecentSearches, setIsLoadingRecentSearches] = useState(true); const [searchTimeoutId, setSearchTimeoutId] = useState(null); - const [showTabs, setShowTabs] = useState(false); useEffect(() => { @@ -52,7 +47,7 @@ const GroupSearch = () => { } }, [searchStatus]); - const fetchRecentSearches = async () => { + const fetchRecentSearches = useCallback(async () => { try { setIsLoadingRecentSearches(true); const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500)); @@ -63,62 +58,14 @@ const GroupSearch = () => { } finally { setIsLoadingRecentSearches(false); } - }; - - const searchFirstPage = useCallback( - async ( - term: string, - sortKey: SortKey, - status: 'searching' | 'searched', - categoryParam: string, - isAllCategory: boolean = false, - keepPrevious: boolean = false, - ) => { - setIsLoading(true); - setError(null); - if (!keepPrevious) { - setRooms([]); - setNextCursor(null); - setIsLast(true); - } - - try { - const isFinalized = status === 'searched'; - const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500)); - const [res] = await Promise.all([ - getSearchRooms( - term.trim(), - sortKey, - undefined, - isFinalized, - categoryParam, - isAllCategory, - ), - ]); - await minLoadingTime; - if (res.isSuccess) { - const { roomList, nextCursor: nc, isLast: last } = res.data; - - setRooms(roomList); - setNextCursor(nc); - setIsLast(last); - } else { - setError(res.message || '검색 실패'); - } - } catch { - setError('네트워크 오류가 발생했습니다.'); - } finally { - setIsLoading(false); - } - }, - [], - ); + }, []); useEffect(() => { if (location.state?.allRooms) { navigate(location.pathname, { replace: true }); setSearchTerm(''); + setDebouncedSearchTerm(''); setSearchStatus('searched'); setShowTabs(true); setCategory(''); @@ -141,7 +88,7 @@ const GroupSearch = () => { setSearchStatus('searching'); setShowTabs(false); const id = setTimeout(() => { - searchFirstPage(trimmed, toSortKey(selectedFilter), 'searching', category, false, true); + setDebouncedSearchTerm(trimmed); }, 300); setSearchTimeoutId(id); }; @@ -180,81 +127,35 @@ const GroupSearch = () => { setCategory(''); }; - const searchStatusRef = useRef(searchStatus); - const categoryRef = useRef(category); - const selectedFilterRef = useRef(selectedFilter); - const searchTermRef = useRef(searchTerm); - - useEffect(() => { - searchStatusRef.current = searchStatus; - categoryRef.current = category; - selectedFilterRef.current = selectedFilter; - searchTermRef.current = searchTerm; - }); - - useEffect(() => { - if (searchStatus !== 'searched') return; - - const term = searchTermRef.current.trim(); - const currentCategory = categoryRef.current; - const isAllCategory = !term && currentCategory === ''; - - searchFirstPage( - term, - toSortKey(selectedFilterRef.current), - 'searched', - currentCategory, - isAllCategory, - true, - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchStatus, searchTerm]); - - useEffect(() => { - if (searchStatusRef.current !== 'searched') return; - - const term = searchTermRef.current.trim(); - const isAllCategory = !term && category === ''; - - searchFirstPage(term, toSortKey(selectedFilter), 'searched', category, isAllCategory, true); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedFilter, category]); - useEffect(() => { if (searchStatus === 'searched') { setDebouncedSearchTerm(searchTerm.trim()); } }, [searchStatus, searchTerm]); - const id = setTimeout(() => { - const currentCategory = categoryRef.current; - searchFirstPage(term, toSortKey(selectedFilter), 'searching', currentCategory, false, true); - }, 300); - setSearchTimeoutId(id); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchTerm, searchStatus, selectedFilter]); + const queryTerm = searchStatus === 'searched' ? searchTerm.trim() : debouncedSearchTerm.trim(); + const isAllCategory = !queryTerm && category === ''; - const loadMore = useCallback(async () => { - const trimmedTerm = searchTerm.trim(); - const isAllCategory = !trimmedTerm && category === ''; - if ((!isAllCategory && !trimmedTerm) || !nextCursor || isLast || isLoadingMore) return; - try { - setIsLoadingMore(true); - const isFinalized = searchStatus === 'searched'; - const isAllCategory = !queryTerm && category === ''; + const searchResult = useInifinieScroll({ + enabled: searchStatus !== 'idle' && (searchStatus === 'searched' || !!queryTerm), + reloadKey: `${searchStatus}-${queryTerm}-${selectedFilter}-${category}`, + fetchPage: async cursor => { if (searchStatus === 'searching' && !queryTerm) { return { items: [], nextCursor: null, isLast: true }; } + const minLoadingTime = cursor ? null : new Promise(resolve => setTimeout(resolve, 500)); const res = await getSearchRooms( queryTerm, toSortKey(selectedFilter), cursor ?? undefined, - isFinalized, + searchStatus === 'searched', category, isAllCategory, ); + if (minLoadingTime) await minLoadingTime; + if (!res.isSuccess) { throw new Error(res.message || '검색 실패'); } @@ -317,7 +218,7 @@ const GroupSearch = () => { {searchStatus !== 'idle' ? ( <> - {isLoading && rooms.length === 0 ? ( + {searchResult.isLoading && searchResult.items.length === 0 ? ( {Array.from({ length: 5 }).map((_, i) => ( diff --git a/src/pages/memory/Memory.tsx b/src/pages/memory/Memory.tsx index f2ef959..0f81557 100644 --- a/src/pages/memory/Memory.tsx +++ b/src/pages/memory/Memory.tsx @@ -6,7 +6,6 @@ import MemoryContent from '../../components/memory/MemoryContent/MemoryContent'; import MemoryAddButton from '../../components/memory/MemoryAddButton/MemoryAddButton'; import Snackbar from '../../components/common/Modal/Snackbar'; import GlobalCommentBottomSheet from '../../components/common/CommentBottomSheet/GlobalCommentBottomSheet'; -import LoadingSpinner from '../../components/common/LoadingSpinner'; import { useCommentBottomSheetStore } from '@/stores/commentBottomSheetStore'; import { useInifinieScroll } from '@/hooks/useInifinieScroll'; import { Container, FixedHeader, ScrollableContent, FloatingElements } from './Memory.styled'; @@ -71,6 +70,11 @@ const Memory = () => { const [selectedPageRange, setSelectedPageRange] = useState<{ start: number; end: number } | null>( null, ); + const [showUploadProgress, setShowUploadProgress] = useState(false); + const [roomCompleted, setRoomCompleted] = useState(false); + const [totalPages, setTotalPages] = useState(0); + const [currentUserPage, setCurrentUserPage] = useState(0); + const scrollRootRef = useRef(null); useEffect(() => { const searchParams = new URLSearchParams(location.search); @@ -78,27 +82,15 @@ const Memory = () => { const filterParam = searchParams.get('filter'); if (pageParam && filterParam === 'poll') { - const page = parseInt(pageParam); + const page = parseInt(pageParam, 10); if (!isNaN(page)) { setSelectedPageRange({ start: page, end: page }); setActiveFilter('page'); setActiveTab('group'); - navigate(location.pathname, { replace: true }); } } - }, [location.search]); - - const [error, setError] = useState(null); - const [loading, setLoading] = useState(true); - - const [showUploadProgress, setShowUploadProgress] = useState(false); - - const [roomCompleted, setRoomCompleted] = useState(false); - - const [totalPages, setTotalPages] = useState(0); - const [currentUserPage, setCurrentUserPage] = useState(0); - const scrollRootRef = useRef(null); + }, [location.search, location.pathname, navigate]); const recordsList = useInifinieScroll({ enabled: !!roomId, @@ -109,14 +101,6 @@ const Memory = () => { return { items: [], nextCursor: null, isLast: true }; } - const loadMemoryPosts = useCallback(async () => { - if (!roomId) { - return; - } - setError(null); - setLoading(true); - - try { const params: GetMemoryPostsParams = { roomId: parseInt(roomId, 10), type: activeTab === 'group' ? 'group' : 'mine', @@ -135,11 +119,10 @@ const Memory = () => { params.isPageFilter = true; } - const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500)); - const [response] = await Promise.all([getMemoryPosts(params), minLoadingTime]); - - if (response.isSuccess) { - const convertedRecords = response.data.postList.map(convertPostToRecord); + try { + const minLoadingTime = cursor ? null : new Promise(resolve => setTimeout(resolve, 500)); + const response = await getMemoryPosts(params); + if (minLoadingTime) await minLoadingTime; if (!response.isSuccess) { throw new Error(response.message || '기록을 불러오는 중 오류가 발생했습니다.'); @@ -167,22 +150,20 @@ const Memory = () => { } throw new Error('기록을 불러오는 중 오류가 발생했습니다.'); } - - setError('기록을 불러오는 중 오류가 발생했습니다.'); - } finally { - setLoading(false); - } - }, [roomId, activeTab, selectedSort, activeFilter, selectedPageRange]); + }, + rootMargin: '100px 0px', + threshold: 0.1, + }); + const { setItems: setRecordItems } = recordsList; useEffect(() => { const checkRoomStatus = async () => { if (!roomId) return; try { - const response = await getRoomPlaying(parseInt(roomId)); + const response = await getRoomPlaying(parseInt(roomId, 10)); if (response.isSuccess) { - const completed = isRoomCompleted(response.data.progressEndDate); - setRoomCompleted(completed); + setRoomCompleted(isRoomCompleted(response.data.progressEndDate)); } } catch (error) { console.error('모임방 상태 확인 오류:', error); @@ -199,6 +180,7 @@ const Memory = () => { postType?: 'RECORD' | 'VOTE'; openComments?: boolean; } | null; + const state = (location.state as MemoryLocationState) || null; const initialPage = state?.page; if (initialPage && !selectedPageRange) { @@ -218,34 +200,25 @@ const Memory = () => { const newRecord = location.state.newRecord as Record; setShowUploadProgress(true); setRecordItems(prev => [newRecord, ...prev]); - navigate(location.pathname, { replace: true }); } }, [location.state, navigate, location.pathname, setRecordItems]); - const currentRecords = useMemo(() => recordsList.items, [recordsList.items]); - - const sortedRecords = useMemo(() => { - return currentRecords; - }, [currentRecords]); - const filteredRecords = useMemo(() => { - const filtered = sortedRecords; - if (activeFilter === 'overall') { - const overallRecords = filtered.filter(record => record.recordType === 'overall'); - return overallRecords; - } else if (activeFilter === 'page' && selectedPageRange) { - const pageRecords = filtered.filter(record => { + return recordsList.items.filter(record => record.recordType === 'overall'); + } + + if (activeFilter === 'page' && selectedPageRange) { + return recordsList.items.filter(record => { if (record.recordType === 'overall') return false; - const page = parseInt(record.pageRange || '0'); + const page = parseInt(record.pageRange || '0', 10); return page >= selectedPageRange.start && page <= selectedPageRange.end; }); - return pageRecords; } - return filtered; - }, [sortedRecords, activeFilter, selectedPageRange]); + return recordsList.items; + }, [recordsList.items, activeFilter, selectedPageRange]); const handleBackClick = useCallback(() => { if (roomId) { @@ -293,6 +266,7 @@ const Memory = () => { }, []); const readingProgress = totalPages > 0 ? Math.round((currentUserPage / totalPages) * 100) : 0; + const showInitialSkeleton = recordsList.isLoading && recordsList.items.length === 0; if (recordsList.error) { return ( @@ -315,10 +289,11 @@ const Memory = () => { - - {loading ? ( + + + {showInitialSkeleton ? ( - + {activeTab === 'group' && ( { ) : ( - + <> + + {!recordsList.isLast &&
} + )} diff --git a/src/pages/mypage/SavePage.tsx b/src/pages/mypage/SavePage.tsx index a953466..67ef556 100644 --- a/src/pages/mypage/SavePage.tsx +++ b/src/pages/mypage/SavePage.tsx @@ -69,121 +69,6 @@ const SavePage = () => { navigate('/mypage'); }; - const loadSavedBooks = useCallback(async (cursor: string | null = null) => { - try { - setBookLoading(true); - const response = await getSavedBooksInMy(cursor); - - if (cursor === null) { - setSavedBooks(response.data.bookList); - } else { - setSavedBooks(prev => [...prev, ...response.data.bookList]); - } - - setBookNextCursor(response.data.nextCursor); - setBookIsLast(response.data.isLast); - } catch (error) { - console.error('저장된 책 목록 로드 실패:', error); - } finally { - setBookLoading(false); - } - }, []); - - const loadSavedFeeds = useCallback(async (cursor: string | null = null) => { - try { - setFeedLoading(true); - const response = await getSavedFeedsInMy(cursor); - - if (cursor === null) { - setSavedFeeds(response.data.feedList); - } else { - setSavedFeeds(prev => [...prev, ...response.data.feedList]); - } - - setFeedNextCursor(response.data.nextCursor); - setFeedIsLast(response.data.isLast); - } catch (error) { - console.error('저장된 피드 로드 실패:', error); - } finally { - setFeedLoading(false); - } - }, []); - - const loadMoreBooks = useCallback(async () => { - if (!bookNextCursor || bookIsLast || bookLoading) return; - - try { - await loadSavedBooks(bookNextCursor); - } catch (error) { - console.error('책 추가 로드 실패:', error); - } - }, [bookNextCursor, bookIsLast, bookLoading, loadSavedBooks]); - - const lastBookElementCallback = useCallback( - (node: HTMLDivElement | null) => { - if (bookLoading || bookIsLast) return; - - if (node) { - const observer = new IntersectionObserver(entries => { - if (entries[0].isIntersecting && !bookLoading && !bookIsLast) { - loadMoreBooks(); - } - }); - - observer.observe(node); - } - }, - [bookLoading, bookIsLast, loadMoreBooks], - ); - - useEffect(() => { - const loadAllData = async () => { - try { - setInitialLoading(true); - - const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500)); - const [feedsResponse, booksResponse] = await Promise.all([ - getSavedFeedsInMy(null), - getSavedBooksInMy(), - ]); - await minLoadingTime; - - setSavedFeeds(feedsResponse.data.feedList); - setFeedNextCursor(feedsResponse.data.nextCursor); - setFeedIsLast(feedsResponse.data.isLast); - - setSavedBooks(booksResponse.data.bookList); - setBookNextCursor(booksResponse.data.nextCursor); - setBookIsLast(booksResponse.data.isLast); - } catch (error) { - console.error('초기 데이터 로드 실패:', error); - } finally { - setInitialLoading(false); - } - }; - - loadAllData(); - }, []); - - useEffect(() => { - const observer = new IntersectionObserver( - entries => { - entries.forEach(entry => { - if (entry.isIntersecting && !feedIsLast && !feedLoading && feedNextCursor) { - loadSavedFeeds(feedNextCursor); - } - }); - }, - { threshold: 0.1 }, - ); - - if (feedObserverRef.current) { - observer.observe(feedObserverRef.current); - } - - return () => observer.disconnect(); - }, [feedIsLast, feedLoading, feedNextCursor, loadSavedFeeds]); - useEffect(() => { window.scrollTo(0, 0); }, [activeTab]); @@ -230,7 +115,7 @@ const SavePage = () => { title="저장" /> - {initialLoading ? ( + {showInitialLoading ? ( activeTab === '피드' ? ( {Array.from({ length: 3 }).map((_, index) => ( diff --git a/src/pages/searchBook/SearchBook.tsx b/src/pages/searchBook/SearchBook.tsx index 804e6b8..a2494dc 100644 --- a/src/pages/searchBook/SearchBook.tsx +++ b/src/pages/searchBook/SearchBook.tsx @@ -21,6 +21,7 @@ import { EmptyTitle, EmptySubText, FeedPostContainer, + LoadingBox, } from './SearchBook.styled'; import { useNavigate, useParams } from 'react-router-dom'; import leftArrow from '../../assets/common/leftArrow.svg'; @@ -103,64 +104,23 @@ const SearchBook = () => { fetchBookDetail(); }, [isbn]); - const loadFirstFeeds = useCallback(async () => { - if (!isbn) return; - try { - setIsLoadingFeeds(true); - setFeeds([]); - setNextCursor(null); - setIsLast(true); - - const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500)); - const [res] = await Promise.all([ - getFeedsByIsbn(isbn, toFeedSort(selectedFilter), null), - ]); - await minLoadingTime; - if (res.isSuccess) { - setFeeds(res.data.feeds); - setNextCursor(res.data.nextCursor); - setIsLast(res.data.isLast); - } - } catch { - // no-op - } finally { - setIsLoadingFeeds(false); - } - }, [isbn, selectedFilter]); - - useEffect(() => { - loadFirstFeeds(); - }, [loadFirstFeeds]); - - const loadMore = useCallback(async () => { - if (!isbn || !nextCursor || isLast || isLoadingMore) return; - try { - setIsLoadingMore(true); - const res = await getFeedsByIsbn(isbn, toFeedSort(selectedFilter), nextCursor); - if (res.isSuccess) { - setFeeds(prev => [...prev, ...res.data.feeds]); - setNextCursor(res.data.nextCursor); - setIsLast(res.data.isLast); + const feeds = useInifinieScroll({ + enabled: !!isbn, + reloadKey: `${isbn ?? ''}-${selectedFilter}`, + fetchPage: async cursor => { + if (!isbn) { + return { items: [], nextCursor: null, isLast: true }; } - } catch { - // no-op - } finally { - setIsLoadingMore(false); - } - }, [isbn, nextCursor, isLast, isLoadingMore, selectedFilter]); - - const lastFeedElementCallback = useCallback( - (node: HTMLDivElement | null) => { - if (isLoadingMore || isLast) return; - if (observerRef.current) observerRef.current.disconnect(); + const minLoadingTime = cursor ? null : new Promise(resolve => setTimeout(resolve, 500)); + const res = await getFeedsByIsbn(isbn, toFeedSort(selectedFilter), cursor); + if (minLoadingTime) await minLoadingTime; - observerRef.current = new IntersectionObserver(entries => { - if (entries[0].isIntersecting && !isLoadingMore && !isLast) { - loadMore(); - } - }); - if (node) observerRef.current.observe(node); + return { + items: res.data.feeds, + nextCursor: res.data.nextCursor || null, + isLast: res.data.isLast, + }; }, rootMargin: '100px 0px', threshold: 0.1, @@ -302,13 +262,13 @@ const SearchBook = () => { setSelectedFilter={filter => setSelectedFilter(filter as (typeof FILTER)[number])} /> - {isLoadingFeeds && feeds.length === 0 ? ( + {feeds.isLoading && feeds.items.length === 0 ? ( {Array.from({ length: 3 }).map((_, i) => ( ))} - ) : feeds.length > 0 ? ( + ) : feeds.items.length > 0 ? ( {feeds.items.map(post => (