Skip to content

fix: 책 검색 모달 탭 유지 및 무한스크롤 기능 추가#166

Merged
ljh130334 merged 1 commit into
developfrom
feat/search-hotfix
Aug 19, 2025
Merged

fix: 책 검색 모달 탭 유지 및 무한스크롤 기능 추가#166
ljh130334 merged 1 commit into
developfrom
feat/search-hotfix

Conversation

@ljh130334

@ljh130334 ljh130334 commented Aug 19, 2025

Copy link
Copy Markdown
Member

#️⃣ 연관된 이슈

생략

📝 작업 내용

이번 PR에서는 책 검색 바텀시트의 사용성을 크게 개선하는 두 가지 주요 문제를 해결했습니다.

1. 탭 사라짐 문제 해결
기존에는 '내 모임 책' 탭으로 전환했을 때 등록된 책이 없어서 예외처리 화면(빈 상태)이 표시되면 탭 자체가 사라지는 문제가 있었습니다. 이는 사용자가 다른 탭으로 다시 전환할 수 없게 만드는 치명적인 UX 문제였습니다. useBookSearch.ts의 showTabs 로직을 수정하여 검색 모드가 아닐 때는 빈 상태와 관계없이 항상 탭이 표시되도록 개선했습니다.

2. 검색 결과 무한스크롤 기능 구현
검색 결과가 많을 때 첫 번째 페이지만 표시되고 추가 결과를 볼 수 없었던 문제를 해결했습니다. API는 이미 페이지네이션을 지원하고 있었지만 클라이언트에서 이를 활용하지 못하고 있었습니다. Intersection Observer API를 활용하여 사용자가 목록 하단에 도달하면 자동으로 다음 페이지를 로드하는 무한스크롤을 구현했습니다. 또한 페이지 간 책 ID 중복을 방지하기 위해 각 페이지의 시작 인덱스를 고려한 고유 ID 생성 로직도 추가했습니다.

🕸️ 주요 변경사항

  • useBookSearch.ts: 페이지네이션 상태 관리(currentPage, hasNextPage, isLoadingMore) 추가
  • performSearch 함수: 신규 검색과 추가 로딩을 구분하여 결과를 누적하는 로직 구현
  • BookList.tsx: Intersection Observer를 이용한 무한스크롤 감지 및 로딩 상태 표시
  • getSearchBooks.ts: 페이지별 고유 ID 생성을 위한 startIndex 파라미터 추가

Summary by CodeRabbit

  • 신규 기능
    • 도서 검색 결과에 무한 스크롤 도입: 화면 하단에 도달하면 추가 결과가 자동으로 로드됩니다.
    • “더 불러오는 중” 상태 표시 추가로 로딩 상황을 명확히 안내합니다.
    • 페이지네이션 지원으로 긴 검색 결과를 순차적으로 불러와 성능과 사용성을 개선했습니다.
    • 검색어가 없을 때 기본 탭 노출 동작을 단순화해 탐색 경험을 향상했습니다.

@vercel

vercel Bot commented Aug 19, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
thip Ready Ready Preview Comment Aug 19, 2025 3:28am

@coderabbitai

coderabbitai Bot commented Aug 19, 2025

Copy link
Copy Markdown

Walkthrough

검색 결과 페이징을 도입했습니다. 변환 유틸에 startIndex 파라미터를 추가해 ID 산정 방식을 변경하고, BookList에 인터섹션 옵저버 기반의 무한 스크롤을 추가했습니다. BookSearchBottomSheet와 useBookSearch 훅은 다음 페이지 로딩, 상태 플래그, API 호출 흐름을 반영하도록 확장되었습니다.

Changes

Cohort / File(s) Summary
API 변환 유틸
src/api/books/getSearchBooks.ts
convertToSearchedBooks에 선택적 startIndex=0 추가. ID 계산을 startIndex + index + 1로 변경. 매핑 필드(제목, 저자, 출판사, 커버, ISBN)는 동일. 내보내기 시그니처 업데이트.
BookList 무한 스크롤
src/components/common/BookSearchBottomSheet/BookList.tsx
IntersectionObserver로 마지막 아이템 관찰해 onLoadMore 트리거. 새 props 추가: onLoadMore, hasNextPage=false, isLoadingMore=false, isSearchMode=false. 마지막 아이템에 ref 부착, 키를 id-isbn로 변경. 로딩 인디케이터 UI 추가. 클린업 처리.
BottomSheet 연동
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx
훅에서 확장된 hasNextPage, isLoadingMore, loadMoreSearchResults 소비. BookList에 전달. isSearchModesearchQuery.trim() !== ''로 계산.
검색 훅 페이징
src/components/common/BookSearchBottomSheet/useBookSearch.ts
페이징 상태(currentPage, hasNextPage, isLoadingMore) 추가. performSearch(query, page=1, isNewSearch=true)로 확장. loadMoreSearchResults 추가. 새 검색은 결과 교체, 추가 페이지는 append하며 convertToSearchedBooks(..., startIndex) 사용. 로딩/에러 처리 분기. 쿼리 비면 상태 리셋.

