diff --git a/public/icons/book-snap/loading.gif b/public/icons/book-snap/loading.gif new file mode 100644 index 0000000..d0d2b2d Binary files /dev/null and b/public/icons/book-snap/loading.gif differ diff --git a/src/api/bookie.api.ts b/src/api/bookie.api.ts new file mode 100644 index 0000000..bc52df4 --- /dev/null +++ b/src/api/bookie.api.ts @@ -0,0 +1,18 @@ +// utils/chatAPI.ts +import instance from './instance'; + +// 한줄 리뷰 등록 (Spring 서버를 통해 FastAPI 프록시로 전달) +export const sendMessageToChatAPI = async (message: string) => { + try { + const response = await instance.post('/bookie/chat', { message: message }); + + if (response.status === 200) { + return response.data; + } else { + throw new Error('Server responded with error'); + } + } catch (error) { + console.error('❌ chat API 호출 실패:', error); + throw error; + } +}; diff --git a/src/constants/bookieLoadingMessages.ts b/src/constants/bookieLoadingMessages.ts new file mode 100644 index 0000000..d7fe66e --- /dev/null +++ b/src/constants/bookieLoadingMessages.ts @@ -0,0 +1,38 @@ +// utils/bookieLoadingMessages.ts + +export const bookieLoadingMessages = [ + // 📖 책 팁 / 독서 팁 + '📚 하루 10분 독서만 해도 1년에 책 12권은 읽을 수 있어요!', + '🕯️ 자기 전 15분 독서는 스트레스를 68% 줄여준대요.', + '📖 집중이 안 될 땐 10분짜리 짧은 독립출판물부터 시작해보세요.', + '📎 종이책을 읽을 땐 포스트잇을 곁에 두는 습관, 생각 정리에 좋아요.', + '📱 전자책도 좋아요, 하지만 눈을 쉬게 하려면 종이책도 가끔씩은 꼭!', + + // 🎯 문학 퀴즈 / 정보 + '퀴즈! 세계에서 가장 많이 팔린 책은 무엇일까요? (힌트: 성경 말고!)', + '퀴즈! 해리포터 1권의 원제는 무엇일까요?', + '작가가 27번 거절당하고도 출간한 책, 알고 있나요? — 《해리 포터》 시리즈!', + '미국의 국민 시인이라 불리는 작가는 누구일까요? (힌트: ‘풀잎 위에서’)', + '‘책의 날’은 매년 4월 23일! 세르반테스와 셰익스피어가 같은 날 세상을 떠났어요.', + + // 🗂️ 독립출판/인디북스 정보 + '독립출판물은 보통 작가가 직접 기획, 집필, 디자인, 제작까지 한답니다.', + '부키 팁! 독립서점마다 특별한 큐레이션 책장이 있는 거, 알고 계셨나요?', + '‘지금 여기, 내가 만든 책’ — 독립출판의 핵심은 바로 이 말이에요.', + '독립출판물은 1쇄 100부도 가능해요. 나만의 책 만들기도 도전해보세요!', + '서울에는 약 100여 개의 독립서점이 존재해요. 부키가 추천해드릴까요?', + + // 💡 랜덤 흥미 정보 (재미용) + '지구상에서 가장 오래된 책은 기원전 2400년의 ‘죽은 자의 서’랍니다.', + '고서 수집가들 사이에서 가장 비싼 책은 3천억 원이 넘게 거래되기도 했어요!', + '세계 최초의 소설은 일본의 《겐지 이야기》라는 거, 알고 계셨나요?', + '📖 부키는 지금 책향기가 나는 데이터 속으로 잠수 중입니다…', + '책도 사람도, 겉보다 속이 더 중요하답니다. 😊', + + '《책방》이라는 단어는 일본식 한자어예요. 원래는 ‘서점’이 맞답니다!', + '세상에서 가장 오래된 도서관은 기원전 7세기 이집트에 있었어요.', + '어떤 책은 종이를 직접 염색해서 만드는 ‘리소’ 인쇄를 써요!', + '책을 선물하면 기억에 오래 남는대요. 이유는 ‘시간을 주는 거니까’.', + '📦 독립출판은 때로 택배 상자 속 한 줄 편지로 완성돼요.', + '누군가는 밤새워 만든 40쪽짜리 책, 당신이 펼칠 차례예요.', +]; diff --git a/src/pages/Bookie.tsx b/src/pages/Bookie.tsx index 4eada0a..6cbd30f 100644 --- a/src/pages/Bookie.tsx +++ b/src/pages/Bookie.tsx @@ -1,16 +1,26 @@ -import Header from '../components/Common/Header'; import bookie from '../../public/icons/bookie/bookie.png'; import MessageBox from '../components/Bookie/MessageBox'; import { useEffect, useRef, useState } from 'react'; import Input from '../components/Bookie/Input'; import { FaRegArrowAltCircleUp } from 'react-icons/fa'; -import { FaAngleLeft } from 'react-icons/fa6'; import { IoCloseOutline } from 'react-icons/io5'; import { useNavigate } from 'react-router-dom'; +import { sendMessageToChatAPI } from '../api/bookie.api'; +import { MdBookmarkAdd } from 'react-icons/md'; +import toast from 'react-hot-toast'; +import { pickBook } from '../api/booksnap.api'; +import { bookieLoadingMessages } from '../constants/bookieLoadingMessages'; type MessageType = { text: string; type: 'system' | 'user'; + books?: BookCardType[]; +}; + +type BookCardType = { + title: string; + bookId: string; + bookImageUrl: string; }; const Bookie = () => { @@ -18,13 +28,15 @@ const Bookie = () => { const [input, setInput] = useState(''); const nav = useNavigate(); const [isComposing, setIsComposing] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [loadingMessage, setLoadingMessage] = useState(''); + const userName = '이구역독서짱'; const [systemRes, setSystemRes] = useState([ { - text: '안녕하세요! 이구역 독서짱님이 좋아하실만한책을 추천해드리는 Bookie입니다! 더 많은 정보를 알려주시면, 책을 찾아드릴게요.', + text: `안녕하세요! ${userName}님이 좋아하실만한책을 추천해드리는 Bookie입니다! 더 많은 정보를 알려주시면, 책을 찾아드릴게요.`, type: 'system', }, ]); - const userName = '이구역 독서짱'; useEffect(() => { if (endOfMessages.current) { @@ -32,13 +44,31 @@ const Bookie = () => { } }, [systemRes]); - const sendMessage = () => { + // 메세지 보내기 + const sendMessage = async () => { if (!input.trim() || isComposing) return; - if (input.trim() !== '') { - const userMessage: MessageType = { text: input, type: 'user' }; - setSystemRes((prevMessages) => [...prevMessages, userMessage]); - setInput(''); + const userMessage: MessageType = { text: input, type: 'user' }; + setSystemRes((prev) => [...prev, userMessage]); // 사용자 메시지 먼저 출력 + setInput(''); + + const random = bookieLoadingMessages[Math.floor(Math.random() * bookieLoadingMessages.length)]; + setLoadingMessage(random); + setIsLoading(true); + + try { + const reply = await sendMessageToChatAPI(input); + const systemMessage: MessageType = { text: reply.message, type: 'system', books: reply.books }; + setSystemRes((prev) => [...prev, systemMessage]); // GPT 응답 추가 + } catch (error) { + const errorMessage: MessageType = { + text: '서버와 연결할 수 없습니다.', + type: 'system', + }; + setSystemRes((prev) => [...prev, errorMessage]); + } finally { + setIsLoading(false); + setLoadingMessage(''); } }; @@ -53,10 +83,25 @@ const Bookie = () => { } }; + const handlePickBook = (book: BookCardType) => { + console.log(book); + if (!localStorage.getItem('accessToken')) { + toast.error('로그인이 필요한 서비스입니다.'); + } else { + pickBook(book.bookId).then((data) => { + if (data?.success) { + toast.success(`${book.title}을(를) 책장에 담았어요!`); + } else { + toast.error(`${data?.message}`); + } + }); + } + }; + return (
{/* 헤더 */} -
+
nav('/')}> @@ -69,7 +114,7 @@ const Bookie = () => {
{/* 내용 */} -
+
{/* 설명 */}
{userName} @@ -78,29 +123,60 @@ const Bookie = () => { 책과 서점을 추천해드릴게요!
-
{/* 채팅구역 */}
e.stopPropagation()} // 휠 이벤트 차단 > {systemRes.map((msg, index) => ( - +
+ + {msg.books && msg.books.length > 0 && ( +
+
+ {msg.books.map((book, idx) => ( +
+
+ {book.title} + +
+

{book.title}

+
+ ))} +
+
+ )} +
))} + {isLoading && ( +
+

부키가 책을 고르러 작은 서점 골목으로 들어갔어요...📚

+

{loadingMessage}

+
+ )}
{/* 입력창 */} -
+
{input == '' ? ( ) : ( -
+
)} diff --git a/src/pages/Booksnap/BookSnap.tsx b/src/pages/Booksnap/BookSnap.tsx index 66ee98b..de11e1e 100644 --- a/src/pages/Booksnap/BookSnap.tsx +++ b/src/pages/Booksnap/BookSnap.tsx @@ -6,7 +6,6 @@ import Loading from '../Loading'; import WriteButton from '../../components/Booksnap/WriteButton'; import { getReview } from '../../api/booksnap.api'; import Toast from '../../components/Common/Toast'; -import { useNavigate } from 'react-router-dom'; import BooksnapHeader from '../../components/Header/BooksnapHeader'; const BookSnap = () => { @@ -17,9 +16,11 @@ const BookSnap = () => { const [isBottom, setIsBottom] = useState(false); const mainRef = useRef(null); const isLastRef = useRef(false); + const [isLoading, setIsLoading] = useState(false); // 리뷰 목록 받아오기 const getReviews = async () => { + setIsLoading(true); try { const data = await getReview(filter, page); setReview((prev) => (page === 1 ? data.data.booksnapPreview : [...prev, ...data.data.booksnapPreview])); @@ -27,6 +28,8 @@ const BookSnap = () => { setIsBottom(false); } catch (error) { console.error('리뷰를 불러오는 중 에러 발생:', error); + } finally { + setIsLoading(false); } }; @@ -83,6 +86,10 @@ const BookSnap = () => { } }, []); + if (isLoading) { + return ; + } + if (!review) { return ; } diff --git a/src/pages/Booksnap/CreateBook.tsx b/src/pages/Booksnap/CreateBook.tsx index a0697ea..a21a8bd 100644 --- a/src/pages/Booksnap/CreateBook.tsx +++ b/src/pages/Booksnap/CreateBook.tsx @@ -9,6 +9,7 @@ import AddBookstore from '../../components/Booksnap/AddBookstore'; import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; import { postIndepBook } from '../../api/booksnap.api'; import cropImage from '../../utils/cropImage'; +import Loading from '../Loading'; export type Option = { bookstoreId: number; @@ -28,8 +29,10 @@ const CreateBook = () => { const [previewList, setPreviewList] = useState([]); // 미리보기용 URL 저장 const [selectedBookstores, setSelectedBookstores] = useState([]); + const [isLoading, setIsLoading] = useState(false); const handleReviewPost = async () => { + setIsLoading(true); const bookstoreIds = selectedBookstores.map((b) => b.bookstoreId); const payload = { @@ -47,6 +50,7 @@ const CreateBook = () => { imageToUpload = await cropImage(url, 80, 120); // 자른 File 반환 } postIndepBook(imageToUpload, payload).then(() => { + setIsLoading(false); nav('/booksnap'); }); }; @@ -60,6 +64,10 @@ const CreateBook = () => { } }; + if (isLoading) { + return ; + } + return (
{/* 헤더 */} diff --git a/src/pages/Booksnap/CreateBooksnapReview2.tsx b/src/pages/Booksnap/CreateBooksnapReview2.tsx index 3aff029..aedec42 100644 --- a/src/pages/Booksnap/CreateBooksnapReview2.tsx +++ b/src/pages/Booksnap/CreateBooksnapReview2.tsx @@ -7,6 +7,7 @@ import WritingReview from '../../components/Zip/WritingReview'; import Button from '../../components/Button/Button'; import { postBookReview } from '../../api/booksnap.api'; import Step from '../../components/Booksnap/Step'; +import Loading from '../Loading'; const CreateBooksnapReview2 = () => { const location = useLocation(); @@ -16,17 +17,25 @@ const CreateBooksnapReview2 = () => { const [rating, setRating] = useState(0); const [review, setReview] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const handleReviewPost = () => { + setIsLoading(true); const payload = { isbn: book.isbn, rating: rating, reviewText: review, }; postBookReview('normal', payload).then((data) => { + setIsLoading(false); nav('/booksnap'); }); }; + if (isLoading) { + return ; + } + return (
{/* 헤더 */} diff --git a/src/pages/Booksnap/IndiCreateBookReview2.tsx b/src/pages/Booksnap/IndiCreateBookReview2.tsx index db0c73e..f8bf1d1 100644 --- a/src/pages/Booksnap/IndiCreateBookReview2.tsx +++ b/src/pages/Booksnap/IndiCreateBookReview2.tsx @@ -8,6 +8,7 @@ import Button from '../../components/Button/Button'; import { postBookReview } from '../../api/booksnap.api'; import Step from '../../components/Booksnap/Step'; import AddBookstore from '../../components/Booksnap/AddBookstore'; +import Loading from '../Loading'; export type Option = { bookstoreId: number; @@ -25,8 +26,10 @@ const IndiCreateBookReview2 = () => { const [review, setReview] = useState(''); const [selectedBookstores, setSelectedBookstores] = useState([]); + const [isLoading, setIsLoading] = useState(false); const handleReviewPost = () => { + setIsLoading(true); const bookstoreIds = selectedBookstores.map((b) => b.bookstoreId); const payload = { @@ -36,11 +39,15 @@ const IndiCreateBookReview2 = () => { reviewText: review, }; postBookReview('indep', payload).then((data) => { - console.log('리뷰 등록 성공'); + setIsLoading(false); nav('/booksnap'); }); }; + if (isLoading) { + return ; + } + return (
{/* 헤더 */} diff --git a/src/pages/Loading.tsx b/src/pages/Loading.tsx index e5d2caf..7c89380 100644 --- a/src/pages/Loading.tsx +++ b/src/pages/Loading.tsx @@ -1,4 +1,4 @@ -import loading_logo from '../../public/icons/login-signup/Logo.png'; +import loading from '../../public/icons/book-snap/loading.gif'; interface LoadingProps { text: string; @@ -6,11 +6,11 @@ interface LoadingProps { const Loading = ({ text }: LoadingProps) => { return ( -
- -
-

{text}

-

잠시만 기다려주세요!

+
+ +
+

{text}

+

잠시만 기다려주세요!

);