Skip to content

Conversation

@sunhwaaRj
Copy link
Collaborator

@sunhwaaRj sunhwaaRj commented Nov 7, 2025

  • 채팅 주고받는 UI
  • 파일 전송 버튼 및 전송기능

추후 구현사항

  • 초기 접근 시 채팅방 상단에 파일 전송 유도하는 문구와 파일전송버튼, 이 때는 입력 불가능
  • 그리고 정해진 응답 반환
  • 채팅 길어질 때 채팅 구역 높이 약간 줄이기, 지금은 이중으로 스크롤 발생

Summary by CodeRabbit

  • 새로운 기능

    • CSV 파일 업로드 버튼 추가 — 확장자 검사, 업로드 진행 표시, 성공/오류 안내 및 업로드 콜백 지원
    • 채팅 UI 개선 — 메시지 기반 대화 표시, 사용자/어시스턴트 구분, 자동 스크롤 및 입력창 자동 높이 조정
    • 입력 동작 향상 — Enter로 전송, Shift+Enter로 새 줄 입력 지원
  • 기타

    • 개발/배포 환경에서 /chat 경로 프록시/리라이트 및 기본 API 클라이언트 설정 추가

@sunhwaaRj sunhwaaRj self-assigned this Nov 7, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 7, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

CSV 업로드 버튼 컴포넌트 추가, ChatPageTest를 메시지 배열 기반 UI 및 WebSocket 스트리밍 누적 처리로 리팩터링, Axios apiClient 모듈 추가(기본 baseURL 및 multipart/form-data 헤더), Vite dev-server에 /chat 프록시 설정 및 Vercel에 /chat 리라이트 규칙 추가, 라우트에서 ChatPageTest 경로를 chat으로 변경 및 기존 Chat 페이지 제거.

Changes

Cohort / File(s) 변경 요약
FileSendButton 컴포넌트
src/components/chat/FileSendButton.tsx
숨김 CSV 파일 입력과 트리거 버튼 렌더링; .csv 확장자 검증; apiClient로 multipart/form-data POST /chat/upload 호출; 업로드 상태·메시지 관리; 업로드 중 UI 변경; 성공 시 입력 리셋 및 onUploadSuccess 콜백 호출; 기본 export 추가.
ChatPageTest 리팩터링
src/pages/chat/ChatPageTest.tsx
단일 출력에서 Message[] 모델로 전환; Navigation, FileSendButton 통합; 사용자/어시스턴트 메시지 버블 렌더링(정렬/스타일 분리); 자동 스크롤 및 textarea 자동 높이 조정; Enter/Shift+Enter 전송/줄바꿈 처리; WebSocket 스트리밍 청크를 최신 assistant 메시지에 누적; 업로드 성공 핸들러 추가; 기존 출력 영역 제거.
API 클라이언트 추가
src/api/api.ts
Axios 인스턴스 apiClient 추가: baseURLVITE_API_BASE_URL 또는 https://backendbase.site, 기본 헤더 Content-Type: multipart/form-data, withCredentials: false.
개발 서버 프록시 설정
vite.config.ts
Vite dev-server에 /chat 프록시 추가: 대상 https://backendbase.site, changeOrigin: true, secure: false.
Vercel 리라이트 추가
vercel.json
/chat/:path* 경로에 대해 https://backendbase.site/chat/:path*로 리라이트 규칙 추가(기존 catch-all 유지).
라우팅 변경 및 제거
src/index.tsx, src/pages/chat/Chat.tsx
ChatPageTest 경로를 test에서 chat으로 변경 (src/index.tsx); 기존 Chat 페이지(src/pages/chat/Chat.tsx)의 기본 export 제거(파일 삭제/제거됨).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as ChatPageTest
    participant WS as WebSocket
    participant Backend

    User->>UI: 텍스트 입력 및 전송(Enter)
    UI->>UI: user Message 추가 및 렌더링(자동 스크롤)
    UI->>WS: 메시지 전송
    WS->>Backend: 포워딩
    Backend-->>WS: 스트리밍 응답(청크)
    loop 청크 수신
        WS-->>UI: 콘텐츠 청크
        UI->>UI: 최신 assistant Message에 누적 및 렌더링
    end
