- 메인 컨테이너 padding-top을 pt-4에서 pt-20으로 증가 - 드롭다운 z-index를 z-[60]으로 설정하여 헤더보다 높은 우선순위 부여 - 스토리 선택 드롭다운이 정상적으로 작동하도록 수정 - 디버깅용 코드 정리
1131 lines
69 KiB
HTML
1131 lines
69 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>문서 뷰어 - Document Server</title>
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📄</text></svg>">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<script src="https://cdn.quilljs.com/1.3.6/quill.min.js"></script>
|
|
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="/static/css/viewer.css">
|
|
</head>
|
|
<body class="bg-gray-50 min-h-screen">
|
|
<!-- 인증 확인 및 리다이렉트 -->
|
|
<script>
|
|
// 페이지 로드 시 인증 상태 확인
|
|
(function() {
|
|
const token = localStorage.getItem('access_token');
|
|
if (!token) {
|
|
console.log('🔐 인증 토큰이 없습니다. 메인 페이지로 리다이렉트합니다.');
|
|
window.location.href = '/';
|
|
return;
|
|
}
|
|
console.log('✅ 인증 토큰 확인됨:', token.substring(0, 20) + '...');
|
|
})();
|
|
</script>
|
|
|
|
<div x-data="documentViewer" x-init="init(); $store.documentViewer.init()">
|
|
<!-- 헤더 - 투명하고 세련된 3줄 디자인 -->
|
|
<header class="bg-white/80 backdrop-blur-md shadow-lg border-b border-white/20 sticky top-0 z-50 w-full">
|
|
<div class="w-full px-6 py-4">
|
|
<!-- 첫 번째 줄: 네비게이션 + 제목 -->
|
|
<div class="flex items-center justify-between mb-2">
|
|
<!-- 왼쪽: 네비게이션 -->
|
|
<div class="flex items-center space-x-3">
|
|
<!-- 뒤로가기 -->
|
|
<button @click="goBack" class="w-9 h-9 bg-white/60 hover:bg-white/80 rounded-xl flex items-center justify-center transition-all duration-200 text-gray-700 hover:text-gray-900 shadow-sm">
|
|
<i class="fas fa-arrow-left text-sm"></i>
|
|
</button>
|
|
|
|
<!-- 서적 네비게이션 -->
|
|
<div x-show="navigation" class="flex items-center space-x-2">
|
|
<button @click="navigateToDocument(navigation?.previous?.id)"
|
|
x-show="navigation?.previous"
|
|
:title="navigation?.previous ? `이전: ${navigation.previous.title}` : ''"
|
|
class="w-8 h-8 bg-blue-500/20 hover:bg-blue-500/30 rounded-lg flex items-center justify-center transition-all duration-200 text-blue-700">
|
|
<i class="fas fa-chevron-left text-xs"></i>
|
|
</button>
|
|
<button @click="goToBookContents()"
|
|
x-show="navigation?.book_info"
|
|
:title="navigation?.book_info ? `목차: ${navigation.book_info.title}` : ''"
|
|
class="w-8 h-8 bg-green-500/20 hover:bg-green-500/30 rounded-lg flex items-center justify-center transition-all duration-200 text-green-700">
|
|
<i class="fas fa-list text-xs"></i>
|
|
</button>
|
|
<button @click="navigateToDocument(navigation?.next?.id)"
|
|
x-show="navigation?.next"
|
|
:title="navigation?.next ? `다음: ${navigation.next.title}` : ''"
|
|
class="w-8 h-8 bg-blue-500/20 hover:bg-blue-500/30 rounded-lg flex items-center justify-center transition-all duration-200 text-blue-700">
|
|
<i class="fas fa-chevron-right text-xs"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 중앙: 문서 제목 -->
|
|
<div class="flex-1 text-center">
|
|
<h1 class="text-xl font-bold text-gray-800" x-text="document?.title || '로딩 중...'"></h1>
|
|
</div>
|
|
|
|
<!-- 오른쪽: 검색 -->
|
|
<div class="flex items-center">
|
|
<div class="relative">
|
|
<input type="text" x-model="searchQuery" @input="searchInDocument"
|
|
placeholder="검색..."
|
|
class="w-60 pl-9 pr-3 py-2 bg-white/50 border border-white/30 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-400/50 focus:bg-white/70 shadow-sm transition-all duration-200 text-sm">
|
|
<i class="fas fa-search absolute left-3 top-2.5 text-gray-500 text-xs"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 두 번째 줄: 문서 정보 -->
|
|
<div x-show="document" class="flex items-center justify-between text-sm text-gray-600 mb-3 px-2">
|
|
<!-- 왼쪽: 업로더 정보 -->
|
|
<div class="flex items-center space-x-4">
|
|
<span class="flex items-center space-x-1">
|
|
<i class="fas fa-user text-xs"></i>
|
|
<span x-text="document?.uploader_name"></span>
|
|
</span>
|
|
<span x-show="navigation?.book_info" class="flex items-center space-x-1">
|
|
<i class="fas fa-book text-xs"></i>
|
|
<span x-text="navigation?.book_info?.title"></span>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- 오른쪽: 문서 통계 -->
|
|
<div class="flex items-center space-x-4">
|
|
<span x-show="notes.length > 0" class="flex items-center space-x-1">
|
|
<i class="fas fa-sticky-note text-xs text-blue-500"></i>
|
|
<span x-text="`메모 ${notes.length}개`"></span>
|
|
</span>
|
|
<span x-show="bookmarks.length > 0" class="flex items-center space-x-1">
|
|
<i class="fas fa-bookmark text-xs text-amber-500"></i>
|
|
<span x-text="`책갈피 ${bookmarks.length}개`"></span>
|
|
</span>
|
|
<span x-show="documentLinks.length > 0" class="flex items-center space-x-1">
|
|
<i class="fas fa-link text-xs text-purple-500"></i>
|
|
<span x-text="`링크 ${documentLinks.length}개`"></span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 세 번째 줄: 도구 모음 -->
|
|
<div class="flex items-center justify-between">
|
|
<!-- 왼쪽: 하이라이트 색상 -->
|
|
<div class="flex items-center space-x-1 bg-white/50 rounded-xl shadow-sm border border-white/30 px-3 py-2">
|
|
<span class="text-xs font-medium text-gray-600 mr-2">하이라이트</span>
|
|
<button @click="createHighlightWithColor('#FFFF00')"
|
|
:class="selectedHighlightColor === '#FFFF00' ? 'ring-2 ring-yellow-400 scale-110' : 'hover:scale-110'"
|
|
class="w-6 h-6 bg-gradient-to-br from-yellow-300 to-yellow-400 rounded-full border border-white shadow-sm transition-all duration-200"
|
|
title="노란색"></button>
|
|
<button @click="createHighlightWithColor('#90EE90')"
|
|
:class="selectedHighlightColor === '#90EE90' ? 'ring-2 ring-green-400 scale-110' : 'hover:scale-110'"
|
|
class="w-6 h-6 bg-gradient-to-br from-green-300 to-green-400 rounded-full border border-white shadow-sm transition-all duration-200"
|
|
title="초록색"></button>
|
|
<button @click="createHighlightWithColor('#FFCCCB')"
|
|
:class="selectedHighlightColor === '#FFCCCB' ? 'ring-2 ring-pink-400 scale-110' : 'hover:scale-110'"
|
|
class="w-6 h-6 bg-gradient-to-br from-pink-200 to-pink-300 rounded-full border border-white shadow-sm transition-all duration-200"
|
|
title="분홍색"></button>
|
|
<button @click="createHighlightWithColor('#87CEEB')"
|
|
:class="selectedHighlightColor === '#87CEEB' ? 'ring-2 ring-blue-400 scale-110' : 'hover:scale-110'"
|
|
class="w-6 h-6 bg-gradient-to-br from-blue-300 to-blue-400 rounded-full border border-white shadow-sm transition-all duration-200"
|
|
title="파란색"></button>
|
|
</div>
|
|
|
|
<!-- 중앙: 기능 버튼들 -->
|
|
<div class="flex items-center space-x-3">
|
|
<!-- 링크 버튼 - 드래그 후 클릭으로 링크 생성 -->
|
|
<button @click="activateLinkMode()"
|
|
:class="activeMode === 'link' ? 'bg-purple-600' : 'bg-purple-500/80 hover:bg-purple-500'"
|
|
class="px-4 py-2 text-white rounded-xl transition-all duration-200 flex items-center space-x-2 shadow-sm"
|
|
title="텍스트를 드래그한 후 이 버튼을 클릭하여 링크를 생성하세요">
|
|
<i class="fas fa-link text-sm"></i>
|
|
<span class="text-sm font-medium">링크</span>
|
|
<span x-show="documentLinks.length > 0"
|
|
class="bg-white text-purple-600 text-xs rounded-full w-5 h-5 flex items-center justify-center font-bold"
|
|
x-text="documentLinks.length"></span>
|
|
</button>
|
|
|
|
<!-- 백링크 표시 (읽기 전용) -->
|
|
<div class="px-4 py-2 bg-orange-500/80 text-white rounded-xl flex items-center space-x-2 shadow-sm"
|
|
title="이 문서를 참조하는 다른 문서들">
|
|
<i class="fas fa-arrow-left text-sm"></i>
|
|
<span class="text-sm font-medium">백링크</span>
|
|
<span x-show="backlinks.length > 0"
|
|
class="bg-white text-orange-600 text-xs rounded-full w-5 h-5 flex items-center justify-center font-bold"
|
|
x-text="backlinks.length"></span>
|
|
</div>
|
|
|
|
<!-- 메모 버튼 그룹 -->
|
|
<div class="relative">
|
|
<button @click="toggleFeatureMenu('memo')"
|
|
:class="activeFeatureMenu === 'memo' ? 'bg-blue-600' : 'bg-blue-500/80 hover:bg-blue-500'"
|
|
class="px-4 py-2 text-white rounded-xl transition-all duration-200 flex items-center space-x-2 shadow-sm">
|
|
<i class="fas fa-sticky-note text-sm"></i>
|
|
<span class="text-sm font-medium">메모</span>
|
|
<span x-show="notes.length > 0"
|
|
class="bg-white text-blue-600 text-xs rounded-full w-5 h-5 flex items-center justify-center font-bold"
|
|
x-text="notes.length"></span>
|
|
<i class="fas fa-chevron-down text-xs ml-1" :class="activeFeatureMenu === 'memo' ? 'rotate-180' : ''"></i>
|
|
</button>
|
|
<!-- 메모 서브메뉴 -->
|
|
<div x-show="activeFeatureMenu === 'memo'"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
class="absolute top-full left-0 mt-2 bg-white/90 backdrop-blur-md rounded-xl shadow-lg border border-white/30 p-2 z-10 min-w-max">
|
|
<button @click="showNotesModal = true; activeFeatureMenu = null"
|
|
class="w-full px-3 py-2 text-sm text-gray-700 hover:bg-blue-100 rounded-lg flex items-center space-x-2 transition-colors">
|
|
<i class="fas fa-eye text-blue-500"></i>
|
|
<span>메모 보기</span>
|
|
</button>
|
|
<button @click="activateNoteMode(); activeFeatureMenu = null"
|
|
class="w-full px-3 py-2 text-sm text-gray-700 hover:bg-green-100 rounded-lg flex items-center space-x-2 transition-colors">
|
|
<i class="fas fa-plus text-green-500"></i>
|
|
<span>메모 만들기</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 책갈피 버튼 그룹 -->
|
|
<div class="relative">
|
|
<button @click="toggleFeatureMenu('bookmark')"
|
|
:class="activeFeatureMenu === 'bookmark' ? 'bg-amber-600' : 'bg-amber-500/80 hover:bg-amber-500'"
|
|
class="px-4 py-2 text-white rounded-xl transition-all duration-200 flex items-center space-x-2 shadow-sm">
|
|
<i class="fas fa-bookmark text-sm"></i>
|
|
<span class="text-sm font-medium">책갈피</span>
|
|
<span x-show="bookmarks.length > 0"
|
|
class="bg-white text-amber-600 text-xs rounded-full w-5 h-5 flex items-center justify-center font-bold"
|
|
x-text="bookmarks.length"></span>
|
|
<i class="fas fa-chevron-down text-xs ml-1" :class="activeFeatureMenu === 'bookmark' ? 'rotate-180' : ''"></i>
|
|
</button>
|
|
<!-- 책갈피 서브메뉴 -->
|
|
<div x-show="activeFeatureMenu === 'bookmark'"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
class="absolute top-full left-0 mt-2 bg-white/90 backdrop-blur-md rounded-xl shadow-lg border border-white/30 p-2 z-10 min-w-max">
|
|
<button @click="showBookmarksModal = true; activeFeatureMenu = null"
|
|
class="w-full px-3 py-2 text-sm text-gray-700 hover:bg-amber-100 rounded-lg flex items-center space-x-2 transition-colors">
|
|
<i class="fas fa-eye text-amber-500"></i>
|
|
<span>책갈피 보기</span>
|
|
</button>
|
|
<button @click="console.log('책갈피 만들기 클릭됨'); activateBookmarkMode(); activeFeatureMenu = null"
|
|
class="w-full px-3 py-2 text-sm text-gray-700 hover:bg-amber-100 rounded-lg flex items-center space-x-2 transition-colors">
|
|
<i class="fas fa-plus text-amber-500"></i>
|
|
<span>책갈피 만들기</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 오른쪽: 액션 버튼들 -->
|
|
<div class="flex items-center space-x-3">
|
|
<button @click="$store.documentViewer.downloadOriginalFile()"
|
|
class="bg-red-500/80 hover:bg-red-500 text-white px-4 py-2 rounded-xl transition-all duration-200 flex items-center space-x-2 shadow-sm"
|
|
title="원본 PDF 다운로드">
|
|
<i class="fas fa-file-pdf text-sm"></i>
|
|
<span class="text-sm font-medium">PDF</span>
|
|
</button>
|
|
|
|
<button @click="$store.documentViewer.toggleLanguage()"
|
|
class="bg-blue-500/80 hover:bg-blue-500 text-white px-4 py-2 rounded-xl transition-all duration-200 flex items-center space-x-2 shadow-sm"
|
|
id="language-toggle-btn">
|
|
<i class="fas fa-globe text-sm"></i>
|
|
<span class="text-sm font-medium">언어전환</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- 메인 컨텐츠 -->
|
|
<div class="flex w-full min-h-screen">
|
|
<!-- 문서 뷰어 -->
|
|
<main class="flex-1 bg-white">
|
|
<div class="px-12 py-8 max-w-5xl mx-auto">
|
|
<!-- 로딩 상태 -->
|
|
<div x-show="loading" class="text-center py-16">
|
|
<i class="fas fa-spinner fa-spin text-4xl text-gray-400 mb-4"></i>
|
|
<p class="text-gray-600">문서를 불러오는 중...</p>
|
|
</div>
|
|
|
|
<!-- 에러 상태 -->
|
|
<div x-show="error" class="text-center py-16">
|
|
<i class="fas fa-exclamation-triangle text-4xl text-red-400 mb-4"></i>
|
|
<p class="text-red-600" x-text="error"></p>
|
|
<button @click="goBack" class="mt-4 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
|
|
돌아가기
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 문서 내용 -->
|
|
<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"
|
|
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="showLinksModal = false">
|
|
<div class="bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[80vh] overflow-hidden">
|
|
<!-- 헤더 -->
|
|
<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-link text-purple-500"></i>
|
|
<span>링크</span>
|
|
</h3>
|
|
<button @click="showLinksModal = false"
|
|
class="w-8 h-8 bg-gray-100 hover:bg-gray-200 rounded-lg flex items-center justify-center transition-colors">
|
|
<i class="fas fa-times text-gray-600"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 내용 -->
|
|
<div class="p-6 overflow-y-auto max-h-96">
|
|
<!-- 빈 상태 -->
|
|
<template x-if="documentLinks.length === 0">
|
|
<div class="text-center py-8">
|
|
<i class="fas fa-link text-4xl text-gray-300 mb-4"></i>
|
|
<p class="text-gray-500">아직 링크가 없습니다.</p>
|
|
<p class="text-sm text-gray-400 mt-2">텍스트를 선택하고 링크를 만들어보세요.</p>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 링크 목록 -->
|
|
<template x-for="link in documentLinks" :key="link.id">
|
|
<div class="border rounded-lg p-4 mb-3 hover:bg-purple-50 cursor-pointer transition-colors"
|
|
@click="navigateToLink(link)">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<!-- 대상 문서 제목 -->
|
|
<div class="font-medium text-purple-700 mb-2 flex items-center">
|
|
<span x-text="link.target_document_title || link.target_note_title"></span>
|
|
<span x-show="link.target_content_type === 'note'" class="ml-2 text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded">노트</span>
|
|
<span x-show="link.target_content_type === 'document'" class="ml-2 text-xs bg-green-100 text-green-700 px-2 py-1 rounded">문서</span>
|
|
</div>
|
|
|
|
<!-- 현재 문서에서 선택한 텍스트 (출발점) -->
|
|
<div x-show="link.selected_text" class="mb-3">
|
|
<div class="text-xs text-gray-500 mb-1">📍 현재 문서에서 선택한 텍스트:</div>
|
|
<div class="text-sm text-gray-700 bg-purple-50 px-3 py-2 rounded border-l-4 border-purple-500" x-text="link.selected_text"></div>
|
|
</div>
|
|
|
|
<!-- 대상 문서의 텍스트 (도착점) -->
|
|
<div x-show="link.target_text" class="mb-3">
|
|
<div class="text-xs text-gray-500 mb-1">🎯 대상 문서의 텍스트:</div>
|
|
<div class="text-sm text-gray-700 bg-blue-50 px-3 py-2 rounded border-l-4 border-blue-500" x-text="link.target_text"></div>
|
|
</div>
|
|
|
|
<!-- 문서 전체 링크인 경우 -->
|
|
<div x-show="!link.selected_text && !link.target_text" class="mb-3">
|
|
<div class="text-sm text-gray-600 italic bg-gray-50 px-3 py-2 rounded">📄 문서 전체 링크</div>
|
|
</div>
|
|
|
|
<!-- 설명 -->
|
|
<div x-show="link.description" class="mb-3">
|
|
<div class="text-xs text-gray-500 mb-1">💬 설명:</div>
|
|
<div class="text-sm text-gray-600 bg-yellow-50 px-3 py-2 rounded" x-text="link.description"></div>
|
|
</div>
|
|
|
|
<!-- 링크 타입과 날짜 -->
|
|
<div class="flex items-center justify-between text-xs text-gray-500">
|
|
<span x-text="link.link_type === 'text_fragment' ? '🔗 텍스트 조각 링크' : '📄 문서 링크'"></span>
|
|
<span x-text="formatDate(link.created_at)"></span>
|
|
</div>
|
|
</div>
|
|
<div class="ml-3 flex-shrink-0">
|
|
<svg class="w-5 h-5 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 링크 생성 모달 -->
|
|
<div x-show="showLinkModal"
|
|
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="closeLinkModal()">
|
|
<div class="bg-white rounded-2xl shadow-2xl max-w-3xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
|
<!-- 헤더 -->
|
|
<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-link text-purple-500"></i>
|
|
<span>링크 생성</span>
|
|
</h3>
|
|
<button @click="closeLinkModal()"
|
|
class="w-8 h-8 bg-gray-100 hover:bg-gray-200 rounded-lg flex items-center justify-center transition-colors">
|
|
<i class="fas fa-times text-gray-600"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 내용 -->
|
|
<div class="flex-1 overflow-y-auto p-6">
|
|
<!-- 선택된 텍스트 표시 -->
|
|
<div class="bg-purple-50 rounded-lg p-4 mb-6">
|
|
<h4 class="font-semibold text-purple-800 mb-2">선택된 텍스트</h4>
|
|
<p class="text-purple-700" x-text="linkForm?.selected_text || ''"></p>
|
|
</div>
|
|
|
|
<!-- 링크 대상 타입 선택 -->
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-semibold text-gray-700 mb-3">링크 대상 타입</label>
|
|
<div class="flex space-x-4">
|
|
<label class="flex items-center">
|
|
<input type="radio"
|
|
x-model="linkForm.target_type"
|
|
value="document"
|
|
@change="onTargetTypeChange()"
|
|
class="mr-2 text-blue-600">
|
|
<span class="text-sm text-gray-700">📄 서적 문서</span>
|
|
</label>
|
|
<label class="flex items-center">
|
|
<input type="radio"
|
|
x-model="linkForm.target_type"
|
|
value="note"
|
|
@change="onTargetTypeChange()"
|
|
class="mr-2 text-blue-600">
|
|
<span class="text-sm text-gray-700">📝 노트북 노트</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 서적/노트북 선택 -->
|
|
<div class="mb-6" x-show="linkForm.target_type">
|
|
<label class="block text-sm font-semibold text-gray-700 mb-3"
|
|
x-text="linkForm.target_type === 'note' ? '노트북 선택' : '서적 선택'"></label>
|
|
<select
|
|
x-model="linkForm.target_book_id"
|
|
@change="loadDocumentsFromBook()"
|
|
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
|
|
>
|
|
<option value="" x-text="linkForm.target_type === 'note' ? '노트북을 선택하세요' : '서적을 선택하세요'"></option>
|
|
<template x-for="book in availableBooks" :key="book.id">
|
|
<option :value="book.id" x-text="book.title"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 대상 문서 선택 -->
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
|
대상 문서
|
|
<span x-show="linkForm.target_book_id" class="text-blue-600 text-xs" x-text="`(${getSelectedBookTitle()})`"></span>
|
|
</label>
|
|
<select x-model="linkForm.target_document_id"
|
|
@change="onTargetDocumentChange()"
|
|
:disabled="!linkForm.target_book_id"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 disabled:bg-gray-100 disabled:cursor-not-allowed">
|
|
<option value="">
|
|
<span x-show="!linkForm.target_book_id"
|
|
x-text="linkForm.target_type === 'note' ? '먼저 노트북을 선택하세요' : '먼저 서적을 선택하세요'"></span>
|
|
<span x-show="linkForm.target_book_id"
|
|
x-text="linkForm.target_type === 'note' ? '노트를 선택하세요' : '문서를 선택하세요'"></span>
|
|
</option>
|
|
<template x-for="doc in filteredDocuments" :key="doc.id">
|
|
<option :value="doc.id" x-text="doc.title"></option>
|
|
</template>
|
|
</select>
|
|
<p x-show="filteredDocuments.length > 0" class="text-xs text-gray-500 mt-1" x-text="`${filteredDocuments.length}개 문서`"></p>
|
|
</div>
|
|
|
|
<!-- 대상 텍스트 선택 (무조건 텍스트 선택만 지원) -->
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2">대상 텍스트</label>
|
|
<div class="space-y-3">
|
|
<button @click="selectTextFromDocument()"
|
|
:disabled="!linkForm.target_document_id"
|
|
:class="linkForm.target_document_id ? 'bg-blue-500 hover:bg-blue-600 text-white' : 'bg-gray-300 text-gray-500 cursor-not-allowed'"
|
|
class="w-full px-4 py-2 rounded-lg transition-colors flex items-center justify-center space-x-2">
|
|
<i class="fas fa-crosshairs"></i>
|
|
<span>대상 문서에서 텍스트 선택</span>
|
|
</button>
|
|
<div x-show="linkForm.target_text" class="bg-blue-50 rounded-lg p-3">
|
|
<p class="text-sm text-blue-800 font-medium">선택된 대상 텍스트:</p>
|
|
<p class="text-blue-700 mt-1" x-text="linkForm.target_text"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 링크 설명 -->
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2">설명 (선택사항)</label>
|
|
<textarea x-model="linkForm.description"
|
|
placeholder="링크에 대한 설명을 입력하세요..."
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 resize-none"
|
|
rows="3"></textarea>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- 버튼 영역 (고정) -->
|
|
<div class="border-t border-gray-200 p-6">
|
|
<div class="flex justify-end space-x-3">
|
|
<button @click="closeLinkModal()"
|
|
class="px-4 py-2 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors">
|
|
취소
|
|
</button>
|
|
<button @click="createDocumentLink()"
|
|
:disabled="!linkForm?.target_document_id"
|
|
:class="linkForm?.target_document_id ? 'bg-purple-500 hover:bg-purple-600' : 'bg-gray-300 cursor-not-allowed'"
|
|
class="px-4 py-2 text-white rounded-lg transition-colors">
|
|
링크 생성
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 메모 모달 -->
|
|
<div x-show="showNotesModal"
|
|
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="showNotesModal = false">
|
|
|
|
<div @click.stop class="bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[80vh] overflow-hidden">
|
|
<div class="flex justify-between items-center p-6 border-b">
|
|
<h3 class="text-xl font-bold text-gray-800 flex items-center space-x-2">
|
|
<i class="fas fa-sticky-note text-blue-600"></i>
|
|
<span>메모</span>
|
|
</h3>
|
|
<button @click="showNotesModal = false"
|
|
class="w-8 h-8 bg-gray-100 hover:bg-gray-200 rounded-full flex items-center justify-center transition-colors">
|
|
<i class="fas fa-times text-gray-600"></i>
|
|
</button>
|
|
</div>
|
|
<div class="p-6 overflow-y-auto max-h-96">
|
|
<!-- 메모가 없을 때 -->
|
|
<template x-if="notes.length === 0">
|
|
<div class="text-center text-gray-500">
|
|
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<i class="fas fa-sticky-note text-2xl text-blue-500"></i>
|
|
</div>
|
|
<h4 class="font-semibold text-gray-700 mb-2">메모가 없습니다</h4>
|
|
<p class="text-sm text-gray-500">텍스트를 선택하고 메모를 추가해보세요</p>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 메모 목록 -->
|
|
<div class="space-y-3">
|
|
<template x-for="note in notes" :key="note.id">
|
|
<div class="bg-gray-50 rounded-lg p-4 hover:bg-gray-100 transition-colors cursor-pointer"
|
|
@click="scrollToHighlight(note.highlight.id)">
|
|
<!-- 선택된 텍스트 -->
|
|
<div class="bg-blue-50 rounded-md p-2 mb-3" x-show="note.highlight?.selected_text">
|
|
<p class="text-sm text-blue-800 font-medium" x-text="note.highlight?.selected_text || ''"></p>
|
|
</div>
|
|
|
|
<!-- 메모 내용 -->
|
|
<p class="text-gray-800 mb-2 leading-relaxed" x-text="note.content"></p>
|
|
|
|
<!-- 날짜 -->
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-xs text-gray-500" x-text="formatDate(note.created_at)"></span>
|
|
<div class="flex space-x-1">
|
|
<button @click.stop="editNote(note)"
|
|
class="w-6 h-6 bg-blue-100 text-blue-600 rounded-md hover:bg-blue-200 flex items-center justify-center transition-colors">
|
|
<i class="fas fa-edit text-xs"></i>
|
|
</button>
|
|
<button @click.stop="deleteNote(note.id)"
|
|
class="w-6 h-6 bg-red-100 text-red-600 rounded-md hover:bg-red-200 flex items-center justify-center transition-colors">
|
|
<i class="fas fa-trash text-xs"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 메모 입력 모달 (하이라이트 생성 후) -->
|
|
<div x-show="showNoteInputModal"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"
|
|
@click="showNoteInputModal = false">
|
|
|
|
<div @click.stop class="bg-white rounded-2xl shadow-2xl max-w-md w-full">
|
|
<div class="flex justify-between items-center p-6 border-b">
|
|
<h3 class="text-xl font-bold text-gray-800 flex items-center space-x-2">
|
|
<i class="fas fa-sticky-note text-blue-600"></i>
|
|
<span>메모 추가</span>
|
|
</h3>
|
|
<button @click="showNoteInputModal = false"
|
|
class="w-8 h-8 bg-gray-100 hover:bg-gray-200 rounded-full flex items-center justify-center transition-colors">
|
|
<i class="fas fa-times text-gray-600"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-6">
|
|
<!-- 선택된 텍스트 표시 -->
|
|
<div class="bg-blue-50 rounded-lg p-3 mb-4">
|
|
<p class="text-sm text-gray-600 mb-1">선택된 텍스트:</p>
|
|
<p class="text-blue-800 font-medium" x-text="selectedText"></p>
|
|
</div>
|
|
|
|
<!-- 메모 입력 질문 -->
|
|
<p class="text-gray-700 mb-4">이 하이라이트에 메모를 추가하시겠습니까?</p>
|
|
|
|
<!-- 메모 입력 칸 -->
|
|
<textarea x-model="noteForm.content"
|
|
placeholder="메모 내용을 입력하세요..."
|
|
class="w-full h-32 p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
|
|
@keydown.ctrl.enter="createNoteForHighlight()"
|
|
@keydown.meta.enter="createNoteForHighlight()"></textarea>
|
|
|
|
<!-- 태그 입력 (선택사항) -->
|
|
<input x-model="noteForm.tags"
|
|
type="text"
|
|
placeholder="태그 (선택사항, 쉼표로 구분)"
|
|
class="w-full mt-3 p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
|
|
<!-- 버튼들 -->
|
|
<div class="flex justify-end space-x-3 mt-6">
|
|
<button @click="skipNoteForHighlight()"
|
|
class="px-4 py-2 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors">
|
|
나중에 입력
|
|
</button>
|
|
<button @click="createNoteForHighlight()"
|
|
:disabled="!noteForm.content.trim()"
|
|
:class="noteForm.content.trim() ? 'bg-blue-600 hover:bg-blue-700' : 'bg-gray-300 cursor-not-allowed'"
|
|
class="px-6 py-2 text-white rounded-lg transition-colors">
|
|
확인
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 책갈피 모달 -->
|
|
<div x-show="showBookmarksModal"
|
|
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="showBookmarksModal = false">
|
|
|
|
<div @click.stop class="bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[80vh] overflow-hidden">
|
|
<div class="flex justify-between items-center p-6 border-b">
|
|
<h3 class="text-xl font-bold text-gray-800 flex items-center space-x-2">
|
|
<i class="fas fa-bookmark text-amber-600"></i>
|
|
<span>책갈피</span>
|
|
</h3>
|
|
<button @click="showBookmarksModal = false"
|
|
class="w-8 h-8 bg-gray-100 hover:bg-gray-200 rounded-full flex items-center justify-center transition-colors">
|
|
<i class="fas fa-times text-gray-600"></i>
|
|
</button>
|
|
</div>
|
|
<div class="p-6 overflow-y-auto max-h-96">
|
|
<!-- 책갈피가 없을 때 -->
|
|
<template x-if="bookmarks.length === 0">
|
|
<div class="text-center text-gray-500">
|
|
<div class="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<i class="fas fa-bookmark text-2xl text-amber-500"></i>
|
|
</div>
|
|
<h4 class="font-semibold text-gray-700 mb-2">책갈피가 없습니다</h4>
|
|
<p class="text-sm text-gray-500">텍스트를 선택하고 책갈피를 추가해보세요</p>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 책갈피 목록 -->
|
|
<div class="space-y-3">
|
|
<template x-for="bookmark in bookmarks" :key="bookmark.id">
|
|
<div class="bg-gray-50 rounded-lg p-4 hover:bg-gray-100 transition-colors cursor-pointer"
|
|
@click="scrollToHighlight(bookmark.highlight.id)">
|
|
<!-- 선택된 텍스트 -->
|
|
<div class="bg-amber-50 rounded-md p-2 mb-3">
|
|
<p class="text-sm text-amber-800 font-medium" x-text="bookmark.highlight.selected_text"></p>
|
|
</div>
|
|
|
|
<!-- 책갈피 제목 -->
|
|
<p class="text-gray-800 mb-2 leading-relaxed font-semibold" x-text="bookmark.title || '제목 없음'"></p>
|
|
|
|
<!-- 날짜 -->
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-xs text-gray-500" x-text="formatDate(bookmark.created_at)"></span>
|
|
<div class="flex space-x-1">
|
|
<button @click.stop="editBookmark(bookmark)"
|
|
class="w-6 h-6 bg-amber-100 text-amber-600 rounded-md hover:bg-amber-200 flex items-center justify-center transition-colors">
|
|
<i class="fas fa-edit text-xs"></i>
|
|
</button>
|
|
<button @click.stop="deleteBookmark(bookmark.id)"
|
|
class="w-6 h-6 bg-red-100 text-red-600 rounded-md hover:bg-red-200 flex items-center justify-center transition-colors">
|
|
<i class="fas fa-trash text-xs"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 백링크 모달 -->
|
|
<div x-show="showBacklinksModal"
|
|
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="showBacklinksModal = false">
|
|
|
|
<div @click.stop class="bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[80vh] overflow-hidden">
|
|
<div class="flex justify-between items-center p-6 border-b">
|
|
<h3 class="text-xl font-bold text-gray-800 flex items-center space-x-2">
|
|
<i class="fas fa-arrow-left text-orange-600"></i>
|
|
<span>백링크</span>
|
|
</h3>
|
|
<button @click="showBacklinksModal = false"
|
|
class="w-8 h-8 bg-gray-100 hover:bg-gray-200 rounded-full flex items-center justify-center transition-colors">
|
|
<i class="fas fa-times text-gray-600"></i>
|
|
</button>
|
|
</div>
|
|
<div class="p-6 overflow-y-auto max-h-96">
|
|
<!-- 백링크가 없을 때 -->
|
|
<template x-if="backlinks.length === 0">
|
|
<div class="text-center text-gray-500">
|
|
<div class="w-16 h-16 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<i class="fas fa-arrow-left text-2xl text-orange-500"></i>
|
|
</div>
|
|
<h4 class="font-semibold text-gray-700 mb-2">백링크가 없습니다</h4>
|
|
<p class="text-sm text-gray-500">다른 문서에서 이 문서를 참조하는 링크가 없습니다</p>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 백링크 목록 -->
|
|
<div class="space-y-3">
|
|
<template x-for="backlink in backlinks" :key="backlink.id">
|
|
<div class="bg-gray-50 rounded-lg p-4 hover:bg-gray-100 transition-colors cursor-pointer"
|
|
@click="navigateToBacklink(backlink)">
|
|
<!-- 참조하는 문서 정보 -->
|
|
<div class="bg-orange-50 rounded-md p-2 mb-3">
|
|
<p class="text-sm text-orange-800 font-medium" x-text="backlink.source_document_title"></p>
|
|
</div>
|
|
|
|
<!-- 선택된 텍스트 또는 문서 전체 링크 -->
|
|
<div x-show="backlink.selected_text" class="mb-2">
|
|
<p class="text-gray-800 leading-relaxed" x-text="backlink.selected_text"></p>
|
|
</div>
|
|
<div x-show="!backlink.selected_text" class="mb-2">
|
|
<p class="text-gray-600 italic">📄 문서 전체 링크</p>
|
|
</div>
|
|
|
|
<!-- 설명 -->
|
|
<p class="text-sm text-gray-600 mb-2" x-text="backlink.description || '설명 없음'"></p>
|
|
|
|
<!-- 날짜 -->
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-xs text-gray-500" x-text="formatDate(backlink.created_at)"></span>
|
|
<button class="text-xs text-orange-600 hover:text-orange-800 font-medium">
|
|
문서로 이동 →
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 스크립트 -->
|
|
<script src="/static/js/api.js?v=2025012618"></script>
|
|
|
|
<!-- 캐시 및 성능 최적화 시스템 -->
|
|
<script src="/static/js/viewer/utils/cache-manager.js?v=2025012607"></script>
|
|
<script src="/static/js/viewer/utils/cached-api.js?v=2025012618"></script>
|
|
<script src="/static/js/viewer/utils/module-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=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>
|
|
|
|
<style>
|
|
[x-cloak] { display: none !important; }
|
|
|
|
/* 링크된 텍스트 하이라이트 (URL에서 온 것) - 레이아웃 안전 */
|
|
.linked-text-highlight {
|
|
background-color: #FEF3C7 !important;
|
|
border: 1px solid #F59E0B !important;
|
|
border-radius: 2px !important;
|
|
padding: 0 1px !important;
|
|
display: inline !important;
|
|
box-decoration-break: clone !important;
|
|
-webkit-box-decoration-break: clone !important;
|
|
line-height: inherit !important;
|
|
vertical-align: baseline !important;
|
|
margin: 0 !important;
|
|
box-sizing: border-box !important;
|
|
}
|
|
|
|
/* 백링크 강제 스타일 - 레이아웃 안전 */
|
|
.backlink-highlight {
|
|
color: #EA580C !important;
|
|
background-color: rgba(234, 88, 12, 0.2) !important;
|
|
border: 1px solid #EA580C !important;
|
|
border-radius: 3px !important;
|
|
padding: 0 2px !important;
|
|
font-weight: bold !important;
|
|
display: inline !important;
|
|
box-decoration-break: clone !important;
|
|
-webkit-box-decoration-break: clone !important;
|
|
line-height: inherit !important;
|
|
vertical-align: baseline !important;
|
|
margin: 0 !important;
|
|
box-sizing: border-box !important;
|
|
text-decoration: underline !important;
|
|
cursor: pointer !important;
|
|
}
|
|
|
|
/* 링크 스타일 */
|
|
.document-link {
|
|
color: #7C3AED !important;
|
|
background-color: rgba(124, 58, 237, 0.1) !important;
|
|
border-radius: 2px !important;
|
|
padding: 0 1px !important;
|
|
display: inline !important;
|
|
line-height: inherit !important;
|
|
vertical-align: baseline !important;
|
|
margin: 0 !important;
|
|
box-sizing: border-box !important;
|
|
text-decoration: underline !important;
|
|
cursor: pointer !important;
|
|
}
|
|
|
|
/* 겹치는 영역 처리 - 백링크 안에 링크가 있는 경우 */
|
|
.backlink-highlight .document-link {
|
|
background: linear-gradient(to bottom,
|
|
rgba(234, 88, 12, 0.3) 0%,
|
|
rgba(234, 88, 12, 0.3) 50%,
|
|
rgba(124, 58, 237, 0.2) 50%,
|
|
rgba(124, 58, 237, 0.2) 100%) !important;
|
|
border-top: 1px solid #EA580C !important;
|
|
border-bottom: 1px solid #7C3AED !important;
|
|
}
|
|
|
|
/* 겹치는 영역 처리 - 링크 안에 백링크가 있는 경우 */
|
|
.document-link .backlink-highlight {
|
|
background: linear-gradient(to bottom,
|
|
rgba(124, 58, 237, 0.2) 0%,
|
|
rgba(124, 58, 237, 0.2) 50%,
|
|
rgba(234, 88, 12, 0.3) 50%,
|
|
rgba(234, 88, 12, 0.3) 100%) !important;
|
|
border-top: 1px solid #7C3AED !important;
|
|
border-bottom: 1px solid #EA580C !important;
|
|
}
|
|
|
|
/* 하이라이트 스타일 개선 */
|
|
.highlight, .highlight-span {
|
|
padding: 1px 2px;
|
|
border-radius: 2px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.highlight:hover, .highlight-span:hover {
|
|
box-shadow: 0 0 4px rgba(0,0,0,0.3);
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.multi-highlight {
|
|
padding: 1px 2px;
|
|
border-radius: 2px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.multi-highlight:hover {
|
|
box-shadow: 0 0 6px rgba(0,0,0,0.4);
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.multi-highlight::after {
|
|
content: "🎨";
|
|
position: absolute;
|
|
top: -8px;
|
|
right: -8px;
|
|
font-size: 10px;
|
|
background: white;
|
|
border-radius: 50%;
|
|
width: 16px;
|
|
height: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
/* 기존 언어 전환 버튼 숨기기 */
|
|
.language-toggle-old,
|
|
button[onclick*="toggleLanguage"],
|
|
button[onclick*="language"],
|
|
.lang-toggle,
|
|
#old-language-toggle {
|
|
display: none !important;
|
|
}
|
|
|
|
/* 문서 내 기존 언어 버튼들 숨기기 */
|
|
#document-content button[onclick*="toggleLanguage"],
|
|
#document-content button[onclick*="language"],
|
|
#document-content .language-toggle,
|
|
#document-content .lang-btn {
|
|
display: none !important;
|
|
}
|
|
</style>
|
|
</body>
|
|
</html>
|