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
4 changes: 2 additions & 2 deletions src/api/books/getSearchBooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ export const getSearchBooks = async (
}
};

export const convertToSearchedBooks = (apiBooks: BookSearchItem[]): SearchedBook[] => {
export const convertToSearchedBooks = (apiBooks: BookSearchItem[], startIndex: number = 0): SearchedBook[] => {
return apiBooks.map((book, index) => ({
id: index + 1,
id: startIndex + index + 1,
title: book.title,
author: book.authorName,
publisher: book.publisher,
Expand Down
57 changes: 54 additions & 3 deletions src/components/common/BookSearchBottomSheet/BookList.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useEffect, useRef, useCallback } from 'react';
import {
BookList as StyledBookList,
BookItem,
BookCover,
BookInfo,
BookTitle,
LoadingContainer,
LoadingText,
} from './BookSearchBottomSheet.styled';

export interface Book {
Expand All @@ -17,17 +20,58 @@ export interface Book {
interface BookListProps {
books: Book[];
onBookSelect: (book: Book) => void;
onLoadMore?: () => Promise<void>;
hasNextPage?: boolean;
isLoadingMore?: boolean;
isSearchMode?: boolean;
}

const BookList = ({ books, onBookSelect }: BookListProps) => {
const BookList = ({
books,
onBookSelect,
onLoadMore,
hasNextPage = false,
isLoadingMore = false,
isSearchMode = false
}: BookListProps) => {
const observerRef = useRef<IntersectionObserver | null>(null);
const loadingRef = useRef<HTMLDivElement | null>(null);

const handleImageError = (e: React.SyntheticEvent<HTMLImageElement>) => {
e.currentTarget.style.display = 'none';
};

// 무한스크롤을 위한 Intersection Observer 설정
const lastBookElementRef = useCallback((node: HTMLDivElement | null) => {
if (isLoadingMore) return;

if (observerRef.current) observerRef.current.disconnect();

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

if (node) observerRef.current.observe(node);
}, [isLoadingMore, hasNextPage, onLoadMore, isSearchMode]);

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

return (
<StyledBookList>
{books.map(book => (
<BookItem key={book.id} onClick={() => onBookSelect(book)}>
{books.map((book, index) => (
<BookItem
key={`${book.id}-${book.isbn}`}
onClick={() => onBookSelect(book)}
ref={index === books.length - 1 ? lastBookElementRef : null}
>
<BookCover>
<img src={book.cover} alt={book.title} onError={handleImageError} />
</BookCover>
Expand All @@ -36,6 +80,13 @@ const BookList = ({ books, onBookSelect }: BookListProps) => {
</BookInfo>
</BookItem>
))}

{/* 검색 모드에서 더 많은 데이터가 있고 로딩 중일 때 로딩 표시 */}
{isSearchMode && isLoadingMore && (
<LoadingContainer ref={loadingRef}>
<LoadingText>더 많은 책을 불러오는 중...</LoadingText>
</LoadingContainer>
)}
</StyledBookList>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ const BookSearchBottomSheet = ({ isOpen, onClose, onSelectBook }: BookSearchBott
error,
showEmptyState,
showTabs,
hasNextPage,
isLoadingMore,
setSearchQuery,
handleTabChange,
loadInitialData,
performSearch,
loadMoreSearchResults,
} = useBookSearch();

// 컴포넌트가 열릴 때 초기 데이터 로드
Expand Down Expand Up @@ -101,7 +104,16 @@ const BookSearchBottomSheet = ({ isOpen, onClose, onSelectBook }: BookSearchBott
onClose={onClose}
/>

{showBookList && <BookList books={filteredBooks} onBookSelect={handleBookSelect} />}
{showBookList && (
<BookList
books={filteredBooks}
onBookSelect={handleBookSelect}
onLoadMore={loadMoreSearchResults}
hasNextPage={hasNextPage}
isLoadingMore={isLoadingMore}
isSearchMode={searchQuery.trim() !== ''}
/>
)}
</BookListContainer>
</Content>
</BottomSheetContainer>
Expand Down
59 changes: 50 additions & 9 deletions src/components/common/BookSearchBottomSheet/useBookSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const useBookSearch = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [searchTimeoutId, setSearchTimeoutId] = useState<NodeJS.Timeout | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [hasNextPage, setHasNextPage] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);

// API에서 받은 데이터를 Book 타입으로 변환하는 함수
const convertSavedBookToBook = (savedBook: SavedBook): Book => ({
Expand Down Expand Up @@ -76,34 +79,67 @@ export const useBookSearch = () => {
};

// 실제 검색 API 호출 함수
const performSearch = async (query: string) => {
const performSearch = async (query: string, page: number = 1, isNewSearch: boolean = true) => {
if (!query.trim()) {
setSearchResults([]);
setCurrentPage(1);
setHasNextPage(false);
return;
}

try {
setIsLoading(true);
if (isNewSearch) {
setIsLoading(true);
setCurrentPage(1);
} else {
setIsLoadingMore(true);
}
setError(null);

const response = await getSearchBooks(query.trim(), 1, true);
const response = await getSearchBooks(query.trim(), page, true);

if (response.isSuccess) {
const convertedResults = convertToSearchedBooks(response.data.searchResult);
setSearchResults(convertedResults);
const startIndex = isNewSearch ? 0 : searchResults.length;
const convertedResults = convertToSearchedBooks(response.data.searchResult, startIndex);

if (isNewSearch) {
setSearchResults(convertedResults);
} else {
setSearchResults(prev => [...prev, ...convertedResults]);
}

setCurrentPage(page);
setHasNextPage(!response.data.last);
} else {
setError(response.message || '검색에 실패했습니다.');
setSearchResults([]);
if (isNewSearch) {
setSearchResults([]);
}
}
} catch (err) {
console.error('검색 오류:', err);
setError('검색 중 오류가 발생했습니다.');
setSearchResults([]);
if (isNewSearch) {
setSearchResults([]);
}
} finally {
setIsLoading(false);
if (isNewSearch) {
setIsLoading(false);
} else {
setIsLoadingMore(false);
}
}
};

// 더 많은 검색 결과 로드
const loadMoreSearchResults = async () => {
if (!searchQuery.trim() || isLoadingMore || !hasNextPage) {
return;
}

await performSearch(searchQuery.trim(), currentPage + 1, false);
};

// 검색어 변경 핸들러 (디바운싱 적용)
const handleSearchQueryChange = (query: string) => {
setSearchQuery(query);
Expand All @@ -120,6 +156,8 @@ export const useBookSearch = () => {
} else {
setSearchResults([]);
setError(null);
setCurrentPage(1);
setHasNextPage(false);
}
};

Expand Down Expand Up @@ -165,7 +203,7 @@ export const useBookSearch = () => {
const currentTabBooks = activeTab === 'saved' ? savedBooks : groupBooks;
const hasBooks = isSearchMode ? searchResults.length > 0 : currentTabBooks.length > 0;
const showEmptyState = !isLoading && !error && !hasBooks;
const showTabs = !isSearchMode && !showEmptyState;
const showTabs = !isSearchMode; // 검색 모드가 아닐 때는 항상 탭 표시

// 컴포넌트 언마운트 시 타이머 정리
useEffect(() => {
Expand All @@ -186,11 +224,14 @@ export const useBookSearch = () => {
hasBooks,
showEmptyState,
showTabs,
hasNextPage,
isLoadingMore,

// Actions
setSearchQuery: handleSearchQueryChange,
handleTabChange,
loadInitialData,
performSearch,
loadMoreSearchResults,
};
};