스토리 뷰 페이지 헤더 z-index 충돌 문제 해결
- 메인 컨테이너 padding-top을 pt-4에서 pt-20으로 증가 - 드롭다운 z-index를 z-[60]으로 설정하여 헤더보다 높은 우선순위 부여 - 스토리 선택 드롭다운이 정상적으로 작동하도록 수정 - 디버깅용 코드 정리
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -193,6 +193,12 @@
|
||||
|
||||
<!-- 액션 버튼 -->
|
||||
<div class="flex items-center space-x-2 ml-4">
|
||||
<button @click="previewPDF(pdf)"
|
||||
class="p-2 text-gray-400 hover:text-green-600 transition-colors"
|
||||
title="PDF 미리보기">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
|
||||
<button @click="downloadPDF(pdf)"
|
||||
class="p-2 text-gray-400 hover:text-blue-600 transition-colors"
|
||||
title="PDF 다운로드">
|
||||
@@ -225,10 +231,86 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- PDF 미리보기 모달 -->
|
||||
<div x-show="showPreviewModal"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"
|
||||
@click.self="closePreview()">
|
||||
<div class="bg-white rounded-2xl shadow-2xl max-w-6xl w-full max-h-[90vh] overflow-hidden">
|
||||
<!-- 헤더 -->
|
||||
<div class="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<div class="flex items-center space-x-3">
|
||||
<i class="fas fa-file-pdf text-red-600 text-xl"></i>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900" x-text="previewPdf?.title"></h3>
|
||||
<p class="text-sm text-gray-500" x-text="previewPdf?.original_filename"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button @click="downloadPDF(previewPdf)"
|
||||
class="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2">
|
||||
<i class="fas fa-download"></i>
|
||||
<span>다운로드</span>
|
||||
</button>
|
||||
<button @click="closePreview()"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF 뷰어 -->
|
||||
<div class="p-6 overflow-y-auto" style="max-height: calc(90vh - 120px);">
|
||||
<!-- PDF 미리보기 -->
|
||||
<div x-show="previewPdf" class="mb-4">
|
||||
<!-- PDF 뷰어 컨테이너 -->
|
||||
<div class="border rounded-lg overflow-hidden bg-gray-100 relative" style="height: 600px;">
|
||||
<!-- PDF iframe 뷰어 -->
|
||||
<iframe x-show="!pdfPreviewError && !pdfPreviewLoading && pdfPreviewSrc"
|
||||
class="w-full h-full border-0"
|
||||
:src="pdfPreviewSrc"
|
||||
@load="pdfPreviewLoaded = true"
|
||||
@error="handlePdfPreviewError()">
|
||||
</iframe>
|
||||
|
||||
<!-- PDF 로딩 상태 -->
|
||||
<div x-show="pdfPreviewLoading" class="flex items-center justify-center h-full">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-spinner fa-spin text-3xl text-gray-500 mb-4"></i>
|
||||
<p class="text-gray-600 text-lg">PDF를 로드하는 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF 에러 상태 -->
|
||||
<div x-show="pdfPreviewError" class="flex items-center justify-center h-full text-gray-500">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-exclamation-triangle text-3xl mb-4 text-red-500"></i>
|
||||
<p class="text-lg mb-4">PDF를 로드할 수 없습니다</p>
|
||||
<button @click="retryPdfPreview()"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 mr-2">
|
||||
다시 시도
|
||||
</button>
|
||||
<button @click="downloadPDF(previewPdf)"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700">
|
||||
파일 다운로드
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript 파일들 -->
|
||||
<script src="/static/js/api.js?v=2025012384"></script>
|
||||
<script src="/static/js/auth.js?v=2025012351"></script>
|
||||
<script src="/static/js/header-loader.js?v=2025012351"></script>
|
||||
<script src="/static/js/pdf-manager.js?v=2025012459"></script>
|
||||
<script src="/static/js/pdf-manager.js?v=2025012627"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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 '';
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<div id="header-container"></div>
|
||||
|
||||
<!-- 메인 컨테이너 -->
|
||||
<div class="min-h-screen pt-4" x-show="currentUser">
|
||||
<div class="min-h-screen pt-20" x-show="currentUser">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
<!-- 상단 툴바 -->
|
||||
@@ -54,8 +54,8 @@
|
||||
<div class="flex items-center space-x-4">
|
||||
<select
|
||||
x-model="selectedTreeId"
|
||||
@change="loadStory(selectedTreeId)"
|
||||
class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
@change="loadStory($event.target.value)"
|
||||
class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer relative z-[60]"
|
||||
>
|
||||
<option value="">📚 스토리 선택</option>
|
||||
<template x-for="tree in userTrees" :key="tree.id">
|
||||
@@ -63,6 +63,7 @@
|
||||
</template>
|
||||
</select>
|
||||
|
||||
|
||||
<div x-show="selectedTree" class="text-sm text-gray-600">
|
||||
<span x-text="`총 ${canonicalNodes.length}개 챕터`"></span>
|
||||
<span class="mx-2">•</span>
|
||||
@@ -178,7 +179,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 p-6 overflow-hidden">
|
||||
<div class="flex-1 p-6 overflow-hidden" x-show="editingNode">
|
||||
<div class="h-full flex flex-col space-y-4">
|
||||
<!-- 제목 편집 -->
|
||||
<div>
|
||||
@@ -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);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -266,13 +266,210 @@
|
||||
</div>
|
||||
|
||||
<!-- 문서 내용 -->
|
||||
<div x-show="!loading && !error" id="document-content" class="prose max-w-none">
|
||||
<!-- 문서 HTML이 여기에 로드됩니다 -->
|
||||
<div x-show="!loading && !error">
|
||||
<!-- HTML 문서 내용 -->
|
||||
<div x-show="contentType === 'document' && document && !document.pdf_path"
|
||||
id="document-content" class="prose max-w-none">
|
||||
<!-- 문서 HTML이 여기에 로드됩니다 -->
|
||||
</div>
|
||||
|
||||
<!-- PDF 뷰어 -->
|
||||
<div x-show="contentType === 'document' && document && document.pdf_path"
|
||||
class="pdf-viewer-container">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<i class="fas fa-file-pdf text-red-600 text-xl"></i>
|
||||
<h1 class="text-2xl font-bold text-gray-900" x-text="document?.title"></h1>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button @click="openPdfSearchModal()"
|
||||
class="px-3 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center space-x-2">
|
||||
<i class="fas fa-search"></i>
|
||||
<span>PDF에서 검색</span>
|
||||
</button>
|
||||
<button @click="downloadOriginalFile()"
|
||||
class="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2">
|
||||
<i class="fas fa-download"></i>
|
||||
<span>다운로드</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF 뷰어 컨테이너 -->
|
||||
<div class="border rounded-lg overflow-hidden bg-white relative" style="min-height: 800px;">
|
||||
<!-- PDF.js 뷰어 -->
|
||||
<div x-show="!pdfError && !pdfLoading && pdfSrc" class="w-full h-full">
|
||||
<!-- PDF 뷰어 툴바 -->
|
||||
<div class="bg-gray-100 border-b px-4 py-2 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<button @click="previousPage()" :disabled="currentPage <= 1"
|
||||
class="px-3 py-1 bg-blue-600 text-white rounded disabled:bg-gray-300">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<span class="text-sm">
|
||||
<input type="number" x-model="currentPage" @change="goToPage(currentPage)"
|
||||
class="w-16 px-2 py-1 border rounded text-center" min="1" :max="totalPages">
|
||||
/ <span x-text="totalPages"></span>
|
||||
</span>
|
||||
<button @click="nextPage()" :disabled="currentPage >= totalPages"
|
||||
class="px-3 py-1 bg-blue-600 text-white rounded disabled:bg-gray-300">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button @click="zoomOut()" class="px-2 py-1 bg-gray-600 text-white rounded">
|
||||
<i class="fas fa-search-minus"></i>
|
||||
</button>
|
||||
<span class="text-sm" x-text="Math.round(pdfScale * 100) + '%'"></span>
|
||||
<button @click="zoomIn()" class="px-2 py-1 bg-gray-600 text-white rounded">
|
||||
<i class="fas fa-search-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF 캔버스 -->
|
||||
<div class="overflow-auto" style="height: 750px;">
|
||||
<canvas id="pdf-canvas" class="mx-auto block"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 기존 iframe (폴백용) -->
|
||||
<iframe id="pdf-viewer-iframe"
|
||||
x-show="false"
|
||||
class="w-full border-0"
|
||||
style="height: 800px;"
|
||||
:src="pdfSrc"
|
||||
@load="pdfLoaded = true; console.log('PDF iframe 로드 완료')"
|
||||
@error="handlePdfError()"
|
||||
allow="fullscreen">
|
||||
</iframe>
|
||||
|
||||
<!-- PDF 로딩 상태 -->
|
||||
<div x-show="pdfLoading" class="flex items-center justify-center h-full" style="min-height: 400px;">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-spinner fa-spin text-3xl text-gray-500 mb-4"></i>
|
||||
<p class="text-gray-600 text-lg">PDF를 로드하는 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF 에러 상태 -->
|
||||
<div x-show="pdfError" class="flex items-center justify-center h-full text-gray-500" style="min-height: 400px;">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-exclamation-triangle text-3xl mb-4 text-red-500"></i>
|
||||
<p class="text-lg mb-4">PDF를 로드할 수 없습니다</p>
|
||||
<button @click="retryPdfLoad()"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 mr-2">
|
||||
다시 시도
|
||||
</button>
|
||||
<button @click="downloadOriginalFile()"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700">
|
||||
파일 다운로드
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 노트 문서 내용 -->
|
||||
<div x-show="contentType === 'note'"
|
||||
id="note-content" class="prose max-w-none">
|
||||
<!-- 노트 내용이 여기에 로드됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- PDF 검색 모달 -->
|
||||
<div x-show="showPdfSearchModal"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"
|
||||
@click.self="showPdfSearchModal = false">
|
||||
<div class="bg-white rounded-2xl shadow-2xl max-w-md w-full">
|
||||
<!-- 헤더 -->
|
||||
<div class="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<h3 class="text-xl font-bold text-gray-900 flex items-center space-x-2">
|
||||
<i class="fas fa-search text-green-600"></i>
|
||||
<span>PDF에서 검색</span>
|
||||
</h3>
|
||||
<button @click="showPdfSearchModal = false"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 내용 -->
|
||||
<div class="p-6">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">검색어</label>
|
||||
<div class="relative">
|
||||
<input type="text"
|
||||
x-model="pdfSearchQuery"
|
||||
@keydown.enter="searchInPdf()"
|
||||
@input="pdfSearchResults = []"
|
||||
placeholder="예: pressure, vessel, design..."
|
||||
class="w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||||
x-ref="searchInput">
|
||||
<div class="absolute inset-y-0 right-0 pr-3 flex items-center">
|
||||
<i class="fas fa-search text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-500">
|
||||
Enter 키를 누르거나 검색 버튼을 클릭하세요
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 검색 결과 -->
|
||||
<div x-show="pdfSearchResults.length > 0" class="mb-4">
|
||||
<div class="text-sm text-gray-600 mb-2">
|
||||
<i class="fas fa-check-circle text-green-500 mr-1"></i>
|
||||
<span x-text="pdfSearchResults.length"></span>개의 결과를 찾았습니다.
|
||||
</div>
|
||||
<div class="max-h-40 overflow-y-auto space-y-2">
|
||||
<template x-for="(result, index) in pdfSearchResults" :key="index">
|
||||
<div class="p-3 bg-gray-50 rounded cursor-pointer hover:bg-green-50 border hover:border-green-200 transition-colors"
|
||||
@click="jumpToPdfResult(result)">
|
||||
<div class="text-sm font-medium text-green-700">
|
||||
<i class="fas fa-file-pdf mr-1"></i>
|
||||
페이지 <span x-text="result.page"></span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1" x-text="result.context"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 검색 결과 없음 -->
|
||||
<div x-show="pdfSearchQuery.trim() && !pdfSearchLoading && pdfSearchResults.length === 0" class="mb-4">
|
||||
<div class="text-center py-4 text-gray-500">
|
||||
<i class="fas fa-search text-2xl mb-2"></i>
|
||||
<p class="text-sm">검색 결과가 없습니다.</p>
|
||||
<p class="text-xs mt-1">다른 검색어로 시도해보세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 버튼 -->
|
||||
<div class="flex space-x-3">
|
||||
<button @click="searchInPdf()"
|
||||
:disabled="!pdfSearchQuery.trim() || pdfSearchLoading"
|
||||
class="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors">
|
||||
<i :class="pdfSearchLoading ? 'fas fa-spinner fa-spin' : 'fas fa-search'" class="mr-2"></i>
|
||||
<span x-text="pdfSearchLoading ? '검색 중...' : '검색'"></span>
|
||||
</button>
|
||||
<button @click="showPdfSearchModal = false"
|
||||
class="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400 transition-colors">
|
||||
닫기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 링크 모달 -->
|
||||
<div x-show="showLinksModal"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
@@ -781,14 +978,17 @@
|
||||
<script src="/static/js/viewer/utils/module-loader.js?v=2025012607"></script>
|
||||
|
||||
<!-- 모든 모듈들 직접 로드 -->
|
||||
<script src="/static/js/viewer/core/document-loader.js?v=2025012607"></script>
|
||||
<script src="/static/js/viewer/core/document-loader.js?v=2025012624"></script>
|
||||
<script src="/static/js/viewer/features/ui-manager.js?v=2025012607"></script>
|
||||
<script src="/static/js/viewer/features/highlight-manager.js?v=2025012619"></script>
|
||||
<script src="/static/js/viewer/features/link-manager.js?v=2025012622"></script>
|
||||
<script src="/static/js/viewer/features/bookmark-manager.js?v=2025012607"></script>
|
||||
|
||||
<!-- ViewerCore (Alpine.js 컴포넌트) -->
|
||||
<script src="/static/js/viewer/viewer-core.js?v=2025012623"></script>
|
||||
<script src="/static/js/viewer/viewer-core.js?v=2025012626"></script>
|
||||
|
||||
<!-- PDF.js 라이브러리 -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||||
|
||||
<!-- Alpine.js 프레임워크 -->
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user