diff --git a/src/api/books/getBookDetail.ts b/src/api/books/getBookDetail.ts new file mode 100644 index 00000000..44b6d86b --- /dev/null +++ b/src/api/books/getBookDetail.ts @@ -0,0 +1,33 @@ +import { apiClient } from '../index'; + +// 책 상세 정보 타입 +export interface BookDetail { + title: string; + imageUrl: string; + authorName: string; + publisher: string; + isbn: string; + description: string; + recruitingRoomCount: number; + readCount: number; + isSaved: boolean; +} + +// API 응답 타입 +export interface BookDetailResponse { + isSuccess: boolean; + code: number; + message: string; + data: BookDetail; +} + +export const getBookDetail = async (isbn: string): Promise => { + try { + const response = await apiClient.get(`/books/${isbn}`); + + return response.data; + } catch (error) { + console.error('책 상세 정보 API 오류:', error); + throw error; + } +}; diff --git a/src/api/books/getMostSearchedBooks.ts b/src/api/books/getMostSearchedBooks.ts new file mode 100644 index 00000000..fbdbcf3e --- /dev/null +++ b/src/api/books/getMostSearchedBooks.ts @@ -0,0 +1,32 @@ +import { apiClient } from '../index'; + +// 인기 검색 도서 타입 +export interface MostSearchedBook { + rank: number; + title: string; + imageUrl: string; + isbn: string; +} + +// API 응답 데이터 타입 +export interface MostSearchedBooksData { + bookList: MostSearchedBook[]; +} + +// API 응답 타입 +export interface MostSearchedBooksResponse { + isSuccess: boolean; + code: number; + message: string; + data: MostSearchedBooksData; +} + +export const getMostSearchedBooks = async (): Promise => { + try { + const response = await apiClient.get('/books/most-searched'); + return response.data; + } catch (error) { + console.error('인기 검색 도서 조회 API 오류:', error); + throw error; + } +}; diff --git a/src/api/books/getRecruitingRooms.ts b/src/api/books/getRecruitingRooms.ts new file mode 100644 index 00000000..f83eb397 --- /dev/null +++ b/src/api/books/getRecruitingRooms.ts @@ -0,0 +1,40 @@ +import { apiClient } from '../index'; + +// 모집중인 모임방 타입 +export interface RecruitingRoom { + roomId: number; + bookImageUrl: string; + roomName: string; + memberCount: number; + recruitCount: number; + deadlineEndDate: string; +} + +// API 응답 데이터 타입 +export interface RecruitingRoomsData { + recruitingRoomList: RecruitingRoom[]; + totalRoomCount: number; + nextCursor: string; + isLast: boolean; +} + +// API 응답 타입 +export interface RecruitingRoomsResponse { + isSuccess: boolean; + code: number; + message: string; + data: RecruitingRoomsData; +} + +export const getRecruitingRooms = async (isbn: string): Promise => { + try { + const response = await apiClient.get( + `/books/${isbn}/recruiting-rooms`, + ); + + return response.data; + } catch (error) { + console.error('모집중인 모임방 조회 API 오류:', error); + throw error; + } +}; diff --git a/src/api/books/getSearchBooks.ts b/src/api/books/getSearchBooks.ts new file mode 100644 index 00000000..6f3add94 --- /dev/null +++ b/src/api/books/getSearchBooks.ts @@ -0,0 +1,70 @@ +import { apiClient } from '../index'; + +// 검색된 책 타입 (API 응답에서 받는 형태) +export interface BookSearchItem { + title: string; + imageUrl: string; + authorName: string; + publisher: string; + isbn: string; +} + +// 검색된 책 타입 (컴포넌트에서 사용하는 형태) +export interface SearchedBook { + id: number; + title: string; + author: string; + publisher: string; + coverUrl: string; + isbn: string; +} + +// API 응답 데이터 타입 +export interface SearchBooksData { + searchResult: BookSearchItem[]; + page: number; + size: number; + totalElements: number; + totalPages: number; + last: boolean; + first: boolean; +} + +// API 응답 타입 +export interface SearchBooksResponse { + isSuccess: boolean; + code: number; + message: string; + data: SearchBooksData; +} + +export const getSearchBooks = async ( + query: string, + page: number = 1, + isFinalized: boolean = false, +): Promise => { + try { + const response = await apiClient.get('/books', { + params: { + keyword: query.trim(), + page: page, + isFinalized: isFinalized, + }, + }); + return response.data; + } catch (error) { + console.error('책 검색 API 오류:', error); + throw error; + } +}; + +export const convertToSearchedBooks = (apiBooks: BookSearchItem[]): SearchedBook[] => { + return apiBooks.map((book, index) => ({ + id: index + 1, + title: book.title, + author: book.authorName, + publisher: book.publisher, + coverUrl: book.imageUrl, + isbn: book.isbn, + })); +}; diff --git a/src/api/books/postSaveBook.ts b/src/api/books/postSaveBook.ts new file mode 100644 index 00000000..8822e4d3 --- /dev/null +++ b/src/api/books/postSaveBook.ts @@ -0,0 +1,32 @@ +import { apiClient } from '../index'; + +// 북마크 요청 타입 +export interface SaveBookRequest { + type: boolean; +} + +// 북마크 응답 데이터 타입 +export interface SaveBookData { + isbn: string; + isSaved: boolean; +} + +// 북마크 응답 타입 +export interface SaveBookResponse { + isSuccess: boolean; + code: number; + message: string; + data: SaveBookData; +} + +export const postSaveBook = async (isbn: string, type: boolean): Promise => { + try { + const response = await apiClient.post(`/books/${isbn}/saved`, { + type: type, + }); + return response.data; + } catch (error) { + console.error('책 저장 API 오류:', error); + throw error; + } +}; diff --git a/src/assets/common/SaveIcon.svg b/src/assets/common/SaveIcon.svg index f0f5b383..34504748 100644 --- a/src/assets/common/SaveIcon.svg +++ b/src/assets/common/SaveIcon.svg @@ -1,3 +1,4 @@ - - + + + diff --git a/src/assets/common/filledSaveIcon.svg b/src/assets/common/filledSaveIcon.svg new file mode 100644 index 00000000..9fffb2da --- /dev/null +++ b/src/assets/common/filledSaveIcon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/search/BookSearchResult.tsx b/src/components/search/BookSearchResult.tsx index 39b26624..a061e8ef 100644 --- a/src/components/search/BookSearchResult.tsx +++ b/src/components/search/BookSearchResult.tsx @@ -31,7 +31,7 @@ export function BookSearchResult({ type, searchedBookList }: BookSearchResultPro ) : ( searchedBookList.map(book => ( - + navigate(`/search/book/${book.isbn}`)}> {book.title} @@ -63,6 +63,7 @@ const BookItem = styled.div` display: flex; border-bottom: 1px solid ${colors.darkgrey.dark}; padding: 12px 0; + cursor: pointer; `; const ResultHeader = styled.div` diff --git a/src/components/search/MostSearchedBooks.tsx b/src/components/search/MostSearchedBooks.tsx index a485b25c..cacab507 100644 --- a/src/components/search/MostSearchedBooks.tsx +++ b/src/components/search/MostSearchedBooks.tsx @@ -1,64 +1,71 @@ import { colors, typography } from '@/styles/global/global'; import styled from '@emotion/styled'; +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { getMostSearchedBooks, type MostSearchedBook } from '@/api/books/getMostSearchedBooks'; -interface Book { - id: number; - title: string; - coverUrl: string; -} +export default function MostSearchedBooks() { + const [books, setBooks] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const navigate = useNavigate(); -const dummyBooks: Book[] = [ - { - id: 1, - title: '토마토 컵라면', - coverUrl: 'https://cdn.imweb.me/upload/S20230204e049098f5e744/e6fd3d849546d.jpg', - }, - { - id: 2, - title: '사슴', - coverUrl: 'https://cdn.imweb.me/upload/S20230204e049098f5e744/e6fd3d849546d.jpg', - }, - { - id: 3, - title: '호르몬 체인지지', - coverUrl: 'https://cdn.imweb.me/upload/S20230204e049098f5e744/e6fd3d849546d.jpg', - }, - { - id: 4, - title: '호르몬 체인지지', - coverUrl: 'https://cdn.imweb.me/upload/S20230204e049098f5e744/e6fd3d849546d.jpg', - }, - { - id: 5, - title: '호르몬 체인지지', - coverUrl: 'https://cdn.imweb.me/upload/S20230204e049098f5e744/e6fd3d849546d.jpg', - }, - { - id: 6, - title: '호르몬 체인지지', - coverUrl: 'https://cdn.imweb.me/upload/S20230204e049098f5e744/e6fd3d849546d.jpg', - }, -]; + useEffect(() => { + const fetchMostSearchedBooks = async () => { + try { + setIsLoading(true); + const response = await getMostSearchedBooks(); -export default function MostSearchedBooks() { + if (response.isSuccess) { + setBooks(response.data.bookList); + } else { + setError(response.message); + } + } catch (error) { + console.error('인기 검색 도서 조회 오류:', error); + setError('인기 검색 도서를 불러오는데 실패했습니다.'); + } finally { + setIsLoading(false); + } + }; + + fetchMostSearchedBooks(); + }, []); + + const handleBookClick = (isbn: string) => { + navigate(`/search/book/${isbn}`); + }; + + const getCurrentDate = () => { + const now = new Date(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + return `${month}.${day}. 기준`; + }; return (
가장 많이 검색된 책 - {/* 서버 응답 포맷을 모르기에 우선 하드 코딩 */} - 01.12. 기준 + {getCurrentDate()}
- {dummyBooks.length === 0 ? ( + {isLoading ? ( + 로딩 중... + ) : error ? ( + + 데이터를 불러올 수 없어요. + {error} + + ) : books.length === 0 ? ( 아직 순위가 집계되지 않았어요. 조금만 기다려주세요! ) : ( - {dummyBooks.map((book, index) => ( - - {index + 1}. - + {books.map(book => ( + handleBookClick(book.isbn)}> + {book.rank}. + {book.title} ))} @@ -106,6 +113,12 @@ const BookItem = styled.li` align-items: center; padding: 12px 0; border-bottom: 1px solid ${colors.darkgrey.dark}; + cursor: pointer; + transition: background-color 0.2s ease; + + &:hover { + background-color: ${colors.darkgrey.main}; + } `; const Rank = styled.span` @@ -151,3 +164,13 @@ const SubText = styled.div` color: ${colors.grey[100]}; font-weight: ${typography.fontWeight.regular}; `; + +const LoadingMessage = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 200px; + font-size: ${typography.fontSize.base}; + color: ${colors.grey[200]}; + font-weight: ${typography.fontWeight.regular}; +`; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 343e91c2..bbf63d10 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -63,7 +63,7 @@ const Router = () => { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/pages/search/Search.tsx b/src/pages/search/Search.tsx index b5f4007a..e71b882a 100644 --- a/src/pages/search/Search.tsx +++ b/src/pages/search/Search.tsx @@ -6,8 +6,10 @@ import RecentSearchTabs from '@/components/search/RecentSearchTabs'; import SearchBar from '@/components/search/SearchBar'; import { colors, typography } from '@/styles/global/global'; import styled from '@emotion/styled'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; +import { useSearchParams } from 'react-router-dom'; import leftArrow from '../../assets/common/leftArrow.svg'; +import { getSearchBooks, convertToSearchedBooks } from '@/api/books/getSearchBooks'; export interface SearchedBook { id: number; @@ -15,59 +17,108 @@ export interface SearchedBook { author: string; publisher: string; coverUrl: string; + isbn: string; } -const dummySearchedBook: SearchedBook[] = [ - { - id: 1, - title: '채식주의자', - author: '한강', - publisher: '창비', - coverUrl: 'https://image.yes24.com/goods/17122707/XL', - }, - { - id: 2, - title: '채소 마스터 클래스', - author: '백지혜', - publisher: '세미콜론', - coverUrl: 'https://image.yes24.com/goods/109378551/XL', - }, - { - id: 3, - title: '채소 식탁', - author: '김경민', - publisher: '래디시', - coverUrl: 'https://image.yes24.com/goods/117194041/XL', - }, -]; const Search = () => { + const [searchParams, setSearchParams] = useSearchParams(); const [searchTerm, setSearchTerm] = useState(''); const [isSearching, setIsSearching] = useState(false); - const [isSearched, setIsSearched] = useState(false); + const [isFinalized, setIsFinalized] = useState(false); + const [searchResults, setSearchResults] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isInitialized, setIsInitialized] = useState(false); - const [recentSearches, setRecentSearches] = useState([ - '딸기12', - '당근', - '수박245', - '참', - '메론1', - ]); + const [recentSearches, setRecentSearches] = useState([]); + const [searchTimeoutId, setSearchTimeoutId] = useState(null); const handleChange = (value: string) => { setSearchTerm(value); - setIsSearched(false); + setIsFinalized(false); setIsSearching(value.trim() !== ''); + + if (value.trim()) { + setSearchParams({ q: value.trim() }, { replace: true }); + + if (searchTimeoutId) { + clearTimeout(searchTimeoutId); + } + + const timeoutId = setTimeout(() => { + handleSearch(value.trim(), false); + }, 300); + + setSearchTimeoutId(timeoutId); + } else { + setSearchParams({}, { replace: true }); + + setSearchResults([]); + setIsFinalized(false); + + if (searchTimeoutId) { + clearTimeout(searchTimeoutId); + setSearchTimeoutId(null); + } + } }; - const handleSearch = (term: string) => { + const handleSearch = useCallback(async (term: string, isManualSearch: boolean = false) => { if (!term.trim()) return; + setIsSearching(true); - setIsSearched(true); + + if (isManualSearch) { + setIsFinalized(false); + } + + setIsLoading(true); + + try { + const response = await getSearchBooks(term, 1, isManualSearch); + + if (response.isSuccess) { + const convertedResults = convertToSearchedBooks(response.data.searchResult); + setSearchResults(convertedResults); + } else { + console.log('검색 실패:', response.message); + setSearchResults([]); + } + + if (isManualSearch) { + setIsFinalized(true); + } + } catch (error) { + console.error('검색 중 오류 발생:', error); + setSearchResults([]); + if (isManualSearch) { + setIsFinalized(true); + } + } finally { + setIsLoading(false); + } + setRecentSearches(prev => { const filtered = prev.filter(t => t !== term); return [term, ...filtered].slice(0, 5); }); - }; + }, []); + + useEffect(() => { + const query = searchParams.get('q') || ''; + if (query && !isInitialized) { + setSearchTerm(query); + setIsSearching(true); + handleSearch(query, true); + setIsInitialized(true); + } + }, [searchParams, handleSearch, isInitialized]); + + useEffect(() => { + if (searchTerm.trim() === '') { + setIsSearching(false); + setIsFinalized(false); + } + }, [searchTerm]); const handleDelete = (recentSearch: string) => { setRecentSearches(prev => prev.filter(t => t !== recentSearch)); @@ -75,20 +126,30 @@ const Search = () => { const handleRecentSearchClick = (recentSearch: string) => { setSearchTerm(recentSearch); - setIsSearched(true); - setIsSearching(true); + handleSearch(recentSearch, true); }; const handleBackButton = () => { + if (searchTimeoutId) { + clearTimeout(searchTimeoutId); + setSearchTimeoutId(null); + } + setSearchTerm(''); + setSearchResults([]); + setIsSearching(false); + setIsFinalized(false); + setIsInitialized(false); + setSearchParams({}, { replace: true }); }; useEffect(() => { - if (searchTerm.trim() === '') { - setIsSearching(false); - setIsSearched(false); - } - }, [searchTerm]); + return () => { + if (searchTimeoutId) { + clearTimeout(searchTimeoutId); + } + }; + }, [searchTimeoutId]); return ( @@ -106,26 +167,21 @@ const Search = () => { placeholder="책 제목, 작가명을 검색해보세요." value={searchTerm} onChange={handleChange} - onSearch={() => handleSearch(searchTerm.trim())} - isSearched={isSearched} + onSearch={() => handleSearch(searchTerm.trim(), true)} + isSearched={isFinalized} /> {isSearching ? ( <> - ( - {isSearched ? ( - + {isLoading ? ( + 검색 중... ) : ( + type={isFinalized ? 'searched' : 'searching'} + searchedBookList={searchResults} + /> )} - ) ) : ( <> @@ -151,7 +207,8 @@ const Wrapper = styled.div` flex-direction: column; min-width: 320px; max-width: 767px; - height: 100vh; + height: 100%; + min-height: 100vh; margin: 0 auto; background: ${colors.black.main}; `; @@ -186,3 +243,12 @@ const SearchBarContainer = styled.div` const Content = styled.div` margin-top: 132px; `; + +const LoadingMessage = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 40px 20px; + color: ${colors.white}; + font-size: ${typography.fontSize.base}; +`; diff --git a/src/pages/searchBook/SearchBook.styled.ts b/src/pages/searchBook/SearchBook.styled.ts index 7ada1f38..20f5c38c 100644 --- a/src/pages/searchBook/SearchBook.styled.ts +++ b/src/pages/searchBook/SearchBook.styled.ts @@ -126,6 +126,7 @@ export const RecruitingGroupButton = styled.button` justify-content: center; align-items: center; padding: 10px 12px; + cursor: pointer; `; export const RightArea = styled.div` @@ -150,17 +151,19 @@ export const WritePostButton = styled.button` gap: 8px; min-width: 200px; border: none; + cursor: pointer; `; export const SaveButton = styled.button` width: 48px; height: 48px; background: transparent; - border: 1px solid ${colors.grey[200]}; + border: none; border-radius: 12px; display: flex; align-items: center; justify-content: center; + cursor: pointer; `; export const FeedSection = styled.section` diff --git a/src/pages/searchBook/SearchBook.tsx b/src/pages/searchBook/SearchBook.tsx index 9d3b0085..3aeb03b7 100644 --- a/src/pages/searchBook/SearchBook.tsx +++ b/src/pages/searchBook/SearchBook.tsx @@ -21,28 +21,77 @@ import { EmptyTitle, EmptySubText, } from './SearchBook.styled'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import leftArrow from '../../assets/common/leftArrow.svg'; import moreIcon from '../../assets/common/more.svg'; import { IconButton } from '@/components/common/IconButton'; -import { mockSearchBook } from '@/mocks/searchBook.mock'; import saveIcon from '../../assets/common/SaveIcon.svg'; +import filledSaveIcon from '../../assets/common/filledSaveIcon.svg'; import rightChevron from '../../assets/common/right-Chevron.svg'; import plusIcon from '../../assets/common/plus.svg'; +import { useState, useEffect } from 'react'; +import { IntroModal } from '@/components/search/IntroModal'; +import { getBookDetail, type BookDetail } from '@/api/books/getBookDetail'; +import { getRecruitingRooms, type RecruitingRoomsData } from '@/api/books/getRecruitingRooms'; +import { postSaveBook } from '@/api/books/postSaveBook'; import { Filter } from '@/components/common/Filter'; -import { useState } from 'react'; import FeedPost from '@/components/feed/FeedPost'; -import { IntroModal } from '@/components/search/IntroModal'; +import { mockSearchBook } from '@/mocks/searchBook.mock'; const FILTER = ['최신순', '인기순']; const SearchBook = () => { - const { title, author, introduction, coverUrl, recruitGroups, posts } = mockSearchBook; + const { isbn } = useParams<{ isbn: string }>(); const [selectedFilter, setSelectedFilter] = useState('인기순'); const [showIntroModal, setShowIntroModal] = useState(false); + const [bookDetail, setBookDetail] = useState(null); + const [recruitingRoomsData, setRecruitingRoomsData] = useState(null); + const [isSaved, setIsSaved] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); const navigate = useNavigate(); + useEffect(() => { + const fetchBookDetail = async () => { + if (!isbn) { + setError('ISBN이 필요합니다.'); + setIsLoading(false); + return; + } + + try { + setIsLoading(true); + + const [bookResponse, recruitingResponse] = await Promise.all([ + getBookDetail(isbn), + getRecruitingRooms(isbn), + ]); + + if (bookResponse.isSuccess) { + setBookDetail(bookResponse.data); + setIsSaved(bookResponse.data.isSaved); + } else { + setError(bookResponse.message); + } + + if (recruitingResponse.isSuccess) { + setRecruitingRoomsData(recruitingResponse.data); + } else { + console.error('모집중인 모임방 조회 실패:', recruitingResponse.message); + } + } catch (error) { + console.error('데이터 조회 오류:', error); + setError('정보를 불러오는데 실패했습니다.'); + } finally { + setIsLoading(false); + } + }; + + fetchBookDetail(); + }, [isbn]); + const handleBackButton = () => { navigate(-1); }; @@ -54,48 +103,93 @@ const SearchBook = () => { const handleMoreButton = () => {}; const handleRecruitingGroupButton = () => { - navigate('./group'); + if (bookDetail) { + navigate('/search/book/group', { + state: { + recruitingRooms: recruitingRoomsData || { + recruitingRoomList: [], + totalRoomCount: 0, + nextCursor: '', + isLast: true, + }, + bookInfo: { + isbn: bookDetail.isbn, + title: bookDetail.title, + author: bookDetail.authorName, + imageUrl: bookDetail.imageUrl, + }, + }, + }); + } }; const handleWritePostButton = () => {}; - const handleSaveButton = () => {}; + const handleSaveButton = async () => { + if (!isbn || isSaving) return; + + try { + setIsSaving(true); + const response = await postSaveBook(isbn, !isSaved); + + if (response.isSuccess) { + setIsSaved(response.data.isSaved); + } else { + console.error('북마크 실패:', response.message); + } + } catch (error) { + console.error('북마크 중 오류 발생:', error); + } finally { + setIsSaving(false); + } + }; - const hasFeeds = mockSearchBook.posts.length > 0; + if (isLoading || error || !bookDetail) { + return ( + +
+ + +
+
+ {isLoading ? '로딩 중...' : error || '책 정보를 찾을 수 없습니다.'} +
+ + ); + } return ( - +
- {title} - {author} + {bookDetail.title} + {bookDetail.authorName} 소개 - {introduction} + {bookDetail.description} - 모집중인 모임방 {recruitGroups.length}개{' '} + 모집중인 모임방 {recruitingRoomsData?.totalRoomCount || 0}개{' '} 오른쪽 화살표 아이콘 피드에 글쓰기 더하기 아이콘 - - 저장 버튼 + + 저장 버튼 - 피드 글 둘러보기 @@ -106,10 +200,10 @@ const SearchBook = () => { /> - {hasFeeds ? ( + {mockSearchBook.posts.length > 0 ? ( <> - {posts.map(post => ( - + {mockSearchBook.posts.map((post, index) => ( + ))} ) : ( @@ -118,10 +212,9 @@ const SearchBook = () => { 첫 번째 피드를 작성해보세요! )} - - + {' '} {showIntroModal && ( - + )}
); diff --git a/src/pages/searchBook/SearchBookGroup.tsx b/src/pages/searchBook/SearchBookGroup.tsx index 02fe4941..7db261db 100644 --- a/src/pages/searchBook/SearchBookGroup.tsx +++ b/src/pages/searchBook/SearchBookGroup.tsx @@ -2,19 +2,33 @@ import TitleHeader from '@/components/common/TitleHeader'; import { colors, typography } from '@/styles/global/global'; import styled from '@emotion/styled'; import leftArrow from '../../assets/common/leftArrow.svg'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { GroupCard } from '@/components/group/GroupCard'; -import { mockSearchBookGroup } from '@/mocks/searchBook.mock'; +import { type RecruitingRoomsData } from '@/api/books/getRecruitingRooms'; + +interface LocationState { + recruitingRooms: RecruitingRoomsData; + bookInfo: { + isbn: string; + title: string; + author: string; + imageUrl: string; + }; +} const SearchBookGroup = () => { const navigate = useNavigate(); + const location = useLocation(); + const { recruitingRooms, bookInfo } = (location.state as LocationState) || {}; const handleBackButton = () => { navigate(-1); }; const handleMakeGroup = () => {}; - const hasGroups = mockSearchBookGroup.length > 0; + const groupList = recruitingRooms?.recruitingRoomList || []; + const totalCount = recruitingRooms?.totalRoomCount || 0; + const hasGroups = groupList.length > 0; return ( @@ -23,11 +37,23 @@ const SearchBookGroup = () => { leftIcon={뒤로 가기} onLeftClick={handleBackButton} /> - 전체 {mockSearchBookGroup.length} + 전체 {totalCount} {hasGroups ? ( - {mockSearchBookGroup.map(group => ( - + {groupList.map((room, index) => ( + ))} ) : (