Files
document-server/frontend/pdf-manager.html
hyungi cfb9485d4f 🚀 배포용: PDF 뷰어 개선 및 서적별 UI 데본씽크 스타일 적용
 주요 개선사항:
- PDF API 500 에러 수정 (한글 파일명 UTF-8 인코딩 처리)
- PDF 뷰어 기능 완전 구현 (PDF.js 통합, 네비게이션, 확대/축소)
- 서적별 문서 그룹화 UI 데본씽크 스타일로 개선
- PDF Manager 페이지 서적별 보기 기능 추가
- Alpine.js 로드 순서 최적화로 JavaScript 에러 해결

🎨 UI/UX 개선:
- 확장/축소 가능한 아코디언 스타일 서적 목록
- 간결하고 직관적인 데본씽크 스타일 인터페이스
- PDF 상태 표시 (HTML 연결, 서적 분류)
- 반응형 디자인 및 부드러운 애니메이션

🔧 기술적 개선:
- PDF.js 워커 설정 및 토큰 인증 처리
- 서적별 PDF 자동 그룹화 로직
- Alpine.js 컴포넌트 초기화 최적화
2025-09-05 07:13:49 +09:00

445 lines
26 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PDF 파일 관리 - Document Server</title>
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://unpkg.com/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen" x-data="pdfManagerApp()" x-init="init()">
<!-- 헤더 -->
<div id="header-container"></div>
<!-- 메인 컨텐츠 -->
<main class="container mx-auto px-4 pt-20 pb-8">
<!-- 페이지 헤더 -->
<div class="mb-8">
<div class="flex items-center justify-between mb-4">
<div>
<h1 class="text-3xl font-bold text-gray-900">PDF 파일 관리</h1>
<p class="text-gray-600 mt-2">업로드된 PDF 파일들을 관리하고 삭제할 수 있습니다</p>
</div>
<button @click="refreshPDFs()"
:disabled="loading"
class="flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg transition-colors">
<i class="fas fa-sync-alt mr-2" :class="{'fa-spin': loading}"></i>
<span>새로고침</span>
</button>
</div>
</div>
<!-- 통계 카드 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-lg shadow-sm border p-6">
<div class="flex items-center">
<div class="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center mr-4">
<i class="fas fa-file-pdf text-red-600 text-xl"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900">전체 PDF</h3>
<p class="text-2xl font-bold text-red-600" x-text="pdfDocuments.length"></p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm border p-6">
<div class="flex items-center">
<div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mr-4">
<i class="fas fa-book text-blue-600 text-xl"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900">서적 포함</h3>
<p class="text-2xl font-bold text-blue-600" x-text="bookPDFs"></p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm border p-6">
<div class="flex items-center">
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mr-4">
<i class="fas fa-link text-green-600 text-xl"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900">HTML 연결</h3>
<p class="text-2xl font-bold text-green-600" x-text="linkedPDFs"></p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm border p-6">
<div class="flex items-center">
<div class="w-12 h-12 bg-yellow-100 rounded-lg flex items-center justify-center mr-4">
<i class="fas fa-unlink text-yellow-600 text-xl"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900">독립 파일</h3>
<p class="text-2xl font-bold text-yellow-600" x-text="standalonePDFs"></p>
</div>
</div>
</div>
</div>
<!-- 뷰 모드 및 필터 -->
<div class="mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900">PDF 파일 관리</h2>
<!-- 뷰 모드 선택 -->
<div class="flex items-center space-x-2">
<button @click="viewMode = 'list'"
:class="viewMode === 'list' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'"
class="px-3 py-2 rounded-md text-sm">
<i class="fas fa-list mr-2"></i>전체 목록
</button>
<button @click="viewMode = 'books'"
:class="viewMode === 'books' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'"
class="px-3 py-2 rounded-md text-sm">
<i class="fas fa-book mr-2"></i>서적별 보기
</button>
</div>
</div>
<!-- 필터 버튼 (목록 뷰에서만 표시) -->
<div x-show="viewMode === 'list'" class="flex flex-wrap gap-2">
<button @click="filterType = 'all'"
:class="filterType === 'all' ? 'bg-gray-600 text-white' : 'bg-gray-200 text-gray-700'"
class="px-3 py-1.5 rounded-lg text-sm transition-colors">
전체
</button>
<button @click="filterType = 'book'"
:class="filterType === 'book' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'"
class="px-3 py-1.5 rounded-lg text-sm transition-colors">
서적 포함
</button>
<button @click="filterType = 'linked'"
:class="filterType === 'linked' ? 'bg-green-600 text-white' : 'bg-gray-200 text-gray-700'"
class="px-3 py-1.5 rounded-lg text-sm transition-colors">
HTML 연결
</button>
<button @click="filterType = 'standalone'"
:class="filterType === 'standalone' ? 'bg-yellow-600 text-white' : 'bg-gray-200 text-gray-700'"
class="px-3 py-1.5 rounded-lg text-sm transition-colors">
독립 파일
</button>
</div>
</div>
<!-- 전체 목록 뷰 -->
<div x-show="viewMode === 'list'" class="bg-white rounded-lg border border-gray-200">
<!-- 로딩 상태 -->
<div x-show="loading" class="p-8 text-center">
<i class="fas fa-spinner fa-spin text-2xl text-gray-400 mb-2"></i>
<p class="text-gray-500">PDF 파일을 불러오는 중...</p>
</div>
<!-- PDF 목록 -->
<div x-show="!loading && filteredPDFs.length > 0" class="divide-y divide-gray-100">
<template x-for="pdf in filteredPDFs" :key="pdf.id">
<div class="p-6 hover:bg-gray-50 transition-colors">
<div class="flex items-start justify-between">
<div class="flex items-start space-x-4 flex-1">
<!-- PDF 아이콘 -->
<div class="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-file-pdf text-red-600 text-xl"></i>
</div>
<!-- PDF 정보 -->
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-gray-900 mb-1" x-text="pdf.title"></h3>
<p class="text-sm text-gray-500 mb-2" x-text="pdf.original_filename"></p>
<p class="text-sm text-gray-600 line-clamp-2" x-text="pdf.description || '설명이 없습니다'"></p>
<!-- 서적 정보 및 연결 상태 -->
<div class="mt-3 space-y-2">
<!-- 서적 정보 -->
<div x-show="pdf.book_title" class="flex items-center space-x-2">
<span class="inline-flex items-center px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full">
<i class="fas fa-book mr-1"></i>
<span x-text="pdf.book_title"></span>
</span>
<span x-show="pdf.isLinked" class="inline-flex items-center px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
<i class="fas fa-link mr-1"></i>
HTML 연결됨
</span>
</div>
<!-- 서적 없는 경우 -->
<div x-show="!pdf.book_title" class="flex items-center space-x-2">
<span class="inline-flex items-center px-3 py-1 bg-gray-100 text-gray-600 text-sm rounded-full">
<i class="fas fa-file mr-1"></i>
서적 미분류
</span>
<span x-show="pdf.isLinked" class="inline-flex items-center px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
<i class="fas fa-link mr-1"></i>
HTML 연결됨
</span>
<span x-show="!pdf.isLinked" class="inline-flex items-center px-2 py-1 bg-yellow-100 text-yellow-800 text-xs rounded-full">
<i class="fas fa-unlink mr-1"></i>
독립 파일
</span>
</div>
<!-- 업로드 날짜 -->
<div class="flex items-center space-x-4">
<span class="text-sm text-gray-500">
<i class="fas fa-calendar mr-1"></i>
<span x-text="formatDate(pdf.created_at)"></span>
</span>
<span x-show="pdf.uploaded_by" class="text-sm text-gray-500">
<i class="fas fa-user mr-1"></i>
<span x-text="pdf.uploaded_by"></span>
</span>
</div>
</div>
</div>
</div>
<!-- 액션 버튼 -->
<div class="flex items-center space-x-2 ml-4">
<button @click="previewPDF(pdf)"
class="p-2 text-gray-400 hover:text-green-600 transition-colors"
title="PDF 미리보기">
<i class="fas fa-eye"></i>
</button>
<button @click="downloadPDF(pdf)"
class="p-2 text-gray-400 hover:text-blue-600 transition-colors"
title="PDF 다운로드">
<i class="fas fa-download"></i>
</button>
<button x-show="currentUser && currentUser.is_admin"
@click="deletePDF(pdf)"
class="p-2 text-gray-400 hover:text-red-600 transition-colors"
title="PDF 삭제">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</template>
</div>
<!-- 빈 상태 -->
<div x-show="!loading && filteredPDFs.length === 0" class="p-8 text-center">
<i class="fas fa-file-pdf text-gray-400 text-4xl mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 mb-2">PDF 파일이 없습니다</h3>
<p class="text-gray-500">
<span x-show="filterType === 'all'">업로드된 PDF 파일이 없습니다</span>
<span x-show="filterType === 'book'">서적에 포함된 PDF 파일이 없습니다</span>
<span x-show="filterType === 'linked'">HTML과 연결된 PDF 파일이 없습니다</span>
<span x-show="filterType === 'standalone'">독립 PDF 파일이 없습니다</span>
</p>
</div>
</div>
<!-- 서적별 뷰 (데본씽크 스타일) -->
<div x-show="viewMode === 'books'" class="space-y-4">
<!-- 로딩 상태 -->
<div x-show="loading" class="text-center py-12">
<i class="fas fa-spinner fa-spin text-2xl text-gray-400 mb-2"></i>
<p class="text-gray-500">PDF 파일을 불러오는 중...</p>
</div>
<!-- 서적별 그룹 -->
<template x-for="bookGroup in groupedPDFs" :key="bookGroup.book?.id || 'no-book'">
<div class="bg-white rounded-lg border border-gray-200 hover:border-gray-300 transition-all duration-200">
<!-- 서적 헤더 -->
<div class="p-4 border-b border-gray-100 cursor-pointer hover:bg-gray-50 transition-colors"
@click="bookGroup.expanded = !bookGroup.expanded">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<!-- 서적 아이콘 -->
<div class="w-10 h-10 bg-gradient-to-br from-red-500 to-red-600 rounded-md flex items-center justify-center">
<i class="fas fa-file-pdf text-white text-sm"></i>
</div>
<!-- 서적 정보 -->
<div>
<h3 class="font-semibold text-gray-900" x-text="bookGroup.book?.title || 'PDF 미분류'"></h3>
<div class="flex items-center space-x-2 text-sm text-gray-500">
<span x-show="bookGroup.book?.author" x-text="bookGroup.book.author"></span>
<span x-show="bookGroup.book?.author" class="text-gray-300"></span>
<span x-text="bookGroup.pdfs.length + '개 PDF'"></span>
<span class="text-gray-300"></span>
<span x-text="bookGroup.linkedCount + '개 연결됨'"></span>
</div>
</div>
</div>
<!-- 확장/축소 아이콘 -->
<div class="flex items-center space-x-2">
<span class="text-xs text-gray-500" x-text="bookGroup.pdfs.length + '개'"></span>
<i class="fas fa-chevron-down text-gray-400 transition-transform duration-200"
:class="{'rotate-180': bookGroup.expanded}"></i>
</div>
</div>
</div>
<!-- PDF 목록 (확장 시 표시) -->
<div x-show="bookGroup.expanded" x-collapse class="border-t border-gray-100">
<div class="divide-y divide-gray-50">
<template x-for="(pdf, index) in bookGroup.pdfs.slice(0, 20)" :key="pdf.id">
<div class="p-3 hover:bg-gray-50 cursor-pointer transition-colors flex items-center justify-between group"
@click="previewPDF(pdf)">
<div class="flex items-center space-x-3 flex-1">
<!-- PDF 아이콘 -->
<div class="w-8 h-8 bg-red-100 text-red-600 rounded-md flex items-center justify-center">
<i class="fas fa-file-pdf text-xs"></i>
</div>
<!-- PDF 정보 -->
<div class="flex-1 min-w-0">
<h4 class="text-sm font-medium text-gray-900 truncate" x-text="pdf.title"></h4>
<div class="flex items-center space-x-2 text-xs text-gray-500">
<span x-text="pdf.original_filename"></span>
<span x-show="pdf.isLinked" class="flex items-center text-green-600">
<i class="fas fa-link mr-1"></i>HTML
</span>
</div>
</div>
</div>
<!-- 액션 버튼들 -->
<div class="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button @click.stop="downloadPDF(pdf)"
class="p-2 text-gray-400 hover:text-blue-600 transition-colors rounded-md hover:bg-blue-50"
title="다운로드">
<i class="fas fa-download text-xs"></i>
</button>
<button @click.stop="deletePDF(pdf.id)"
class="p-2 text-gray-400 hover:text-red-600 transition-colors rounded-md hover:bg-red-50"
title="삭제">
<i class="fas fa-trash text-xs"></i>
</button>
<i class="fas fa-chevron-right text-gray-300 ml-2"></i>
</div>
</div>
</template>
<!-- 더 많은 PDF가 있을 때 -->
<div x-show="bookGroup.pdfs.length > 20"
class="p-3 text-center border-t border-gray-100">
<button @click="viewMode = 'list'; filterType = 'book'"
class="text-sm text-blue-600 hover:text-blue-800 font-medium">
<span x-text="`${bookGroup.pdfs.length - 20}개 PDF 더 보기`"></span>
<i class="fas fa-arrow-right ml-1"></i>
</button>
</div>
</div>
</div>
</div>
</template>
<!-- 서적이 없을 때 -->
<div x-show="!loading && groupedPDFs.length === 0" class="text-center py-12">
<i class="fas fa-file-pdf text-4xl text-gray-300 mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 mb-2">PDF 파일이 없습니다</h3>
<p class="text-gray-500 mb-4">PDF 파일을 업로드하고 서적으로 분류해보세요</p>
<button onclick="window.location.href='/upload.html'"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
파일 업로드하기
</button>
</div>
</div>
</main>
<!-- PDF 미리보기 모달 -->
<div x-show="showPreviewModal"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"
@click.self="closePreview()">
<div class="bg-white rounded-2xl shadow-2xl max-w-6xl w-full max-h-[90vh] overflow-hidden">
<!-- 헤더 -->
<div class="flex items-center justify-between p-6 border-b border-gray-200">
<div class="flex items-center space-x-3">
<i class="fas fa-file-pdf text-red-600 text-xl"></i>
<div>
<h3 class="text-xl font-bold text-gray-900" x-text="previewPdf?.title"></h3>
<p class="text-sm text-gray-500" x-text="previewPdf?.original_filename"></p>
</div>
</div>
<div class="flex items-center space-x-2">
<button @click="downloadPDF(previewPdf)"
class="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2">
<i class="fas fa-download"></i>
<span>다운로드</span>
</button>
<button @click="closePreview()"
class="text-gray-400 hover:text-gray-600 transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<!-- PDF 뷰어 -->
<div class="p-6 overflow-y-auto" style="max-height: calc(90vh - 120px);">
<!-- PDF 미리보기 -->
<div x-show="previewPdf" class="mb-4">
<!-- PDF 뷰어 컨테이너 -->
<div class="border rounded-lg overflow-hidden bg-gray-100 relative" style="height: 600px;">
<!-- PDF iframe 뷰어 -->
<iframe x-show="!pdfPreviewError && !pdfPreviewLoading && pdfPreviewSrc"
class="w-full h-full border-0"
:src="pdfPreviewSrc"
@load="pdfPreviewLoaded = true"
@error="handlePdfPreviewError()">
</iframe>
<!-- PDF 로딩 상태 -->
<div x-show="pdfPreviewLoading" class="flex items-center justify-center h-full">
<div class="text-center">
<i class="fas fa-spinner fa-spin text-3xl text-gray-500 mb-4"></i>
<p class="text-gray-600 text-lg">PDF를 로드하는 중...</p>
</div>
</div>
<!-- PDF 에러 상태 -->
<div x-show="pdfPreviewError" class="flex items-center justify-center h-full text-gray-500">
<div class="text-center">
<i class="fas fa-exclamation-triangle text-3xl mb-4 text-red-500"></i>
<p class="text-lg mb-4">PDF를 로드할 수 없습니다</p>
<button @click="retryPdfPreview()"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 mr-2">
다시 시도
</button>
<button @click="downloadPDF(previewPdf)"
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700">
파일 다운로드
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- JavaScript 파일들 -->
<script src="/static/js/api.js?v=2025012384"></script>
<script src="/static/js/auth.js?v=2025012351"></script>
<script src="/static/js/header-loader.js?v=2025012351"></script>
<script src="/static/js/pdf-manager.js?v=2025012627"></script>
<!-- Alpine.js (JavaScript 파일들 다음에 로드) -->
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
</body>
</html>