🚀 배포용: PDF 뷰어 개선 및 서적별 UI 데본씽크 스타일 적용

 주요 개선사항:
- PDF API 500 에러 수정 (한글 파일명 UTF-8 인코딩 처리)
- PDF 뷰어 기능 완전 구현 (PDF.js 통합, 네비게이션, 확대/축소)
- 서적별 문서 그룹화 UI 데본씽크 스타일로 개선
- PDF Manager 페이지 서적별 보기 기능 추가
- Alpine.js 로드 순서 최적화로 JavaScript 에러 해결

🎨 UI/UX 개선:
- 확장/축소 가능한 아코디언 스타일 서적 목록
- 간결하고 직관적인 데본씽크 스타일 인터페이스
- PDF 상태 표시 (HTML 연결, 서적 분류)
- 반응형 디자인 및 부드러운 애니메이션

🔧 기술적 개선:
- PDF.js 워커 설정 및 토큰 인증 처리
- 서적별 PDF 자동 그룹화 로직
- Alpine.js 컴포넌트 초기화 최적화
This commit is contained in:
hyungi
2025-09-05 07:13:49 +09:00
commit cfb9485d4f
170 changed files with 41113 additions and 0 deletions

View File

@@ -0,0 +1,268 @@
/**
* BookmarkManager 모듈
* 북마크 관리
*/
class BookmarkManager {
constructor(api) {
this.api = api;
// 캐싱된 API 사용 (사용 가능한 경우)
this.cachedApi = window.cachedApi || api;
this.bookmarks = [];
this.bookmarkForm = {
title: '',
description: ''
};
this.editingBookmark = null;
this.currentScrollPosition = null;
}
/**
* 북마크 데이터 로드
*/
async loadBookmarks(documentId) {
try {
this.bookmarks = await this.cachedApi.get('/bookmarks', { document_id: documentId }, { category: 'bookmarks' }).catch(() => []);
return this.bookmarks || [];
} catch (error) {
console.error('북마크 로드 실패:', error);
return [];
}
}
/**
* 북마크 추가
*/
async addBookmark(document) {
const scrollPosition = window.scrollY;
this.bookmarkForm = {
title: `${document.title} - ${new Date().toLocaleString()}`,
description: ''
};
this.currentScrollPosition = scrollPosition;
// ViewerCore의 모달 상태 업데이트
if (window.documentViewerInstance) {
window.documentViewerInstance.showBookmarkModal = true;
}
}
/**
* 북마크 편집
*/
editBookmark(bookmark) {
this.editingBookmark = bookmark;
this.bookmarkForm = {
title: bookmark.title,
description: bookmark.description || ''
};
// ViewerCore의 모달 상태 업데이트
if (window.documentViewerInstance) {
window.documentViewerInstance.showBookmarkModal = true;
}
}
/**
* 북마크 저장
*/
async saveBookmark(documentId) {
try {
// ViewerCore의 로딩 상태 업데이트
if (window.documentViewerInstance) {
window.documentViewerInstance.bookmarkLoading = true;
}
const bookmarkData = {
title: this.bookmarkForm.title,
description: this.bookmarkForm.description,
scroll_position: this.currentScrollPosition || 0
};
if (this.editingBookmark) {
// 북마크 수정
const updatedBookmark = await this.api.updateBookmark(this.editingBookmark.id, bookmarkData);
const index = this.bookmarks.findIndex(b => b.id === this.editingBookmark.id);
if (index !== -1) {
this.bookmarks[index] = updatedBookmark;
}
} else {
// 새 북마크 생성
bookmarkData.document_id = documentId;
const newBookmark = await this.api.createBookmark(bookmarkData);
this.bookmarks.push(newBookmark);
}
this.closeBookmarkModal();
console.log('북마크 저장 완료');
} catch (error) {
console.error('Failed to save bookmark:', error);
alert('북마크 저장에 실패했습니다');
} finally {
// ViewerCore의 로딩 상태 업데이트
if (window.documentViewerInstance) {
window.documentViewerInstance.bookmarkLoading = false;
}
}
}
/**
* 북마크 삭제
*/
async deleteBookmark(bookmarkId) {
if (!confirm('이 북마크를 삭제하시겠습니까?')) {
return;
}
try {
await this.api.deleteBookmark(bookmarkId);
this.bookmarks = this.bookmarks.filter(b => b.id !== bookmarkId);
console.log('북마크 삭제 완료:', bookmarkId);
} catch (error) {
console.error('Failed to delete bookmark:', error);
alert('북마크 삭제에 실패했습니다');
}
}
/**
* 북마크로 스크롤
*/
scrollToBookmark(bookmark) {
window.scrollTo({
top: bookmark.scroll_position,
behavior: 'smooth'
});
}
/**
* 북마크 모달 닫기
*/
closeBookmarkModal() {
this.editingBookmark = null;
this.bookmarkForm = { title: '', description: '' };
this.currentScrollPosition = null;
// ViewerCore의 모달 상태 업데이트
if (window.documentViewerInstance) {
window.documentViewerInstance.showBookmarkModal = false;
}
}
/**
* 선택된 텍스트로 북마크 생성
*/
async createBookmarkFromSelection(documentId, selectedText, selectedRange) {
if (!selectedText || !selectedRange) return;
try {
// 하이라이트 생성 (북마크는 주황색)
const highlightData = await this.createHighlight(selectedText, selectedRange, '#FFA500');
// 북마크 생성
const bookmarkData = {
highlight_id: highlightData.id,
title: selectedText.substring(0, 50) + (selectedText.length > 50 ? '...' : ''),
description: `선택된 텍스트: "${selectedText}"`
};
const bookmark = await this.api.createBookmark(documentId, bookmarkData);
this.bookmarks.push(bookmark);
console.log('선택 텍스트 북마크 생성 완료:', bookmark);
alert('북마크가 생성되었습니다.');
} catch (error) {
console.error('북마크 생성 실패:', error);
alert('북마크 생성에 실패했습니다: ' + error.message);
}
}
/**
* 하이라이트 생성 (북마크용)
* HighlightManager와 연동
*/
async createHighlight(selectedText, selectedRange, color) {
try {
const viewerInstance = window.documentViewerInstance;
if (viewerInstance && viewerInstance.highlightManager) {
// HighlightManager의 상태 설정
viewerInstance.highlightManager.selectedText = selectedText;
viewerInstance.highlightManager.selectedRange = selectedRange;
viewerInstance.highlightManager.selectedHighlightColor = color;
// ViewerCore의 상태도 동기화
viewerInstance.selectedText = selectedText;
viewerInstance.selectedRange = selectedRange;
viewerInstance.selectedHighlightColor = color;
// HighlightManager의 createHighlight 호출
await viewerInstance.highlightManager.createHighlight();
// 생성된 하이라이트 찾기 (가장 최근 생성된 것)
const highlights = viewerInstance.highlightManager.highlights;
if (highlights && highlights.length > 0) {
return highlights[highlights.length - 1];
}
}
// 폴백: 간단한 하이라이트 데이터 반환
console.warn('HighlightManager 연동 실패, 폴백 데이터 사용');
return {
id: Date.now().toString(),
selected_text: selectedText,
color: color,
start_offset: 0,
end_offset: selectedText.length
};
} catch (error) {
console.error('하이라이트 생성 실패:', error);
// 폴백: 간단한 하이라이트 데이터 반환
return {
id: Date.now().toString(),
selected_text: selectedText,
color: color,
start_offset: 0,
end_offset: selectedText.length
};
}
}
/**
* 북마크 모드 활성화
*/
activateBookmarkMode() {
console.log('🔖 북마크 모드 활성화');
// 현재 선택된 텍스트가 있는지 확인
const selection = window.getSelection();
if (selection.rangeCount > 0 && !selection.isCollapsed) {
const selectedText = selection.toString().trim();
if (selectedText.length > 0) {
// ViewerCore의 선택된 텍스트 상태 업데이트
if (window.documentViewerInstance) {
window.documentViewerInstance.selectedText = selectedText;
window.documentViewerInstance.selectedRange = selection.getRangeAt(0);
}
this.createBookmarkFromSelection(
window.documentViewerInstance?.documentId,
selectedText,
selection.getRangeAt(0)
);
return;
}
}
// 텍스트 선택 모드 활성화
console.log('📝 텍스트 선택 모드 활성화');
if (window.documentViewerInstance) {
window.documentViewerInstance.activeMode = 'bookmark';
window.documentViewerInstance.showSelectionMessage('텍스트를 선택하세요.');
window.documentViewerInstance.setupTextSelectionListener();
}
}
}
// 전역으로 내보내기
window.BookmarkManager = BookmarkManager;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,413 @@
/**
* UIManager 모듈
* UI 컴포넌트 및 상태 관리
*/
class UIManager {
constructor() {
console.log('🎨 UIManager 초기화 시작');
// UI 상태
this.showNotesPanel = false;
this.showBookmarksPanel = false;
this.showBacklinks = false;
this.activePanel = 'notes';
// 모달 상태
this.showNoteModal = false;
this.showBookmarkModal = false;
this.showLinkModal = false;
this.showNotesModal = false;
this.showBookmarksModal = false;
this.showLinksModal = false;
this.showBacklinksModal = false;
// 기능 메뉴 상태
this.activeFeatureMenu = null;
// 검색 상태
this.searchQuery = '';
this.noteSearchQuery = '';
this.filteredNotes = [];
// 텍스트 선택 모드
this.textSelectorUISetup = false;
console.log('✅ UIManager 초기화 완료');
}
/**
* 기능 메뉴 토글
*/
toggleFeatureMenu(feature) {
if (this.activeFeatureMenu === feature) {
this.activeFeatureMenu = null;
} else {
this.activeFeatureMenu = feature;
// 해당 기능의 모달 표시
switch(feature) {
case 'link':
this.showLinksModal = true;
break;
case 'memo':
this.showNotesModal = true;
break;
case 'bookmark':
this.showBookmarksModal = true;
break;
case 'backlink':
this.showBacklinksModal = true;
break;
}
}
}
/**
* 노트 모달 열기
*/
openNoteModal(highlight = null) {
console.log('📝 노트 모달 열기');
if (highlight) {
console.log('🔍 하이라이트와 연결된 노트 모달:', highlight);
}
this.showNoteModal = true;
}
/**
* 노트 모달 닫기
*/
closeNoteModal() {
this.showNoteModal = false;
}
/**
* 링크 모달 열기
*/
openLinkModal() {
console.log('🔗 링크 모달 열기');
console.log('🔗 showLinksModal 설정 전:', this.showLinksModal);
this.showLinksModal = true;
this.showLinkModal = true; // 기존 호환성
console.log('🔗 showLinksModal 설정 후:', this.showLinksModal);
}
/**
* 링크 모달 닫기
*/
closeLinkModal() {
this.showLinksModal = false;
this.showLinkModal = false;
}
/**
* 북마크 모달 닫기
*/
closeBookmarkModal() {
this.showBookmarkModal = false;
}
/**
* 검색 결과 하이라이트
*/
highlightSearchResults(element, searchText) {
if (!searchText.trim()) return;
// 기존 검색 하이라이트 제거
const existingHighlights = element.querySelectorAll('.search-highlight');
existingHighlights.forEach(highlight => {
const parent = highlight.parentNode;
parent.replaceChild(document.createTextNode(highlight.textContent), highlight);
parent.normalize();
});
if (!searchText) return;
// 새로운 검색 하이라이트 적용
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null,
false
);
const textNodes = [];
let node;
while (node = walker.nextNode()) {
textNodes.push(node);
}
const searchRegex = new RegExp(`(${searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
textNodes.forEach(textNode => {
const text = textNode.textContent;
if (searchRegex.test(text)) {
const highlightedHTML = text.replace(searchRegex, '<span class="search-highlight bg-yellow-200">$1</span>');
const wrapper = document.createElement('div');
wrapper.innerHTML = highlightedHTML;
const fragment = document.createDocumentFragment();
while (wrapper.firstChild) {
fragment.appendChild(wrapper.firstChild);
}
textNode.parentNode.replaceChild(fragment, textNode);
}
});
}
/**
* 노트 검색 필터링
*/
filterNotes(notes) {
if (!this.noteSearchQuery.trim()) {
this.filteredNotes = notes;
return notes;
}
const query = this.noteSearchQuery.toLowerCase();
this.filteredNotes = notes.filter(note =>
note.content.toLowerCase().includes(query) ||
(note.tags && note.tags.some(tag => tag.toLowerCase().includes(query)))
);
return this.filteredNotes;
}
/**
* 텍스트 선택 모드 UI 설정
*/
setupTextSelectorUI() {
console.log('🔧 setupTextSelectorUI 함수 실행됨');
// 중복 실행 방지
if (this.textSelectorUISetup) {
console.log('⚠️ 텍스트 선택 모드 UI가 이미 설정됨 - 중복 실행 방지');
return;
}
// 헤더 숨기기
const header = document.querySelector('header');
if (header) {
header.style.display = 'none';
}
// 안내 메시지 표시
const messageDiv = document.createElement('div');
messageDiv.id = 'text-selection-message';
messageDiv.className = 'fixed top-4 left-1/2 transform -translate-x-1/2 bg-blue-500 text-white px-6 py-3 rounded-lg shadow-lg z-50';
messageDiv.innerHTML = `
<div class="flex items-center space-x-2">
<i class="fas fa-mouse-pointer"></i>
<span>연결할 텍스트를 드래그하여 선택해주세요</span>
</div>
`;
document.body.appendChild(messageDiv);
this.textSelectorUISetup = true;
console.log('✅ 텍스트 선택 모드 UI 설정 완료');
}
/**
* 텍스트 선택 확인 UI 표시
*/
showTextSelectionConfirm(selectedText, startOffset, endOffset) {
// 기존 확인 UI 제거
const existingConfirm = document.getElementById('text-selection-confirm');
if (existingConfirm) {
existingConfirm.remove();
}
// 확인 UI 생성
const confirmDiv = document.createElement('div');
confirmDiv.id = 'text-selection-confirm';
confirmDiv.className = 'fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white border border-gray-300 rounded-lg shadow-xl p-6 z-50 max-w-md';
confirmDiv.innerHTML = `
<div class="mb-4">
<h3 class="text-lg font-semibold text-gray-800 mb-2">선택된 텍스트</h3>
<div class="bg-blue-50 p-3 rounded border-l-4 border-blue-500">
<p class="text-blue-800 font-medium">"${selectedText}"</p>
</div>
</div>
<div class="flex justify-end space-x-3">
<button onclick="window.cancelTextSelection()"
class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors">
취소
</button>
<button onclick="window.confirmTextSelection('${selectedText}', ${startOffset}, ${endOffset})"
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
이 텍스트로 링크 생성
</button>
</div>
`;
document.body.appendChild(confirmDiv);
}
/**
* 링크 생성 UI 표시
*/
showLinkCreationUI() {
console.log('🔗 링크 생성 UI 표시');
this.openLinkModal();
}
/**
* 성공 메시지 표시
*/
showSuccessMessage(message) {
const messageDiv = document.createElement('div');
messageDiv.className = 'fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 flex items-center space-x-2';
messageDiv.innerHTML = `
<i class="fas fa-check-circle"></i>
<span>${message}</span>
`;
document.body.appendChild(messageDiv);
// 3초 후 자동 제거
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.parentNode.removeChild(messageDiv);
}
}, 3000);
}
/**
* 오류 메시지 표시
*/
showErrorMessage(message) {
const messageDiv = document.createElement('div');
messageDiv.className = 'fixed top-4 right-4 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 flex items-center space-x-2';
messageDiv.innerHTML = `
<i class="fas fa-exclamation-circle"></i>
<span>${message}</span>
`;
document.body.appendChild(messageDiv);
// 5초 후 자동 제거
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.parentNode.removeChild(messageDiv);
}
}, 5000);
}
/**
* 로딩 스피너 표시
*/
showLoadingSpinner(container, message = '로딩 중...') {
const spinner = document.createElement('div');
spinner.className = 'flex items-center justify-center py-8';
spinner.innerHTML = `
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mr-3"></div>
<span class="text-gray-600">${message}</span>
`;
if (container) {
container.innerHTML = '';
container.appendChild(spinner);
}
return spinner;
}
/**
* 로딩 스피너 제거
*/
hideLoadingSpinner(container) {
if (container) {
const spinner = container.querySelector('.animate-spin');
if (spinner && spinner.parentElement) {
spinner.parentElement.remove();
}
}
}
/**
* 모달 배경 클릭 시 닫기
*/
handleModalBackgroundClick(event, modalId) {
if (event.target === event.currentTarget) {
switch(modalId) {
case 'notes':
this.closeNoteModal();
break;
case 'bookmarks':
this.closeBookmarkModal();
break;
case 'links':
this.closeLinkModal();
break;
}
}
}
/**
* 패널 토글
*/
togglePanel(panelType) {
switch(panelType) {
case 'notes':
this.showNotesPanel = !this.showNotesPanel;
if (this.showNotesPanel) {
this.showBookmarksPanel = false;
this.activePanel = 'notes';
}
break;
case 'bookmarks':
this.showBookmarksPanel = !this.showBookmarksPanel;
if (this.showBookmarksPanel) {
this.showNotesPanel = false;
this.activePanel = 'bookmarks';
}
break;
case 'backlinks':
this.showBacklinks = !this.showBacklinks;
break;
}
}
/**
* 검색 쿼리 업데이트
*/
updateSearchQuery(query) {
this.searchQuery = query;
}
/**
* 노트 검색 쿼리 업데이트
*/
updateNoteSearchQuery(query) {
this.noteSearchQuery = query;
}
/**
* UI 상태 초기화
*/
resetUIState() {
this.showNotesPanel = false;
this.showBookmarksPanel = false;
this.showBacklinks = false;
this.activePanel = 'notes';
this.activeFeatureMenu = null;
this.searchQuery = '';
this.noteSearchQuery = '';
this.filteredNotes = [];
}
/**
* 모든 모달 닫기
*/
closeAllModals() {
this.showNoteModal = false;
this.showBookmarkModal = false;
this.showLinkModal = false;
this.showNotesModal = false;
this.showBookmarksModal = false;
this.showLinksModal = false;
this.showBacklinksModal = false;
}
}
// 전역으로 내보내기
window.UIManager = UIManager;