Sequence Diagram(s)

sequenceDiagram
  participant U as User
  participant BL as BookList (IntersectionObserver)
  participant BS as BookSearchBottomSheet
  participant HOOK as useBookSearch
  participant API as getSearchBooks
  participant CVT as convertToSearchedBooks

  U->>BL: 스크롤하여 마지막 아이템 노출
  BL-->>BL: 조건 확인(isSearchMode && hasNextPage && !isLoadingMore)
  BL->>BS: onLoadMore()
  BS->>HOOK: loadMoreSearchResults()
  HOOK->>API: 검색 API 호출(page = currentPage+1)
  API-->>HOOK: 응답(data, last)
  HOOK->>CVT: convertToSearchedBooks(items, startIndex = 기존길이)
  CVT-->>HOOK: 변환된 SearchedBook[]
  HOOK-->>BS: 상태 업데이트(append, hasNextPage, isLoadingMore=false)
  BS-->>BL: props 업데이트(hasNextPage/isLoadingMore)
  BL-->>U: 더 많은 결과 표시
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

🐞 BugFix, ✨ Feature, 📬 API

Suggested reviewers

  • heeeeyong

Poem

꼬꼬닥 키보드 위, 토끼 두 귀 쫑긋 세워
스르륵 스크롤 끝마다 책이 톡톡 나타나네 📚
페이지 또 한 장, startIndex 살포시 더해
키도 꽉, 로딩도 살짝—부지런한 앞발 춤춘다 🐇
다음 페이지로 둥실, 독서 모험 계속!

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/search-hotfix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@ljh130334 ljh130334 added the 🐞 BugFix Something isn't working label Aug 19, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (7)
src/api/books/getSearchBooks.ts (1)

61-69: SearchedBook 인터페이스 중복 정의 통합 권장

타입 드리프트를 방지하고 유지보수성을 높이기 위해 SearchedBook 인터페이스를 한 곳에서만 정의하고 나머지에서는 import 해서 사용하는 구조로 리팩토링을 권장합니다.

확인된 중복 정의 위치:

  • src/api/books/getSearchBooks.ts (line 13): export interface SearchedBook { … }
  • src/pages/search/Search.tsx (line 16): export interface SearchedBook { … }

제안:

  • 공통 타입 정의 전용 파일(e.g. src/types/SearchedBook.ts) 또는 기존 getSearchBooks.ts에서만 export
  • Search.tsx 등 다른 파일에서는 해당 타입을 import하여 사용
src/components/common/BookSearchBottomSheet/BookList.tsx (2)

44-58: 무한스크롤 중복 호출 가능성: IntersectionObserver 콜백에 isLoadingMore 체크 및 안전 처리 필요

현재 콜백 조건에 isLoadingMore가 포함되어 있지 않아, 관찰 대상이 한 번에 여러 차례 교차되면 onLoadMore가 연속 호출될 수 있습니다. 네트워크 중복 요청과 결과 중복 누적을 방지하려면 콜백에서 로딩 중 여부를 추가로 확인하고, Promise 거부를 안전하게 무시(또는 로깅)하세요. 또한 약간의 rootMargin을 주면 사용자 도달 직전에 프리페치가 가능합니다.

적용 예시:

-    observerRef.current = new IntersectionObserver(entries => {
-      if (entries[0].isIntersecting && hasNextPage && onLoadMore && isSearchMode) {
-        onLoadMore();
-      }
-    });
+    observerRef.current = new IntersectionObserver(
+      (entries) => {
+        if (entries[0].isIntersecting && hasNextPage && onLoadMore && isSearchMode && !isLoadingMore) {
+          onLoadMore().catch(() => {});
+        }
+      },
+      { root: null, rootMargin: '160px 0px', threshold: 0 }
+    );

37-39: 불필요한 ref 제거 제안

loadingRef가 선언만 되고 별도 사용되지 않습니다. 제거하여 간소화하세요.

-  const loadingRef = useRef<HTMLDivElement | null>(null);
...
-        <LoadingContainer ref={loadingRef}>
+        <LoadingContainer>

Also applies to: 86-88

src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (1)

107-116: isSearchMode 계산은 훅에서 제공하도록 일관성 유지 제안