Loading
sequenceDiagram
    participant User
    participant UI as FileSendButton
    participant API as apiClient/Backend

    User->>UI: 파일 선택 트리거
    UI->>UI: 확장자 검증 (.csv)
    UI->>API: POST /chat/upload (multipart/form-data)
    alt 업로드 성공
        API-->>UI: 업로드 메타데이터 (filename, url)
        UI->>UI: 상태 업데이트, 입력 리셋, onUploadSuccess 호출
    else 업로드 실패
        API-->>UI: 에러 응답
        UI->>User: 에러 메시지 표시
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • 집중 검토 항목:
    • src/pages/chat/ChatPageTest.tsx: WebSocket 스트리밍 누적 로직, 메시지 배열 동기화, 종료/에러 경계 처리
    • src/components/chat/FileSendButton.tsx: 파일 확장자 검증, multipart 폼 구성, Axios 에러 핸들링, input 리셋과 onUploadSuccess 호출 타이밍
    • src/api/api.ts의 전역 Content-Type: multipart/form-data 헤더가 다른 요청에 미치는 영향
    • vite.config.tssecure: falsevercel.json 리라이트가 로컬/배포 환경에서의 보안/라우팅 동작

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 변경 사항의 주요 내용을 명확하게 요약하고 있습니다. 채팅 UI와 파일 전송 버튼 기능이 정확히 반영되어 있으며, 간결하고 구체적입니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ddc1dc and 25297e8.

📒 Files selected for processing (2)
  • src/index.tsx (1 hunks)
  • src/pages/chat/Chat.tsx (0 hunks)

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Nov 7, 2025

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/pages/chat/ChatPageTest.tsx (1)

49-82: 메시지 누적 로직을 단순화하세요.

Lines 54-61과 64-71의 로직이 중복됩니다. 첫 번째 조건(content가 비어있는 경우)은 두 번째 조건(assistant 메시지)의 하위 케이스이므로 하나로 통합할 수 있습니다.

다음과 같이 리팩토링하세요:

       // 어시스턴트 메시지 처리
       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,
           },
         ];
       });
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e985a9 and 63e3258.

📒 Files selected for processing (3)
  • src/components/chat/FileSendButton.tsx (1 hunks)
  • src/pages/chat/ChatPageTest.tsx (3 hunks)
  • vite.config.ts (1 hunks)
🔇 Additional comments (6)
vite.config.ts (1)

8-16: 개발 환경 전용 설정임을 확인하세요.

secure: false 설정은 SSL 인증서 검증을 무시하므로 개발 환경에서만 사용해야 합니다. Vite 설정 파일은 일반적으로 개발 환경에만 적용되지만, 프로덕션 빌드에 이 설정이 영향을 미치지 않는지 확인하세요.

src/components/chat/FileSendButton.tsx (2)

54-68: 에러 처리 로직이 잘 구현되어 있습니다.

axios 에러와 일반 에러를 구분하여 처리하고, 상세한 에러 메시지를 사용자에게 제공하는 로직이 우수합니다.


43-43: 백엔드 API 인증 요구사항 확인 필요

현재 코드는 상대 경로 /chat/upload를 사용하지만, vite.config.ts의 프록시 설정에서 실제 백엔드는 https://backendbase.site로 설정되어 있습니다. 프로덕션 배포 시 이는 크로스 오리진 요청이 되며, 이 경우 withCredentials: false로 설정하면 쿠키가 전송되지 않습니다.

