🌟 주요 기능: - 트리 구조 메모장 시스템 - 소설 분기 관리 (정사 경로 설정) - 중앙 배치 트리 다이어그램 - 정사 경로 목차 뷰 - 인라인 편집 기능 📚 백엔드: - MemoTree, MemoNode 모델 추가 - 정사 경로 자동 순서 관리 - 분기점에서 하나만 선택 가능한 로직 - RESTful API 엔드포인트 🎨 프론트엔드: - memo-tree.html: 트리 다이어그램 에디터 - story-view.html: 정사 경로 목차 뷰 - SVG 연결선으로 시각적 트리 표현 - Alpine.js 기반 반응형 UI - Monaco Editor 통합 ✨ 특별 기능: - 정사 경로 황금색 배지 표시 - 확대/축소 및 패닝 지원 - 드래그 앤 드롭 준비 - 내보내기 및 인쇄 기능 - 인라인 편집 모달
845 lines
45 KiB
HTML
845 lines
45 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>문서 관리 시스템 - 계층구조</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
|
<style>
|
|
/* 드래그 앤 드롭 스타일 */
|
|
.draggable {
|
|
cursor: move;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.draggable:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
}
|
|
.drag-over {
|
|
border: 2px dashed #3b82f6;
|
|
background-color: #eff6ff;
|
|
}
|
|
.tree-item {
|
|
position: relative;
|
|
}
|
|
.tree-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -12px;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 1px;
|
|
background: #e5e7eb;
|
|
}
|
|
.tree-item:last-child::before {
|
|
height: 20px;
|
|
}
|
|
.tree-item::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: -12px;
|
|
top: 20px;
|
|
width: 12px;
|
|
height: 1px;
|
|
background: #e5e7eb;
|
|
}
|
|
.sidebar-scroll {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: #cbd5e1 #f1f5f9;
|
|
}
|
|
.sidebar-scroll::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
.sidebar-scroll::-webkit-scrollbar-track {
|
|
background: #f1f5f9;
|
|
}
|
|
.sidebar-scroll::-webkit-scrollbar-thumb {
|
|
background: #cbd5e1;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
/* 하이라이트 스타일 */
|
|
.highlight {
|
|
border-radius: 2px;
|
|
padding: 1px 2px;
|
|
margin: 0 1px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.highlight:hover {
|
|
opacity: 0.8;
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
/* 텍스트 선택 시 하이라이트 메뉴 */
|
|
.highlight-menu {
|
|
animation: fadeInUp 0.2s ease-out;
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3) !important;
|
|
border: 3px solid #3b82f6 !important;
|
|
background: white !important;
|
|
z-index: 99999 !important;
|
|
}
|
|
|
|
@keyframes fadeInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* 문서 콘텐츠 영역 */
|
|
.prose {
|
|
user-select: text;
|
|
-webkit-user-select: text;
|
|
-moz-user-select: text;
|
|
-ms-user-select: text;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50" x-data="hierarchyApp()" x-init="init()">
|
|
<!-- 헤더 -->
|
|
<header class="bg-white shadow-sm border-b">
|
|
<div class="px-6 py-4">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-4">
|
|
<h1 class="text-2xl font-bold text-gray-900">📚 문서 관리 시스템</h1>
|
|
<div class="flex space-x-2">
|
|
<a href="index.html" class="px-3 py-1 text-sm bg-gray-100 text-gray-600 rounded-full hover:bg-gray-200">📖 그리드 뷰</a>
|
|
<span class="px-3 py-1 text-sm bg-blue-100 text-blue-800 rounded-full">📚 계층구조 뷰</span>
|
|
<a href="memo-tree.html" class="px-3 py-1 text-sm bg-gray-100 text-gray-600 rounded-full hover:bg-gray-200">🌳 트리 메모장</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-4">
|
|
<!-- 검색 -->
|
|
<div class="relative" x-show="currentUser">
|
|
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
|
|
<input
|
|
type="text"
|
|
x-model="searchQuery"
|
|
@input="searchDocuments"
|
|
placeholder="문서, 메모 검색..."
|
|
class="w-80 pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
</div>
|
|
|
|
<!-- 사용자 메뉴 -->
|
|
<div x-show="currentUser" class="flex items-center space-x-3">
|
|
<span class="text-sm text-gray-600" x-text="currentUser?.full_name || currentUser?.email"></span>
|
|
<button @click="logout()" class="text-sm text-red-600 hover:text-red-800">로그아웃</button>
|
|
</div>
|
|
|
|
<!-- 로그인 버튼 -->
|
|
<button x-show="!currentUser" @click="openLoginModal()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
|
로그인
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- 메인 컨테이너 -->
|
|
<div class="flex h-screen pt-16">
|
|
<!-- 왼쪽 사이드바 (항상 표시) -->
|
|
<aside class="w-80 bg-white shadow-lg border-r flex flex-col">
|
|
<!-- 사이드바 헤더 -->
|
|
<div class="p-4 border-b bg-gray-50">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="font-semibold text-gray-900">📖 문서 구조</h2>
|
|
<div class="flex space-x-2">
|
|
<button
|
|
@click="showUploadModal = true"
|
|
x-show="currentUser"
|
|
class="p-2 text-blue-600 hover:bg-blue-50 rounded-lg"
|
|
title="문서 업로드"
|
|
>
|
|
<i class="fas fa-plus"></i>
|
|
</button>
|
|
<button
|
|
@click="refreshHierarchy()"
|
|
class="p-2 text-gray-600 hover:bg-gray-100 rounded-lg"
|
|
title="새로고침"
|
|
>
|
|
<i class="fas fa-sync-alt"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 트리 구조 -->
|
|
<div class="flex-1 overflow-y-auto sidebar-scroll p-4">
|
|
<div x-show="loading" class="text-center py-8">
|
|
<i class="fas fa-spinner fa-spin text-gray-400"></i>
|
|
<p class="text-gray-500 mt-2">로딩 중...</p>
|
|
</div>
|
|
|
|
<div x-show="!loading">
|
|
<!-- 서적들 -->
|
|
<template x-for="(book, bookId) in hierarchy.books" :key="bookId">
|
|
<div class="mb-4">
|
|
<!-- 서적 헤더 -->
|
|
<div
|
|
class="flex items-center p-3 bg-blue-50 border border-blue-200 rounded-lg cursor-pointer hover:bg-blue-100"
|
|
@click="toggleBook(bookId)"
|
|
>
|
|
<i class="fas fa-book text-blue-600 mr-3"></i>
|
|
<div class="flex-1">
|
|
<h3 class="font-medium text-blue-900" x-text="book.title"></h3>
|
|
<p class="text-sm text-blue-700" x-text="book.author || '저자 미상'"></p>
|
|
</div>
|
|
<i
|
|
class="fas fa-chevron-down transition-transform duration-200"
|
|
:class="expandedBooks.includes(bookId) ? 'rotate-180' : ''"
|
|
></i>
|
|
</div>
|
|
|
|
<!-- 서적 내용 -->
|
|
<div x-show="expandedBooks.includes(bookId)" class="mt-2 ml-4 space-y-2">
|
|
<!-- 소분류들 -->
|
|
<template x-for="(category, categoryId) in book.categories" :key="categoryId">
|
|
<div class="tree-item">
|
|
<!-- 소분류 헤더 -->
|
|
<div
|
|
class="flex items-center p-2 bg-green-50 border border-green-200 rounded cursor-pointer hover:bg-green-100"
|
|
@click="toggleCategory(categoryId)"
|
|
>
|
|
<i class="fas fa-folder text-green-600 mr-2"></i>
|
|
<span class="flex-1 font-medium text-green-900" x-text="category.name"></span>
|
|
<span class="text-xs text-green-700" x-text="`${category.documents.length}개`"></span>
|
|
<i
|
|
class="fas fa-chevron-down ml-2 transition-transform duration-200"
|
|
:class="expandedCategories.includes(categoryId) ? 'rotate-180' : ''"
|
|
></i>
|
|
</div>
|
|
|
|
<!-- 소분류 문서들 -->
|
|
<div x-show="expandedCategories.includes(categoryId)" class="mt-1 ml-4 space-y-1">
|
|
<template x-for="doc in category.documents" :key="doc.id">
|
|
<div
|
|
class="flex items-center p-2 bg-white border rounded cursor-pointer hover:bg-gray-50 draggable"
|
|
@click="openDocument(doc.id)"
|
|
draggable="true"
|
|
@dragstart="handleDragStart($event, doc)"
|
|
@dragover.prevent
|
|
@drop="handleDrop($event, categoryId)"
|
|
>
|
|
<i class="fas fa-file-alt text-gray-500 mr-2"></i>
|
|
<span class="flex-1 text-sm" x-text="doc.title"></span>
|
|
<div class="flex space-x-1">
|
|
<button
|
|
@click.stop="editDocument(doc)"
|
|
class="p-1 text-gray-400 hover:text-blue-600"
|
|
title="편집"
|
|
>
|
|
<i class="fas fa-edit text-xs"></i>
|
|
</button>
|
|
<button
|
|
x-show="currentUser && currentUser.is_admin"
|
|
@click.stop="deleteDocument(doc.id)"
|
|
class="p-1 text-gray-400 hover:text-red-600"
|
|
title="삭제"
|
|
>
|
|
<i class="fas fa-trash text-xs"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 서적 내 미분류 문서들 -->
|
|
<div x-show="book.uncategorized_documents.length > 0" class="tree-item">
|
|
<div
|
|
class="flex items-center p-2 bg-yellow-50 border border-yellow-200 rounded cursor-pointer hover:bg-yellow-100"
|
|
@click="toggleUncategorizedInBook(bookId)"
|
|
>
|
|
<i class="fas fa-folder-open text-yellow-600 mr-2"></i>
|
|
<span class="flex-1 font-medium text-yellow-900">미분류</span>
|
|
<span class="text-xs text-yellow-700" x-text="`${book.uncategorized_documents.length}개`"></span>
|
|
<i
|
|
class="fas fa-chevron-down ml-2 transition-transform duration-200"
|
|
:class="expandedUncategorizedInBooks.includes(bookId) ? 'rotate-180' : ''"
|
|
></i>
|
|
</div>
|
|
|
|
<div x-show="expandedUncategorizedInBooks.includes(bookId)" class="mt-1 ml-4 space-y-1">
|
|
<template x-for="doc in book.uncategorized_documents" :key="doc.id">
|
|
<div
|
|
class="flex items-center p-2 bg-white border rounded cursor-pointer hover:bg-gray-50 draggable"
|
|
@click="openDocument(doc.id)"
|
|
draggable="true"
|
|
@dragstart="handleDragStart($event, doc)"
|
|
>
|
|
<i class="fas fa-file-alt text-gray-500 mr-2"></i>
|
|
<span class="flex-1 text-sm" x-text="doc.title"></span>
|
|
<div class="flex space-x-1">
|
|
<button
|
|
@click.stop="editDocument(doc)"
|
|
class="p-1 text-gray-400 hover:text-blue-600"
|
|
title="편집"
|
|
>
|
|
<i class="fas fa-edit text-xs"></i>
|
|
</button>
|
|
<button
|
|
x-show="currentUser && currentUser.is_admin"
|
|
@click.stop="deleteDocument(doc.id)"
|
|
class="p-1 text-gray-400 hover:text-red-600"
|
|
title="삭제"
|
|
>
|
|
<i class="fas fa-trash text-xs"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 전체 미분류 문서들 -->
|
|
<div x-show="hierarchy.uncategorized.length > 0" class="mb-4">
|
|
<div
|
|
class="flex items-center p-3 bg-gray-100 border border-gray-300 rounded-lg cursor-pointer hover:bg-gray-200"
|
|
@click="toggleUncategorized()"
|
|
>
|
|
<i class="fas fa-folder-open text-gray-600 mr-3"></i>
|
|
<div class="flex-1">
|
|
<h3 class="font-medium text-gray-900">📄 미분류 문서</h3>
|
|
<p class="text-sm text-gray-600" x-text="`${hierarchy.uncategorized.length}개 문서`"></p>
|
|
</div>
|
|
<i
|
|
class="fas fa-chevron-down transition-transform duration-200"
|
|
:class="showUncategorized ? 'rotate-180' : ''"
|
|
></i>
|
|
</div>
|
|
|
|
<div x-show="showUncategorized" class="mt-2 ml-4 space-y-1">
|
|
<template x-for="doc in hierarchy.uncategorized" :key="doc.id">
|
|
<div
|
|
class="flex items-center p-2 bg-white border rounded cursor-pointer hover:bg-gray-50 draggable"
|
|
@click="openDocument(doc.id)"
|
|
draggable="true"
|
|
@dragstart="handleDragStart($event, doc)"
|
|
>
|
|
<i class="fas fa-file-alt text-gray-500 mr-2"></i>
|
|
<span class="flex-1 text-sm" x-text="doc.title"></span>
|
|
<div class="flex space-x-1">
|
|
<button
|
|
@click.stop="editDocument(doc)"
|
|
class="p-1 text-gray-400 hover:text-blue-600"
|
|
title="편집"
|
|
>
|
|
<i class="fas fa-edit text-xs"></i>
|
|
</button>
|
|
<button
|
|
x-show="currentUser && currentUser.is_admin"
|
|
@click.stop="deleteDocument(doc.id)"
|
|
class="p-1 text-gray-400 hover:text-red-600"
|
|
title="삭제"
|
|
>
|
|
<i class="fas fa-trash text-xs"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 빈 상태 -->
|
|
<div x-show="Object.keys(hierarchy.books).length === 0 && hierarchy.uncategorized.length === 0" class="text-center py-8">
|
|
<i class="fas fa-folder-open text-gray-300 text-4xl mb-4"></i>
|
|
<p class="text-gray-500">아직 문서가 없습니다.</p>
|
|
<button
|
|
x-show="currentUser"
|
|
@click="showUploadModal = true"
|
|
class="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
|
>
|
|
첫 문서 업로드하기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- 메인 콘텐츠 영역 -->
|
|
<main class="flex-1 flex flex-col">
|
|
<!-- 콘텐츠 헤더 -->
|
|
<div class="bg-white border-b p-4">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h2 class="text-lg font-semibold text-gray-900" x-text="selectedDocument ? selectedDocument.title : '문서를 선택하세요'"></h2>
|
|
<p class="text-sm text-gray-500" x-text="selectedDocument ? `${selectedDocument.book_title || '미분류'} ${selectedDocument.category_name ? '> ' + selectedDocument.category_name : ''}` : '사이드바에서 문서를 클릭하여 내용을 확인하세요'"></p>
|
|
</div>
|
|
|
|
<div x-show="selectedDocument" class="flex space-x-2">
|
|
<!-- 플로팅 메모창 토글 -->
|
|
<button
|
|
@click="console.log('🪟 메모창 토글:', showFloatingMemo, '→', !showFloatingMemo); showFloatingMemo = !showFloatingMemo"
|
|
class="px-3 py-2 rounded transition-colors"
|
|
:class="showFloatingMemo ? 'bg-blue-500 text-white' : 'text-blue-600 border border-blue-600 hover:bg-blue-50'"
|
|
title="메모 & 하이라이트 창"
|
|
>
|
|
<i class="fas fa-window-restore mr-1"></i>
|
|
메모창 (<span x-text="notes.length + highlights.length"></span>)
|
|
</button>
|
|
|
|
<button
|
|
@click="toggleHighlightMode()"
|
|
class="px-3 py-2 rounded transition-colors"
|
|
:class="highlightMode ? 'bg-yellow-500 text-white' : 'text-yellow-600 border border-yellow-600 hover:bg-yellow-50'"
|
|
>
|
|
<i class="fas fa-highlighter mr-1"></i>
|
|
<span x-text="highlightMode ? '하이라이트 ON' : '하이라이트 OFF'"></span>
|
|
</button>
|
|
<button
|
|
@click="editDocument(selectedDocument)"
|
|
class="px-3 py-2 text-blue-600 border border-blue-600 rounded hover:bg-blue-50"
|
|
>
|
|
<i class="fas fa-edit mr-1"></i> 편집
|
|
</button>
|
|
<button
|
|
x-show="currentUser && currentUser.is_admin"
|
|
@click="deleteDocument(selectedDocument.id)"
|
|
class="px-3 py-2 text-red-600 border border-red-600 rounded hover:bg-red-50"
|
|
>
|
|
<i class="fas fa-trash mr-1"></i> 삭제
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 문서 뷰어 -->
|
|
<div class="flex-1 overflow-auto">
|
|
<div x-show="!selectedDocument" class="flex items-center justify-center h-full">
|
|
<div class="text-center">
|
|
<i class="fas fa-file-alt text-gray-300 text-6xl mb-4"></i>
|
|
<h3 class="text-xl font-medium text-gray-600 mb-2">문서를 선택하세요</h3>
|
|
<p class="text-gray-500">왼쪽 사이드바에서 문서를 클릭하면 내용이 표시됩니다.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div x-show="selectedDocument && documentContent" class="p-6">
|
|
<div x-html="documentContent" class="prose max-w-none"></div>
|
|
</div>
|
|
|
|
<div x-show="selectedDocument && !documentContent && loadingDocument" class="flex items-center justify-center h-full">
|
|
<div class="text-center">
|
|
<i class="fas fa-spinner fa-spin text-gray-400 text-3xl mb-4"></i>
|
|
<p class="text-gray-500">문서를 불러오는 중...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 하이라이트 메뉴 (직접 DOM 조작) -->
|
|
<div
|
|
id="highlight-menu"
|
|
class="fixed top-20 left-1/2 transform -translate-x-1/2 z-50 bg-white border border-gray-300 rounded-lg shadow-lg p-3"
|
|
style="display: none;"
|
|
>
|
|
<div class="text-center mb-2">
|
|
<h4 class="font-semibold text-sm">하이라이트 색상</h4>
|
|
</div>
|
|
<div class="flex space-x-2">
|
|
<button onclick="window.hierarchyInstance.createHighlight('yellow')" class="w-8 h-8 bg-yellow-300 rounded hover:bg-yellow-400" title="노랑"></button>
|
|
<button onclick="window.hierarchyInstance.createHighlight('blue')" class="w-8 h-8 bg-blue-300 rounded hover:bg-blue-400" title="파랑"></button>
|
|
<button onclick="window.hierarchyInstance.createHighlight('green')" class="w-8 h-8 bg-green-300 rounded hover:bg-green-400" title="초록"></button>
|
|
<button onclick="window.hierarchyInstance.createHighlight('red')" class="w-8 h-8 bg-red-300 rounded hover:bg-red-400" title="빨강"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</main>
|
|
</div>
|
|
|
|
<!-- 플로팅 메모창 -->
|
|
<div
|
|
x-show="showFloatingMemo && selectedDocument"
|
|
x-init="console.log('🪟 플로팅 메모창 초기화:', { showFloatingMemo, selectedDocument: !!selectedDocument })"
|
|
class="fixed bg-white rounded-lg shadow-2xl border z-40 flex flex-col"
|
|
:style="`left: ${floatingMemoPosition.x}px; top: ${floatingMemoPosition.y}px; width: ${floatingMemoSize.width}px; height: ${floatingMemoSize.height}px;`"
|
|
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"
|
|
>
|
|
<!-- 플로팅 창 헤더 (드래그 가능) -->
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-t-lg cursor-move"
|
|
@mousedown="startDragging($event)"
|
|
>
|
|
<div class="flex items-center space-x-2">
|
|
<i class="fas fa-window-restore"></i>
|
|
<h3 class="font-semibold">📝 메모 & 하이라이트</h3>
|
|
<div class="flex space-x-1">
|
|
<span class="text-xs bg-white bg-opacity-20 px-2 py-1 rounded-full" x-text="`${notes.length}개 메모`"></span>
|
|
<span class="text-xs bg-white bg-opacity-20 px-2 py-1 rounded-full" x-text="`${highlights.length}개 하이라이트`"></span>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<!-- 크기 조절 버튼들 -->
|
|
<button
|
|
@click="toggleFloatingMemoSize()"
|
|
class="text-white hover:bg-white hover:bg-opacity-20 p-1 rounded"
|
|
title="크기 조절"
|
|
>
|
|
<i class="fas fa-expand-arrows-alt text-sm"></i>
|
|
</button>
|
|
<!-- 닫기 버튼 -->
|
|
<button
|
|
@click="showFloatingMemo = false"
|
|
class="text-white hover:bg-white hover:bg-opacity-20 p-1 rounded"
|
|
title="닫기"
|
|
>
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 메모 검색 -->
|
|
<div class="p-3 border-b bg-gray-50">
|
|
<div class="relative">
|
|
<input
|
|
type="text"
|
|
x-model="memoSearchQuery"
|
|
@input="filterMemos()"
|
|
placeholder="메모 검색..."
|
|
class="w-full pl-8 pr-4 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
<i class="fas fa-search absolute left-2.5 top-2.5 text-gray-400 text-sm"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 메모 목록 -->
|
|
<div class="flex-1 overflow-y-auto p-3">
|
|
<div x-show="(filteredNotes.length === 0 && highlights.length === 0)" class="text-center text-gray-500 py-8">
|
|
<i class="fas fa-sticky-note text-3xl text-gray-300 mb-2"></i>
|
|
<p class="text-sm">아직 메모나 하이라이트가 없습니다.</p>
|
|
<p class="text-xs text-gray-400 mt-1">문서에서 텍스트를 선택하여 하이라이트를 만들어보세요.</p>
|
|
</div>
|
|
|
|
<!-- 하이라이트 섹션 -->
|
|
<div x-show="highlights.length > 0" class="mb-4">
|
|
<h4 class="font-medium text-gray-700 mb-3 flex items-center">
|
|
<i class="fas fa-highlighter text-yellow-500 mr-2"></i>
|
|
하이라이트 (<span x-text="highlights.length"></span>)
|
|
</h4>
|
|
<template x-for="highlight in highlights" :key="highlight.id">
|
|
<div class="mb-3 p-3 bg-gray-50 rounded-lg border-l-4" :class="{
|
|
'border-yellow-400': highlight.highlight_color === 'yellow',
|
|
'border-blue-400': highlight.highlight_color === 'blue',
|
|
'border-green-400': highlight.highlight_color === 'green',
|
|
'border-red-400': highlight.highlight_color === 'red'
|
|
}">
|
|
<div class="text-sm text-gray-600 mb-1" x-text="highlight.selected_text"></div>
|
|
<div x-show="highlight.notes && highlight.notes.content" class="text-xs text-gray-500 mt-2 p-2 bg-white rounded border">
|
|
<i class="fas fa-comment mr-1"></i>
|
|
<span x-text="highlight.notes?.content || ''"></span>
|
|
</div>
|
|
<div class="flex items-center justify-between mt-2">
|
|
<span class="text-xs text-gray-400" x-text="new Date(highlight.created_at).toLocaleDateString()"></span>
|
|
<div class="flex space-x-1">
|
|
<button
|
|
@click="scrollToHighlight(highlight.id)"
|
|
class="text-xs text-blue-600 hover:text-blue-800 p-1"
|
|
title="하이라이트로 이동"
|
|
>
|
|
<i class="fas fa-arrow-right"></i>
|
|
</button>
|
|
<button
|
|
@click="deleteHighlight(highlight.id)"
|
|
class="text-xs text-red-600 hover:text-red-800 p-1"
|
|
title="삭제"
|
|
>
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- 독립 메모 섹션 -->
|
|
<div x-show="filteredNotes.length > 0">
|
|
<h4 class="font-medium text-gray-700 mb-3 flex items-center">
|
|
<i class="fas fa-sticky-note text-blue-500 mr-2"></i>
|
|
독립 메모 (<span x-text="filteredNotes.length"></span>)
|
|
</h4>
|
|
<template x-for="note in filteredNotes" :key="note.id">
|
|
<div class="mb-3 p-3 bg-blue-50 rounded-lg border-l-4 border-blue-400">
|
|
<div class="text-sm text-gray-700" x-text="note.content"></div>
|
|
<div class="flex items-center justify-between mt-2">
|
|
<span class="text-xs text-gray-400" x-text="new Date(note.created_at).toLocaleDateString()"></span>
|
|
<button
|
|
@click="deleteNote(note.id)"
|
|
class="text-xs text-red-600 hover:text-red-800 p-1"
|
|
title="삭제"
|
|
>
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 크기 조절 핸들 -->
|
|
<div
|
|
class="absolute bottom-0 right-0 w-4 h-4 cursor-se-resize bg-gray-300 hover:bg-gray-400"
|
|
@mousedown="startResizing($event)"
|
|
title="크기 조절"
|
|
>
|
|
<i class="fas fa-grip-lines text-xs text-gray-600 absolute bottom-0.5 right-0.5"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 로그인 모달 -->
|
|
<div x-data="authModal()" x-show="showLoginModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div class="bg-white rounded-lg p-6 w-96 max-w-md mx-4">
|
|
<h2 class="text-xl font-bold mb-4">로그인</h2>
|
|
<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>
|
|
<div class="flex justify-end space-x-3">
|
|
<button
|
|
type="button"
|
|
@click="showLoginModal = false"
|
|
class="px-4 py-2 text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50"
|
|
>
|
|
취소
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
:disabled="loggingIn"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
|
|
>
|
|
<span x-show="!loggingIn">로그인</span>
|
|
<span x-show="loggingIn">로그인 중...</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 업로드 모달 -->
|
|
<div x-data="uploadModal()" x-show="showUploadModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div class="bg-white rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
|
|
<h2 class="text-xl font-bold mb-4">문서 업로드</h2>
|
|
<form @submit.prevent="upload()">
|
|
<!-- 파일 선택 -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">HTML 파일 *</label>
|
|
<input
|
|
type="file"
|
|
accept=".html,.htm"
|
|
@change="handleFileSelect($event, 'html_file')"
|
|
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-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">PDF 파일 (선택사항)</label>
|
|
<input
|
|
type="file"
|
|
accept=".pdf"
|
|
@change="handleFileSelect($event, 'pdf_file')"
|
|
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-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">서적 분류</label>
|
|
<div class="space-y-2">
|
|
<label class="flex items-center">
|
|
<input type="radio" x-model="bookSelectionMode" value="existing" class="mr-2 text-blue-600" name="bookSelectionMode">
|
|
기존 서적 선택
|
|
</label>
|
|
<label class="flex items-center">
|
|
<input type="radio" x-model="bookSelectionMode" value="new" class="mr-2 text-blue-600" name="bookSelectionMode">
|
|
새 서적 생성
|
|
</label>
|
|
<label class="flex items-center">
|
|
<input type="radio" x-model="bookSelectionMode" value="none" class="mr-2 text-blue-600" name="bookSelectionMode">
|
|
서적 없이 업로드
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 기존 서적 선택 -->
|
|
<div x-show="bookSelectionMode === 'existing'" class="mb-4 space-y-3">
|
|
<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 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="p-3 hover:bg-gray-50 cursor-pointer border-b last:border-b-0"
|
|
>
|
|
<div class="font-medium" x-text="book.title"></div>
|
|
<div class="text-sm text-gray-600" x-text="book.author || '저자 미상'"></div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<div x-show="selectedBook" class="p-3 bg-blue-50 border border-blue-200 rounded-md">
|
|
<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>
|
|
</div>
|
|
|
|
<!-- 새 서적 생성 -->
|
|
<div x-show="bookSelectionMode === 'new'" class="mb-4 space-y-3">
|
|
<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"
|
|
>
|
|
<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 x-show="suggestions.length > 0" class="p-3 bg-yellow-50 border border-yellow-200 rounded-md">
|
|
<p class="text-sm text-yellow-800 mb-2">비슷한 서적이 있습니다:</p>
|
|
<template x-for="suggestion in suggestions" :key="suggestion.id">
|
|
<div
|
|
@click="selectExistingFromSuggestion(suggestion)"
|
|
class="p-2 hover:bg-yellow-100 cursor-pointer rounded text-sm"
|
|
>
|
|
<span class="font-medium" x-text="suggestion.title"></span>
|
|
<span class="text-yellow-700" x-text="suggestion.author ? ` - ${suggestion.author}` : ''"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 문서 정보 -->
|
|
<div class="mb-4">
|
|
<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 class="mb-4">
|
|
<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 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="closeUploadModal()"
|
|
class="px-4 py-2 text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50"
|
|
>
|
|
취소
|
|
</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>
|
|
|
|
<!-- JavaScript -->
|
|
<script src="static/js/api.js?v=2025012227"></script>
|
|
<script src="static/js/auth.js?v=2025012227"></script>
|
|
<script src="static/js/hierarchy.js?v=2025012227"></script>
|
|
|
|
<!-- 디버깅 스크립트 -->
|
|
<script>
|
|
console.log('🔍 디버깅: hierarchyApp 함수 존재 여부:', typeof window.hierarchyApp);
|
|
if (typeof window.hierarchyApp === 'function') {
|
|
const testInstance = window.hierarchyApp();
|
|
console.log('🔍 디버깅: showFloatingMemo 변수 존재 여부:', 'showFloatingMemo' in testInstance);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|