feat: 저장한 책 또는 참여 중 모임의 책 조회 API 연동#121
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. Walkthrough서버에서 저장/참여 도서 목록을 조회하는 API 헬퍼(getSavedBooks)를 추가하고 BookSearchBottomSheet를 컴포넌트·훅(useBookSearch) 기반으로 재구성해 실데이터 연동, 지연 로딩, 로딩/에러/빈 상태 UI 및 스타일을 도입했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant U as User
participant B as BookSearchBottomSheet
participant H as useBookSearch
participant API as getSavedBooks
participant S as /books/selectable-list
U->>B: 시트 열기
B->>H: loadInitialData()
alt 탭 데이터 미로딩
H->>API: getSavedBooks('SAVED'|'JOINING')
API->>S: GET /books/selectable-list?type=TYPE
S-->>API: 200 { data }
API-->>H: SavedBooksResponse
H->>H: 변환 및 상태 업데이트
else 캐시 사용
H->>H: 기존 데이터 사용
end
U->>B: 검색어 입력 / 탭 전환
B->>H: setSearchQuery / handleTabChange
H->>H: 필터링 및(필요시) 추가 로드
U->>B: 책 선택
B->>U: onSelectBook(book)
sequenceDiagram
participant U as User
participant S as BookSearchStates
participant R as Router
U->>S: "책 신청하기" 클릭
S->>R: navigate("/search/applybook")
S->>S: onClose() 호출
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20–25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
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. 📜 Recent review detailsConfiguration used: CodeRabbit UI 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (6)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 1
🔭 Outside diff range comments (1)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (1)
32-39: 로컬 Book 타입과 글로벌 Book 타입 불일치 — 통합하거나 네이밍 정리 필요요약(간단): src/types/book.ts에 글로벌 Book(export interface Book { isbn, title, author, coverUrl, publisher })가 존재합니다. 반면 BookSearchBottomSheet.tsx는 로컬 Book(interface Book { id, title, author, cover, isbn })를 사용하고 있고, CreatePost/CreateGroup는 BookSearchBottomSheet의 로컬 타입과 구조적으로 일치하므로 현재 onSelectBook 호출부에서는 타입 오류가 발생하지 않습니다. 그러나 필드명(cover vs coverUrl), publisher 존재 여부 등 불일치로 혼동·버그 위험이 있으므로 정리 권장합니다.
문제 위치(확인됨)
- src/types/book.ts
- export interface Book { isbn: string; title: string; author: string; coverUrl: string; publisher: string; }
- src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx
- 로컬 interface Book { id: number; title: string; author: string; cover: string; isbn: string; } 및 onSelectBook prop
- src/pages/post/CreatePost.tsx
- 로컬 Book 인터페이스(BookSearchBottomSheet와 동일 구조) 및 onSelectBook 사용
- src/pages/group/CreateGroup.tsx
- 로컬 Book 인터페이스(유사) 및 onSelectBook 사용
- src/data/bookData.ts
- 글로벌 Book 타입(import type { Book } from '@/types/book') 사용 (publisher, coverUrl 사용)
권장 조치(간단)
- 권장(우선): 하나의 타입으로 통합
- BookSearchBottomSheet에서 글로벌 타입 import: import type { Book as GlobalBook } from '@/types/book';
- convertSavedBookToBook가 GlobalBook 형태(coverUrl 등)를 반환하도록 변경하고 컴포넌트 props/state 타입을 GlobalBook으로 변경.
- 대안: 로컬 타입명 변경으로 혼동 방지
- BookSearchBottomSheet의 로컬 타입명을 SearchBook/SelectedBook 등으로 변경하고, CreatePost/CreateGroup에서도 동일 타입을 import/재사용.
- 기타: 글로벌 타입이 실제 API 응답과 다른 경우 글로벌 타입을 확장하거나 optional 필드로 조정(publisher optional 등).
수정이 필요한 파일(우선순위)
- src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx
- src/pages/post/CreatePost.tsx
- src/pages/group/CreateGroup.tsx
- (선택) src/types/book.ts / src/data/bookData.ts — 글로벌 타입이 실제 사용에 맞는지 검토
🧹 Nitpick comments (6)
src/api/books/getSavedBooks.ts (2)
27-35: 요청 취소(Abort) 지원 추가 제안시트 닫힘/탭 전환 중 중복 호출이나 레이스로 인한 불필요한 상태 업데이트를 줄이기 위해 AbortSignal을 옵션으로 받아 axios 요청에 전달하는 것을 권장합니다.
아래처럼 옵션 인자로 signal을 받도록 확장할 수 있습니다:
-export const getSavedBooks = async (type: 'saved' | 'joining'): Promise<SavedBooksResponse> => { +export const getSavedBooks = async ( + type: 'saved' | 'joining', + options?: { signal?: AbortSignal }, +): Promise<SavedBooksResponse> => { try { - const response = await apiClient.get<SavedBooksResponse>('/books/selectable-list', { - params: { - type: type.toUpperCase(), - }, - }); + const response = await apiClient.get<SavedBooksResponse>('/books/selectable-list', { + params: { type: type.toUpperCase() }, + signal: options?.signal, + }); return response.data;선택사항: toUpperCase 대신 매핑을 써서 오타/타입 리스크를 더 줄일 수 있습니다.
const TYPE_MAP = { saved: 'SAVED', joining: 'JOINING' } as const; params: { type: TYPE_MAP[type] }
35-38: 콘솔 에러 메시지 범용화 (저장/참여 케이스 모두 해당)이 헬퍼는 저장/참여 목록 모두에서 사용되므로, 로그 메시지를 범용적으로 바꾸는 것이 추후 디버깅에 유리합니다.
- console.error('저장한 책 조회 API 오류:', error); + console.error('책 선택 목록 조회 API 오류:', error);src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (3)
169-173: onKeyPress는 React에서 비권장(deprecated)입니다 → onKeyDown으로 교체 권장React 17+에서 onKeyPress는 비권장이라 onKeyDown으로 대체하는 것이 안전합니다.
- const handleKeyPress = (e: React.KeyboardEvent) => { + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handleSearch(); } };- onKeyPress={handleKeyPress} + onKeyDown={handleKeyDown}Also applies to: 215-221
58-60: 탭별 로딩 상태 분리로 레이스 컨디션/깜빡임 완화단일 isLoading 상태를 공유하면, 탭 전환 중 동시 요청 시 먼저 끝난 요청이 로딩을 false로 바꿔 아직 진행 중인 다른 요청의 로딩 UI를 끄는 문제가 생길 수 있습니다. 탭별 로딩 상태를 분리하고, 현재 탭 기준으로 파생 isLoading을 계산하도록 권장합니다.
- 탭별 로딩 상태 추가
- 각 fetch 함수에서 해당 탭의 로딩만 on/off
- 렌더링에서는 현재 탭에 맞는 로딩 상태로 판정
- const [isLoading, setIsLoading] = useState(false); + const [loadingSaved, setLoadingSaved] = useState(false); + const [loadingGroup, setLoadingGroup] = useState(false);- const fetchSavedBooks = async () => { + const fetchSavedBooks = async () => { try { - setIsLoading(true); + setLoadingSaved(true); setError(null); const response = await getSavedBooks('saved'); if (response.isSuccess && response.data) { setSavedBooks(response.data.bookList); } else { setError(response.message || '저장한 책을 불러오는데 실패했습니다.'); } } catch (err) { console.error('저장한 책 조회 오류:', err); setError('저장한 책을 불러오는데 실패했습니다.'); } finally { - setIsLoading(false); + setLoadingSaved(false); } };- const fetchGroupBooks = async () => { + const fetchGroupBooks = async () => { try { - setIsLoading(true); + setLoadingGroup(true); setError(null); const response = await getSavedBooks('joining'); if (response.isSuccess && response.data) { setGroupBooks(response.data.bookList); } else { setError(response.message || '모임 책을 불러오는데 실패했습니다.'); } } catch (err) { console.error('모임 책 조회 오류:', err); setError('모임 책을 불러오는데 실패했습니다.'); } finally { - setIsLoading(false); + setLoadingGroup(false); } };- const currentTabBooks = activeTab === 'saved' ? savedBooks : groupBooks; - const hasBooks = currentTabBooks.length > 0; - const showEmptyState = !isLoading && !error && !hasBooks; + const currentTabBooks = activeTab === 'saved' ? savedBooks : groupBooks; + const isLoading = activeTab === 'saved' ? loadingSaved : loadingGroup; + const hasBooks = currentTabBooks.length > 0; + const showEmptyState = !isLoading && !error && !hasBooks;- {isLoading ? ( + {isLoading ? ( <LoadingContainer> <LoadingText>책 목록을 불러오는 중...</LoadingText> </LoadingContainer> ) : error ? ( <ErrorContainer> <ErrorText>{error}</ErrorText> </ErrorContainer> ) : showEmptyState ? ( <EmptyContainer> <EmptyText>현재 등록된 책이 아닙니다.</EmptyText> <EmptyText>원하시는 책을 신청해주세요.</EmptyText> <ApplyButton onClick={handleApplyBook}>책 신청하기</ApplyButton> </EmptyContainer> ) : (보완 아이디어(선택): getSavedBooks가 AbortSignal을 받도록 확장하면(상단 API 코멘트 참조), 탭 전환/닫기 시 이전 요청을 취소해 불필요한 setState를 더 줄일 수 있습니다.
Also applies to: 71-89, 92-111, 201-206, 246-260
244-274: 검색 결과가 0건일 때 전용 빈 상태 메시지 제공 제안현재는 전체 데이터가 0건일 때만 빈 상태 UI가 노출됩니다. 검색어 입력 후 필터 결과가 0건인 경우엔 빈 리스트만 보여 사용자 경험이 애매할 수 있습니다. “검색 결과가 없습니다”와 같은 경고를 추가해 보세요.
간단한 방식:
- isSearching = searchQuery.trim() !== ''
- showSearchEmpty = isSearching && !isLoading && !error && filteredBooks.length === 0
- 렌더 분기에서 showEmptyState 이전에 showSearchEmpty 분기 추가
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (1)
168-173: CSS 값 수정: background-color는 none 대신 transparent를 사용background-color에 none은 유효하지 않습니다. transparent가 올바른 값입니다.
- background-color: none; + background-color: transparent;
📜 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.
📒 Files selected for processing (3)
src/api/books/getSavedBooks.ts(1 hunks)src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts(3 hunks)src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx(6 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/api/books/getSavedBooks.ts (1)
src/api/index.ts (1)
apiClient(7-14)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (3)
src/types/book.ts (1)
Book(1-7)src/api/books/getSavedBooks.ts (2)
SavedBook(4-11)getSavedBooks(27-39)src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (12)
LoadingContainer(213-218)LoadingText(220-224)ErrorContainer(227-232)ErrorText(234-239)EmptyContainer(242-249)EmptyText(251-257)ApplyButton(259-274)BookList(159-163)BookItem(165-177)BookCover(179-192)BookInfo(194-197)BookTitle(199-210)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (1)
src/styles/global/global.ts (2)
colors(4-53)typography(56-77)
🔇 Additional comments (3)
src/api/books/getSavedBooks.ts (1)
4-24: 타입 정의 명확하고 일관적입니다API 응답 스키마(SavedBook/SavedBooksData/SavedBooksResponse) 정의가 명확하고, UI에서 필요한 필드만 깔끔히 노출됩니다. 컴파일 타임 타입 안전성 측면에서도 적절합니다.
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (1)
195-198: 경로 확인 필요: '/search/applybook' vs '/search/applybooks'검색 결과 요약: 코드베이스에는 "/search/applybook" 호출만 존재하고 "/search/applybooks"는 검색되지 않았습니다. 다만 라우터(경로 등록) 파일은 찾지 못했습니다. 아래 위치에서 확인됨:
- src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx — navigate('/search/applybook') (약 line 196)
- src/components/search/BookSearchResult.tsx — navigate('/search/applybook') (약 line 19)
- BookSearchBottomSheet 사용처:
- src/pages/post/CreatePost.tsx (약 lines 179–182)
- src/pages/group/CreateGroup.tsx (약 lines 265–268)
현재 코드 스니펫:
const handleApplyBook = () => { navigate('/search/applybook'); onClose(); };요청하는 조치:
- 라우터 설정(예: routes 파일, App.tsx 등)에서 실제 등록된 경로가 '/search/applybook'인지 '/search/applybooks'인지 확인해주세요.
- 라우트가 복수형('/search/applybooks')이면 navigate를 수정하고, 복수형이 아니라면 PR 설명을 단수형('/search/applybook')으로 정정해주세요.
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (1)
212-274: 로딩/에러/빈 상태 컴포넌트 도입 훌륭합니다상태별 전용 스타일 컴포넌트를 추가해 UI 가독성과 유지보수성이 좋아졌습니다. 버튼 hover, 텍스트 톤, 리스트 보더 처리도 일관적입니다.
| export const EmptyText = styled.p` | ||
| color: #e0e0e0;}; | ||
| font-size: ${typography.fontSize.sm}; | ||
| font-weight: ${typography.fontWeight.regular}; | ||
| line-height: 1.4; | ||
| margin: 0; | ||
| text-align: center; | ||
| `; |
There was a problem hiding this comment.
문법 오류: 여분의 중괄호로 인해 스타일 파싱 실패
EmptyText의 color 속성에 불필요한 };가 포함되어 있습니다. 이대로면 CSS-in-JS 파싱 오류가 발생할 수 있습니다. 즉시 수정이 필요합니다.
-export const EmptyText = styled.p`
- color: #e0e0e0;};
+export const EmptyText = styled.p`
+ color: #e0e0e0;
font-size: ${typography.fontSize.sm};
font-weight: ${typography.fontWeight.regular};
margin: 0;
text-align: center;
`;선택: 색 값을 토큰으로 통일하고 싶다면 ${colors.grey[200]} 사용도 고려해 주세요.
📝 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.
| export const EmptyText = styled.p` | |
| color: #e0e0e0;}; | |
| font-size: ${typography.fontSize.sm}; | |
| font-weight: ${typography.fontWeight.regular}; | |
| line-height: 1.4; | |
| margin: 0; | |
| text-align: center; | |
| `; | |
| export const EmptyText = styled.p` | |
| color: #e0e0e0; | |
| font-size: ${typography.fontSize.sm}; | |
| font-weight: ${typography.fontWeight.regular}; | |
| margin: 0; | |
| text-align: center; | |
| `; |
🤖 Prompt for AI Agents
In src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts
around lines 251 to 257, remove the stray characters after the color property
(the extra "};") so the CSS-in-JS block parses correctly; change the color line
to a valid property assignment and, if you want consistent tokens, replace the
hex with the design token `${colors.grey[200]}` (ensure colors is imported) and
keep the rest of the styled.p block intact.
#️⃣ 연관된 이슈
#86
📝 작업 내용
저장한 책 및 참여 중 모임의 책 조회 기능을 구현했습니다. 사용자가 기록 작성 시 책을 선택할 수 있도록 하는 BookSearchBottomSheet 컴포넌트를 완전히 새로 개발하고 API 연동을 완료했습니다.
🕸️ 주요 구현 사항
1. API 연동 구현
/books/selectable-list엔드포인트를 활용한 저장한 책(SAVED) 및 참여 중 모임 책(JOINING) 조회 기능을 구현했습니다.getSavedBooksAPI 함수를 개발하여 타입별 책 목록을 가져올 수 있도록 했습니다.2. BookSearchBottomSheet 컴포넌트 개선
3. 상태 관리 및 UX 개선
4. 책 신청 페이지 연동
/search/applybooks페이지로 이동하는 기능을 구현했습니다.Summary by CodeRabbit
New Features
Bug Fixes / Improvements
Style