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
39 changes: 39 additions & 0 deletions src/api/booksnap.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
4 changes: 3 additions & 1 deletion src/api/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/api/zip.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
6 changes: 5 additions & 1 deletion src/components/Header/BookSearchHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="fixed left-0 right-0 top-0 z-20 m-auto flex max-w-[500px] items-center gap-[15px] border-b-[1px] border-[#544F4F] bg-bg px-[20px] py-[10px]">
Expand Down
11 changes: 11 additions & 0 deletions src/components/ScrollContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createContext, useContext, useRef } from 'react';

export const ScrollContext = createContext<React.RefObject<HTMLDivElement> | null>(null);

export const useScrollRef = () => {
const context = useContext(ScrollContext);
if (!context) {
throw new Error('useScrollRef must be used within a ScrollProvider');
}
return context;
};
35 changes: 19 additions & 16 deletions src/components/layout.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null);
const mainRef = useRef<HTMLDivElement>(null);
const menuBarHeight = useRef<HTMLDivElement>(null);
const [heights, setHeights] = useState(0);

Expand All @@ -24,21 +25,23 @@ const Layout = () => {
}, []);

return (
<div className="relative flex flex-col">
<main
className="overflow-auto scrollbar-none"
style={{
// marginTop: `${heights.header}px`,
marginBottom: `${heights}px`,
height: `calc(100dvh - ${heights}px)`,
}}
>
<Outlet />
</main>
<footer className="fixed bottom-0 z-30 w-full max-w-[500px]" ref={menuBarHeight}>
<MenuBar />
</footer>
</div>
<ScrollContext.Provider value={mainRef}>
<div className="relative flex flex-col">
<main
className="overflow-y-auto scrollbar-none"
ref={mainRef}
style={{
marginBottom: `${heights}px`,
height: `calc(100dvh - ${heights}px)`,
}}
>
<Outlet />
</main>
<footer className="fixed bottom-0 z-30 w-full max-w-[500px]" ref={menuBarHeight}>
<MenuBar />
</footer>
</div>
</ScrollContext.Provider>
);
};

Expand Down
2 changes: 1 addition & 1 deletion src/pages/Bookie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const Bookie = () => {
const [isComposing, setIsComposing] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [loadingMessage, setLoadingMessage] = useState<string>('');
const userName = '이구역독서짱';
const userName = localStorage.getItem('nickname');
const [systemRes, setSystemRes] = useState<MessageType[]>([
{
text: `안녕하세요! ${userName}님이 좋아하실만한책을 추천해드리는 Bookie입니다! 더 많은 정보를 알려주시면, 책을 찾아드릴게요.`,
Expand Down
28 changes: 17 additions & 11 deletions src/pages/Booksnap/BookSearch.tsx
Original file line number Diff line number Diff line change
@@ -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<RecentType[]>([]);

useEffect(() => {
getRecentSearch('booktitle', 1, 10).then((data) => {
setRecent(data.data.searchHistory);
});
}, []);

return (
<div className="bg-bg flex h-full flex-col">
<div className="flex h-full flex-col bg-bg">
{/* 헤더 */}
<BookSearchHeader />
<div className="bg-bg mt-[53px] flex flex-col gap-[15px] px-[15px] py-[10px]">
<div className="mt-[53px] flex flex-col gap-[15px] bg-bg px-[15px] py-[10px]">
{/* 인기 검색어 */}
<div className="flex w-full flex-col gap-[10px] border-b-[1px] border-[#A09F9F] p-[10px] text-body3 font-bold text-white">
<p>zipzip이들이 많이 찾아본 리뷰</p>
Expand All @@ -32,7 +38,7 @@ const BookSearch = () => {
<p className="px-[10px] text-body3 font-bold text-white">최근 검색</p>
<div className="flex flex-col gap-[5px]">
{recent.map((book, index) => (
<RecentSearch name={book} key={index} />
<RecentSearch name={book.searchWord} key={index} />
))}
</div>
</div>
Expand Down
59 changes: 34 additions & 25 deletions src/pages/Booksnap/BookSnap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,31 @@ 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<FilterType>('createdAt');
const [review, setReview] = useState<BooksnapPreview[]>([]);
const [page, setPage] = useState(1);
const [isLast, setIsLast] = useState<boolean>(false);
const [isBottom, setIsBottom] = useState<boolean>(false);
const mainRef = useRef<HTMLDivElement>(null);
const isLastRef = useRef<boolean>(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 () => {
Expand All @@ -35,6 +47,7 @@ const BookSnap = () => {

// filter가 변경될 때 상태 초기화 및 getReviews 호출
useEffect(() => {
if (query) return;
setReview([]);
setIsLast(false);
setIsBottom(false);
Expand All @@ -48,6 +61,8 @@ const BookSnap = () => {

// page가 변경될 때만 getReviews 호출
useEffect(() => {
if (query) return;

if (page !== 1 || review.length === 0) {
// 🔄 리뷰가 없거나 페이지가 1이 아닐 때만 호출
getReviews();
Expand All @@ -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 <Loading text="리뷰 목록을 불러오는 중입니다!" />;
Expand All @@ -95,7 +104,7 @@ const BookSnap = () => {
}

return (
<div ref={mainRef} className="h-screen overflow-y-auto bg-bg scrollbar-none">
<div className="overflow-hidden bg-bg scrollbar-none">
{/* 헤더 */}
<BooksnapHeader />
<div className="mt-[50px] flex flex-col">
Expand Down