-
Notifications
You must be signed in to change notification settings - Fork 1
[FEATURE] 채팅 주고받기 UI 및 파일 전송 버튼 #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
bd6ae89
fbe2359
63e3258
f8e1e2b
dc26c6f
3c6e50d
2aab64d
1b4b969
e8de473
95e8fb0
ef5c71e
7ddc1dc
25297e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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': 'application/json', | ||
| }, | ||
| withCredentials: false, | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,133 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { useRef, useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { apiClient } from '../../api/api'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import axios from 'axios'; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| interface UploadResponse { | ||||||||||||||||||||||||||||||||||||||||||||||||
| filename: string; | ||||||||||||||||||||||||||||||||||||||||||||||||
| url: string; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const FileSendButton = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const [uploading, setUploading] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const [uploadStatus, setUploadStatus] = useState<string | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const [uploadedFile, setUploadedFile] = useState<UploadResponse | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const fileInputRef = useRef<HTMLInputElement>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const file = event.target.files?.[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!file) return; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (!file.name.toLowerCase().endsWith('.csv')) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| setUploadStatus('CSV 파일만 업로드 가능합니다.'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+23
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 서버 측 파일 타입 검증을 추가하세요. 클라이언트 측 CSV 확장자 검증만으로는 충분하지 않습니다. 사용자가 개발자 도구를 통해 우회할 수 있으므로 백엔드에서도 파일 타입과 내용을 검증해야 합니다.
Comment on lines
+19
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 파일 크기 검증을 추가하세요. 현재 CSV 확장자만 검증하고 있으나, 파일 크기 제한이 없어 매우 큰 파일을 업로드 시도할 수 있습니다. 이는 다음과 같은 문제를 발생시킬 수 있습니다:
다음과 같이 파일 크기 검증을 추가하세요: if (!file.name.toLowerCase().endsWith('.csv')) {
setUploadStatus('CSV 파일만 업로드 가능합니다.');
return;
}
+
+ const maxSizeInMB = 10;
+ const maxSizeInBytes = maxSizeInMB * 1024 * 1024;
+ if (file.size > maxSizeInBytes) {
+ setUploadStatus(`파일 크기는 ${maxSizeInMB}MB를 초과할 수 없습니다.`);
+ return;
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| setUploading(true); | ||||||||||||||||||||||||||||||||||||||||||||||||
| setUploadStatus(null); | ||||||||||||||||||||||||||||||||||||||||||||||||
| setUploadedFile(null); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const formData = new FormData(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| formData.append('file', file); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Uploading file:', file.name); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await apiClient.post<UploadResponse>('/chat/upload', formData, { | ||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'Content-Type': 'multipart/form-data', | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('=== 응답 정보 ==='); | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Status:', response.status); | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Data:', response.data); | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Data type:', typeof response.data); | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Is null?', response.data === null); | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Is undefined?', response.data === undefined); | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Keys:', response.data ? Object.keys(response.data) : 'no data'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Stringified:', JSON.stringify(response.data)); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // 응답이 있으면 성공 처리 | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (response.status === 200) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const data = response.data; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // 데이터가 있으면 저장 | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (data && typeof data === 'object') { | ||||||||||||||||||||||||||||||||||||||||||||||||
| setUploadedFile({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| filename: data.filename || file.name, | ||||||||||||||||||||||||||||||||||||||||||||||||
| url: data.url || '', | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| setUploadStatus(`업로드 성공! (${data.filename || file.name})`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // 데이터가 없어도 200이면 성공으로 간주 | ||||||||||||||||||||||||||||||||||||||||||||||||
| setUploadStatus('업로드 성공!'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // 파일 입력 초기화 | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (fileInputRef.current) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| fileInputRef.current.value = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Upload error:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (axios.isAxiosError(error)) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Axios error response:', error.response?.data); | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Axios error status:', error.response?.status); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const message = | ||||||||||||||||||||||||||||||||||||||||||||||||
| error.response?.data?.message || error.response?.statusText || error.message; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const status = error.response?.status || ''; | ||||||||||||||||||||||||||||||||||||||||||||||||
| setUploadStatus(`업로드 실패${status ? ` (${status})` : ''}: ${message}`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||
| setUploadStatus(error instanceof Error ? error.message : '업로드 실패'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||
| setUploading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const handleButtonClick = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| fileInputRef.current?.click(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex flex-col gap-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||
| ref={fileInputRef} | ||||||||||||||||||||||||||||||||||||||||||||||||
| type="file" | ||||||||||||||||||||||||||||||||||||||||||||||||
| accept=".csv" | ||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={handleFileSelect} | ||||||||||||||||||||||||||||||||||||||||||||||||
| className="hidden" | ||||||||||||||||||||||||||||||||||||||||||||||||
| disabled={uploading} | ||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={handleButtonClick} | ||||||||||||||||||||||||||||||||||||||||||||||||
| disabled={uploading} | ||||||||||||||||||||||||||||||||||||||||||||||||
| className="rounded-md bg-blue-500 p-2 text-white transition-colors hover:bg-blue-600 disabled:cursor-not-allowed disabled:bg-gray-400" | ||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||
| {uploading ? '업로드 중...' : '📎 CSV 파일 업로드'} | ||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||
| {uploadStatus && ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex flex-col gap-1"> | ||||||||||||||||||||||||||||||||||||||||||||||||
| <p | ||||||||||||||||||||||||||||||||||||||||||||||||
| className={`text-sm ${uploadStatus.includes('성공') ? 'text-green-600' : 'text-red-600'}`} | ||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||
| {uploadStatus} | ||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||
| {uploadedFile && ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <a | ||||||||||||||||||||||||||||||||||||||||||||||||
| href={uploadedFile.url} | ||||||||||||||||||||||||||||||||||||||||||||||||
| target="_blank" | ||||||||||||||||||||||||||||||||||||||||||||||||
| rel="noopener noreferrer" | ||||||||||||||||||||||||||||||||||||||||||||||||
| className="text-xs text-blue-500 underline hover:text-blue-700" | ||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||
| 파일 보기 | ||||||||||||||||||||||||||||||||||||||||||||||||
| </a> | ||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| export default FileSendButton; | ||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,11 +1,20 @@ | ||||||||||||||||||||||||||||||||||||||
| 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<Message[]>([]); | ||||||||||||||||||||||||||||||||||||||
| const [input, setInput] = useState(''); | ||||||||||||||||||||||||||||||||||||||
| const [sessionId, setSessionId] = useState<string | null>(null); | ||||||||||||||||||||||||||||||||||||||
| const socketRef = useRef<WebSocket | null>(null); | ||||||||||||||||||||||||||||||||||||||
| const outputRef = useRef<HTMLTextAreaElement>(null); | ||||||||||||||||||||||||||||||||||||||
| const messagesEndRef = useRef<HTMLDivElement>(null); | ||||||||||||||||||||||||||||||||||||||
| const inputRef = useRef<HTMLTextAreaElement>(null); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const mountedRef = useRef(false); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
@@ -18,11 +27,9 @@ | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 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,101 +39,150 @@ | |||||||||||||||||||||||||||||||||||||
| 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}`); | ||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 잘못된 변수 참조를 수정하세요.
다음과 같이 수정하세요: if (json.type === 'session') {
setSessionId(json.session_id);
- console.log(`${sessionId}`);
+ console.log(`Session ID: ${json.session_id}`);
return;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| 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 () => { | ||||||||||||||||||||||||||||||||||||||
| socket.close(); | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // 자동 스크롤 | ||||||||||||||||||||||||||||||||||||||
| 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 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<HTMLInputElement>) => { | ||||||||||||||||||||||||||||||||||||||
| if (e.key === 'Enter') { | ||||||||||||||||||||||||||||||||||||||
| const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { | ||||||||||||||||||||||||||||||||||||||
| if (e.key === 'Enter' && !e.shiftKey) { | ||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||
| sendMessage(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||
| <div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}> | ||||||||||||||||||||||||||||||||||||||
| <h3>Chat Test</h3> | ||||||||||||||||||||||||||||||||||||||
| {sessionId && <p style={{ color: '#666' }}>세션 ID: {sessionId}</p>} | ||||||||||||||||||||||||||||||||||||||
| <textarea | ||||||||||||||||||||||||||||||||||||||
| ref={outputRef} | ||||||||||||||||||||||||||||||||||||||
| value={output} | ||||||||||||||||||||||||||||||||||||||
| readOnly | ||||||||||||||||||||||||||||||||||||||
| rows={15} | ||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||
| width: '100%', | ||||||||||||||||||||||||||||||||||||||
| marginBottom: '10px', | ||||||||||||||||||||||||||||||||||||||
| padding: '10px', | ||||||||||||||||||||||||||||||||||||||
| border: '1px solid #ccc', | ||||||||||||||||||||||||||||||||||||||
| borderRadius: '4px', | ||||||||||||||||||||||||||||||||||||||
| fontFamily: 'monospace', | ||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| <br /> | ||||||||||||||||||||||||||||||||||||||
| <div style={{ display: 'flex', gap: '10px' }}> | ||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||
| value={input} | ||||||||||||||||||||||||||||||||||||||
| onChange={(e) => setInput(e.target.value)} | ||||||||||||||||||||||||||||||||||||||
| onKeyPress={handleKeyPress} | ||||||||||||||||||||||||||||||||||||||
| placeholder="메시지를 입력하세요" | ||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||
| flex: 1, | ||||||||||||||||||||||||||||||||||||||
| padding: '8px', | ||||||||||||||||||||||||||||||||||||||
| border: '1px solid #ccc', | ||||||||||||||||||||||||||||||||||||||
| borderRadius: '4px', | ||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||
| onClick={sendMessage} | ||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||
| padding: '8px 20px', | ||||||||||||||||||||||||||||||||||||||
| background: '#007bff', | ||||||||||||||||||||||||||||||||||||||
| color: 'white', | ||||||||||||||||||||||||||||||||||||||
| border: 'none', | ||||||||||||||||||||||||||||||||||||||
| borderRadius: '4px', | ||||||||||||||||||||||||||||||||||||||
| cursor: 'pointer', | ||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| 보내기 | ||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||
| <Navigation /> | ||||||||||||||||||||||||||||||||||||||
| <div className="flex h-full flex-col p-5"> | ||||||||||||||||||||||||||||||||||||||
| {/* 메시지 목록 */} | ||||||||||||||||||||||||||||||||||||||
| <div className="mb-4 flex-1 overflow-y-auto"> | ||||||||||||||||||||||||||||||||||||||
| {messages.map((message) => ( | ||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||
| key={message.id} | ||||||||||||||||||||||||||||||||||||||
| className={`mb-4 flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`} | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||
| className={`max-w-[70%] rounded-2xl px-4 py-3 ${ | ||||||||||||||||||||||||||||||||||||||
| message.role === 'user' ? 'bg-[#0D2D84] text-white' : 'bg-gray-200 text-gray-800' | ||||||||||||||||||||||||||||||||||||||
| }`} | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| <p className="break-words whitespace-pre-wrap">{message.content}</p> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||
| <div ref={messagesEndRef} /> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| {/* 파일 업로드 버튼 */} | ||||||||||||||||||||||||||||||||||||||
| <FileSendButton /> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| {/* 입력창 */} | ||||||||||||||||||||||||||||||||||||||
| <div className="mt-2 flex gap-2"> | ||||||||||||||||||||||||||||||||||||||
| <textarea | ||||||||||||||||||||||||||||||||||||||
| ref={inputRef} | ||||||||||||||||||||||||||||||||||||||
| value={input} | ||||||||||||||||||||||||||||||||||||||
| onChange={(e) => setInput(e.target.value)} | ||||||||||||||||||||||||||||||||||||||
| onKeyDown={handleKeyPress} | ||||||||||||||||||||||||||||||||||||||
| placeholder="메세지를 입력하세요" | ||||||||||||||||||||||||||||||||||||||
| rows={1} | ||||||||||||||||||||||||||||||||||||||
| 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" | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| <textarea | |
| ref={inputRef} | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| onKeyDown={handleKeyPress} | |
| placeholder="메세지를 입력하세요" | |
| rows={1} | |
| 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" | |
| /> | |
| <textarea | |
| ref={inputRef} | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| onKeyDown={handleKeyPress} | |
| placeholder="메세지를 입력하세요" | |
| rows={1} | |
| className="max-h-32 flex-1 resize-none overflow-y-auto rounded-xl border px-3 py-4 text-xl outline-none focus:border-blue-500" | |
| /> |
🤖 Prompt for AI Agents
In src/pages/chat/ChatPageTest.tsx around lines 169 to 177, the textarea uses
max-h-20 and overflow-hidden which causes long messages to be visually clipped;
change the styling to allow vertical scrolling or larger max height — replace
overflow-hidden with overflow-y-auto (or increase max-h value) so users can
scroll through multi-line input, and ensure rows/minHeight remain appropriate
for UX.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
전역 Content-Type 헤더를 제거하세요.
Content-Type: multipart/form-data를 전역 헤더로 설정하면 두 가지 문제가 발생합니다:boundary파라미터가 필요한데, 수동으로 헤더를 설정하면 boundary가 포함되지 않아 업로드가 실패합니다.Axios는
FormData객체를 전달할 때 자동으로 올바른 헤더와 boundary를 설정하므로, 이 헤더 설정을 완전히 제거해야 합니다.다음 diff를 적용하세요:
export const apiClient = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || 'https://backendbase.site', - headers: { - 'Content-Type': 'multipart/form-data', - }, withCredentials: false, });📝 Committable suggestion
🤖 Prompt for AI Agents