diff --git a/src/api/booksnap.api.ts b/src/api/booksnap.api.ts index 8a818a5..3938b1e 100644 --- a/src/api/booksnap.api.ts +++ b/src/api/booksnap.api.ts @@ -115,3 +115,42 @@ export const searchBookstore = async (query: string | null) => { console.log(error); } }; + +// 책 리뷰검색하기 +export const searchReview = async (bookName: string) => { + try { + const response = await instance.get(`/api/search?bookName=${bookName}`); + if (response.status === 200) { + return response.data; + } + } catch (err) { + console.log(err); + } +}; + +// 책 검색 기록 저장 +export const postSearchHistory = async (searchType: string, searchWord: string) => { + try { + const response = await instance.post(`/api/search-history`, { + searchType: searchType, + searchWord: searchWord, + }); + if (response.status === 200) { + return response.data; + } + } catch (err) { + console.log(err); + } +}; + +// 최근 검색어 불러오기 +export const getRecentSearch = async (searchtype: string, page: number, size: number) => { + try { + const response = await instance.get(`/api/search-history?searchtype=${searchtype}&page=${page}&size=${size}`); + if (response.status === 200) { + return response.data; + } + } catch (err) { + console.log(err); + } +}; diff --git a/src/api/instance.ts b/src/api/instance.ts index b84d20b..f12f8be 100644 --- a/src/api/instance.ts +++ b/src/api/instance.ts @@ -24,10 +24,11 @@ instance.interceptors.request.use( // 응답 인터셉터 instance.interceptors.response.use( (response) => { - const { accessToken, refreshToken } = response.data.data || {}; + const { accessToken, refreshToken, nickname } = response.data.data || {}; if (accessToken && refreshToken) { localStorage.setItem('accessToken', accessToken); localStorage.setItem('refreshToken', refreshToken); + localStorage.setItem('nickname', nickname); } return response; }, @@ -57,6 +58,7 @@ instance.interceptors.response.use( console.log('Token 갱신 실패:', refreshErr); localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); + localStorage.removeItem('nickname'); window.location.href = '/login'; return Promise.reject(refreshErr); } diff --git a/src/api/zip.api.ts b/src/api/zip.api.ts index e2e113c..a21ee85 100644 --- a/src/api/zip.api.ts +++ b/src/api/zip.api.ts @@ -52,7 +52,7 @@ export const likeZip = async (bookstoreId: number) => { // 서점 상세 정보 export const getZipDetail = async (bookstoreId: number, type: string, sortFiled: string) => { try { - const response = await instance.get(`api/bookstores/${bookstoreId}/details?type=${type}&sortFiled?=${sortFiled}`); + const response = await instance.get(`api/bookstores/${bookstoreId}/details?type=${type}&sortField=${sortFiled}`); if (response.status == 200) { return response.data; } diff --git a/src/components/Header/BookSearchHeader.tsx b/src/components/Header/BookSearchHeader.tsx index ab55acf..c289135 100644 --- a/src/components/Header/BookSearchHeader.tsx +++ b/src/components/Header/BookSearchHeader.tsx @@ -3,12 +3,16 @@ import Arrow from '../../../public/icons/menu-bar/ArrowLeft.svg?react'; import { useNavigate } from 'react-router-dom'; import SearchBar from '../Zip/SearchBar'; +import { postSearchHistory } from '../../api/booksnap.api'; const BookSearchHeader = () => { const [searchWord, setSearchWord] = useState(''); const nav = useNavigate(); - const handleSearch = () => {}; + const handleSearch = () => { + nav(`/booksnap?query=${searchWord}`); + postSearchHistory('booktitle', searchWord); + }; return (
diff --git a/src/components/ScrollContext.tsx b/src/components/ScrollContext.tsx new file mode 100644 index 0000000..0518af4 --- /dev/null +++ b/src/components/ScrollContext.tsx @@ -0,0 +1,11 @@ +import { createContext, useContext, useRef } from 'react'; + +export const ScrollContext = createContext | null>(null); + +export const useScrollRef = () => { + const context = useContext(ScrollContext); + if (!context) { + throw new Error('useScrollRef must be used within a ScrollProvider'); + } + return context; +}; diff --git a/src/components/layout.tsx b/src/components/layout.tsx index db925a9..656a38d 100644 --- a/src/components/layout.tsx +++ b/src/components/layout.tsx @@ -1,9 +1,10 @@ import { Outlet } from 'react-router-dom'; import MenuBar from './Common/MenuBar'; import { useEffect, useRef, useState } from 'react'; +import { ScrollContext } from './ScrollContext'; const Layout = () => { - const headerHeight = useRef(null); + const mainRef = useRef(null); const menuBarHeight = useRef(null); const [heights, setHeights] = useState(0); @@ -24,21 +25,23 @@ const Layout = () => { }, []); return ( -
-
- -
-
- -
-
+ +
+
+ +
+
+ +
+
+
); }; diff --git a/src/pages/Bookie.tsx b/src/pages/Bookie.tsx index 1d39f27..1240a88 100644 --- a/src/pages/Bookie.tsx +++ b/src/pages/Bookie.tsx @@ -30,7 +30,7 @@ const Bookie = () => { const [isComposing, setIsComposing] = useState(false); const [isLoading, setIsLoading] = useState(false); const [loadingMessage, setLoadingMessage] = useState(''); - const userName = '이구역독서짱'; + const userName = localStorage.getItem('nickname'); const [systemRes, setSystemRes] = useState([ { text: `안녕하세요! ${userName}님이 좋아하실만한책을 추천해드리는 Bookie입니다! 더 많은 정보를 알려주시면, 책을 찾아드릴게요.`, diff --git a/src/pages/Booksnap/BookSearch.tsx b/src/pages/Booksnap/BookSearch.tsx index 6e32deb..7b2b6e3 100644 --- a/src/pages/Booksnap/BookSearch.tsx +++ b/src/pages/Booksnap/BookSearch.tsx @@ -1,23 +1,29 @@ +import { useEffect, useState } from 'react'; +import { getRecentSearch } from '../../api/booksnap.api'; import RecentSearch from '../../components/Booksnap/RecentSearch'; import Tag from '../../components/Common/Tag'; -import SearchBar from '../../components/Zip/SearchBar'; -import { useState } from 'react'; -import Arrow from '../../../public/icons/menu-bar/ArrowLeft.svg?react'; -import { useNavigate } from 'react-router-dom'; -import Header from '../../components/Common/Header'; import BookSearchHeader from '../../components/Header/BookSearchHeader'; -const BookSearch = () => { - const nav = useNavigate(); +interface RecentType { + id: number; + searchWord: string; +} +const BookSearch = () => { const bookname = ['구원의 날', '지구에서 한아뿐', '사이키쿠스오']; - const recent = ['하이큐 10권', '우리가 빛의 속도로 갈 수 없다면', '천 개의 파랑']; + const [recent, setRecent] = useState([]); + + useEffect(() => { + getRecentSearch('booktitle', 1, 10).then((data) => { + setRecent(data.data.searchHistory); + }); + }, []); return ( -
+
{/* 헤더 */} -
+
{/* 인기 검색어 */}

zipzip이들이 많이 찾아본 리뷰

@@ -32,7 +38,7 @@ const BookSearch = () => {

최근 검색

{recent.map((book, index) => ( - + ))}
diff --git a/src/pages/Booksnap/BookSnap.tsx b/src/pages/Booksnap/BookSnap.tsx index de11e1e..5e40d4d 100644 --- a/src/pages/Booksnap/BookSnap.tsx +++ b/src/pages/Booksnap/BookSnap.tsx @@ -4,9 +4,11 @@ import ReviewPreview from '../../components/Booksnap/ReviewPreview'; import { BooksnapPreview } from '../../model/booksnap.model'; import Loading from '../Loading'; import WriteButton from '../../components/Booksnap/WriteButton'; -import { getReview } from '../../api/booksnap.api'; +import { getReview, searchReview } from '../../api/booksnap.api'; import Toast from '../../components/Common/Toast'; import BooksnapHeader from '../../components/Header/BooksnapHeader'; +import { useScrollRef } from '../../components/ScrollContext'; +import { useSearchParams } from 'react-router-dom'; const BookSnap = () => { const [filter, setFilter] = useState('createdAt'); @@ -14,9 +16,19 @@ const BookSnap = () => { const [page, setPage] = useState(1); const [isLast, setIsLast] = useState(false); const [isBottom, setIsBottom] = useState(false); - const mainRef = useRef(null); const isLastRef = useRef(false); const [isLoading, setIsLoading] = useState(false); + const mainRef = useScrollRef(); + const [searchParams] = useSearchParams(); + const query = searchParams.get('query'); + + useEffect(() => { + if (query) { + searchReview(query).then((data) => { + setReview(data.booksnapPreview); + }); + } + }, [query]); // 리뷰 목록 받아오기 const getReviews = async () => { @@ -35,6 +47,7 @@ const BookSnap = () => { // filter가 변경될 때 상태 초기화 및 getReviews 호출 useEffect(() => { + if (query) return; setReview([]); setIsLast(false); setIsBottom(false); @@ -48,6 +61,8 @@ const BookSnap = () => { // page가 변경될 때만 getReviews 호출 useEffect(() => { + if (query) return; + if (page !== 1 || review.length === 0) { // 🔄 리뷰가 없거나 페이지가 1이 아닐 때만 호출 getReviews(); @@ -59,32 +74,26 @@ const BookSnap = () => { isLastRef.current = isLast; }, [isLast]); - // 바닥 감지 - const detectBottom = () => { - if (mainRef.current) { + useEffect(() => { + const handleScroll = () => { + if (!mainRef.current) return; + const { scrollTop, clientHeight, scrollHeight } = mainRef.current; - return scrollHeight > clientHeight && scrollTop + clientHeight >= scrollHeight - 1; - } - return false; - }; + if (scrollTop + clientHeight >= scrollHeight - 100 && !isBottom && !isLastRef.current) { + setIsBottom(true); + setPage((prev) => prev + 1); + } + }; - // 스크롤이 바닥에 있고, 마지막 페이지가 아니면 page + 1 - const handleScrollEvent = () => { - if (detectBottom() && !isBottom && !isLastRef.current) { - setIsBottom(true); - setPage((prev) => prev + 1); + const el = mainRef.current; + if (el) { + el.addEventListener('scroll', handleScroll); } - }; - // 스크롤 이벤트 등록 - useEffect(() => { - if (mainRef.current) { - mainRef.current.addEventListener('scroll', handleScrollEvent); - return () => { - mainRef.current?.removeEventListener('scroll', handleScrollEvent); - }; - } - }, []); + return () => { + el?.removeEventListener('scroll', handleScroll); + }; + }, [mainRef, isBottom]); if (isLoading) { return ; @@ -95,7 +104,7 @@ const BookSnap = () => { } return ( -
+
{/* 헤더 */}