확인 필요 사항:

  • https://backendbase.site/chat/upload 백엔드 API가 쿠키 기반 인증(세션)을 요구하는가?
  • 만약 필요하면 withCredentials: true로 변경하고, 백엔드에서 Access-Control-Allow-Credentials: true 설정 필요
  • 또는 JWT 토큰 등 다른 인증 방식을 구현할 경우, 현재 설정이 맞는지 확인

현재 코드에서는 명시적인 인증 로직(토큰, 헤더 등)이 없으므로, 백엔드 API 사양과 인증 방식을 확인하여 적절하게 설정해야 합니다.

src/pages/chat/ChatPageTest.tsx (3)

21-96: useEffect 의존성 배열을 검토하세요.

useEffect 내부에서 sessionId를 사용하고 있지만(42번 라인) 의존성 배열에 포함되지 않았습니다. 다만 이는 단순 로깅 용도이고 WebSocket 연결은 한 번만 설정되어야 하므로 현재 구현이 의도된 것일 수 있습니다. 만약 eslint 경고가 발생한다면, 42번 라인의 수정 후에도 경고가 남아있는지 확인하세요.


104-109: textarea 자동 높이 조절 로직이 잘 구현되어 있습니다.

입력 내용에 따라 동적으로 높이를 조절하는 로직이 깔끔하게 구현되었습니다.


134-139: 키보드 입력 처리가 올바르게 구현되었습니다.

Enter로 전송, Shift+Enter로 줄바꿈을 지원하는 표준적인 채팅 UX가 잘 구현되었습니다.