여기서 isSearchMode={searchQuery.trim() !== ''}를 직접 계산하고 있는데, 훅(useBookSearch) 내부에서 동일 로직의 불변값을 계산해 함께 반환하면(예: isSearchMode) 뷰/훅 간 중복을 줄이고 변경 시 한 곳만 고치면 됩니다.

-                isSearchMode={searchQuery.trim() !== ''}
+                isSearchMode={isSearchMode}

훅에서 isSearchMode를 이미 계산하고 있다면 공개(return) 목록에 포함시켜 재사용하는 방향을 권장합니다.

src/components/common/BookSearchBottomSheet/useBookSearch.ts (3)

82-99: 검색 API 호출 시 isFinalized를 true로 고정: 요구사항 확인 필요

getSearchBooks(query.trim(), page, true)로 항상 확정 도서만 검색하는 형태라면, 사용자가 기대하는 결과(예: 모든 상태의 도서 검색)와 다를 수 있습니다. 서버 사양/요구사항을 다시 확인해 주세요. 필요 시 UI에서 필터로 제어하거나 훅의 옵션으로 노출하는 것이 더 유연합니다.


101-113: 검색 응답 레이스 컨디션 방지: 최신 요청만 반영하도록 가드 필요

디바운스/무한스크롤 특성상, 늦게 도착한 이전 요청 응답이 최신 검색 결과를 덮어쓸 수 있습니다. 간단한 요청 키 가드로 해결 가능합니다.

핵심 변경 예시:

   try {
+    const searchKey = `${query.trim()}::${page}`;
+    latestSearchKeyRef.current = searchKey;
     if (isNewSearch) {
       setIsLoading(true);
       setCurrentPage(1);
     } else {
       setIsLoadingMore(true);
     }
     setError(null);
     
     const response = await getSearchBooks(query.trim(), page, true);
     
     if (response.isSuccess) {
+      if (latestSearchKeyRef.current !== searchKey) {
+        // 이미 더 최신 요청이 진행/완료됨 → 이번 결과는 폐기
+        return;
+      }
       const startIndex = isNewSearch ? 0 : searchResults.length;
       const convertedResults = convertToSearchedBooks(response.data.searchResult, startIndex);

이 변경을 위해 훅 상단에 ref를 하나 추가해야 합니다(아래 참고).

추가로 필요한 import/선언(선택 적용):

// 상단 import에 추가
import { useRef } from 'react';

// 훅 내부 상단에 추가
const latestSearchKeyRef = useRef('');

Also applies to: 126-131


134-142: loadMore 가드 보강 제안

초기 검색 로딩 중(isLoading=true)에도 교차 이벤트가 발생하면 loadMore가 호출될 수 있습니다. 가드를 한 줄 보강하면 안전합니다.

-    if (!searchQuery.trim() || isLoadingMore || !hasNextPage) {
+    if (!searchQuery.trim() || isLoadingMore || isLoading || !hasNextPage) {
       return;
     }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0e73085 and 569952c.

📒 Files selected for processing (4)
  • src/api/books/getSearchBooks.ts (1 hunks)
  • src/components/common/BookSearchBottomSheet/BookList.tsx (3 hunks)
  • src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (2 hunks)
  • src/components/common/BookSearchBottomSheet/useBookSearch.ts (5 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (1)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (1)
  • BookList (159-163)
src/api/books/getSearchBooks.ts (1)
src/pages/search/Search.tsx (1)
  • SearchedBook (16-23)
src/components/common/BookSearchBottomSheet/useBookSearch.ts (1)
src/api/books/getSearchBooks.ts (2)
  • getSearchBooks (41-59)
  • convertToSearchedBooks (61-70)
src/components/common/BookSearchBottomSheet/BookList.tsx (1)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (3)
  • BookList (159-163)
  • BookItem (165-177)
  • LoadingText (220-224)
🔇 Additional comments (2)
src/components/common/BookSearchBottomSheet/BookList.tsx (1)

69-75: 키 구성 방식 적절함

key={${book.id}-${book.isbn}}로 페이지 누적 시에도 키 충돌 가능성을 잘 피했습니다. 무한스크롤 시 안정성이 좋아집니다.

src/components/common/BookSearchBottomSheet/useBookSearch.ts (1)

206-207: 탭 표시 정책 단순화 LGTM

검색 모드가 아닐 때 탭을 항상 노출하도록 간소화한 부분이 PR 목적과 일치합니다.

@ljh130334 ljh130334 merged commit 4e6b360 into develop Aug 19, 2025
3 checks passed
@ljh130334 ljh130334 deleted the feat/search-hotfix branch September 1, 2025 03:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 BugFix Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant