diff --git a/frontend/src/components/LoginModal.tsx b/frontend/src/components/LoginModal.tsx
index 9b8e152..0a2932f 100644
--- a/frontend/src/components/LoginModal.tsx
+++ b/frontend/src/components/LoginModal.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuth } from '../contexts/AuthContext';
import { X, Lock, User } from 'lucide-react';
@@ -15,6 +15,20 @@ const LoginModal: React.FC
= ({ onClose, onSuccess }) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
+ // ESC 키로 모달 닫기
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ onClose();
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown);
+ };
+ }, [onClose]);
+
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
@@ -73,37 +87,39 @@ const LoginModal: React.FC = ({ onClose, onSuccess }) => {
)}
-
+
diff --git a/frontend/src/components/MessageBubble.tsx b/frontend/src/components/MessageBubble.tsx
index 9de90a2..97fce22 100644
--- a/frontend/src/components/MessageBubble.tsx
+++ b/frontend/src/components/MessageBubble.tsx
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { User, FileText } from 'lucide-react';
-import { Message } from '../contexts/ChatContext';
+import { Message, ReferenceInfo } from '../contexts/ChatContext';
import PDFViewer from './PDFViewer';
import SimpleMarkdownRenderer from './SimpleMarkdownRenderer';
@@ -71,6 +71,15 @@ const MessageBubble: React.FC
= ({ message }) => {
});
};
+ // 인라인 링크 클릭 핸들러
+ const handleInlineReferenceClick = (fileId: string, pageNumber: number, filename: string) => {
+ setSelectedPage({
+ fileId: fileId,
+ filename: filename,
+ pageNumber: pageNumber
+ });
+ };
+
return (
<>
@@ -113,7 +122,15 @@ const MessageBubble: React.FC = ({ message }) => {
{message.content}
) : (
-
+ <>
+ {console.log('🔍 MessageBubble - message.detailedReferences:', message.detailedReferences)}
+ {console.log('🔍 MessageBubble - message.detailedReferences 길이:', message.detailedReferences?.length)}
+
+ >
)}
{/* 소스 정보 */}
diff --git a/frontend/src/components/PDFViewer.tsx b/frontend/src/components/PDFViewer.tsx
index 0da3cb0..c27f82e 100644
--- a/frontend/src/components/PDFViewer.tsx
+++ b/frontend/src/components/PDFViewer.tsx
@@ -31,8 +31,14 @@ const PDFViewer: React.FC = ({ fileId, filename, pageNumber, onC
// PDF URL 생성
const url = `http://localhost:8000/pdf/${fileId}/view`;
+ console.log('📄 PDF 로드 시작:', { fileId, filename, pageNumber, url });
setPdfUrl(url);
- setIsLoading(false);
+
+ // PDF 로드 완료를 위해 약간의 지연
+ setTimeout(() => {
+ setIsLoading(false);
+ console.log('📄 PDF 로드 완료:', { fileId, filename, pageNumber });
+ }, 100);
} catch (err) {
console.error('PDF 로드 오류:', err);
setError('PDF를 불러올 수 없습니다.');
@@ -41,12 +47,26 @@ const PDFViewer: React.FC = ({ fileId, filename, pageNumber, onC
};
loadPDF();
- }, [fileId]);
+ }, [fileId, filename, pageNumber]);
useEffect(() => {
setCurrentPage(pageNumber);
}, [pageNumber]);
+ // ESC 키로 PDF 뷰어 닫기
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ onClose();
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown);
+ };
+ }, [onClose]);
+
const onDocumentLoadSuccess = useCallback(({ numPages }: { numPages: number }) => {
setNumPages(numPages);
setIsLoading(false);
diff --git a/frontend/src/contexts/ChatContext.tsx b/frontend/src/contexts/ChatContext.tsx
index d1d1ead..50a1f86 100644
--- a/frontend/src/contexts/ChatContext.tsx
+++ b/frontend/src/contexts/ChatContext.tsx
@@ -1,11 +1,22 @@
import React, { createContext, useContext, useState } from 'react';
+export interface ReferenceInfo {
+ filename: string;
+ file_id: string;
+ page_number: number;
+ chunk_index: number;
+ content_preview: string;
+ full_content?: string;
+ is_relevant?: boolean;
+}
+
export interface Message {
id: string;
content: string;
isUser: boolean;
timestamp: Date;
sources?: string[];
+ detailedReferences?: ReferenceInfo[];
}
interface ChatContextType {
diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx
index 489e557..537f0b9 100644
--- a/frontend/src/contexts/FileContext.tsx
+++ b/frontend/src/contexts/FileContext.tsx
@@ -1,4 +1,4 @@
-import React, { createContext, useContext, useState, useEffect } from 'react';
+import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
export interface FileInfo {
id: string;
@@ -10,12 +10,13 @@ export interface FileInfo {
interface FileContextType {
files: FileInfo[];
- uploadFile: (file: File) => Promise;
+ uploadFile: (file: File, signal?: AbortSignal) => Promise;
uploadMultipleFiles: (files: FileList) => Promise<{success: number, error: number, results: any[]}>;
deleteFile: (fileId: string) => Promise;
refreshFiles: () => Promise;
searchFiles: (searchTerm: string) => Promise;
isLoading: boolean;
+ isFileLoading: boolean; // 파일 관련 작업만을 위한 독립적인 로딩 상태
}
const FileContext = createContext(undefined);
@@ -31,9 +32,11 @@ export const useFiles = () => {
export const FileProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [files, setFiles] = useState([]);
const [isLoading, setIsLoading] = useState(false);
+ const [isFileLoading, setIsFileLoading] = useState(false); // 파일 관련 작업만을 위한 독립적인 로딩 상태
- const fetchFiles = async (searchTerm?: string) => {
+ const fetchFiles = useCallback(async (searchTerm?: string) => {
try {
+ setIsFileLoading(true); // 파일 관련 작업만을 위한 독립적인 로딩 상태 사용
console.log('📁 파일 목록 조회 시작');
let url = 'http://localhost:8000/files';
@@ -54,7 +57,14 @@ export const FileProvider: React.FC<{ children: React.ReactNode }> = ({ children
// 인증 없이 요청 (파일 목록은 누구나 조회 가능)
console.log('📋 인증 없이 파일 목록 요청합니다.');
- const response = await fetch(url);
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ // 타임아웃 설정 (10초)
+ signal: AbortSignal.timeout(10000)
+ });
console.log(`📥 응답 받음: ${response.status} ${response.statusText}`);
@@ -62,29 +72,47 @@ export const FileProvider: React.FC<{ children: React.ReactNode }> = ({ children
console.log('✅ 파일 조회 성공');
const data = await response.json();
const filesArray = data.files || []; // Extract files array
- setFiles(filesArray);
- console.log(`📁 파일 조회 완료: ${filesArray.length}개 (검색어: ${searchTerm || '전체'})`);
- console.log(`📋 반환된 파일들:`, filesArray.map((f: FileInfo) => f.filename));
+
+ // 파일 정보 형식 정규화
+ const normalizedFiles = filesArray.map((file: any) => ({
+ id: file.id?.toString() || '',
+ filename: file.filename || '',
+ upload_date: file.upload_time || file.upload_date || '',
+ file_type: file.file_type || 'PDF',
+ status: file.status || '완료'
+ }));
+
+ setFiles(normalizedFiles);
+ console.log(`📁 파일 조회 완료: ${normalizedFiles.length}개 (검색어: ${searchTerm || '전체'})`);
+ console.log(`📋 반환된 파일들:`, normalizedFiles.map((f: FileInfo) => f.filename));
} else {
console.error('❌ 파일 조회 실패');
console.error(`📋 상태 코드: ${response.status} ${response.statusText}`);
const errorText = await response.text();
console.error(`📋 오류 내용: ${errorText}`);
+
+ // 오류 시 빈 배열로 설정하여 "등록된 문서가 없습니다" 메시지 표시
+ setFiles([]);
}
} catch (error) {
console.error('❌ 파일 목록 조회 네트워크 오류');
console.error('📋 오류 타입:', error instanceof Error ? error.name : typeof error);
console.error('📋 오류 메시지:', error instanceof Error ? error.message : String(error));
console.error('📋 전체 오류:', error);
+
+ // 네트워크 오류 시에도 빈 배열로 설정
+ setFiles([]);
+ } finally {
+ setIsFileLoading(false); // 파일 관련 작업만을 위한 독립적인 로딩 상태 사용
}
- };
+ }, []);
- const searchFiles = async (searchTerm: string) => {
+ const searchFiles = useCallback(async (searchTerm: string) => {
console.log('🔍 서버 검색 실행:', searchTerm);
await fetchFiles(searchTerm);
- };
+ }, [fetchFiles]);
- const uploadFile = async (file: File): Promise => {
+ const uploadFile = async (file: File, signal?: AbortSignal): Promise => {
try {
console.log('📤 파일 업로드 시작');
console.log('📋 파일명:', file.name);
@@ -97,7 +125,7 @@ export const FileProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (!token) {
console.log('🔒 파일 업로드를 위해서는 로그인이 필요합니다.');
alert('파일 업로드를 위해서는 로그인이 필요합니다.');
- return false;
+ return null;
}
setIsLoading(true);
@@ -114,6 +142,7 @@ export const FileProvider: React.FC<{ children: React.ReactNode }> = ({ children
'Authorization': `Bearer ${token}`,
},
body: formData,
+ signal: signal, // AbortSignal 추가
});
console.log('📥 업로드 응답 받음');
@@ -121,26 +150,43 @@ export const FileProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (response.ok) {
console.log('✅ 파일 업로드 성공');
+ const responseData = await response.json();
+ const fileId = responseData.file_id;
+ console.log('📋 업로드된 파일 ID:', fileId);
+
// 업로드 성공 후 파일 목록 새로고침
await new Promise(resolve => setTimeout(resolve, 1000)); // 1초 대기
await fetchFiles();
console.log('📁 파일 목록 새로고침 완료');
- return true;
+ return fileId;
} else {
console.log('❌ 파일 업로드 실패');
+
+ // 499 상태 코드 처리 (클라이언트 연결 끊어짐)
+ if (response.status === 499) {
+ console.log('🛑 클라이언트 연결 끊어짐 - 업로드 중단됨');
+ return null;
+ }
+
const errorData = await response.json();
console.error('📋 오류 데이터:', errorData);
alert(`파일 업로드 실패: ${errorData.detail || '알 수 없는 오류가 발생했습니다.'}`);
- return false;
+ return null;
}
} catch (error) {
+ // AbortError 처리 (업로드 중단)
+ if (error instanceof Error && error.name === 'AbortError') {
+ console.log('🛑 파일 업로드 중단됨 (AbortError)');
+ return null;
+ }
+
console.error('❌ 파일 업로드 네트워크 오류');
console.error('📋 오류 타입:', error instanceof Error ? error.name : typeof error);
console.error('📋 오류 메시지:', error instanceof Error ? error.message : String(error));
console.error('📋 전체 오류:', error);
const errorMessage = error instanceof Error ? error.message : '네트워크 오류가 발생했습니다.';
alert(`파일 업로드 실패: ${errorMessage}`);
- return false;
+ return null;
} finally {
console.log('🏁 파일 업로드 완료');
setIsLoading(false);
@@ -247,19 +293,19 @@ export const FileProvider: React.FC<{ children: React.ReactNode }> = ({ children
}
};
- const refreshFiles = async () => {
+ const refreshFiles = useCallback(async () => {
console.log('🔄 파일 목록 새로고침 시작');
await fetchFiles();
console.log('✅ 파일 목록 새로고침 완료');
- };
+ }, [fetchFiles]);
useEffect(() => {
console.log('🚀 FileContext 초기화 - 파일 목록 조회 시작');
fetchFiles();
- }, []);
+ }, [fetchFiles]);
return (
-
+
{children}
);