Comment on lines +19 to +22
if (!file.name.toLowerCase().endsWith('.csv')) {
setUploadStatus('CSV 파일만 업로드 가능합니다.');
return;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

서버 측 파일 타입 검증을 추가하세요.

클라이언트 측 CSV 확장자 검증만으로는 충분하지 않습니다. 사용자가 개발자 도구를 통해 우회할 수 있으므로 백엔드에서도 파일 타입과 내용을 검증해야 합니다.

Comment on lines 39 to 44
const response = await axios.post<UploadResponse>(uploadUrl, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
withCredentials: false,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

불필요한 헤더 설정을 제거하세요.

axios는 FormData를 전송할 때 자동으로 적절한 Content-Type 헤더(boundary 포함)를 설정합니다. 수동으로 설정하면 오히려 boundary가 누락되어 요청이 실패할 수 있습니다.

다음과 같이 수정하세요:

       const response = await axios.post<UploadResponse>(uploadUrl, formData, {
-        headers: {
-          'Content-Type': 'multipart/form-data',
-        },
         withCredentials: false,
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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).

if (json.type === 'session') {
setSessionId(json.session_id);
setOutput((prev) => prev + `[세션 ID: ${json.session_id}]\n`);
console.log(`${sessionId}`);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

잘못된 변수 참조를 수정하세요.

sessionId 상태 변수는 이전 라인(41)에서 방금 설정되었지만, 이 로그에서 사용되는 값은 아직 업데이트되지 않은 이전 상태입니다. React의 상태 업데이트는 비동기이므로 json.session_id를 직접 사용해야 합니다.

다음과 같이 수정하세요:

         if (json.type === 'session') {
           setSessionId(json.session_id);
-          console.log(`${sessionId}`);
+          console.log(`Session ID: ${json.session_id}`);
           return;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log(`${sessionId}`);
if (json.type === 'session') {
setSessionId(json.session_id);
console.log(`Session ID: ${json.session_id}`);
return;
}
🤖 Prompt for AI Agents
In src/pages/chat/ChatPageTest.tsx around line 42, the console.log uses the
stale state variable sessionId which hasn't updated yet; replace the log to use
the freshly received value json.session_id (e.g., log json.session_id directly)
so you log the correct session id instead of the previous React state value.

Comment on lines 171 to 177
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}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

입력창 높이 제한을 검토하세요.

max-h-20(5rem)과 overflow-hidden 설정으로 인해 사용자가 긴 메시지를 입력할 때 텍스트가 잘려 보이지 않게 됩니다. overflow-hidden 대신 overflow-y-auto를 사용하여 스크롤을 허용하거나, 최대 높이를 늘리는 것을 고려하세요.

다음과 같이 수정하는 것을 고려하세요:

           <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"
+            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"
           />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<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.

@github-actions
Copy link

github-actions bot commented Nov 7, 2025

@github-actions
Copy link

github-actions bot commented Nov 7, 2025

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/components/chat/FileSendButton.tsx (1)

35-39: FormData 전송 시 Content-Type 헤더를 수동으로 설정하지 마세요.

이 문제는 이전 리뷰에서도 지적되었으나 아직 수정되지 않았습니다. axios는 FormData를 전송할 때 필수적인 boundary 파라미터를 포함한 적절한 Content-Type 헤더를 자동으로 설정합니다. 수동으로 설정하면 boundary가 누락되어 서버에서 파일을 파싱할 수 없어 요청이 실패합니다.

다음과 같이 수정하세요:

       const response = await apiClient.post<UploadResponse>('/chat/upload', formData, {
-        headers: {
-          'Content-Type': 'multipart/form-data',
-        },
       });
🧹 Nitpick comments (1)
src/components/chat/FileSendButton.tsx (1)

29-39: 업로드 타임아웃 및 취소 기능 추가를 고려하세요.

현재 업로드가 시작되면 사용자가 취소할 수 없으며 타임아웃도 설정되어 있지 않습니다. 큰 파일 업로드 시 UX 개선을 위해 AbortController를 사용한 취소 기능과 타임아웃 설정을 추가하는 것을 권장합니다.

예시 구현:

const abortControllerRef = useRef<AbortController | null>(null);

const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
  // ... existing validation ...
  
  try {
    abortControllerRef.current = new AbortController();
    
    const response = await apiClient.post<UploadResponse>('/chat/upload', formData, {
      signal: abortControllerRef.current.signal,
      timeout: 30000, // 30초 타임아웃
    });
    // ... rest of the code
  } catch (error) {
    if (axios.isAxiosError(error) && error.code === 'ECONNABORTED') {
      setUploadStatus('업로드 시간 초과');
    }
    // ... existing error handling
  }
};

// 취소 버튼 추가
const handleCancel = () => {
  abortControllerRef.current?.abort();
};
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8e1e2b and dc26c6f.

📒 Files selected for processing (2)
  • src/api/api.ts (1 hunks)
  • src/components/chat/FileSendButton.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/chat/FileSendButton.tsx (1)
src/api/api.ts (1)
  • apiClient (3-9)
🔇 Additional comments (4)
src/api/api.ts (1)

3-9: API 클라이언트 설정을 검토하세요.

전체적인 구조는 양호하나 다음 사항을 확인해주세요:

  1. withCredentials: false 설정이 의도된 것인지 확인이 필요합니다. 만약 향후 인증 쿠키나 세션이 필요한 요청이 있다면 true로 변경해야 합니다.

  2. 기본 Content-Type: 'application/json' 헤더는 대부분의 API 요청에 적합하지만, FormData 업로드 시에는 이 헤더를 명시적으로 제거하거나 undefined로 설정해야 axios가 올바른 boundary 파라미터를 포함한 헤더를 자동 설정할 수 있습니다.

src/components/chat/FileSendButton.tsx (3)

1-8: LGTM!

임포트 구조와 인터페이스 정의가 적절합니다. axios를 별도로 임포트하여 타입 가드(isAxiosError)에 사용하는 것은 올바른 패턴입니다.


49-62: LGTM!

에러 처리 로직이 잘 구현되었습니다:

  • Axios 에러와 일반 에러를 구분하여 처리
  • 사용자에게 의미 있는 에러 메시지 제공
  • finally 블록에서 상태를 올바르게 초기화

94-101: LGTM!

외부 링크에 rel="noopener noreferrer" 속성을 포함하여 보안 모범 사례를 준수했습니다. 이는 target="_blank"와 함께 사용 시 발생할 수 있는 보안 취약점을 방지합니다.

Comment on lines +16 to +23
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;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

파일 크기 검증을 추가하세요.

현재 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
}
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;
}
const maxSizeInMB = 10;
const maxSizeInBytes = maxSizeInMB * 1024 * 1024;
if (file.size > maxSizeInBytes) {
setUploadStatus(`파일 크기는 ${maxSizeInMB}MB를 초과할 수 없습니다.`);
return;
}
🤖 Prompt for AI Agents
In src/components/chat/FileSendButton.tsx around lines 16 to 23, add a file size
validation after you verify the .csv extension: define or import a
MAX_FILE_SIZE_BYTES (e.g. 5 * 1024 * 1024 for 5MB) and check file.size against
it; if the file exceeds the limit, call setUploadStatus with a clear error
message (e.g. "파일 크기가 XMB를 초과했습니다.") and return early to prevent further
processing/upload; ensure the validation happens before any memory-heavy
operations and make the size limit configurable via a constant or prop.

@github-actions
Copy link

github-actions bot commented Nov 7, 2025

@github-actions
Copy link

github-actions bot commented Nov 7, 2025

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/components/chat/FileSendButton.tsx (2)

20-23: 파일 크기 검증을 추가하세요.

이전 리뷰에서 지적되었으나 여전히 구현되지 않았습니다. 파일 크기 제한이 없으면 매우 큰 파일 업로드 시도로 인한 문제가 발생할 수 있습니다:

  • 긴 업로드 시간 후 서버 실패로 인한 나쁜 사용자 경험
  • 클라이언트 메모리 및 네트워크 리소스 과도한 소비
  • 서버 부하 증가

다음과 같이 파일 크기 검증을 추가하세요:

     if (!file.name.toLowerCase().endsWith('.csv')) {
       setUploadStatus('CSV 파일만 업로드 가능합니다.');
       return;
     }
+
+    const MAX_FILE_SIZE_MB = 10;
+    const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
+    if (file.size > MAX_FILE_SIZE_BYTES) {
+      setUploadStatus(`파일 크기는 ${MAX_FILE_SIZE_MB}MB를 초과할 수 없습니다.`);
+      return;
+    }

35-39: Content-Type 헤더 설정을 제거하세요.

이전 리뷰에서 해결되었다고 표시되었으나(commit dc26c6f), 코드에 여전히 수동 Content-Type 헤더 설정이 남아있습니다. axios는 FormData 전송 시 자동으로 적절한 Content-Type 헤더(boundary 포함)를 설정합니다. 수동으로 설정하면 boundary가 누락되어 요청이 실패할 수 있습니다.

다음과 같이 수정하세요:

       const response = await apiClient.post<UploadResponse>('/chat/upload', formData, {
-        headers: {
-          'Content-Type': 'multipart/form-data',
-        },
       });
🧹 Nitpick comments (2)
src/components/chat/FileSendButton.tsx (2)

41-48: 디버그 콘솔 로그를 제거하세요.

과도한 디버그 로그는 프로덕션 환경에서 콘솔을 어지럽히고 민감한 정보를 노출할 수 있습니다.

다음과 같이 수정하세요:

-      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) {

50-70: 응답 처리 로직을 단순화할 수 있습니다.

axios는 2xx 상태 코드에서만 Promise를 resolve하므로 if (response.status === 200) 체크는 중복입니다. 백엔드가 빈 응답을 반환할 가능성에 대비한 방어적 코딩으로 보이지만, 코드를 단순화할 수 있습니다.

다음과 같이 간소화할 수 있습니다:

-      // 응답이 있으면 성공 처리
-      if (response.status === 200) {
-        const data = response.data;
+      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 (data && typeof data === 'object') {
+        setUploadedFile({
+          filename: data.filename || file.name,
+          url: data.url || '',
+        });
+        setUploadStatus(`업로드 성공! (${data.filename || file.name})`);
+      } else {
+        // 데이터가 없어도 성공으로 간주
+        setUploadStatus('업로드 성공!');
+      }

-        // 파일 입력 초기화
-        if (fileInputRef.current) {
-          fileInputRef.current.value = '';
-        }
+      // 파일 입력 초기화
+      if (fileInputRef.current) {
+        fileInputRef.current.value = '';
       }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c6e50d and 2aab64d.

📒 Files selected for processing (1)
  • src/components/chat/FileSendButton.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/chat/FileSendButton.tsx (1)
src/api/api.ts (1)
  • apiClient (3-9)
🔇 Additional comments (6)
src/components/chat/FileSendButton.tsx (6)

1-3: LGTM!

필요한 의존성이 적절하게 임포트되었습니다.


5-8: LGTM!

인터페이스 정의가 명확하고 적절합니다.


10-14: LGTM!

상태 관리가 적절하게 설정되었습니다.


71-87: LGTM!

에러 처리가 포괄적이고 적절합니다. axios 에러와 일반 에러를 구분하여 처리하고, 다양한 에러 메시지 소스에서 정보를 추출하는 방식이 우수합니다.


89-91: LGTM!

숨겨진 파일 입력을 트리거하는 표준 패턴이 올바르게 구현되었습니다.


93-130: LGTM!

JSX 구조가 명확하고 사용자 경험이 잘 고려되었습니다. 로딩 상태, 상태 메시지 색상 구분, 파일 링크 보안 속성 등이 적절하게 구현되었습니다.

@github-actions
Copy link

github-actions bot commented Nov 7, 2025

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
src/components/chat/FileSendButton.tsx (2)

37-41: Content-Type 헤더를 제거하세요.

Axios는 FormData 전송 시 자동으로 적절한 Content-Type 헤더(boundary 포함)를 설정합니다. 수동으로 설정하면 boundary가 누락되어 서버에서 multipart 파싱이 실패합니다.

다음과 같이 수정하세요:

       const response = await apiClient.post<UploadResponse>('/chat/upload', formData, {
-        headers: {
-          'Content-Type': 'multipart/form-data',
-        },
       });

23-26: 파일 크기 검증을 추가하세요.

CSV 확장자만 검증하고 있으며 파일 크기 제한이 없습니다. 대용량 파일 업로드 시도 시 다음 문제가 발생할 수 있습니다:

  • 긴 업로드 시간 후 서버에서 실패하는 나쁜 UX
  • 클라이언트 메모리 및 네트워크 리소스 낭비
  • 서버 부하 증가

다음과 같이 파일 크기 검증을 추가하세요:

     if (!file.name.toLowerCase().endsWith('.csv')) {
       setUploadStatus('CSV 파일만 업로드 가능합니다.');
       return;
     }
+
+    const MAX_FILE_SIZE_MB = 10;
+    const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
+    if (file.size > MAX_FILE_SIZE_BYTES) {
+      setUploadStatus(`파일 크기는 ${MAX_FILE_SIZE_MB}MB를 초과할 수 없습니다.`);
+      return;
+    }
src/pages/chat/ChatPageTest.tsx (2)

49-50: 중복 로그를 제거하세요.

Line 49는 아직 업데이트되지 않은 이전 상태값 sessionId를 로그하며(React 상태 업데이트는 비동기), Line 50은 올바른 값 json.session_id를 로그합니다. Line 49는 불필요합니다.

다음과 같이 수정하세요:

         if (json.type === 'session') {
           setSessionId(json.session_id);
-          console.log(`${sessionId}`);
           console.log(`Session ID: ${json.session_id}`);
           return;
         }

193-201: 입력창 overflow 설정을 변경하세요.

overflow-hiddenmax-h-20(5rem) 설정으로 인해 긴 메시지 입력 시 텍스트가 잘려 보이지 않습니다. 사용자가 여러 줄을 입력할 때 스크롤할 수 없어 불편합니다.

다음과 같이 수정하세요:

             <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"
+              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"
             />
🧹 Nitpick comments (1)
src/pages/chat/ChatPageTest.tsx (1)

57-90: 메시지 누적 로직을 단순화하세요.

Lines 62-69와 Lines 72-78은 마지막 메시지가 어시스턴트일 때 콘텐츠를 이어붙이는 동일한 로직을 수행합니다. 첫 번째 블록의 빈 콘텐츠 체크(content === '')는 불필요하며, 두 블록을 하나로 통합할 수 있습니다.

다음과 같이 리팩터링하세요:

       // 어시스턴트 메시지 처리
       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,
           },
         ];
       });
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2aab64d and ef5c71e.

