-
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 all 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': 'multipart/form-data', | ||
| }, | ||
| withCredentials: false, | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<string | 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); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| 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('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 ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <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="my-2 w-fit rounded-md bg-blue-500 p-4 text-white transition-colors hover:bg-blue-600 disabled:cursor-not-allowed disabled:bg-gray-400" | ||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||
| {uploading ? '업로드 중...' : '📎파일 업로드'} | ||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||
| {uploadStatus && <p className="text-sm text-red-600">dd{uploadStatus}</p>} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
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. 오타를 수정하세요. 에러 메시지 표시 부분에 "dd" 텍스트가 잘못 포함되어 있습니다. 다음과 같이 수정하세요: - {uploadStatus && <p className="text-sm text-red-600">dd{uploadStatus}</p>}
+ {uploadStatus && <p className="text-sm text-red-600">{uploadStatus}</p>}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| export default FileSendButton; | ||||||||||||||||||||||||||||||||||||||||||||||||
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<Message[]>([ | ||||||||||||||
| { | ||||||||||||||
| id: 'initial', | ||||||||||||||
| role: 'assistant', | ||||||||||||||
| content: '안녕하세요! Snowgent입니다❄️ \n재고 데이터 파일을 업로드 해주세요', | ||||||||||||||
| }, | ||||||||||||||
| ]); | ||||||||||||||
| const [input, setInput] = useState(''); | ||||||||||||||
| const [sessionId, setSessionId] = useState<string | null>(null); | ||||||||||||||
| const [isFileUploaded, setIsFileUploaded] = useState(false); | ||||||||||||||
| 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 +34,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,100 +46,167 @@ | |||||||||||||
| 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 |
||||||||||||||
| 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 () => { | ||||||||||||||
| 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 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<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> | ||||||||||||||
| <div className="flex h-screen flex-col"> | ||||||||||||||
| <Navigation /> | ||||||||||||||
| <div className="flex flex-1 flex-col overflow-hidden p-5"> | ||||||||||||||
| {/* 메시지 목록 */} | ||||||||||||||
| {/* 파일 업로드 버튼 - 업로드 전에만 표시 */} | ||||||||||||||
| {!isFileUploaded && ( | ||||||||||||||
| <div className="shrink-0"> | ||||||||||||||
| <FileSendButton onUploadSuccess={handleUploadSuccess} /> | ||||||||||||||
| </div> | ||||||||||||||
| )} | ||||||||||||||
| <div className="flex-1 overflow-y-auto pb-4"> | ||||||||||||||
| {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> | ||||||||||||||
|
|
||||||||||||||
| {/* 입력창 - 업로드 후에만 표시 */} | ||||||||||||||
| {isFileUploaded && ( | ||||||||||||||
| <div className="flex shrink-0 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" | ||||||||||||||
| /> | ||||||||||||||
| <button | ||||||||||||||
| onClick={sendMessage} | ||||||||||||||
| className="rounded-xl bg-[#0D2D84] px-6 text-lg text-white hover:bg-[#0a2366]" | ||||||||||||||
| > | ||||||||||||||
| ▶ | ||||||||||||||
| </button> | ||||||||||||||
| </div> | ||||||||||||||
| )} | ||||||||||||||
| </div> | ||||||||||||||
| </div> | ||||||||||||||
| ); | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,9 @@ | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| "rewrites": [ | ||||||||||||||||||||||||||||||||
| { "source": "/(.*)", "destination": "/index.html" } | ||||||||||||||||||||||||||||||||
| { "source": "/(.*)", "destination": "/index.html" }, | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| "source": "/chat/:path*", | ||||||||||||||||||||||||||||||||
| "destination": "https://backendbase.site/chat/:path*" | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||
|
Comment on lines
2
to
8
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. 라우팅 순서를 수정하세요. Vercel은 rewrites를 배열 순서대로 처리합니다. 현재 Line 3의 catch-all 패턴 다음과 같이 순서를 변경하세요: {
"rewrites": [
- { "source": "/(.*)", "destination": "/index.html" },
{
"source": "/chat/:path*",
"destination": "https://backendbase.site/chat/:path*"
- }
+ },
+ { "source": "/(.*)", "destination": "/index.html" }
]
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
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