✨ 주요 개선사항: - PDF API 500 에러 수정 (한글 파일명 UTF-8 인코딩 처리) - PDF 뷰어 기능 완전 구현 (PDF.js 통합, 네비게이션, 확대/축소) - 서적별 문서 그룹화 UI 데본씽크 스타일로 개선 - PDF Manager 페이지 서적별 보기 기능 추가 - Alpine.js 로드 순서 최적화로 JavaScript 에러 해결 🎨 UI/UX 개선: - 확장/축소 가능한 아코디언 스타일 서적 목록 - 간결하고 직관적인 데본씽크 스타일 인터페이스 - PDF 상태 표시 (HTML 연결, 서적 분류) - 반응형 디자인 및 부드러운 애니메이션 🔧 기술적 개선: - PDF.js 워커 설정 및 토큰 인증 처리 - 서적별 PDF 자동 그룹화 로직 - Alpine.js 컴포넌트 초기화 최적화
563 lines
37 KiB
HTML
563 lines
37 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>
|
|
<script defer src="https://unpkg.com/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
|
|
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<link rel="stylesheet" href="/static/css/main.css">
|
|
|
|
<!-- 인증 가드 -->
|
|
<script src="static/js/auth-guard.js"></script>
|
|
|
|
<!-- 헤더 로더 -->
|
|
<script src="static/js/header-loader.js"></script>
|
|
</head>
|
|
<body class="bg-gray-50 min-h-screen">
|
|
<!-- 메인 앱 -->
|
|
<div x-data="documentApp()" x-init="init()">
|
|
<!-- 로그인 모달 -->
|
|
<div x-data="authModal()" x-show="showLoginModal" x-cloak class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div class="bg-white rounded-lg p-8 max-w-md w-full mx-4">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h2 class="text-2xl font-bold text-gray-900">로그인</h2>
|
|
<button @click="showLoginModal = false" class="text-gray-400 hover:text-gray-600">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<form @submit.prevent="login">
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">이메일</label>
|
|
<input type="email" x-model="loginForm.email" required
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">비밀번호</label>
|
|
<input type="password" x-model="loginForm.password" required
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
|
|
<div x-show="loginError" class="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
|
<span x-text="loginError"></span>
|
|
</div>
|
|
|
|
<button type="submit" :disabled="loginLoading"
|
|
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50">
|
|
<span x-show="!loginLoading">로그인</span>
|
|
<span x-show="loginLoading">로그인 중...</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<!-- 공통 헤더 컨테이너 -->
|
|
<div id="header-container"></div>
|
|
|
|
<!-- 메인 컨텐츠 -->
|
|
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-8">
|
|
<!-- 로그인하지 않은 경우 -->
|
|
<template x-if="!isAuthenticated">
|
|
<div class="text-center py-16">
|
|
<i class="fas fa-file-alt text-6xl text-gray-400 mb-4"></i>
|
|
<h2 class="text-3xl font-bold text-gray-900 mb-4">Document Server에 오신 것을 환영합니다</h2>
|
|
<p class="text-xl text-gray-600 mb-8">HTML 문서를 관리하고 메모, 하이라이트를 추가해보세요</p>
|
|
<button @click="showLoginModal = true"
|
|
class="bg-blue-600 text-white px-8 py-3 rounded-lg text-lg hover:bg-blue-700">
|
|
시작하기
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 로그인한 경우 - 문서 목록 -->
|
|
<template x-if="isAuthenticated">
|
|
<div>
|
|
<!-- 필터 및 정렬 -->
|
|
<div class="mb-6">
|
|
<!-- 첫 번째 줄: 제목과 뷰 모드 -->
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="text-2xl font-bold text-gray-900">문서 목록</h2>
|
|
<div class="flex items-center space-x-2">
|
|
<button @click="viewMode = 'grid'"
|
|
:class="viewMode === 'grid' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'"
|
|
class="px-3 py-2 rounded-md">
|
|
<i class="fas fa-th-large 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">
|
|
<i class="fas fa-book mr-2"></i>서적별 보기
|
|
</button>
|
|
|
|
<button onclick="window.location.href='/notes.html'"
|
|
class="px-3 py-2 rounded-md bg-green-600 text-white hover:bg-green-700 transition-colors">
|
|
<i class="fas fa-sticky-note mr-2"></i>노트
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 두 번째 줄: 검색과 필터 -->
|
|
<div class="flex items-center space-x-4">
|
|
<!-- 검색 입력창 -->
|
|
<div class="flex-1 relative">
|
|
<input type="text"
|
|
x-model="searchQuery"
|
|
@input="filterDocuments"
|
|
placeholder="문서 제목, 내용, 태그로 검색..."
|
|
class="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-sm transition-all duration-200">
|
|
<i class="fas fa-search absolute left-3 top-3.5 text-gray-400"></i>
|
|
<!-- 검색 결과 개수 표시 -->
|
|
<span x-show="searchQuery && filteredDocuments.length !== documents.length"
|
|
class="absolute right-3 top-3 text-sm text-gray-500">
|
|
<span x-text="filteredDocuments.length"></span>개 결과
|
|
</span>
|
|
</div>
|
|
|
|
<!-- 태그 필터 -->
|
|
<select x-model="selectedTag" @change="filterDocuments"
|
|
class="px-4 py-2.5 border border-gray-200 rounded-xl bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 min-w-[140px]">
|
|
<option value="">모든 태그</option>
|
|
<template x-for="tag in tags" :key="tag.id">
|
|
<option :value="tag.name" x-text="tag.name"></option>
|
|
</template>
|
|
</select>
|
|
|
|
<!-- 업로드 버튼 -->
|
|
<button @click="openUploadPage()"
|
|
class="px-6 py-2.5 bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-xl hover:from-blue-700 hover:to-indigo-700 transition-all duration-200 shadow-md hover:shadow-lg flex items-center space-x-2">
|
|
<i class="fas fa-upload"></i>
|
|
<span>업로드</span>
|
|
</button>
|
|
|
|
<!-- 검색 초기화 버튼 -->
|
|
<button x-show="searchQuery || selectedTag"
|
|
@click="clearFilters"
|
|
class="px-4 py-2.5 bg-gray-100 text-gray-600 rounded-xl hover:bg-gray-200 transition-all duration-200">
|
|
<i class="fas fa-times mr-2"></i>초기화
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 기존 그리드 뷰 (모든 문서 평면적으로) -->
|
|
<div x-show="viewMode === 'grid'" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
<template x-for="doc in filteredDocuments" :key="doc.id">
|
|
<div class="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow cursor-pointer"
|
|
@click="openDocument(doc.id)">
|
|
<div class="p-6">
|
|
<div class="flex items-start justify-between mb-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 line-clamp-2" x-text="doc.title"></h3>
|
|
<i class="fas fa-file-alt text-blue-500 text-xl"></i>
|
|
</div>
|
|
|
|
<p class="text-gray-600 text-sm mb-3 line-clamp-3" x-text="doc.description || '설명 없음'"></p>
|
|
|
|
<!-- 서적 정보 -->
|
|
<div x-show="doc.book_title" class="mb-3 p-2 bg-green-50 border border-green-200 rounded-md">
|
|
<div class="flex items-center text-sm">
|
|
<i class="fas fa-book text-green-600 mr-2"></i>
|
|
<span class="font-medium text-green-800" x-text="doc.book_title"></span>
|
|
<span x-show="doc.book_author" class="text-green-600 ml-1" x-text="' by ' + doc.book_author"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-1 mb-4">
|
|
<template x-for="tag in doc.tags" :key="tag">
|
|
<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full" x-text="tag"></span>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="flex justify-between items-center text-sm text-gray-500">
|
|
<span x-text="formatDate(doc.created_at)"></span>
|
|
<div class="flex items-center space-x-2">
|
|
<span x-text="doc.uploader_name"></span>
|
|
<!-- 액션 버튼들 -->
|
|
<div class="flex space-x-1 ml-2">
|
|
<button @click.stop="editDocument(doc)"
|
|
class="p-1 text-gray-400 hover:text-blue-600 transition-colors"
|
|
title="문서 수정">
|
|
<i class="fas fa-edit text-sm"></i>
|
|
</button>
|
|
<!-- 관리자만 삭제 버튼 표시 -->
|
|
<button x-show="currentUser && currentUser.is_admin"
|
|
@click.stop="deleteDocument(doc.id)"
|
|
class="p-1 text-gray-400 hover:text-red-600 transition-colors"
|
|
title="문서 삭제 (관리자 전용)">
|
|
<i class="fas fa-trash text-sm"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- 서적별 그룹화 뷰 (데본씽크 스타일) -->
|
|
<div x-show="viewMode === 'books'" class="space-y-4">
|
|
<!-- 서적 목록 -->
|
|
<template x-for="bookGroup in groupedDocuments" :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-blue-500 to-indigo-600 rounded-md flex items-center justify-center">
|
|
<i class="fas fa-book text-white text-sm"></i>
|
|
</div>
|
|
|
|
<!-- 서적 정보 -->
|
|
<div>
|
|
<h3 class="font-semibold text-gray-900" x-text="bookGroup.book?.title || '서적 미분류'"></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 class="text-gray-300">•</span>
|
|
<span x-text="bookGroup.documents.length + '개 문서'"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 확장/축소 아이콘 -->
|
|
<div class="flex items-center space-x-2">
|
|
<button @click.stop="openBookDocuments(bookGroup.book)"
|
|
class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-full hover:bg-blue-200 transition-colors">
|
|
편집
|
|
</button>
|
|
<i class="fas fa-chevron-down text-gray-400 transition-transform duration-200"
|
|
:class="{'rotate-180': bookGroup.expanded}"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 문서 목록 (확장 시 표시) -->
|
|
<div x-show="bookGroup.expanded" x-collapse class="border-t border-gray-100">
|
|
<div class="divide-y divide-gray-50">
|
|
<template x-for="(doc, index) in bookGroup.documents.slice(0, 10)" :key="doc.id">
|
|
<div class="p-3 hover:bg-gray-50 cursor-pointer transition-colors flex items-center justify-between"
|
|
@click="openDocument(doc.id)">
|
|
<div class="flex items-center space-x-3 flex-1">
|
|
<!-- 문서 타입 아이콘 -->
|
|
<div class="w-8 h-8 rounded-md flex items-center justify-center"
|
|
:class="doc.pdf_path ? 'bg-red-100 text-red-600' : 'bg-gray-100 text-gray-600'">
|
|
<i :class="doc.pdf_path ? 'fas fa-file-pdf' : 'fas fa-file-alt'" class="text-xs"></i>
|
|
</div>
|
|
|
|
<!-- 문서 정보 -->
|
|
<div class="flex-1 min-w-0">
|
|
<h4 class="text-sm font-medium text-gray-900 truncate" x-text="doc.title"></h4>
|
|
<p class="text-xs text-gray-500 truncate" x-text="doc.description || '설명 없음'"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 문서 메타 정보 -->
|
|
<div class="flex items-center space-x-2 text-xs text-gray-400">
|
|
<span x-text="formatDate(doc.created_at)"></span>
|
|
<i class="fas fa-chevron-right"></i>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 더 많은 문서가 있을 때 -->
|
|
<div x-show="bookGroup.documents.length > 10"
|
|
class="p-3 text-center border-t border-gray-100">
|
|
<button @click="openBookDocuments(bookGroup.book)"
|
|
class="text-sm text-blue-600 hover:text-blue-800 font-medium">
|
|
<span x-text="`${bookGroup.documents.length - 10}개 문서 더 보기`"></span>
|
|
<i class="fas fa-arrow-right ml-1"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 서적이 없을 때 -->
|
|
<div x-show="groupedDocuments.length === 0" class="text-center py-12">
|
|
<i class="fas fa-book text-4xl text-gray-300 mb-4"></i>
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">서적이 없습니다</h3>
|
|
<p class="text-gray-500 mb-4">문서를 업로드하고 서적으로 분류해보세요</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>
|
|
|
|
<!-- 빈 상태 -->
|
|
<template x-if="filteredDocuments.length === 0 && !loading">
|
|
<div class="text-center py-16">
|
|
<template x-if="searchQuery || selectedTag">
|
|
<!-- 검색 결과 없음 -->
|
|
<div>
|
|
<i class="fas fa-search text-6xl text-gray-400 mb-4"></i>
|
|
<h3 class="text-xl font-semibold text-gray-900 mb-2">검색 결과가 없습니다</h3>
|
|
<p class="text-gray-600 mb-6">다른 검색어나 필터를 시도해보세요</p>
|
|
<button @click="clearFilters"
|
|
class="bg-gray-600 text-white px-6 py-3 rounded-lg hover:bg-gray-700">
|
|
<i class="fas fa-times mr-2"></i>
|
|
필터 초기화
|
|
</button>
|
|
</div>
|
|
</template>
|
|
<template x-if="!searchQuery && !selectedTag">
|
|
<!-- 문서 없음 -->
|
|
<div>
|
|
<i class="fas fa-folder-open text-6xl text-gray-400 mb-4"></i>
|
|
<h3 class="text-xl font-semibold text-gray-900 mb-2">문서가 없습니다</h3>
|
|
<p class="text-gray-600 mb-6">첫 번째 문서를 업로드해보세요</p>
|
|
<button @click="showUploadModal = true"
|
|
class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700">
|
|
<i class="fas fa-upload mr-2"></i>
|
|
문서 업로드
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</main>
|
|
|
|
<!-- 문서 업로드 모달 -->
|
|
<div x-show="showUploadModal" x-cloak class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div class="bg-white rounded-lg p-8 max-w-2xl w-full mx-4 max-h-96 overflow-y-auto">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h2 class="text-2xl font-bold text-gray-900">문서 업로드</h2>
|
|
<button @click="showUploadModal = false" class="text-gray-400 hover:text-gray-600">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div x-data="uploadModal()">
|
|
<form @submit.prevent="upload">
|
|
<!-- 파일 업로드 영역 -->
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">HTML 문서 *</label>
|
|
<div class="file-drop-zone"
|
|
@dragover.prevent="$el.classList.add('dragover')"
|
|
@dragleave.prevent="$el.classList.remove('dragover')"
|
|
@drop.prevent="handleFileDrop($event, 'html_file')">
|
|
<input type="file" @change="onFileSelect($event, 'html_file')"
|
|
accept=".html,.htm" class="hidden" id="html-file">
|
|
<label for="html-file" class="cursor-pointer">
|
|
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-4"></i>
|
|
<p class="text-lg text-gray-600 mb-2">HTML 파일을 드래그하거나 클릭하여 선택</p>
|
|
<p class="text-sm text-gray-500">지원 형식: .html, .htm</p>
|
|
</label>
|
|
<div x-show="uploadForm.html_file" class="mt-4 p-3 bg-blue-50 rounded-lg">
|
|
<p class="text-sm text-blue-800">
|
|
<i class="fas fa-file-alt mr-2"></i>
|
|
<span x-text="uploadForm.html_file?.name"></span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PDF 파일 (선택사항) -->
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">PDF 원본 (선택사항)</label>
|
|
<div class="file-drop-zone"
|
|
@dragover.prevent="$el.classList.add('dragover')"
|
|
@dragleave.prevent="$el.classList.remove('dragover')"
|
|
@drop.prevent="handleFileDrop($event, 'pdf_file')">
|
|
<input type="file" @change="onFileSelect($event, 'pdf_file')"
|
|
accept=".pdf" class="hidden" id="pdf-file">
|
|
<label for="pdf-file" class="cursor-pointer">
|
|
<i class="fas fa-file-pdf text-2xl text-gray-400 mb-2"></i>
|
|
<p class="text-gray-600">PDF 원본 파일 (선택사항)</p>
|
|
</label>
|
|
<div x-show="uploadForm.pdf_file" class="mt-4 p-3 bg-red-50 rounded-lg">
|
|
<p class="text-sm text-red-800">
|
|
<i class="fas fa-file-pdf mr-2"></i>
|
|
<span x-text="uploadForm.pdf_file?.name"></span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 서적 선택/생성 -->
|
|
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
|
|
<label class="block text-sm font-medium text-gray-700 mb-3">📚 서적 설정</label>
|
|
|
|
<!-- 서적 선택 방식 -->
|
|
<div class="flex space-x-4 mb-4">
|
|
<label class="flex items-center">
|
|
<input type="radio" x-model="bookSelectionMode" value="existing"
|
|
class="mr-2 text-blue-600">
|
|
<span class="text-sm">기존 서적에 추가</span>
|
|
</label>
|
|
<label class="flex items-center">
|
|
<input type="radio" x-model="bookSelectionMode" value="new"
|
|
class="mr-2 text-blue-600">
|
|
<span class="text-sm">새 서적 생성</span>
|
|
</label>
|
|
<label class="flex items-center">
|
|
<input type="radio" x-model="bookSelectionMode" value="none"
|
|
class="mr-2 text-blue-600">
|
|
<span class="text-sm">서적 없이 업로드</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- 기존 서적 선택 -->
|
|
<div x-show="bookSelectionMode === 'existing'" class="space-y-3">
|
|
<div>
|
|
<input type="text" x-model="bookSearchQuery" @input="searchBooks"
|
|
placeholder="서적 제목으로 검색..."
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
|
|
<!-- 검색 결과 -->
|
|
<div x-show="searchedBooks.length > 0" class="max-h-40 overflow-y-auto border border-gray-200 rounded-md">
|
|
<template x-for="book in searchedBooks" :key="book.id">
|
|
<div @click="selectBook(book)"
|
|
:class="selectedBook?.id === book.id ? 'bg-blue-100 border-blue-500' : 'hover:bg-gray-50'"
|
|
class="p-3 border-b border-gray-200 cursor-pointer">
|
|
<div class="font-medium text-sm" x-text="book.title"></div>
|
|
<div class="text-xs text-gray-500">
|
|
<span x-text="book.author || '저자 미상'"></span> ·
|
|
<span x-text="book.document_count + '개 문서'"></span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- 선택된 서적 표시 -->
|
|
<div x-show="selectedBook" class="p-3 bg-blue-50 border border-blue-200 rounded-md">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<div class="font-medium text-blue-900" x-text="selectedBook?.title"></div>
|
|
<div class="text-sm text-blue-700" x-text="selectedBook?.author || '저자 미상'"></div>
|
|
</div>
|
|
<button @click="selectedBook = null" class="text-blue-500 hover:text-blue-700">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 새 서적 생성 -->
|
|
<div x-show="bookSelectionMode === 'new'" class="space-y-3">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div>
|
|
<input type="text" x-model="newBook.title" @input="getSuggestions"
|
|
placeholder="서적 제목 *" :required="bookSelectionMode === 'new'"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
<div>
|
|
<input type="text" x-model="newBook.author"
|
|
placeholder="저자"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 유사 서적 추천 -->
|
|
<div x-show="suggestions.length > 0" class="p-3 bg-yellow-50 border border-yellow-200 rounded-md">
|
|
<div class="text-sm font-medium text-yellow-800 mb-2">💡 유사한 서적이 있습니다:</div>
|
|
<template x-for="suggestion in suggestions" :key="suggestion.id">
|
|
<div @click="selectExistingFromSuggestion(suggestion)"
|
|
class="p-2 bg-white border border-yellow-300 rounded cursor-pointer hover:bg-yellow-50 mb-1">
|
|
<div class="text-sm font-medium" x-text="suggestion.title"></div>
|
|
<div class="text-xs text-gray-600">
|
|
<span x-text="suggestion.author || '저자 미상'"></span> ·
|
|
<span x-text="Math.round(suggestion.similarity_score * 100) + '% 유사'"></span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 문서 정보 -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">제목 *</label>
|
|
<input type="text" x-model="uploadForm.title" required
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="문서 제목">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">문서 날짜</label>
|
|
<input type="date" x-model="uploadForm.document_date"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">설명</label>
|
|
<textarea x-model="uploadForm.description" rows="3"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="문서에 대한 간단한 설명"></textarea>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">태그</label>
|
|
<input type="text" x-model="uploadForm.tags"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="태그를 쉼표로 구분하여 입력 (예: 중요, 회의록, 2024)">
|
|
</div>
|
|
|
|
<div class="mb-6">
|
|
<label class="flex items-center">
|
|
<input type="checkbox" x-model="uploadForm.is_public" class="mr-2">
|
|
<span class="text-sm text-gray-700">공개 문서로 설정 (다른 사용자도 볼 수 있음)</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- 에러 메시지 -->
|
|
<div x-show="uploadError" class="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
|
<span x-text="uploadError"></span>
|
|
</div>
|
|
|
|
<!-- 버튼 -->
|
|
<div class="flex justify-end space-x-3">
|
|
<button type="button" @click="showUploadModal = false; resetForm()"
|
|
class="px-6 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300">
|
|
취소
|
|
</button>
|
|
<button type="submit" :disabled="uploading || !uploadForm.html_file || !uploadForm.title"
|
|
class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
<span x-show="!uploading">업로드</span>
|
|
<span x-show="uploading">
|
|
<i class="fas fa-spinner fa-spin mr-2"></i>업로드 중...
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 인증 모달 컴포넌트 -->
|
|
<div x-data="authModal" x-ref="authModal"></div>
|
|
|
|
<!-- 스크립트는 하단에서 로드됩니다 -->
|
|
|
|
<style>
|
|
[x-cloak] { display: none !important; }
|
|
.line-clamp-2 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
.line-clamp-3 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
</style>
|
|
|
|
<!-- JavaScript 파일들 -->
|
|
<script src="/static/js/api.js?v=2025012380"></script>
|
|
<script src="/static/js/auth.js?v=2025012351"></script>
|
|
<script src="/static/js/main.js?v=2025012462"></script>
|
|
</body>
|
|
</html>
|