-
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 3 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,114 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| import axios from 'axios'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { useRef, useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| 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
+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); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const uploadUrl = '/chat/upload'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!uploadUrl) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('업로드 URL이 설정되지 않았습니다.'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Uploading file:', file.name, 'to:', uploadUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await axios.post<UploadResponse>(uploadUrl, formData, { | ||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'Content-Type': 'multipart/form-data', | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| withCredentials: false, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await axios.post<UploadResponse>(uploadUrl, formData, { | |
| headers: { | |
| 'Content-Type': 'multipart/form-data', | |
| }, | |
| withCredentials: false, | |
| }); | |
| const response = await axios.post<UploadResponse>(uploadUrl, formData, { | |
| withCredentials: false, | |
| }); |
🤖 Prompt for AI Agents
In src/components/chat/FileSendButton.tsx around lines 39 to 44, the code is
manually setting the 'Content-Type' header for a FormData POST which can omit
the required boundary and break the request; remove the explicit 'Content-Type'
header from the axios.post call so axios can set the correct multipart/form-data
header automatically (keep other options like withCredentials as-is).
| 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.
서버 측 파일 타입 검증을 추가하세요.
클라이언트 측 CSV 확장자 검증만으로는 충분하지 않습니다. 사용자가 개발자 도구를 통해 우회할 수 있으므로 백엔드에서도 파일 타입과 내용을 검증해야 합니다.