Skip to content

Commit 5e3005c

Browse files
authored
Merge pull request #66 from TEAM-ZIP/feat/bookie
[Feat] Bookie 연동 및 로딩 페이지
2 parents 4080ae9 + 495d02c commit 5e3005c

9 files changed

Lines changed: 187 additions & 24 deletions

File tree

public/icons/book-snap/loading.gif

86.5 KB
Loading

src/api/bookie.api.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// utils/chatAPI.ts
2+
import instance from './instance';
3+
4+
// 한줄 리뷰 등록 (Spring 서버를 통해 FastAPI 프록시로 전달)
5+
export const sendMessageToChatAPI = async (message: string) => {
6+
try {
7+
const response = await instance.post('/bookie/chat', { message: message });
8+
9+
if (response.status === 200) {
10+
return response.data;
11+
} else {
12+
throw new Error('Server responded with error');
13+
}
14+
} catch (error) {
15+
console.error('❌ chat API 호출 실패:', error);
16+
throw error;
17+
}
18+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// utils/bookieLoadingMessages.ts
2+
3+
export const bookieLoadingMessages = [
4+
// 📖 책 팁 / 독서 팁
5+
'📚 하루 10분 독서만 해도 1년에 책 12권은 읽을 수 있어요!',
6+
'🕯️ 자기 전 15분 독서는 스트레스를 68% 줄여준대요.',
7+
'📖 집중이 안 될 땐 10분짜리 짧은 독립출판물부터 시작해보세요.',
8+
'📎 종이책을 읽을 땐 포스트잇을 곁에 두는 습관, 생각 정리에 좋아요.',
9+
'📱 전자책도 좋아요, 하지만 눈을 쉬게 하려면 종이책도 가끔씩은 꼭!',
10+
11+
// 🎯 문학 퀴즈 / 정보
12+
'퀴즈! 세계에서 가장 많이 팔린 책은 무엇일까요? (힌트: 성경 말고!)',
13+
'퀴즈! 해리포터 1권의 원제는 무엇일까요?',
14+
'작가가 27번 거절당하고도 출간한 책, 알고 있나요? — 《해리 포터》 시리즈!',
15+
'미국의 국민 시인이라 불리는 작가는 누구일까요? (힌트: ‘풀잎 위에서’)',
16+
'‘책의 날’은 매년 4월 23일! 세르반테스와 셰익스피어가 같은 날 세상을 떠났어요.',
17+
18+
// 🗂️ 독립출판/인디북스 정보
19+
'독립출판물은 보통 작가가 직접 기획, 집필, 디자인, 제작까지 한답니다.',
20+
'부키 팁! 독립서점마다 특별한 큐레이션 책장이 있는 거, 알고 계셨나요?',
21+
'‘지금 여기, 내가 만든 책’ — 독립출판의 핵심은 바로 이 말이에요.',
22+
'독립출판물은 1쇄 100부도 가능해요. 나만의 책 만들기도 도전해보세요!',
23+
'서울에는 약 100여 개의 독립서점이 존재해요. 부키가 추천해드릴까요?',
24+
25+
// 💡 랜덤 흥미 정보 (재미용)
26+
'지구상에서 가장 오래된 책은 기원전 2400년의 ‘죽은 자의 서’랍니다.',
27+
'고서 수집가들 사이에서 가장 비싼 책은 3천억 원이 넘게 거래되기도 했어요!',
28+
'세계 최초의 소설은 일본의 《겐지 이야기》라는 거, 알고 계셨나요?',
29+
'📖 부키는 지금 책향기가 나는 데이터 속으로 잠수 중입니다…',
30+
'책도 사람도, 겉보다 속이 더 중요하답니다. 😊',
31+
32+
'《책방》이라는 단어는 일본식 한자어예요. 원래는 ‘서점’이 맞답니다!',
33+
'세상에서 가장 오래된 도서관은 기원전 7세기 이집트에 있었어요.',
34+
'어떤 책은 종이를 직접 염색해서 만드는 ‘리소’ 인쇄를 써요!',
35+
'책을 선물하면 기억에 오래 남는대요. 이유는 ‘시간을 주는 거니까’.',
36+
'📦 독립출판은 때로 택배 상자 속 한 줄 편지로 완성돼요.',
37+
'누군가는 밤새워 만든 40쪽짜리 책, 당신이 펼칠 차례예요.',
38+
];

src/pages/Bookie.tsx

Lines changed: 92 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,74 @@
1-
import Header from '../components/Common/Header';
21
import bookie from '../../public/icons/bookie/bookie.png';
32
import MessageBox from '../components/Bookie/MessageBox';
43
import { useEffect, useRef, useState } from 'react';
54
import Input from '../components/Bookie/Input';
65
import { FaRegArrowAltCircleUp } from 'react-icons/fa';
7-
import { FaAngleLeft } from 'react-icons/fa6';
86
import { IoCloseOutline } from 'react-icons/io5';
97
import { useNavigate } from 'react-router-dom';
8+
import { sendMessageToChatAPI } from '../api/bookie.api';
9+
import { MdBookmarkAdd } from 'react-icons/md';
10+
import toast from 'react-hot-toast';
11+
import { pickBook } from '../api/booksnap.api';
12+
import { bookieLoadingMessages } from '../constants/bookieLoadingMessages';
1013

1114
type MessageType = {
1215
text: string;
1316
type: 'system' | 'user';
17+
books?: BookCardType[];
18+
};
19+
20+
type BookCardType = {
21+
title: string;
22+
bookId: string;
23+
bookImageUrl: string;
1424
};
1525

1626
const Bookie = () => {
1727
const endOfMessages = useRef<HTMLDivElement | null>(null);
1828
const [input, setInput] = useState<string>('');
1929
const nav = useNavigate();
2030
const [isComposing, setIsComposing] = useState(false);
31+
const [isLoading, setIsLoading] = useState(false);
32+
const [loadingMessage, setLoadingMessage] = useState<string>('');
33+
const userName = '이구역독서짱';
2134
const [systemRes, setSystemRes] = useState<MessageType[]>([
2235
{
23-
text: '안녕하세요! 이구역 독서짱님이 좋아하실만한책을 추천해드리는 Bookie입니다! 더 많은 정보를 알려주시면, 책을 찾아드릴게요.',
36+
text: `안녕하세요! ${userName}님이 좋아하실만한책을 추천해드리는 Bookie입니다! 더 많은 정보를 알려주시면, 책을 찾아드릴게요.`,
2437
type: 'system',
2538
},
2639
]);
27-
const userName = '이구역 독서짱';
2840

2941
useEffect(() => {
3042
if (endOfMessages.current) {
3143
endOfMessages.current.scrollIntoView({ behavior: 'smooth' });
3244
}
3345
}, [systemRes]);
3446

35-
const sendMessage = () => {
47+
// 메세지 보내기
48+
const sendMessage = async () => {
3649
if (!input.trim() || isComposing) return;
37-
if (input.trim() !== '') {
38-
const userMessage: MessageType = { text: input, type: 'user' };
39-
setSystemRes((prevMessages) => [...prevMessages, userMessage]);
4050

41-
setInput('');
51+
const userMessage: MessageType = { text: input, type: 'user' };
52+
setSystemRes((prev) => [...prev, userMessage]); // 사용자 메시지 먼저 출력
53+
setInput('');
54+
55+
const random = bookieLoadingMessages[Math.floor(Math.random() * bookieLoadingMessages.length)];
56+
setLoadingMessage(random);
57+
setIsLoading(true);
58+
59+
try {
60+
const reply = await sendMessageToChatAPI(input);
61+
const systemMessage: MessageType = { text: reply.message, type: 'system', books: reply.books };
62+
setSystemRes((prev) => [...prev, systemMessage]); // GPT 응답 추가
63+
} catch (error) {
64+
const errorMessage: MessageType = {
65+
text: '서버와 연결할 수 없습니다.',
66+
type: 'system',
67+
};
68+
setSystemRes((prev) => [...prev, errorMessage]);
69+
} finally {
70+
setIsLoading(false);
71+
setLoadingMessage('');
4272
}
4373
};
4474

@@ -53,10 +83,25 @@ const Bookie = () => {
5383
}
5484
};
5585

86+
const handlePickBook = (book: BookCardType) => {
87+
console.log(book);
88+
if (!localStorage.getItem('accessToken')) {
89+
toast.error('로그인이 필요한 서비스입니다.');
90+
} else {
91+
pickBook(book.bookId).then((data) => {
92+
if (data?.success) {
93+
toast.success(`${book.title}을(를) 책장에 담았어요!`);
94+
} else {
95+
toast.error(`${data?.message}`);
96+
}
97+
});
98+
}
99+
};
100+
56101
return (
57102
<div className="mt-[70px] flex flex-col">
58103
{/* 헤더 */}
59-
<div className="fixed left-0 right-0 top-0 m-auto w-full max-w-[500px]">
104+
<div className="fixed left-0 right-0 top-0 z-30 m-auto w-full max-w-[500px]">
60105
<div className="flex items-center bg-bg px-2 py-3">
61106
<div className="flex cursor-pointer items-center justify-center p-2.5" onClick={() => nav('/')}>
62107
<IoCloseOutline size={30} className="stroke-white" />
@@ -69,7 +114,7 @@ const Bookie = () => {
69114
</div>
70115
</div>
71116
{/* 내용 */}
72-
<div className="flex flex-col">
117+
<div className="flex flex-col overflow-y-auto">
73118
{/* 설명 */}
74119
<div className="px-8 py-2 text-[14px] font-light">
75120
<span className="text-pink">{userName}</span>
@@ -78,29 +123,60 @@ const Bookie = () => {
78123
책과 서점을 추천해드릴게요!
79124
</span>
80125
</div>
81-
82126
<div className="flex w-full justify-center bg-gradient-to-b from-[#302D2D] to-[#C0E0D8]">
83127
<img src={bookie} className="w-24 pt-2" />
84128
</div>
85129
{/* 채팅구역 */}
86130
<div
87-
className="pointer-events-auto z-10 my-10 flex max-h-[80%] w-full flex-col gap-3 self-end overflow-y-auto px-8"
131+
className="pointer-events-auto z-10 mb-20 mt-10 flex max-h-[80%] w-full flex-col gap-3 self-end"
88132
onWheel={(e) => e.stopPropagation()} // 휠 이벤트 차단
89133
>
90134
{systemRes.map((msg, index) => (
91-
<MessageBox key={index} text={msg.text} type={msg.type} />
135+
<div key={index} className="flex flex-col gap-2 px-6">
136+
<MessageBox text={msg.text} type={msg.type} />
137+
{msg.books && msg.books.length > 0 && (
138+
<div className="mt-1 flex flex-col gap-3">
139+
<div className="flex gap-4">
140+
{msg.books.map((book, idx) => (
141+
<div key={idx} className="flex flex-col items-center rounded-lg shadow-md">
142+
<div className="relative">
143+
<img
144+
src={book.bookImageUrl}
145+
alt={book.title}
146+
className="mb-2 h-36 w-24 rounded-md object-cover"
147+
/>
148+
<button
149+
className="absolute bottom-4 right-2 rounded-full bg-white p-1 shadow-md"
150+
onClick={() => handlePickBook(book)}
151+
>
152+
<MdBookmarkAdd className="h-4 w-4 text-gray-500" />
153+
</button>
154+
</div>
155+
<p className="text-center text-sm font-medium text-white">{book.title}</p>
156+
</div>
157+
))}
158+
</div>
159+
</div>
160+
)}
161+
</div>
92162
))}
163+
{isLoading && (
164+
<div className="flex flex-col items-center justify-center py-2 text-white">
165+
<h3 className="text-[15px]">부키가 책을 고르러 작은 서점 골목으로 들어갔어요...📚</h3>
166+
<p className="animate-pulse text-[12px] italic">{loadingMessage}</p>
167+
</div>
168+
)}
93169
<div ref={endOfMessages}></div>
94170
</div>
95171
</div>
96172
{/* 입력창 */}
97-
<div className="fixed bottom-8 m-auto flex w-full max-w-[500px] items-center justify-between gap-2 pl-9 pr-[27px]">
173+
<div className="fixed bottom-0 z-30 m-auto flex w-full max-w-[500px] items-center justify-between gap-2 bg-bg pb-8 pl-9 pr-[27px]">
98174
<Input input={input} setInput={setInput} onSend={sendMessage} onComposition={handleComposition} />
99175
<div className="flex h-11 w-11 items-center justify-center drop-shadow-md">
100176
{input == '' ? (
101177
<FaRegArrowAltCircleUp className="h-full w-full fill-white" />
102178
) : (
103-
<div className="bg-pink flex h-9 w-9 items-center justify-center rounded-full">
179+
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-pink">
104180
<FaRegArrowAltCircleUp className="h-7 w-7 fill-bg" />
105181
</div>
106182
)}

src/pages/Booksnap/BookSnap.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import Loading from '../Loading';
66
import WriteButton from '../../components/Booksnap/WriteButton';
77
import { getReview } from '../../api/booksnap.api';
88
import Toast from '../../components/Common/Toast';
9-
import { useNavigate } from 'react-router-dom';
109
import BooksnapHeader from '../../components/Header/BooksnapHeader';
1110

1211
const BookSnap = () => {
@@ -17,16 +16,20 @@ const BookSnap = () => {
1716
const [isBottom, setIsBottom] = useState<boolean>(false);
1817
const mainRef = useRef<HTMLDivElement>(null);
1918
const isLastRef = useRef<boolean>(false);
19+
const [isLoading, setIsLoading] = useState(false);
2020

2121
// 리뷰 목록 받아오기
2222
const getReviews = async () => {
23+
setIsLoading(true);
2324
try {
2425
const data = await getReview(filter, page);
2526
setReview((prev) => (page === 1 ? data.data.booksnapPreview : [...prev, ...data.data.booksnapPreview]));
2627
setIsLast(data.data.last);
2728
setIsBottom(false);
2829
} catch (error) {
2930
console.error('리뷰를 불러오는 중 에러 발생:', error);
31+
} finally {
32+
setIsLoading(false);
3033
}
3134
};
3235

@@ -83,6 +86,10 @@ const BookSnap = () => {
8386
}
8487
}, []);
8588

89+
if (isLoading) {
90+
return <Loading text="리뷰 목록을 불러오는 중입니다!" />;
91+
}
92+
8693
if (!review) {
8794
return <Loading text="데이터를 불러오는 데 실패했습니다!" />;
8895
}

src/pages/Booksnap/CreateBook.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import AddBookstore from '../../components/Booksnap/AddBookstore';
99
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
1010
import { postIndepBook } from '../../api/booksnap.api';
1111
import cropImage from '../../utils/cropImage';
12+
import Loading from '../Loading';
1213

1314
export type Option = {
1415
bookstoreId: number;
@@ -28,8 +29,10 @@ const CreateBook = () => {
2829
const [previewList, setPreviewList] = useState<string[]>([]); // 미리보기용 URL 저장
2930

3031
const [selectedBookstores, setSelectedBookstores] = useState<readonly Option[]>([]);
32+
const [isLoading, setIsLoading] = useState(false);
3133

3234
const handleReviewPost = async () => {
35+
setIsLoading(true);
3336
const bookstoreIds = selectedBookstores.map((b) => b.bookstoreId);
3437

3538
const payload = {
@@ -47,6 +50,7 @@ const CreateBook = () => {
4750
imageToUpload = await cropImage(url, 80, 120); // 자른 File 반환
4851
}
4952
postIndepBook(imageToUpload, payload).then(() => {
53+
setIsLoading(false);
5054
nav('/booksnap');
5155
});
5256
};
@@ -60,6 +64,10 @@ const CreateBook = () => {
6064
}
6165
};
6266

67+
if (isLoading) {
68+
return <Loading text="리뷰를 등록중입니다!" />;
69+
}
70+
6371
return (
6472
<div className="flex h-full flex-col bg-bg pb-[50px] pt-[70px]">
6573
{/* 헤더 */}

src/pages/Booksnap/CreateBooksnapReview2.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import WritingReview from '../../components/Zip/WritingReview';
77
import Button from '../../components/Button/Button';
88
import { postBookReview } from '../../api/booksnap.api';
99
import Step from '../../components/Booksnap/Step';
10+
import Loading from '../Loading';
1011

1112
const CreateBooksnapReview2 = () => {
1213
const location = useLocation();
@@ -16,17 +17,25 @@ const CreateBooksnapReview2 = () => {
1617
const [rating, setRating] = useState(0);
1718
const [review, setReview] = useState('');
1819

20+
const [isLoading, setIsLoading] = useState(false);
21+
1922
const handleReviewPost = () => {
23+
setIsLoading(true);
2024
const payload = {
2125
isbn: book.isbn,
2226
rating: rating,
2327
reviewText: review,
2428
};
2529
postBookReview('normal', payload).then((data) => {
30+
setIsLoading(false);
2631
nav('/booksnap');
2732
});
2833
};
2934

35+
if (isLoading) {
36+
return <Loading text="리뷰를 등록중입니다!" />;
37+
}
38+
3039
return (
3140
<div className="flex h-full flex-col bg-bg pb-[50px] pt-[70px]">
3241
{/* 헤더 */}

src/pages/Booksnap/IndiCreateBookReview2.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Button from '../../components/Button/Button';
88
import { postBookReview } from '../../api/booksnap.api';
99
import Step from '../../components/Booksnap/Step';
1010
import AddBookstore from '../../components/Booksnap/AddBookstore';
11+
import Loading from '../Loading';
1112

1213
export type Option = {
1314
bookstoreId: number;
@@ -25,8 +26,10 @@ const IndiCreateBookReview2 = () => {
2526
const [review, setReview] = useState('');
2627

2728
const [selectedBookstores, setSelectedBookstores] = useState<readonly Option[]>([]);
29+
const [isLoading, setIsLoading] = useState(false);
2830

2931
const handleReviewPost = () => {
32+
setIsLoading(true);
3033
const bookstoreIds = selectedBookstores.map((b) => b.bookstoreId);
3134

3235
const payload = {
@@ -36,11 +39,15 @@ const IndiCreateBookReview2 = () => {
3639
reviewText: review,
3740
};
3841
postBookReview('indep', payload).then((data) => {
39-
console.log('리뷰 등록 성공');
42+
setIsLoading(false);
4043
nav('/booksnap');
4144
});
4245
};
4346

47+
if (isLoading) {
48+
return <Loading text="리뷰를 등록중입니다!" />;
49+
}
50+
4451
return (
4552
<div className="flex h-full flex-col bg-bg pb-[50px] pt-[70px]">
4653
{/* 헤더 */}

0 commit comments

Comments
 (0)