diff --git a/backend/src/api/dependencies.py b/backend/src/api/dependencies.py index ae03205..5724bae 100644 --- a/backend/src/api/dependencies.py +++ b/backend/src/api/dependencies.py @@ -12,8 +12,8 @@ from ..core.security import verify_token, get_user_id_from_token from ..models.user import User -# HTTP Bearer 토큰 스키마 -security = HTTPBearer() +# HTTP Bearer 토큰 스키마 (선택적) +security = HTTPBearer(auto_error=False) async def get_current_user( @@ -94,16 +94,22 @@ async def get_current_user_with_token_param( db: AsyncSession = Depends(get_db) ) -> User: """URL 파라미터 또는 헤더에서 토큰을 가져와서 사용자 인증""" + print(f"🔍 토큰 인증 시작 - URL 파라미터: {_token[:50] if _token else 'None'}...") + print(f"🔍 Authorization 헤더: {credentials.credentials[:50] if credentials else 'None'}...") + token = None # URL 파라미터에서 토큰 확인 if _token: token = _token + print("✅ URL 파라미터에서 토큰 사용") # Authorization 헤더에서 토큰 확인 elif credentials: token = credentials.credentials + print("✅ Authorization 헤더에서 토큰 사용") if not token: + print("❌ 토큰이 제공되지 않음") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="No authentication token provided" @@ -112,23 +118,27 @@ async def get_current_user_with_token_param( try: # 토큰에서 사용자 ID 추출 user_id = get_user_id_from_token(token) + print(f"✅ 토큰에서 사용자 ID 추출: {user_id}") # 데이터베이스에서 사용자 조회 result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: + print(f"❌ 사용자를 찾을 수 없음: {user_id}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found" ) if not user.is_active: + print(f"❌ 비활성 사용자: {user.email}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Inactive user" ) + print(f"✅ 사용자 인증 성공: {user.email}") return user except Exception as e: diff --git a/backend/src/api/routes/documents.py b/backend/src/api/routes/documents.py index dc79d7d..8b887b8 100644 --- a/backend/src/api/routes/documents.py +++ b/backend/src/api/routes/documents.py @@ -516,9 +516,14 @@ async def get_document_pdf( db: AsyncSession = Depends(get_db) ): """문서 PDF 파일 조회""" + print(f"🔍 PDF 요청 - 문서 ID: {document_id}") + print(f"🔍 토큰 파라미터: {_token[:50] if _token else 'None'}...") + print(f"🔍 현재 사용자: {current_user.email if current_user else 'None'}") + try: doc_uuid = UUID(document_id) except ValueError: + print(f"❌ 잘못된 문서 ID 형식: {document_id}") raise HTTPException(status_code=400, detail="Invalid document ID format") # 문서 조회 @@ -527,10 +532,16 @@ async def get_document_pdf( document = result.scalar_one_or_none() if not document: + print(f"❌ 문서를 찾을 수 없음: {document_id}") raise HTTPException(status_code=404, detail="Document not found") + print(f"📄 문서 정보: {document.title}") + print(f"🔐 문서 권한: is_public={document.is_public}, uploaded_by={document.uploaded_by}") + print(f"👤 사용자 권한: is_admin={current_user.is_admin}, user_id={current_user.id}") + # 권한 확인 if not current_user.is_admin and not document.is_public and document.uploaded_by != current_user.id: + print(f"❌ 접근 권한 없음 - 관리자: {current_user.is_admin}, 공개: {document.is_public}, 소유자: {document.uploaded_by == current_user.id}") raise HTTPException(status_code=403, detail="Access denied") # PDF 파일 확인 @@ -562,11 +573,20 @@ async def get_document_pdf( print(f"📂 디렉토리도 없음: {dir_path}") raise HTTPException(status_code=404, detail="PDF file not found on disk") - return FileResponse( + # PDF 인라인 표시를 위한 헤더 설정 + from fastapi.responses import FileResponse + + response = FileResponse( path=file_path, media_type='application/pdf', filename=f"{document.title}.pdf" ) + + # 브라우저에서 인라인으로 표시하도록 설정 (다운로드 방지) + response.headers["Content-Disposition"] = f"inline; filename=\"{document.title}.pdf\"" + response.headers["X-Frame-Options"] = "SAMEORIGIN" # iframe 허용 + + return response @router.get("/{document_id}/search-in-content") diff --git a/frontend/pdf-manager.html b/frontend/pdf-manager.html index 147005e..4ef2969 100644 --- a/frontend/pdf-manager.html +++ b/frontend/pdf-manager.html @@ -193,6 +193,12 @@
+ +
+ +
+
+ +
+
+ +
+