📒 Files selected for processing (3)
  • src/components/chat/FileSendButton.tsx (1 hunks)
  • src/pages/chat/ChatPageTest.tsx (3 hunks)
  • vercel.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/chat/FileSendButton.tsx (1)
src/api/api.ts (1)
  • apiClient (3-9)
🔇 Additional comments (7)
src/components/chat/FileSendButton.tsx (2)

1-3: 불필요한 axios import를 정리하세요.

Line 2에서 apiClient를 import하고 있는데, Line 3에서 axios를 별도로 import하고 있습니다. apiClient는 이미 axios 인스턴스이므로, axios.isAxiosErrorapiClient 또는 전역 axios에서 접근 가능합니다. 하지만 Line 55에서 axios.isAxiosError를 사용하므로 현재 import는 적절합니다.

에러 타입 체크를 위한 axios import는 필요하며 올바르게 사용되고 있습니다.


55-64: 에러 처리 로직이 잘 구현되어 있습니다.

Axios 에러, 일반 Error, 그리고 알 수 없는 에러를 모두 처리하며, 사용자에게 적절한 피드백을 제공합니다.

src/pages/chat/ChatPageTest.tsx (5)

107-109: 자동 스크롤 구현이 잘 되어 있습니다.

새 메시지가 추가될 때마다 부드럽게 하단으로 스크롤되도록 구현되어 있어 좋은 UX를 제공합니다.


