feat: 계층구조 뷰 및 완전한 하이라이트/메모 시스템 구현

주요 기능:
- 📚 Book 및 BookCategory 모델 추가 (서적 그룹화)
- 🏗️ 계층구조 뷰 (Book > Category > Document) 구현
- 🎨 완전한 하이라이트 시스템 (생성, 표시, 삭제)
- 📝 통합 메모 관리 (추가, 수정, 삭제)
- 🔄 그리드 뷰와 계층구조 뷰 간 완전 동기화
- 🛡️ 관리자 전용 문서 삭제 기능
- 🔧 모든 CORS 및 500 오류 해결

기술적 개선:
- API 베이스 URL을 Nginx 프록시로 변경 (/api)
- 외래키 제약 조건 해결 (삭제 순서 최적화)
- SQLAlchemy 관계 로딩 최적화 (selectinload)
- 프론트엔드 캐시 무효화 시스템
- Alpine.js 컴포넌트 구조 개선

UI/UX:
- 계층구조 네비게이션 (사이드바 + 트리 구조)
- 하이라이트 모드 토글 스위치
- 완전한 툴팁 기반 메모 관리 인터페이스
- 반응형 하이라이트 메뉴 (색상 선택)
- 스마트 툴팁 위치 조정 (화면 경계 고려)
This commit is contained in:
Hyungi Ahn
2025-08-23 14:31:30 +09:00
parent 1e2e66d8fe
commit 46546da55f
18 changed files with 3206 additions and 384 deletions

View File

@@ -12,9 +12,9 @@
</head>
<body class="bg-gray-50 min-h-screen">
<!-- 메인 앱 -->
<div x-data="documentApp" x-init="init()">
<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 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>
@@ -53,11 +53,15 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<!-- 로고 -->
<div class="flex items-center">
<div class="flex items-center space-x-4">
<h1 class="text-xl font-bold text-gray-900">
<i class="fas fa-file-alt mr-2"></i>
Document Server
</h1>
<div class="flex space-x-2">
<span class="px-3 py-1 text-sm bg-blue-100 text-blue-800 rounded-full">그리드 뷰</span>
<a href="hierarchy.html" class="px-3 py-1 text-sm bg-gray-100 text-gray-600 rounded-full hover:bg-gray-200">계층구조 뷰</a>
</div>
</div>
<!-- 검색바 -->
@@ -181,7 +185,16 @@
<i class="fas fa-file-alt text-blue-500 text-xl"></i>
</div>
<p class="text-gray-600 text-sm mb-4 line-clamp-3" x-text="doc.description || '설명 없음'"></p>
<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">
@@ -191,7 +204,24 @@
<div class="flex justify-between items-center text-sm text-gray-500">
<span x-text="formatDate(doc.created_at)"></span>
<span x-text="doc.uploader_name"></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>
@@ -225,7 +255,7 @@
</button>
</div>
<div x-data="uploadModal">
<div x-data="uploadModal()">
<form @submit.prevent="upload">
<!-- 파일 업로드 영역 -->
<div class="mb-6">
@@ -272,6 +302,98 @@
</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>
@@ -356,5 +478,10 @@
overflow: hidden;
}
</style>
<!-- JavaScript 파일들 -->
<script src="/static/js/api.js?v=2025012222"></script>
<script src="/static/js/auth.js?v=2025012222"></script>
<script src="/static/js/main.js?v=2025012222"></script>
</body>
</html>