1- import Header from '../components/Common/Header' ;
21import bookie from '../../public/icons/bookie/bookie.png' ;
32import MessageBox from '../components/Bookie/MessageBox' ;
43import { useEffect , useRef , useState } from 'react' ;
54import Input from '../components/Bookie/Input' ;
65import { FaRegArrowAltCircleUp } from 'react-icons/fa' ;
7- import { FaAngleLeft } from 'react-icons/fa6' ;
86import { IoCloseOutline } from 'react-icons/io5' ;
97import { 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
1114type 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
1626const 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 ) }
0 commit comments