112-117: 텍스트영역 자동 높이 조절이 잘 구현되어 있습니다.

입력 내용에 따라 textarea의 높이가 자동으로 조절되어 사용자 편의성을 높입니다.


154-159: 키보드 입력 처리가 적절합니다.

Enter로 전송, Shift+Enter로 줄바꿈 처리가 직관적으로 구현되어 있습니다.


119-129: 파일 업로드 성공 핸들러가 잘 구현되어 있습니다.

업로드 완료 시 상태를 업데이트하고 사용자에게 확인 메시지를 표시하여 명확한 피드백을 제공합니다.


173-188: 메시지 렌더링 및 스타일링이 잘 구현되어 있습니다.

사용자/어시스턴트 메시지를 구분하여 표시하고, 적절한 정렬과 색상을 사용하여 가독성이 좋습니다. whitespace-pre-wrap으로 줄바꿈도 올바르게 처리됩니다.

>
{uploading ? '업로드 중...' : '📎파일 업로드'}
</button>
{uploadStatus && <p className="text-sm text-red-600">dd{uploadStatus}</p>}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

오타를 수정하세요.

에러 메시지 표시 부분에 "dd" 텍스트가 잘못 포함되어 있습니다.

다음과 같이 수정하세요:

-      {uploadStatus && <p className="text-sm text-red-600">dd{uploadStatus}</p>}
+      {uploadStatus && <p className="text-sm text-red-600">{uploadStatus}</p>}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{uploadStatus && <p className="text-sm text-red-600">dd{uploadStatus}</p>}
{uploadStatus && <p className="text-sm text-red-600">{uploadStatus}</p>}
🤖 Prompt for AI Agents
In src/components/chat/FileSendButton.tsx around line 91, there is a stray "dd"
text inside the error message render; remove the "dd" so the paragraph only
renders the uploadStatus (i.e., change the JSX to render {uploadStatus} without
the "dd"), keeping the conditional check and styling intact.

