- note-editor.html: notebook.name → notebook.title 수정 - notes.html: 모든 notebook.name → notebook.title 수정 - note-editor.js: 디버깅 로그 추가하여 노트북 데이터 구조 확인 백엔드 API에서는 'title' 필드를 사용하는데 프론트엔드에서 'name' 필드를 참조하여 노트북 선택창이 빈칸으로 표시되는 문제 해결
424 lines
23 KiB
HTML
424 lines
23 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>
|
|
<script src="https://cdn.tailwindcss.com"></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.0.0/css/all.min.css">
|
|
|
|
<style>
|
|
.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>
|
|
</head>
|
|
<body class="bg-gray-50 min-h-screen" x-data="notesApp()">
|
|
<!-- 헤더 -->
|
|
<div id="header-container"></div>
|
|
|
|
<!-- 메인 컨텐츠 -->
|
|
<main class="container mx-auto px-4 py-8">
|
|
<!-- 페이지 헤더 -->
|
|
<div class="mb-8">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900 flex items-center">
|
|
<i class="fas fa-sticky-note text-blue-600 mr-3"></i>
|
|
노트 관리
|
|
</h1>
|
|
<p class="text-gray-600 mt-2">마크다운으로 노트를 작성하고 서적과 연결하여 관리하세요</p>
|
|
</div>
|
|
|
|
<div class="flex space-x-3">
|
|
<button onclick="window.location.href='/'"
|
|
class="flex items-center px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors">
|
|
<i class="fas fa-arrow-left mr-2"></i>
|
|
<span>돌아가기</span>
|
|
</button>
|
|
|
|
<button @click="refreshNotes()"
|
|
:disabled="loading"
|
|
class="flex items-center px-4 py-2 bg-gray-500 hover:bg-gray-600 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>
|
|
|
|
<button onclick="window.location.href='/note-editor.html'"
|
|
class="flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
|
|
<i class="fas fa-plus mr-2"></i>
|
|
<span>새 노트 작성</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 통계 카드 -->
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8" x-show="stats">
|
|
<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-sticky-note 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="stats?.total_notes || 0"></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-eye text-green-600 text-xl"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-gray-900">공개 노트</h3>
|
|
<p class="text-2xl font-bold text-green-600" x-text="stats?.published_notes || 0"></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-edit 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="stats?.draft_notes || 0"></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-purple-100 rounded-lg flex items-center justify-center mr-4">
|
|
<i class="fas fa-clock text-purple-600 text-xl"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-gray-900">읽기 시간</h3>
|
|
<p class="text-2xl font-bold text-purple-600" x-text="(stats?.total_reading_time || 0) + '분'"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 일괄 작업 도구 -->
|
|
<div x-show="selectedNotes.length > 0"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0 transform -translate-y-2"
|
|
x-transition:enter-end="opacity-100 transform translate-y-0"
|
|
class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-4">
|
|
<span class="text-sm font-medium text-blue-900">
|
|
<span x-text="selectedNotes.length"></span>개 노트 선택됨
|
|
</span>
|
|
<button @click="clearSelection()"
|
|
class="text-sm text-blue-600 hover:text-blue-800">
|
|
선택 해제
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-3">
|
|
<!-- 노트북 할당 -->
|
|
<select x-model="bulkNotebookId"
|
|
class="px-3 py-2 border border-blue-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm">
|
|
<option value="">노트북 선택...</option>
|
|
<template x-for="notebook in availableNotebooks" :key="notebook.id">
|
|
<option :value="notebook.id" x-text="notebook.title"></option>
|
|
</template>
|
|
</select>
|
|
|
|
<button @click="assignToNotebook()"
|
|
:disabled="!bulkNotebookId"
|
|
:class="!bulkNotebookId ? 'bg-gray-400 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700'"
|
|
class="px-4 py-2 text-white rounded-lg transition-colors text-sm">
|
|
노트북에 할당
|
|
</button>
|
|
|
|
<button @click="showCreateNotebookModal = true"
|
|
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors text-sm">
|
|
새 노트북 만들기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 검색 및 필터 -->
|
|
<div class="bg-white rounded-lg shadow-sm border p-6 mb-8">
|
|
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
|
|
<!-- 검색 -->
|
|
<div class="md:col-span-2">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">검색</label>
|
|
<div class="relative">
|
|
<input type="text"
|
|
x-model="searchQuery"
|
|
@input="debounceSearch()"
|
|
placeholder="제목이나 내용으로 검색..."
|
|
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 노트북 필터 -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">노트북</label>
|
|
<select x-model="selectedNotebook" @change="loadNotes()"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
<option value="">전체</option>
|
|
<option value="unassigned">미분류</option>
|
|
<template x-for="notebook in availableNotebooks" :key="notebook.id">
|
|
<option :value="notebook.id" x-text="notebook.title"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 노트 타입 필터 -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">타입</label>
|
|
<select x-model="selectedType" @change="loadNotes()"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
<option value="">전체</option>
|
|
<option value="note">일반 노트</option>
|
|
<option value="research">연구 노트</option>
|
|
<option value="summary">요약</option>
|
|
<option value="idea">아이디어</option>
|
|
<option value="guide">가이드</option>
|
|
<option value="reference">참고 자료</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 공개 상태 필터 -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">상태</label>
|
|
<select x-model="publishedOnly" @change="loadNotes()"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
<option :value="false">전체</option>
|
|
<option :value="true">공개만</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 노트 목록 -->
|
|
<div class="bg-white rounded-lg shadow-sm border">
|
|
<!-- 로딩 상태 -->
|
|
<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">노트를 불러오는 중...</p>
|
|
</div>
|
|
|
|
<!-- 노트 카드들 -->
|
|
<div x-show="!loading && notes.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 p-6">
|
|
<template x-for="note in notes" :key="note.id">
|
|
<div class="border border-gray-200 rounded-lg p-6 hover:shadow-md transition-shadow cursor-pointer relative"
|
|
:class="selectedNotes.includes(note.id) ? 'ring-2 ring-blue-500 bg-blue-50' : ''"
|
|
@click="viewNote(note.id)">
|
|
|
|
<!-- 선택 체크박스 -->
|
|
<div class="absolute top-4 left-4">
|
|
<input type="checkbox"
|
|
:checked="selectedNotes.includes(note.id)"
|
|
@click.stop="toggleNoteSelection(note.id)"
|
|
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
|
</div>
|
|
|
|
<!-- 노트 헤더 -->
|
|
<div class="flex items-start justify-between mb-4 ml-8">
|
|
<div class="flex-1">
|
|
<h3 class="text-lg font-semibold text-gray-900 line-clamp-2 mb-2" x-text="note.title"></h3>
|
|
<div class="flex items-center space-x-2 text-sm text-gray-500">
|
|
<span class="px-2 py-1 bg-gray-100 rounded-full text-xs" x-text="getNoteTypeLabel(note.note_type)"></span>
|
|
<span x-show="note.is_published" class="px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs">공개</span>
|
|
<span x-show="!note.is_published" class="px-2 py-1 bg-yellow-100 text-yellow-800 rounded-full text-xs">초안</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-2 ml-4">
|
|
<button @click.stop="editNote(note.id)"
|
|
class="p-2 text-gray-400 hover:text-blue-600 transition-colors"
|
|
title="편집">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button @click.stop="deleteNote(note)"
|
|
class="p-2 text-gray-400 hover:text-red-600 transition-colors"
|
|
title="삭제">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 태그 -->
|
|
<div x-show="note.tags && note.tags.length > 0" class="mb-4">
|
|
<div class="flex flex-wrap gap-1">
|
|
<template x-for="tag in note.tags.slice(0, 3)" :key="tag">
|
|
<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full" x-text="tag"></span>
|
|
</template>
|
|
<span x-show="note.tags.length > 3" class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full">
|
|
+<span x-text="note.tags.length - 3"></span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 통계 정보 -->
|
|
<div class="flex items-center justify-between text-sm text-gray-500 mb-4">
|
|
<div class="flex items-center space-x-4">
|
|
<span class="flex items-center">
|
|
<i class="fas fa-font mr-1"></i>
|
|
<span x-text="note.word_count"></span>자
|
|
</span>
|
|
<span class="flex items-center">
|
|
<i class="fas fa-clock mr-1"></i>
|
|
<span x-text="note.reading_time"></span>분
|
|
</span>
|
|
<span x-show="note.child_count > 0" class="flex items-center">
|
|
<i class="fas fa-sitemap mr-1"></i>
|
|
<span x-text="note.child_count"></span>개
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 작성 정보 -->
|
|
<div class="text-xs text-gray-400 border-t pt-3">
|
|
<div class="flex items-center justify-between">
|
|
<span x-text="note.created_by"></span>
|
|
<span x-text="formatDate(note.updated_at)"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- 빈 상태 -->
|
|
<div x-show="!loading && notes.length === 0" class="p-8 text-center">
|
|
<i class="fas fa-sticky-note text-gray-400 text-4xl mb-4"></i>
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">노트가 없습니다</h3>
|
|
<p class="text-gray-500 mb-6">첫 번째 노트를 작성해보세요</p>
|
|
<button @click="createNewNote()"
|
|
class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700">
|
|
<i class="fas fa-plus mr-2"></i>
|
|
새 노트 작성
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- 노트북 생성 모달 -->
|
|
<div x-show="showCreateNotebookModal"
|
|
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 flex items-center justify-center z-50 p-4">
|
|
|
|
<div class="bg-white rounded-lg max-w-md w-full p-6"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 transform scale-95"
|
|
x-transition:enter-end="opacity-100 transform scale-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 transform scale-100"
|
|
x-transition:leave-end="opacity-0 transform scale-95">
|
|
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h3 class="text-lg font-semibold text-gray-900">새 노트북 만들기</h3>
|
|
<button @click="closeCreateNotebookModal()" class="text-gray-400 hover:text-gray-600">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<form @submit.prevent="createNotebookAndAssign()">
|
|
<!-- 제목 -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">노트북 이름 *</label>
|
|
<input type="text"
|
|
x-model="newNotebookForm.name"
|
|
placeholder="노트북 이름을 입력하세요..."
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
required>
|
|
</div>
|
|
|
|
<!-- 설명 -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">설명</label>
|
|
<textarea x-model="newNotebookForm.description"
|
|
rows="3"
|
|
placeholder="노트북에 대한 설명을 입력하세요..."
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"></textarea>
|
|
</div>
|
|
|
|
<!-- 색상과 아이콘 -->
|
|
<div class="grid grid-cols-2 gap-4 mb-6">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">색상</label>
|
|
<div class="flex flex-wrap gap-2">
|
|
<template x-for="color in availableColors" :key="color">
|
|
<button type="button"
|
|
@click="newNotebookForm.color = color"
|
|
:class="newNotebookForm.color === color ? 'ring-2 ring-offset-2 ring-gray-400' : ''"
|
|
class="w-8 h-8 rounded-full border-2 border-white shadow-sm"
|
|
:style="`background-color: ${color}`">
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">아이콘</label>
|
|
<select x-model="newNotebookForm.icon"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
<template x-for="icon in availableIcons" :key="icon.value">
|
|
<option :value="icon.value" x-text="icon.label"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 선택된 노트 정보 -->
|
|
<div x-show="selectedNotes.length > 0" class="mb-4 p-3 bg-blue-50 rounded-lg">
|
|
<p class="text-sm text-blue-800">
|
|
<i class="fas fa-info-circle mr-1"></i>
|
|
선택된 <span x-text="selectedNotes.length"></span>개의 노트가 이 노트북에 할당됩니다.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- 버튼 -->
|
|
<div class="flex justify-end space-x-3">
|
|
<button type="button"
|
|
@click="closeCreateNotebookModal()"
|
|
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors">
|
|
취소
|
|
</button>
|
|
<button type="submit"
|
|
:disabled="creatingNotebook || !newNotebookForm.name"
|
|
:class="creatingNotebook || !newNotebookForm.name ? 'bg-gray-400 cursor-not-allowed' : 'bg-green-600 hover:bg-green-700'"
|
|
class="px-4 py-2 text-white rounded-lg transition-colors">
|
|
<span x-show="!creatingNotebook">생성 및 할당</span>
|
|
<span x-show="creatingNotebook">생성 중...</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- JavaScript 파일들 -->
|
|
<script src="/static/js/header-loader.js?v=2025012603"></script>
|
|
<script src="/static/js/api.js?v=2025012607"></script>
|
|
<script src="/static/js/auth.js?v=2025012351"></script>
|
|
<script src="/static/js/notes.js?v=2025012610"></script>
|
|
</body>
|
|
</html>
|