- 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
824 lines
52 KiB
HTML
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>
|