Comment on lines 2 to 8
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
{ "source": "/(.*)", "destination": "/index.html" },
{
"source": "/chat/:path*",
"destination": "https://backendbase.site/chat/:path*"
}
]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

라우팅 순서를 수정하세요.

Vercel은 rewrites를 배열 순서대로 처리합니다. 현재 Line 3의 catch-all 패턴 /(.*)이 먼저 매칭되므로 /chat 경로를 포함한 모든 요청이 /index.html로 리라이트됩니다. 따라서 Lines 4-7의 /chat 백엔드 프록시 규칙이 절대 실행되지 않습니다.

다음과 같이 순서를 변경하세요:

 {
   "rewrites": [
-    { "source": "/(.*)", "destination": "/index.html" },
     {
       "source": "/chat/:path*",
       "destination": "https://backendbase.site/chat/:path*"
-    }
+    },
+    { "source": "/(.*)", "destination": "/index.html" }
   ]
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
{ "source": "/(.*)", "destination": "/index.html" },
{
"source": "/chat/:path*",
"destination": "https://backendbase.site/chat/:path*"
}
]
"rewrites": [
{
"source": "/chat/:path*",
"destination": "https://backendbase.site/chat/:path*"
},
{ "source": "/(.*)", "destination": "/index.html" }
]
🤖 Prompt for AI Agents
vercel.json around lines 2 to 8: the catch-all rewrite "{ "source": "/(.*)",
"destination": "/index.html" }" is listed before the specific "/chat/:path*"
proxy, so it will match every request (including /chat) and prevent the /chat
rule from ever running; fix by moving the "/chat/:path*" rewrite entry above the
catch-all entry so the specific proxy is evaluated first, preserving the
intended backend proxy for /chat paths.

