Files
document-server/frontend/viewer.html
Hyungi Ahn 3e0a03f149 🐛 Fix Alpine.js SyntaxError and backlink visibility issues
- Fix SyntaxError in viewer.js line 2868 (.bind(this) issue in setTimeout)
- Resolve Alpine.js 'Can't find variable' errors (documentViewer, goBack, etc.)
- Fix backlink rendering and persistence during temporary highlights
- Add backlink protection and restoration mechanism in highlightAndScrollToText
- Implement Note Management System with hierarchical notebooks
- Add note highlights and memos functionality
- Update cache version to force browser refresh (v=2025012641)
- Add comprehensive logging for debugging backlink issues
2025-08-26 23:50:48 +09:00

824 lines
52 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">
<div x-data="documentViewer" x-init="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('#FFB6C1')"
:class="selectedHighlightColor === '#FFB6C1' ? 'ring-2 ring-pink-400 scale-110' : 'hover:scale-110'"
class="w-6 h-6 bg-gradient-to-br from-pink-300 to-pink-400 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">
<!-- 링크 버튼 그룹 -->
<div class="relative">
<button @click="toggleFeatureMenu('link')"
:class="activeFeatureMenu === '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">
<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>
<i class="fas fa-chevron-down text-xs ml-1" :class="activeFeatureMenu === 'link' ? 'rotate-180' : ''"></i>
</button>
<!-- 링크 서브메뉴 -->
<div x-show="activeFeatureMenu === 'link'"
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="showLinksModal = true; activeFeatureMenu = null"
class="w-full px-3 py-2 text-sm text-gray-700 hover:bg-purple-100 rounded-lg flex items-center space-x-2 transition-colors">
<i class="fas fa-eye text-purple-500"></i>
<span>링크 보기</span>
</button>
<button @click="console.log('링크 만들기 클릭됨'); activateLinkMode(); activeFeatureMenu = null"
class="w-full px-3 py-2 text-sm text-gray-700 hover:bg-purple-100 rounded-lg flex items-center space-x-2 transition-colors">
<i class="fas fa-plus text-purple-500"></i>
<span>링크 만들기</span>
</button>
</div>
</div>
<!-- 백링크 버튼 그룹 -->
<div class="relative">
<button @click="toggleFeatureMenu('backlink')"
:class="activeFeatureMenu === 'backlink' ? 'bg-orange-600' : 'bg-orange-500/80 hover:bg-orange-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-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>
<i class="fas fa-chevron-down text-xs ml-1" :class="activeFeatureMenu === 'backlink' ? 'rotate-180' : ''"></i>
</button>
<!-- 백링크 서브메뉴 -->
<div x-show="activeFeatureMenu === 'backlink'"
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="showBacklinksModal = true; loadBacklinks(); activeFeatureMenu = null"
class="w-full px-3 py-2 text-sm text-gray-700 hover:bg-orange-100 rounded-lg flex items-center space-x-2 transition-colors">
<i class="fas fa-eye text-orange-500"></i>
<span>백링크 보기</span>
</button>
</div>
</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>
</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="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="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" id="document-content" class="prose max-w-none">
<!-- 문서 HTML이 여기에 로드됩니다 -->
</div>
</div>
</main>
</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-center justify-between">
<div class="flex-1">
<div class="font-medium text-purple-700 mb-1" x-text="link.target_document_title"></div>
<!-- 선택된 텍스트 또는 문서 전체 링크 -->
<div x-show="link.selected_text" class="mb-2">
<div class="text-sm text-gray-600 bg-gray-100 px-3 py-2 rounded border-l-4 border-purple-500" x-text="link.selected_text"></div>
</div>
<div x-show="!link.selected_text" class="mb-2">
<div class="text-sm text-gray-600 italic">📄 문서 전체 링크</div>
</div>
<!-- 설명 -->
<div x-show="link.description" class="text-sm text-gray-600 mb-2" x-text="link.description"></div>
<!-- 링크 타입과 날짜 -->
<div class="flex items-center justify-between">
<span class="text-xs text-gray-500"
x-text="link.link_type === 'text_fragment' ? '텍스트 조각 링크' : '문서 링크'"></span>
<span class="text-xs text-gray-500" x-text="formatDate(link.created_at)"></span>
</div>
</div>
<div class="ml-3">
<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="grid grid-cols-2 gap-3">
<button @click="linkForm.book_scope = 'same'; resetTargetSelection()"
:class="linkForm.book_scope === 'same' ? 'bg-green-100 border-green-500 text-green-700' : 'bg-gray-50 border-gray-300 text-gray-600'"
class="p-4 border-2 rounded-lg transition-all duration-200 text-left">
<div class="flex items-center space-x-2 mb-2">
<i class="fas fa-book"></i>
<span class="font-semibold">같은 서적</span>
</div>
<p class="text-xs">현재 서적 내 문서</p>
</button>
<button @click="linkForm.book_scope = 'other'; resetTargetSelection()"
:class="linkForm.book_scope === 'other' ? 'bg-blue-100 border-blue-500 text-blue-700' : 'bg-gray-50 border-gray-300 text-gray-600'"
class="p-4 border-2 rounded-lg transition-all duration-200 text-left">
<div class="flex items-center space-x-2 mb-2">
<i class="fas fa-books"></i>
<span class="font-semibold">다른 서적</span>
</div>
<p class="text-xs">다른 서적의 문서</p>
</button>
</div>
</div>
<!-- 대상 서적 선택 (다른 서적인 경우만) -->
<div x-show="linkForm.book_scope === 'other'" class="mb-6">
<label class="block text-sm font-semibold text-gray-700 mb-2">대상 서적</label>
<select x-model="linkForm.target_book_id"
@change="loadDocumentsFromBook()"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">서적을 선택하세요</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.book_scope === 'same'" class="text-green-600 text-xs">(현재 서적)</span>
<span x-show="linkForm.book_scope === 'other' && 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.book_scope === 'other' && !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.book_scope === 'same'">문서를 선택하세요</span>
<span x-show="linkForm.book_scope === 'other' && !linkForm.target_book_id">먼저 서적을 선택하세요</span>
<span x-show="linkForm.book_scope === 'other' && linkForm.target_book_id">문서를 선택하세요</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-3">링크 타입</label>
<div class="grid grid-cols-2 gap-3">
<button @click="linkForm.link_type = 'document'"
:class="linkForm.link_type === 'document' ? 'bg-purple-100 border-purple-500 text-purple-700' : 'bg-gray-50 border-gray-300 text-gray-600'"
class="p-4 border-2 rounded-lg transition-all duration-200 text-left">
<div class="flex items-center space-x-2 mb-2">
<i class="fas fa-file-alt"></i>
<span class="font-semibold">문서 전체</span>
</div>
<p class="text-xs">문서 전체로 이동</p>
</button>
<button @click="linkForm.link_type = 'text_fragment'"
:class="linkForm.link_type === 'text_fragment' ? 'bg-purple-100 border-purple-500 text-purple-700' : 'bg-gray-50 border-gray-300 text-gray-600'"
class="p-4 border-2 rounded-lg transition-all duration-200 text-left">
<div class="flex items-center space-x-2 mb-2">
<i class="fas fa-crosshairs"></i>
<span class="font-semibold">특정 텍스트</span>
</div>
<p class="text-xs">문서 내 특정 부분으로 이동</p>
</button>
</div>
</div>
<!-- 특정 텍스트 선택 (text_fragment 타입일 때만) -->
<div x-show="linkForm.link_type === 'text_fragment'" class="mb-6">
<label class="block text-sm font-semibold text-gray-700 mb-2">대상 텍스트</label>
<div class="space-y-3">
<button @click="openTargetDocumentSelector()"
: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="saveDocumentLink()"
: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">
<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="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=2025012614"></script>
<script src="/static/js/viewer.js?v=2025012641"></script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
[x-cloak] { display: none !important; }
/* 백링크 강제 스타일 */
.backlink-highlight {
color: #EA580C !important;
background-color: rgba(234, 88, 12, 0.3) !important;
border: 3px solid #EA580C !important;
border-radius: 6px !important;
padding: 6px 8px !important;
font-weight: bold !important;
text-decoration: underline !important;
cursor: pointer !important;
box-shadow: 0 4px 8px rgba(234, 88, 12, 0.4) !important;
display: inline-block !important;
margin: 2px !important;
}
/* 하이라이트 스타일 개선 */
.highlight {
padding: 1px 2px;
border-radius: 2px;
cursor: pointer;
transition: all 0.2s ease;
}
.highlight: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>