🎯 주요 기능: - 하이라이트 메모 내용 별도 검색 (highlight_note 타입) - PDF/HTML 본문 전체 텍스트 검색 (OCR 데이터 활용) - 검색 결과 미리보기 모달 (전체 내용 로드) - 메모 트리 노드 검색 지원 - 노트 문서 통합 검색 🔧 백엔드 개선: - search_highlight_notes: 하이라이트 메모 내용 검색 - search_document_content: HTML/PDF 본문 검색 (BeautifulSoup) - search_memo_nodes: 메모 트리 노드 검색 - search_note_documents: 노트 문서 검색 - extract_search_context: 검색어 주변 컨텍스트 추출 🎨 프론트엔드 기능: - 통합 검색 UI (/search.html) 완전 구현 - 검색 필터: 문서/노트/메모/하이라이트/메모/본문 - 미리보기 모달: 전체 내용 로드 및 표시 - 검색 결과 하이라이트 및 컨텍스트 표시 - 타입별 배지 및 관련도 점수 표시 📱 사용자 경험: - 실시간 검색 디바운스 (500ms) - 검색어 자동완성 제안 - 검색 통계 및 성능 표시 - 빠른 검색 예시 버튼 - 새 탭에서 결과 열기 🔗 네비게이션 통합: - 헤더에 '통합 검색' 링크 추가 - 페이지별 활성 상태 관리
361 lines
15 KiB
HTML
361 lines
15 KiB
HTML
<!-- 공통 헤더 컴포넌트 -->
|
|
<header class="header-modern fade-in">
|
|
<div class="max-w-full mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between items-center h-16">
|
|
<!-- 로고 -->
|
|
<div class="flex items-center space-x-2">
|
|
<i class="fas fa-book text-blue-600 text-xl"></i>
|
|
<h1 class="text-xl font-bold text-gray-900">Document Server</h1>
|
|
</div>
|
|
|
|
<!-- 메인 네비게이션 - 3가지 기능 -->
|
|
<nav class="flex space-x-6">
|
|
<!-- 문서 관리 시스템 -->
|
|
<div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
|
|
<a href="index.html" class="nav-link" id="doc-nav-link">
|
|
<i class="fas fa-folder-open"></i>
|
|
<span>문서 관리</span>
|
|
<i class="fas fa-chevron-down text-xs ml-1"></i>
|
|
</a>
|
|
<div x-show="open" x-transition class="nav-dropdown">
|
|
<a href="index.html" class="nav-dropdown-item" id="index-nav-item">
|
|
<i class="fas fa-th-large mr-2 text-blue-500"></i>문서 관리
|
|
</a>
|
|
<a href="pdf-manager.html" class="nav-dropdown-item" id="pdf-manager-nav-item">
|
|
<i class="fas fa-file-pdf mr-2 text-red-500"></i>PDF 관리
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 통합 검색 -->
|
|
<a href="search.html" class="nav-link" id="search-nav-link">
|
|
<i class="fas fa-search"></i>
|
|
<span>통합 검색</span>
|
|
</a>
|
|
|
|
<!-- 소설 관리 시스템 -->
|
|
<div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
|
|
<a href="memo-tree.html" class="nav-link" id="novel-nav-link">
|
|
<i class="fas fa-feather-alt"></i>
|
|
<span>소설 관리</span>
|
|
<i class="fas fa-chevron-down text-xs ml-1"></i>
|
|
</a>
|
|
<div x-show="open" x-transition class="nav-dropdown">
|
|
<a href="memo-tree.html" class="nav-dropdown-item" id="memo-tree-nav-item">
|
|
<i class="fas fa-sitemap mr-2 text-purple-500"></i>트리 뷰
|
|
</a>
|
|
<a href="story-view.html" class="nav-dropdown-item" id="story-view-nav-item">
|
|
<i class="fas fa-book-open mr-2 text-orange-500"></i>스토리 뷰
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 노트 관리 시스템 -->
|
|
<div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
|
|
<a href="notes.html" class="nav-link" id="notes-nav-link">
|
|
<i class="fas fa-sticky-note"></i>
|
|
<span>노트 관리</span>
|
|
<i class="fas fa-chevron-down text-xs ml-1"></i>
|
|
</a>
|
|
<div x-show="open" x-transition class="nav-dropdown">
|
|
<a href="notebooks.html" class="nav-dropdown-item" id="notebooks-nav-item">
|
|
<i class="fas fa-book mr-2 text-blue-500"></i>노트북 관리
|
|
</a>
|
|
<a href="notes.html" class="nav-dropdown-item" id="notes-list-nav-item">
|
|
<i class="fas fa-list mr-2 text-green-500"></i>노트 목록
|
|
</a>
|
|
<a href="note-editor.html" class="nav-dropdown-item" id="note-editor-nav-item">
|
|
<i class="fas fa-edit mr-2 text-purple-500"></i>새 노트 작성
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- 사용자 메뉴 -->
|
|
<div class="flex items-center space-x-4">
|
|
<!-- PDF 관리 버튼 -->
|
|
<a href="pdf-manager.html" class="nav-link" title="PDF 관리">
|
|
<i class="fas fa-file-pdf text-red-500"></i>
|
|
<span class="hidden sm:inline">PDF</span>
|
|
</a>
|
|
|
|
<!-- 언어 전환 버튼 -->
|
|
<div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
|
|
<button class="nav-link" title="언어 설정">
|
|
<i class="fas fa-globe"></i>
|
|
<span class="hidden sm:inline">한국어</span>
|
|
<i class="fas fa-chevron-down text-xs ml-1"></i>
|
|
</button>
|
|
<div x-show="open" x-transition class="nav-dropdown">
|
|
<button class="nav-dropdown-item" onclick="handleLanguageChange('ko')">
|
|
<i class="fas fa-flag mr-2 text-blue-500"></i>한국어
|
|
</button>
|
|
<button class="nav-dropdown-item" onclick="handleLanguageChange('en')">
|
|
<i class="fas fa-flag mr-2 text-red-500"></i>English
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 로그인/로그아웃 -->
|
|
<div class="flex items-center space-x-3" id="user-menu">
|
|
<!-- 로그인된 사용자 -->
|
|
<div class="hidden" id="logged-in-menu">
|
|
<span class="text-sm text-gray-600" id="user-name">User</span>
|
|
<button onclick="handleLogout()" class="btn-improved btn-secondary-improved text-sm">
|
|
<i class="fas fa-sign-out-alt"></i> 로그아웃
|
|
</button>
|
|
</div>
|
|
<!-- 로그인 버튼 -->
|
|
<div class="hidden" id="login-button">
|
|
<button onclick="handleLogin()" class="btn-improved btn-primary-improved">
|
|
<i class="fas fa-sign-in-alt"></i> 로그인
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- 헤더 관련 스타일 -->
|
|
<style>
|
|
/* 네비게이션 링크 스타일 */
|
|
.nav-link {
|
|
@apply text-gray-600 hover:text-blue-600 flex items-center space-x-1 py-2 px-3 rounded-lg transition-all duration-200;
|
|
}
|
|
|
|
.nav-link.active {
|
|
@apply text-blue-600 bg-blue-50 font-medium;
|
|
}
|
|
|
|
.nav-link:hover {
|
|
@apply bg-gray-50;
|
|
}
|
|
|
|
/* 드롭다운 메뉴 스타일 */
|
|
.nav-dropdown {
|
|
@apply absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg py-2 min-w-44 z-50;
|
|
}
|
|
|
|
.nav-dropdown-item {
|
|
@apply block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-150;
|
|
}
|
|
|
|
.nav-dropdown-item.active {
|
|
@apply text-blue-600 bg-blue-50 font-medium;
|
|
}
|
|
|
|
/* 헤더 모던 스타일 */
|
|
.header-modern {
|
|
@apply bg-white border-b border-gray-200 shadow-sm;
|
|
}
|
|
|
|
/* 언어 전환 스타일 */
|
|
.lang-ko .lang-en,
|
|
.lang-ko [lang="en"],
|
|
.lang-ko .english {
|
|
display: none !important;
|
|
}
|
|
|
|
.lang-en .lang-ko,
|
|
.lang-en [lang="ko"],
|
|
.lang-en .korean {
|
|
display: none !important;
|
|
}
|
|
</style>
|
|
|
|
<!-- 헤더 관련 JavaScript 함수들 -->
|
|
<script>
|
|
// 헤더 관련 유틸리티 함수들
|
|
window.headerUtils = {
|
|
getCurrentPage() {
|
|
const path = window.location.pathname;
|
|
const filename = path.split('/').pop().replace('.html', '');
|
|
return filename || 'index';
|
|
},
|
|
|
|
isDocumentPage() {
|
|
const page = this.getCurrentPage();
|
|
return ['index', 'hierarchy', 'pdf-manager'].includes(page);
|
|
},
|
|
|
|
isNovelPage() {
|
|
const page = this.getCurrentPage();
|
|
return ['memo-tree', 'story-view', 'story-reader'].includes(page);
|
|
},
|
|
|
|
isNotePage() {
|
|
const page = this.getCurrentPage();
|
|
return ['notes', 'note-editor'].includes(page);
|
|
}
|
|
};
|
|
|
|
// Alpine.js 전역 함수로 등록 - 즉시 실행
|
|
if (typeof Alpine !== 'undefined') {
|
|
// Alpine이 이미 로드된 경우
|
|
Alpine.store('header', {
|
|
getCurrentPage: () => headerUtils.getCurrentPage(),
|
|
isDocumentPage: () => headerUtils.isDocumentPage(),
|
|
isNovelPage: () => headerUtils.isNovelPage(),
|
|
isNotePage: () => headerUtils.isNotePage(),
|
|
});
|
|
} else {
|
|
// Alpine 로드 대기
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.store('header', {
|
|
getCurrentPage: () => headerUtils.getCurrentPage(),
|
|
isDocumentPage: () => headerUtils.isDocumentPage(),
|
|
isNovelPage: () => headerUtils.isNovelPage(),
|
|
isNotePage: () => headerUtils.isNotePage(),
|
|
});
|
|
});
|
|
}
|
|
|
|
// 전역 함수로도 등록 (Alpine 외부에서도 사용 가능)
|
|
window.getCurrentPage = () => headerUtils.getCurrentPage();
|
|
window.isDocumentPage = () => headerUtils.isDocumentPage();
|
|
window.isMemoPage = () => headerUtils.isMemoPage();
|
|
|
|
// 로그인 관련 함수들
|
|
window.handleLogin = () => {
|
|
console.log('🔐 handleLogin 호출됨');
|
|
|
|
// Alpine.js 컨텍스트에서 함수 찾기
|
|
const bodyElement = document.querySelector('body');
|
|
if (bodyElement && bodyElement._x_dataStack) {
|
|
const alpineData = bodyElement._x_dataStack[0];
|
|
if (alpineData && typeof alpineData.openLoginModal === 'function') {
|
|
console.log('✅ Alpine 컨텍스트에서 openLoginModal 호출');
|
|
alpineData.openLoginModal();
|
|
return;
|
|
}
|
|
if (alpineData && alpineData.showLoginModal !== undefined) {
|
|
console.log('✅ Alpine 컨텍스트에서 showLoginModal 설정');
|
|
alpineData.showLoginModal = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 전역 함수로 시도
|
|
if (typeof window.openLoginModal === 'function') {
|
|
console.log('✅ 전역 openLoginModal 호출');
|
|
window.openLoginModal();
|
|
return;
|
|
}
|
|
|
|
// 직접 이벤트 발생
|
|
console.log('🔄 커스텀 이벤트로 로그인 모달 열기');
|
|
document.dispatchEvent(new CustomEvent('open-login-modal'));
|
|
};
|
|
|
|
window.handleLogout = () => {
|
|
// 각 페이지의 로그아웃 함수 호출
|
|
if (typeof logout === 'function') {
|
|
logout();
|
|
} else {
|
|
console.log('로그아웃 함수를 찾을 수 없습니다.');
|
|
}
|
|
};
|
|
|
|
// 사용자 상태 업데이트 함수
|
|
window.updateUserMenu = (user) => {
|
|
const loggedInMenu = document.getElementById('logged-in-menu');
|
|
const loginButton = document.getElementById('login-button');
|
|
const userName = document.getElementById('user-name');
|
|
|
|
if (user) {
|
|
// 로그인된 상태
|
|
if (loggedInMenu) loggedInMenu.classList.remove('hidden');
|
|
if (loginButton) loginButton.classList.add('hidden');
|
|
if (userName) userName.textContent = user.username || user.full_name || user.email || 'User';
|
|
} else {
|
|
// 로그아웃된 상태
|
|
if (loggedInMenu) loggedInMenu.classList.add('hidden');
|
|
if (loginButton) loginButton.classList.remove('hidden');
|
|
}
|
|
};
|
|
|
|
// 언어 토글 함수 (전역)
|
|
// 통합 언어 변경 함수
|
|
window.handleLanguageChange = (lang) => {
|
|
console.log('🌐 언어 변경 요청:', lang);
|
|
localStorage.setItem('preferred_language', lang);
|
|
|
|
// HTML lang 속성 변경
|
|
document.documentElement.lang = lang;
|
|
|
|
// body에 언어 클래스 추가/제거
|
|
document.body.classList.remove('lang-ko', 'lang-en');
|
|
document.body.classList.add(`lang-${lang}`);
|
|
|
|
// 뷰어 페이지인 경우 뷰어의 언어 전환 함수 호출
|
|
if (window.documentViewerInstance && typeof window.documentViewerInstance.toggleLanguage === 'function') {
|
|
window.documentViewerInstance.toggleLanguage();
|
|
}
|
|
|
|
// 문서 내용에서 언어별 요소 처리
|
|
toggleDocumentLanguage(lang);
|
|
|
|
// 헤더 언어 표시 업데이트
|
|
updateLanguageDisplay(lang);
|
|
|
|
console.log(`✅ 언어가 ${lang === 'ko' ? '한국어' : 'English'}로 설정되었습니다.`);
|
|
};
|
|
|
|
// 문서 내용 언어 전환
|
|
function toggleDocumentLanguage(lang) {
|
|
// 언어별 요소 숨기기/보이기
|
|
const koElements = document.querySelectorAll('[lang="ko"], .lang-ko, .korean');
|
|
const enElements = document.querySelectorAll('[lang="en"], .lang-en, .english');
|
|
|
|
if (lang === 'ko') {
|
|
koElements.forEach(el => el.style.display = '');
|
|
enElements.forEach(el => el.style.display = 'none');
|
|
} else {
|
|
koElements.forEach(el => el.style.display = 'none');
|
|
enElements.forEach(el => el.style.display = '');
|
|
}
|
|
|
|
console.log(`🔄 문서 언어 전환: ${koElements.length}개 한국어, ${enElements.length}개 영어 요소 처리`);
|
|
}
|
|
|
|
// 헤더 언어 표시 업데이트
|
|
function updateLanguageDisplay(lang) {
|
|
const langSpan = document.querySelector('.nav-link span:contains("한국어"), .nav-link span:contains("English")');
|
|
if (langSpan) {
|
|
langSpan.textContent = lang === 'ko' ? '한국어' : 'English';
|
|
}
|
|
}
|
|
|
|
// 기존 setLanguage 함수 (호환성 유지)
|
|
window.setLanguage = window.handleLanguageChange;
|
|
|
|
window.toggleLanguage = () => {
|
|
console.log('🌐 언어 토글 기능 (미구현)');
|
|
// 향후 다국어 지원 시 구현
|
|
};
|
|
|
|
// 헤더 로드 완료 후 언어 설정 적용
|
|
document.addEventListener('headerLoaded', () => {
|
|
console.log('🔧 헤더 로드 완료 - 언어 전환 함수 등록');
|
|
const savedLang = localStorage.getItem('preferred_language') || 'ko';
|
|
console.log('💾 저장된 언어 설정 적용:', savedLang);
|
|
|
|
// 약간의 지연 후 적용 (DOM 완전 로드 대기)
|
|
setTimeout(() => {
|
|
handleLanguageChange(savedLang);
|
|
}, 100);
|
|
});
|
|
|
|
// DOMContentLoaded 백업 (헤더가 직접 로드된 경우)
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
setTimeout(() => {
|
|
if (typeof window.handleLanguageChange === 'function') {
|
|
const savedLang = localStorage.getItem('preferred_language') || 'ko';
|
|
console.log('💾 DOMContentLoaded - 언어 설정 적용:', savedLang);
|
|
handleLanguageChange(savedLang);
|
|
}
|
|
}, 200);
|
|
});
|
|
</script>
|