@github-actions
Copy link

github-actions bot commented Nov 7, 2025

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef5c71e and 7ddc1dc.

📒 Files selected for processing (1)
  • src/api/api.ts (1 hunks)
🔇 Additional comments (1)
src/api/api.ts (1)

1-9: ****

axios 버전 1.13.2는 실제로 존재하며 현재 최신 버전입니다. 패키지에서 "^1.13.2"로 지정한 것이 올바릅니다. 보안 취약점 점검 결과, 이 버전은 알려진 HIGH 심각도 취약점(DoS, SSRF 등)의 패치 범위에 포함되어 있어 안전합니다. 추가 조치가 필요하지 않습니다.

Likely an incorrect or invalid review comment.

Comment on lines +5 to +7
headers: {
'Content-Type': 'multipart/form-data',
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

전역 Content-Type 헤더를 제거하세요.

Content-Type: multipart/form-data를 전역 헤더로 설정하면 두 가지 문제가 발생합니다:

  1. 파일 업로드 실패: multipart/form-data는 boundary 파라미터가 필요한데, 수동으로 헤더를 설정하면 boundary가 포함되지 않아 업로드가 실패합니다.
  2. 다른 요청에 영향: 이 클라이언트를 사용하는 모든 요청(JSON 등)에 잘못된 Content-Type이 적용됩니다.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
headers: {
'Content-Type': 'multipart/form-data',
},
export const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'https://backendbase.site',
withCredentials: false,
});
🤖 Prompt for AI Agents
In src/api/api.ts around lines 5 to 7, remove the global headers entry that sets
'Content-Type': 'multipart/form-data' because setting it globally prevents Axios
from auto-generating the required boundary for FormData uploads and incorrectly
forces that Content-Type on all requests; delete that header configuration so
Axios can set Content-Type per-request when a FormData body is used.

@github-actions
Copy link

github-actions bot commented Nov 7, 2025

@sunhwaaRj sunhwaaRj merged commit 7695a7a into develop Nov 7, 2025
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants