diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 0000000..b63e97b --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,9 @@ +import axios from 'axios'; + +export const apiClient = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || 'https://backendbase.site', + headers: { + 'Content-Type': 'multipart/form-data', + }, + withCredentials: false, +}); diff --git a/src/components/chat/FileSendButton.tsx b/src/components/chat/FileSendButton.tsx new file mode 100644 index 0000000..d504008 --- /dev/null +++ b/src/components/chat/FileSendButton.tsx @@ -0,0 +1,96 @@ +import { useRef, useState } from 'react'; +import { apiClient } from '../../api/api'; +import axios from 'axios'; + +interface UploadResponse { + filename: string; + url: string; +} + +interface FileSendButtonProps { + onUploadSuccess?: () => void; +} + +const FileSendButton = ({ onUploadSuccess }: FileSendButtonProps) => { + const [uploading, setUploading] = useState(false); + const [uploadStatus, setUploadStatus] = useState(null); + const fileInputRef = useRef(null); + + const handleFileSelect = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + if (!file.name.toLowerCase().endsWith('.csv')) { + setUploadStatus('CSV 파일만 업로드 가능합니다.'); + return; + } + + setUploading(true); + setUploadStatus(null); + + try { + const formData = new FormData(); + formData.append('file', file); + + console.log('Uploading file:', file.name); + + const response = await apiClient.post('/chat/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + console.log('Upload success:', response.data); + + // 업로드 성공 콜백 호출 + onUploadSuccess?.(); + + // 파일 입력 초기화 + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } catch (error) { + console.error('Upload error:', error); + + if (axios.isAxiosError(error)) { + const message = + error.response?.data?.message || error.response?.statusText || error.message; + const status = error.response?.status || ''; + setUploadStatus(`업로드 실패${status ? ` (${status})` : ''}: ${message}`); + } else if (error instanceof Error) { + setUploadStatus(`에러: ${error.message}`); + } else { + setUploadStatus('알 수 없는 에러가 발생했습니다.'); + } + } finally { + setUploading(false); + } + }; + + const handleButtonClick = () => { + fileInputRef.current?.click(); + }; + + return ( +
+ + + {uploadStatus &&

dd{uploadStatus}

} +
+ ); +}; + +export default FileSendButton; diff --git a/src/index.tsx b/src/index.tsx index 8d90365..838ea52 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,7 +19,7 @@ const router = createBrowserRouter([ }, { - path: 'test', + path: 'chat', element: , }, ], diff --git a/src/pages/chat/Chat.tsx b/src/pages/chat/Chat.tsx deleted file mode 100644 index 5e6d4d7..0000000 --- a/src/pages/chat/Chat.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const Chat = () => { - return
Chat
; -}; - -export default Chat; diff --git a/src/pages/chat/ChatPageTest.tsx b/src/pages/chat/ChatPageTest.tsx index bbf5414..71bd04c 100644 --- a/src/pages/chat/ChatPageTest.tsx +++ b/src/pages/chat/ChatPageTest.tsx @@ -1,11 +1,27 @@ import { useEffect, useRef, useState } from 'react'; +import Navigation from '../../components/Navigation'; +import FileSendButton from '../../components/chat/FileSendButton'; + +interface Message { + id: string; + role: 'user' | 'assistant'; + content: string; +} export default function ChatPageTest() { - const [output, setOutput] = useState(''); + const [messages, setMessages] = useState([ + { + id: 'initial', + role: 'assistant', + content: '안녕하세요! Snowgent입니다❄️ \n재고 데이터 파일을 업로드 해주세요', + }, + ]); const [input, setInput] = useState(''); const [sessionId, setSessionId] = useState(null); + const [isFileUploaded, setIsFileUploaded] = useState(false); const socketRef = useRef(null); - const outputRef = useRef(null); + const messagesEndRef = useRef(null); + const inputRef = useRef(null); const mountedRef = useRef(false); @@ -18,11 +34,9 @@ export default function ChatPageTest() { socket.onopen = () => { console.log('Connected'); - setOutput((prev) => prev + 'Connected\n'); }; socket.onmessage = (event) => { - // bedrock stream done 메시지는 콘솔에만 표시 if (event.data.includes('[bedrock stream done]')) { console.log('Stream done:', event.data); return; @@ -32,23 +46,56 @@ export default function ChatPageTest() { const json = JSON.parse(event.data); if (json.type === 'session') { setSessionId(json.session_id); - setOutput((prev) => prev + `[세션 ID: ${json.session_id}]\n`); + console.log(`${sessionId}`); + console.log(`Session ID: ${json.session_id}`); return; } } catch { /* not JSON */ } - setOutput((prev) => prev + event.data + '\n'); + + // 어시스턴트 메시지 처리 + setMessages((prev) => { + const lastMessage = prev[prev.length - 1]; + + // 마지막 메시지가 어시스턴트이고 내용이 비어있으면 업데이트 + if (lastMessage && lastMessage.role === 'assistant' && lastMessage.content === '') { + const updated = [...prev]; + updated[updated.length - 1] = { + ...lastMessage, + content: lastMessage.content + event.data, + }; + return updated; + } + + // 마지막 메시지가 어시스턴트이면 이어붙이기 + if (lastMessage && lastMessage.role === 'assistant') { + const updated = [...prev]; + updated[updated.length - 1] = { + ...lastMessage, + content: lastMessage.content + event.data, + }; + return updated; + } + + // 새로운 어시스턴트 메시지 생성 + return [ + ...prev, + { + id: Date.now().toString(), + role: 'assistant', + content: event.data, + }, + ]; + }); }; socket.onerror = (error) => { console.error('WebSocket Error:', error); - setOutput((prev) => prev + 'Error occurred\n'); }; socket.onclose = (event) => { console.log('Disconnected:', event.code, event.reason); - setOutput((prev) => prev + `Disconnected: ${event.code}\n`); }; return () => { @@ -58,74 +105,108 @@ export default function ChatPageTest() { // 자동 스크롤 useEffect(() => { - if (outputRef.current) { - outputRef.current.scrollTop = outputRef.current.scrollHeight; + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + // 입력창 자동 높이 조절 + useEffect(() => { + if (inputRef.current) { + inputRef.current.style.height = 'auto'; + inputRef.current.style.height = inputRef.current.scrollHeight + 'px'; } - }, [output]); + }, [input]); + + const handleUploadSuccess = () => { + setIsFileUploaded(true); + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString(), + role: 'assistant', + content: '업로드 완료되었습니다. 재고 관리 채팅을 시작하세요', + }, + ]); + }; const sendMessage = () => { const text = input.trim(); if (!text || !socketRef.current) return; if (socketRef.current.readyState === WebSocket.OPEN) { + // 사용자 메시지 추가 + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString(), + role: 'user', + content: text, + }, + ]); + + // WebSocket으로 전송 socketRef.current.send(JSON.stringify({ role: 'user', content: text })); setInput(''); } else { - setOutput((prev) => prev + 'WebSocket이 연결되지 않음\n'); + console.error('WebSocket이 연결되지 않음'); } }; - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); sendMessage(); } }; return ( -
-

Chat Test

- {sessionId &&

세션 ID: {sessionId}

} -