+

+
+
+
+ + +
+
+ + +
+ +
+ +
+ + + + +
+
+ +

PDF를 로드하는 중...

+
+
+ + +
+
+ +

PDF를 로드할 수 없습니다

+ + +
+
+
+
+
+
+
+ - + diff --git a/frontend/static/js/pdf-manager.js b/frontend/static/js/pdf-manager.js index 1cc3186..c4d6176 100644 --- a/frontend/static/js/pdf-manager.js +++ b/frontend/static/js/pdf-manager.js @@ -11,6 +11,14 @@ window.pdfManagerApp = () => ({ isAuthenticated: false, currentUser: null, + // PDF 미리보기 상태 + showPreviewModal: false, + previewPdf: null, + pdfPreviewSrc: '', + pdfPreviewLoading: false, + pdfPreviewError: false, + pdfPreviewLoaded: false, + // 초기화 async init() { console.log('🚀 PDF Manager App 초기화 시작'); @@ -213,6 +221,56 @@ window.pdfManagerApp = () => ({ } }, + // ==================== PDF 미리보기 관련 ==================== + async previewPDF(pdf) { + console.log('👁️ PDF 미리보기:', pdf.title); + + this.previewPdf = pdf; + this.showPreviewModal = true; + this.pdfPreviewLoading = true; + this.pdfPreviewError = false; + this.pdfPreviewLoaded = false; + + try { + const token = localStorage.getItem('access_token'); + if (!token || token === 'null' || token === null) { + throw new Error('인증 토큰이 없습니다. 다시 로그인해주세요.'); + } + + // PDF 미리보기 URL 설정 + this.pdfPreviewSrc = `/api/documents/${pdf.id}/pdf?_token=${encodeURIComponent(token)}`; + console.log('✅ PDF 미리보기 준비 완료:', this.pdfPreviewSrc); + + } catch (error) { + console.error('❌ PDF 미리보기 로드 실패:', error); + this.pdfPreviewError = true; + this.showNotification('PDF 미리보기 로드에 실패했습니다: ' + error.message, 'error'); + } finally { + this.pdfPreviewLoading = false; + } + }, + + closePreview() { + this.showPreviewModal = false; + this.previewPdf = null; + this.pdfPreviewSrc = ''; + this.pdfPreviewLoading = false; + this.pdfPreviewError = false; + this.pdfPreviewLoaded = false; + }, + + handlePdfPreviewError() { + console.error('❌ PDF 미리보기 iframe 로드 오류'); + this.pdfPreviewError = true; + this.pdfPreviewLoading = false; + }, + + async retryPdfPreview() { + if (this.previewPdf) { + await this.previewPDF(this.previewPdf); + } + }, + // 날짜 포맷팅 formatDate(dateString) { if (!dateString) return ''; diff --git a/frontend/static/js/story-view.js b/frontend/static/js/story-view.js index fa8f494..e623c7d 100644 --- a/frontend/static/js/story-view.js +++ b/frontend/static/js/story-view.js @@ -17,7 +17,10 @@ window.storyViewApp = function() { // UI 상태 showLoginModal: false, showEditModal: false, - editingNode: null, + editingNode: { + title: '', + content: '' + }, // 로그인 폼 상태 loginForm: { @@ -79,11 +82,22 @@ window.storyViewApp = function() { async loadUserTrees() { try { console.log('📊 사용자 트리 목록 로딩...'); + console.log('🔍 API 객체 확인:', window.api); + console.log('🔍 getUserMemoTrees 함수 확인:', typeof window.api?.getUserMemoTrees); + const trees = await window.api.getUserMemoTrees(); this.userTrees = trees || []; console.log(`✅ ${this.userTrees.length}개 트리 로드 완료`); + console.log('📋 트리 목록:', this.userTrees); + + // Alpine.js 반응성 업데이트를 위한 약간의 지연 후 URL 파라미터 확인 + setTimeout(() => { + this.checkUrlParams(); + }, 100); } catch (error) { console.error('❌ 트리 목록 로드 실패:', error); + console.error('❌ 에러 상세:', error.message); + console.error('❌ 에러 스택:', error.stack); this.userTrees = []; } }, diff --git a/frontend/static/js/viewer/core/document-loader.js b/frontend/static/js/viewer/core/document-loader.js index 9b81867..e3943a3 100644 --- a/frontend/static/js/viewer/core/document-loader.js +++ b/frontend/static/js/viewer/core/document-loader.js @@ -24,9 +24,15 @@ class DocumentLoader { document.title = `${noteDocument.title} - Document Server`; // 노트 내용을 HTML로 설정 - const contentElement = document.getElementById('document-content'); - if (contentElement && noteDocument.content) { - contentElement.innerHTML = noteDocument.content; + const noteContentElement = document.getElementById('note-content'); + if (noteContentElement && noteDocument.content) { + noteContentElement.innerHTML = noteDocument.content; + } else { + // 폴백: document-content 사용 + const contentElement = document.getElementById('document-content'); + if (contentElement && noteDocument.content) { + contentElement.innerHTML = noteDocument.content; + } } console.log('📝 노트 로드 완료:', noteDocument.title); @@ -46,25 +52,28 @@ class DocumentLoader { // 백엔드에서 문서 정보 가져오기 (캐싱 적용) const docData = await this.cachedApi.get(`/documents/${documentId}`, { content_type: 'document' }, { category: 'document' }); - // HTML 파일 경로 구성 (백엔드 서버를 통해 접근) - const htmlPath = docData.html_path; - const fileName = htmlPath.split('/').pop(); - const response = await fetch(`http://localhost:24102/uploads/documents/${fileName}`); - - if (!response.ok) { - throw new Error('문서 파일을 불러올 수 없습니다'); - } - - const htmlContent = await response.text(); - document.getElementById('document-content').innerHTML = htmlContent; - // 페이지 제목 업데이트 document.title = `${docData.title} - Document Server`; - // 문서 내 스크립트 오류 방지를 위한 전역 함수들 정의 - this.setupDocumentScriptHandlers(); + // PDF 문서가 아닌 경우에만 HTML 로드 + if (!docData.pdf_path && docData.html_path) { + // HTML 파일 경로 구성 (백엔드 서버를 통해 접근) + const htmlPath = docData.html_path; + const fileName = htmlPath.split('/').pop(); + const response = await fetch(`http://localhost:24102/uploads/documents/${fileName}`); + + if (!response.ok) { + throw new Error('문서 파일을 불러올 수 없습니다'); + } + + const htmlContent = await response.text(); + document.getElementById('document-content').innerHTML = htmlContent; + + // 문서 내 스크립트 오류 방지를 위한 전역 함수들 정의 + this.setupDocumentScriptHandlers(); + } - console.log('✅ 문서 로드 완료:', docData.title); + console.log('✅ 문서 로드 완료:', docData.title, docData.pdf_path ? '(PDF)' : '(HTML)'); return docData; } catch (error) { diff --git a/frontend/static/js/viewer/viewer-core.js b/frontend/static/js/viewer/viewer-core.js index 8e8d2e3..2209914 100644 --- a/frontend/static/js/viewer/viewer-core.js +++ b/frontend/static/js/viewer/viewer-core.js @@ -11,6 +11,27 @@ window.documentViewer = () => ({ contentType: 'document', // 'document' 또는 'note' navigation: null, + // ==================== PDF 뷰어 상태 ==================== + pdfSrc: '', + pdfLoading: false, + pdfError: false, + pdfLoaded: false, + + // ==================== PDF 검색 상태 ==================== + showPdfSearchModal: false, + pdfSearchQuery: '', + pdfSearchResults: [], + pdfSearchLoading: false, + + // ==================== PDF.js 뷰어 상태 ==================== + pdfDocument: null, + currentPage: 1, + totalPages: 0, + pdfScale: 1.0, + pdfCanvas: null, + pdfContext: null, + pdfTextContent: [], + // ==================== 데이터 상태 ==================== highlights: [], notes: [], @@ -280,6 +301,11 @@ window.documentViewer = () => ({ this.document = await this.documentLoader.loadDocument(this.documentId); // 네비게이션 별도 로드 this.navigation = await this.documentLoader.loadNavigation(this.documentId); + + // PDF 문서인 경우 PDF 뷰어 준비 + if (this.document && this.document.pdf_path) { + await this.loadPdfViewer(); + } } // 관련 데이터 병렬 로드 @@ -1825,6 +1851,217 @@ window.documentViewer = () => ({ } }, + // ==================== PDF 뷰어 관련 ==================== + async loadPdfViewer() { + console.log('📄 PDF 뷰어 로드 시작'); + this.pdfLoading = true; + this.pdfError = false; + this.pdfLoaded = false; + + try { + const token = localStorage.getItem('access_token'); + if (!token || token === 'null' || token === null) { + throw new Error('인증 토큰이 없습니다. 다시 로그인해주세요.'); + } + + // PDF 뷰어 URL 설정 (토큰 포함) + this.pdfSrc = `/api/documents/${this.documentId}/pdf?_token=${encodeURIComponent(token)}`; + console.log('✅ PDF 뷰어 준비 완료:', this.pdfSrc); + + // PDF.js로 PDF 로드 + await this.loadPdfWithPdfJs(); + + } catch (error) { + console.error('❌ PDF 뷰어 로드 실패:', error); + this.pdfError = true; + } finally { + this.pdfLoading = false; + } + }, + + async loadPdfWithPdfJs() { + try { + // PDF.js 워커 설정 + if (typeof pdfjsLib !== 'undefined') { + pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; + + console.log('📄 PDF.js로 PDF 로드 시작:', this.pdfSrc); + + // PDF 문서 로드 + const loadingTask = pdfjsLib.getDocument(this.pdfSrc); + this.pdfDocument = await loadingTask.promise; + + this.totalPages = this.pdfDocument.numPages; + this.currentPage = 1; + + console.log(`✅ PDF 로드 완료: ${this.totalPages} 페이지`); + + // 캔버스 초기화 + this.initPdfCanvas(); + + // 첫 페이지 렌더링 + await this.renderPdfPage(1); + + this.pdfLoaded = true; + } else { + throw new Error('PDF.js 라이브러리가 로드되지 않았습니다.'); + } + } catch (error) { + console.error('❌ PDF.js 로드 실패:', error); + throw error; + } + }, + + initPdfCanvas() { + this.pdfCanvas = document.getElementById('pdf-canvas'); + if (this.pdfCanvas) { + this.pdfContext = this.pdfCanvas.getContext('2d'); + } + }, + + async renderPdfPage(pageNum) { + if (!this.pdfDocument || !this.pdfCanvas) return; + + try { + console.log(`📄 페이지 ${pageNum} 렌더링 시작`); + + const page = await this.pdfDocument.getPage(pageNum); + const viewport = page.getViewport({ scale: this.pdfScale }); + + // 캔버스 크기 설정 + this.pdfCanvas.height = viewport.height; + this.pdfCanvas.width = viewport.width; + + // 페이지 렌더링 + const renderContext = { + canvasContext: this.pdfContext, + viewport: viewport + }; + + await page.render(renderContext).promise; + + // 텍스트 내용 추출 (검색용) + const textContent = await page.getTextContent(); + this.pdfTextContent[pageNum] = textContent.items.map(item => item.str).join(' '); + + console.log(`✅ 페이지 ${pageNum} 렌더링 완료`); + + } catch (error) { + console.error(`❌ 페이지 ${pageNum} 렌더링 실패:`, error); + } + }, + + handlePdfError() { + console.error('❌ PDF iframe 로드 오류'); + this.pdfError = true; + this.pdfLoading = false; + }, + + async retryPdfLoad() { + console.log('🔄 PDF 재로드 시도'); + await this.loadPdfViewer(); + }, + + // ==================== PDF 검색 관련 ==================== + openPdfSearchModal() { + this.showPdfSearchModal = true; + this.pdfSearchQuery = ''; + this.pdfSearchResults = []; + + // 모달이 열린 후 입력 필드에 포커스 + setTimeout(() => { + const searchInput = document.querySelector('input[x-ref="searchInput"]'); + if (searchInput) { + searchInput.focus(); + searchInput.select(); + } + }, 100); + }, + + async searchInPdf() { + if (!this.pdfSearchQuery.trim()) { + alert('검색어를 입력해주세요.'); + return; + } + + console.log('🔍 PDF 검색 시작:', this.pdfSearchQuery); + this.pdfSearchLoading = true; + this.pdfSearchResults = []; + + try { + // 백엔드 API를 통해 PDF 내용 검색 + const searchResults = await this.api.get( + `/documents/${this.documentId}/search-in-content?q=${encodeURIComponent(this.pdfSearchQuery)}` + ); + + console.log('✅ PDF 검색 결과:', searchResults); + + if (searchResults.matches && searchResults.matches.length > 0) { + this.pdfSearchResults = searchResults.matches.map(match => ({ + page: match.page || 1, + context: match.context || match.text || this.pdfSearchQuery, + position: match.position || 0 + })); + + console.log(`📄 ${this.pdfSearchResults.length}개의 검색 결과 발견`); + + if (this.pdfSearchResults.length === 0) { + alert('검색 결과를 찾을 수 없습니다.'); + } + } else { + alert('검색 결과를 찾을 수 없습니다.'); + } + + } catch (error) { + console.error('❌ PDF 검색 실패:', error); + alert('PDF 검색 중 오류가 발생했습니다: ' + error.message); + } finally { + this.pdfSearchLoading = false; + } + }, + + jumpToPdfResult(result) { + console.log('📍 PDF 결과로 이동:', result); + + // PDF URL에 페이지 번호 추가하여 해당 페이지로 이동 + const token = localStorage.getItem('access_token'); + let newPdfSrc = `/api/documents/${this.documentId}/pdf?_token=${encodeURIComponent(token)}`; + + // 페이지 번호가 있으면 URL 프래그먼트로 추가 + if (result.page && result.page > 1) { + newPdfSrc += `#page=${result.page}`; + } + + // PDF src 업데이트하여 해당 페이지로 이동 + this.pdfSrc = newPdfSrc; + + console.log(`📄 페이지 ${result.page}로 이동:`, newPdfSrc); + + // 잠시 후 검색 기능 활성화 + setTimeout(() => { + const iframe = document.querySelector('#pdf-viewer-iframe'); + if (iframe && iframe.contentWindow) { + try { + iframe.contentWindow.focus(); + + // 브라우저 내장 검색 기능 활용 + if (iframe.contentWindow.find) { + iframe.contentWindow.find(this.pdfSearchQuery); + } else { + // 대안: 사용자에게 수동 검색 안내 + this.showSuccessMessage(`페이지 ${result.page}로 이동했습니다. Ctrl+F를 눌러 "${this.pdfSearchQuery}"를 검색하세요.`); + } + } catch (e) { + console.warn('PDF iframe 접근 제한:', e); + this.showSuccessMessage(`페이지 ${result.page}로 이동했습니다. Ctrl+F를 눌러 "${this.pdfSearchQuery}"를 검색하세요.`); + } + } + }, 1000); + + // 모달 닫기 + this.showPdfSearchModal = false; + }, + async editNote(noteId, currentContent) { console.log('✏️ 메모 편집:', noteId); console.log('🔍 HighlightManager 상태:', this.highlightManager); diff --git a/frontend/story-view.html b/frontend/story-view.html index d212bce..dc8a0ea 100644 --- a/frontend/story-view.html +++ b/frontend/story-view.html @@ -44,7 +44,7 @@
-
+
@@ -54,8 +54,8 @@
+
@@ -178,7 +179,7 @@
-
+
@@ -288,17 +289,31 @@ // 스크립트 순차 로딩 (async () => { try { - await loadScript('static/js/api.js?v=2025012380'); + console.log('🚀 스크립트 로딩 시작...'); + + await loadScript('static/js/api.js?v=2025012627'); console.log('✅ API 스크립트 로드 완료'); - await loadScript('static/js/story-view.js?v=2025012364'); + + await loadScript('static/js/story-view.js?v=2025012627'); console.log('✅ Story View 스크립트 로드 완료'); + // Alpine.js 로드 전에 함수 등록 확인 + console.log('🔍 storyViewApp 함수 확인:', typeof window.storyViewApp); + // 모든 스크립트 로드 완료 후 Alpine.js 로드 console.log('🚀 Alpine.js 로딩...'); await loadScript('https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js'); console.log('✅ Alpine.js 로드 완료'); + + // Alpine.js 초기화 확인 + setTimeout(() => { + console.log('🔍 Alpine 객체 확인:', typeof Alpine); + console.log('🔍 Alpine 초기화 상태:', Alpine ? 'OK' : 'FAILED'); + }, 500); + } catch (error) { console.error('❌ 스크립트 로드 실패:', error); + console.error('❌ 에러 상세:', error.message); } })(); diff --git a/frontend/viewer.html b/frontend/viewer.html index 77d8df2..736543e 100644 --- a/frontend/viewer.html +++ b/frontend/viewer.html @@ -266,13 +266,210 @@
-
- +
+ +
+ +
+ + +
+
+
+ +

+
+
+ + +
+
+ + +
+ +
+ +
+
+ + + + / + + +
+
+ + + +
+
+ + +
+ +
+
+ + + + + +
+
+ +

PDF를 로드하는 중...

+
+
+ + +
+
+ +

PDF를 로드할 수 없습니다

+ + +
+
+
+
+ + +
+ +
+ +
+
+ +
+

+ + PDF에서 검색 +

+ +
+ + +
+
+ +
+ +
+ +
+
+
+ Enter 키를 누르거나 검색 버튼을 클릭하세요 +
+
+ + +
+
+ + 개의 결과를 찾았습니다. +
+
+ +
+
+ + +
+
+ +

검색 결과가 없습니다.

+

다른 검색어로 시도해보세요.

+
+
+ + +
+ + +
+
+
+
+
- + - + + + + diff --git a/nginx/default.conf b/nginx/default.conf index 723a81d..f8edb38 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -11,6 +11,36 @@ server { access_log off; } + # PDF 파일 요청 (iframe 허용) + location ~ ^/api/documents/[^/]+/pdf$ { + proxy_pass http://backend:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 타임아웃 설정 + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + + # 버퍼링 설정 + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + + # 리다이렉트 방지 + proxy_redirect off; + + # PDF iframe 허용 및 인라인 표시 설정 + add_header X-Frame-Options "SAMEORIGIN" always; + + # PDF 파일이 다운로드되지 않고 브라우저에서 표시되도록 설정 + location ~ \.pdf$ { + add_header Content-Disposition "inline"; + } + } + # API 요청을 백엔드로 프록시 location /api/ { proxy_pass http://backend:8000; diff --git a/nginx/nginx.conf b/nginx/nginx.conf index af561f5..db0ae88 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -45,8 +45,7 @@ http { application/atom+xml image/svg+xml; - # 보안 헤더 - add_header X-Frame-Options "SAMEORIGIN" always; + # 보안 헤더 (PDF 파일은 제외) add_header X-XSS-Protection "1; mode=block" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "no-referrer-when-downgrade" always;