11import { useEffect , useRef , useState } from 'react' ;
2+ import Navigation from '../../components/Navigation' ;
3+ import FileSendButton from '../../components/chat/FileSendButton' ;
4+
5+ interface Message {
6+ id : string ;
7+ role : 'user' | 'assistant' ;
8+ content : string ;
9+ }
210
311export default function ChatPageTest ( ) {
4- const [ output , setOutput ] = useState ( '' ) ;
12+ const [ messages , setMessages ] = useState < Message [ ] > ( [
13+ {
14+ id : 'initial' ,
15+ role : 'assistant' ,
16+ content : '안녕하세요! Snowgent입니다❄️ \n재고 데이터 파일을 업로드 해주세요' ,
17+ } ,
18+ ] ) ;
519 const [ input , setInput ] = useState ( '' ) ;
620 const [ sessionId , setSessionId ] = useState < string | null > ( null ) ;
21+ const [ isFileUploaded , setIsFileUploaded ] = useState ( false ) ;
722 const socketRef = useRef < WebSocket | null > ( null ) ;
8- const outputRef = useRef < HTMLTextAreaElement > ( null ) ;
23+ const messagesEndRef = useRef < HTMLDivElement > ( null ) ;
24+ const inputRef = useRef < HTMLTextAreaElement > ( null ) ;
925
1026 const mountedRef = useRef ( false ) ;
1127
@@ -18,11 +34,9 @@ export default function ChatPageTest() {
1834
1935 socket . onopen = ( ) => {
2036 console . log ( 'Connected' ) ;
21- setOutput ( ( prev ) => prev + 'Connected\n' ) ;
2237 } ;
2338
2439 socket . onmessage = ( event ) => {
25- // bedrock stream done 메시지는 콘솔에만 표시
2640 if ( event . data . includes ( '[bedrock stream done]' ) ) {
2741 console . log ( 'Stream done:' , event . data ) ;
2842 return ;
@@ -32,23 +46,56 @@ export default function ChatPageTest() {
3246 const json = JSON . parse ( event . data ) ;
3347 if ( json . type === 'session' ) {
3448 setSessionId ( json . session_id ) ;
35- setOutput ( ( prev ) => prev + `[세션 ID: ${ json . session_id } ]\n` ) ;
49+ console . log ( `${ sessionId } ` ) ;
50+ console . log ( `Session ID: ${ json . session_id } ` ) ;
3651 return ;
3752 }
3853 } catch {
3954 /* not JSON */
4055 }
41- setOutput ( ( prev ) => prev + event . data + '\n' ) ;
56+
57+ // 어시스턴트 메시지 처리
58+ setMessages ( ( prev ) => {
59+ const lastMessage = prev [ prev . length - 1 ] ;
60+
61+ // 마지막 메시지가 어시스턴트이고 내용이 비어있으면 업데이트
62+ if ( lastMessage && lastMessage . role === 'assistant' && lastMessage . content === '' ) {
63+ const updated = [ ...prev ] ;
64+ updated [ updated . length - 1 ] = {
65+ ...lastMessage ,
66+ content : lastMessage . content + event . data ,
67+ } ;
68+ return updated ;
69+ }
70+
71+ // 마지막 메시지가 어시스턴트이면 이어붙이기
72+ if ( lastMessage && lastMessage . role === 'assistant' ) {
73+ const updated = [ ...prev ] ;
74+ updated [ updated . length - 1 ] = {
75+ ...lastMessage ,
76+ content : lastMessage . content + event . data ,
77+ } ;
78+ return updated ;
79+ }
80+
81+ // 새로운 어시스턴트 메시지 생성
82+ return [
83+ ...prev ,
84+ {
85+ id : Date . now ( ) . toString ( ) ,
86+ role : 'assistant' ,
87+ content : event . data ,
88+ } ,
89+ ] ;
90+ } ) ;
4291 } ;
4392
4493 socket . onerror = ( error ) => {
4594 console . error ( 'WebSocket Error:' , error ) ;
46- setOutput ( ( prev ) => prev + 'Error occurred\n' ) ;
4795 } ;
4896
4997 socket . onclose = ( event ) => {
5098 console . log ( 'Disconnected:' , event . code , event . reason ) ;
51- setOutput ( ( prev ) => prev + `Disconnected: ${ event . code } \n` ) ;
5299 } ;
53100
54101 return ( ) => {
@@ -58,74 +105,108 @@ export default function ChatPageTest() {
58105
59106 // 자동 스크롤
60107 useEffect ( ( ) => {
61- if ( outputRef . current ) {
62- outputRef . current . scrollTop = outputRef . current . scrollHeight ;
108+ messagesEndRef . current ?. scrollIntoView ( { behavior : 'smooth' } ) ;
109+ } , [ messages ] ) ;
110+
111+ // 입력창 자동 높이 조절
112+ useEffect ( ( ) => {
113+ if ( inputRef . current ) {
114+ inputRef . current . style . height = 'auto' ;
115+ inputRef . current . style . height = inputRef . current . scrollHeight + 'px' ;
63116 }
64- } , [ output ] ) ;
117+ } , [ input ] ) ;
118+
119+ const handleUploadSuccess = ( ) => {
120+ setIsFileUploaded ( true ) ;
121+ setMessages ( ( prev ) => [
122+ ...prev ,
123+ {
124+ id : Date . now ( ) . toString ( ) ,
125+ role : 'assistant' ,
126+ content : '업로드 완료되었습니다. 재고 관리 채팅을 시작하세요' ,
127+ } ,
128+ ] ) ;
129+ } ;
65130
66131 const sendMessage = ( ) => {
67132 const text = input . trim ( ) ;
68133 if ( ! text || ! socketRef . current ) return ;
69134
70135 if ( socketRef . current . readyState === WebSocket . OPEN ) {
136+ // 사용자 메시지 추가
137+ setMessages ( ( prev ) => [
138+ ...prev ,
139+ {
140+ id : Date . now ( ) . toString ( ) ,
141+ role : 'user' ,
142+ content : text ,
143+ } ,
144+ ] ) ;
145+
146+ // WebSocket으로 전송
71147 socketRef . current . send ( JSON . stringify ( { role : 'user' , content : text } ) ) ;
72148 setInput ( '' ) ;
73149 } else {
74- setOutput ( ( prev ) => prev + 'WebSocket이 연결되지 않음\n ' ) ;
150+ console . error ( 'WebSocket이 연결되지 않음' ) ;
75151 }
76152 } ;
77153
78- const handleKeyPress = ( e : React . KeyboardEvent < HTMLInputElement > ) => {
79- if ( e . key === 'Enter' ) {
154+ const handleKeyPress = ( e : React . KeyboardEvent < HTMLTextAreaElement > ) => {
155+ if ( e . key === 'Enter' && ! e . shiftKey ) {
156+ e . preventDefault ( ) ;
80157 sendMessage ( ) ;
81158 }
82159 } ;
83160
84161 return (
85- < div style = { { padding : '20px' , maxWidth : '600px' , margin : '0 auto' } } >
86- < h3 > Chat Test</ h3 >
87- { sessionId && < p style = { { color : '#666' } } > 세션 ID: { sessionId } </ p > }
88- < textarea
89- ref = { outputRef }
90- value = { output }
91- readOnly
92- rows = { 15 }
93- style = { {
94- width : '100%' ,
95- marginBottom : '10px' ,
96- padding : '10px' ,
97- border : '1px solid #ccc' ,
98- borderRadius : '4px' ,
99- fontFamily : 'monospace' ,
100- } }
101- />
102- < br />
103- < div style = { { display : 'flex' , gap : '10px' } } >
104- < input
105- value = { input }
106- onChange = { ( e ) => setInput ( e . target . value ) }
107- onKeyPress = { handleKeyPress }
108- placeholder = "메시지를 입력하세요"
109- style = { {
110- flex : 1 ,
111- padding : '8px' ,
112- border : '1px solid #ccc' ,
113- borderRadius : '4px' ,
114- } }
115- />
116- < button
117- onClick = { sendMessage }
118- style = { {
119- padding : '8px 20px' ,
120- background : '#007bff' ,
121- color : 'white' ,
122- border : 'none' ,
123- borderRadius : '4px' ,
124- cursor : 'pointer' ,
125- } }
126- >
127- 보내기
128- </ button >
162+ < div className = "flex h-screen flex-col" >
163+ < Navigation />
164+ < div className = "flex flex-1 flex-col overflow-hidden p-5" >
165+ { /* 메시지 목록 */ }
166+ { /* 파일 업로드 버튼 - 업로드 전에만 표시 */ }
167+ { ! isFileUploaded && (
168+ < div className = "shrink-0" >
169+ < FileSendButton onUploadSuccess = { handleUploadSuccess } />
170+ </ div >
171+ ) }
172+ < div className = "flex-1 overflow-y-auto pb-4" >
173+ { messages . map ( ( message ) => (
174+ < div
175+ key = { message . id }
176+ className = { `mb-4 flex ${ message . role === 'user' ? 'justify-end' : 'justify-start' } ` }
177+ >
178+ < div
179+ className = { `max-w-[70%] rounded-2xl px-4 py-3 ${
180+ message . role === 'user' ? 'bg-[#0D2D84] text-white' : 'bg-gray-200 text-gray-800'
181+ } `}
182+ >
183+ < p className = "break-words whitespace-pre-wrap" > { message . content } </ p >
184+ </ div >
185+ </ div >
186+ ) ) }
187+ < div ref = { messagesEndRef } />
188+ </ div >
189+
190+ { /* 입력창 - 업로드 후에만 표시 */ }
191+ { isFileUploaded && (
192+ < div className = "flex shrink-0 gap-2" >
193+ < textarea
194+ ref = { inputRef }
195+ value = { input }
196+ onChange = { ( e ) => setInput ( e . target . value ) }
197+ onKeyDown = { handleKeyPress }
198+ placeholder = "메시지를 입력하세요"
199+ rows = { 1 }
200+ className = "max-h-20 flex-1 resize-none overflow-hidden rounded-xl border px-3 py-4 text-xl outline-none focus:border-blue-500"
201+ />
202+ < button
203+ onClick = { sendMessage }
204+ className = "rounded-xl bg-[#0D2D84] px-6 text-lg text-white hover:bg-[#0a2366]"
205+ >
206+ ▶
207+ </ button >
208+ </ div >
209+ ) }
129210 </ div >
130211 </ div >
131212 ) ;
0 commit comments