/** * ViewerCore - 문서 뷰어 Alpine.js 컴포넌트 * 모든 모듈을 통합하고 Alpine.js 컴포넌트를 관리합니다. */ window.documentViewer = () => ({ // ==================== 기본 상태 ==================== loading: true, error: null, document: null, documentId: null, contentType: 'document', // 'document' 또는 'note' navigation: null, // ==================== 데이터 상태 ==================== highlights: [], notes: [], bookmarks: [], documentLinks: [], linkableDocuments: [], backlinks: [], // ==================== 선택 상태 ==================== selectedHighlightColor: '#FFFF00', selectedText: '', selectedRange: null, // ==================== 폼 데이터 ==================== noteForm: { content: '', tags: '' }, bookmarkForm: { title: '', description: '' }, linkForm: { target_document_id: '', selected_text: '', start_offset: 0, end_offset: 0, link_text: '', description: '', link_type: 'text_fragment', // 무조건 텍스트 선택만 지원 target_text: '', target_start_offset: 0, target_end_offset: 0, target_book_id: '' }, // ==================== 언어 및 기타 ==================== isKorean: false, // ==================== UI 상태 (Alpine.js 바인딩용) ==================== searchQuery: '', activeFeatureMenu: null, showLinksModal: false, showLinkModal: false, showNotesModal: false, showBookmarksModal: false, showBacklinksModal: false, showNoteInputModal: false, availableBooks: [], filteredDocuments: [], // ==================== 모듈 인스턴스 ==================== documentLoader: null, highlightManager: null, bookmarkManager: null, linkManager: null, uiManager: null, // ==================== 초기화 플래그 ==================== _initialized: false, // ==================== 초기화 ==================== async init() { // 중복 초기화 방지 if (this._initialized) { console.log('⚠️ 이미 초기화됨, 중복 실행 방지'); return; } this._initialized = true; console.log('🚀 DocumentViewer 초기화 시작'); // 전역 인스턴스 설정 (말풍선에서 함수 호출용) window.documentViewerInstance = this; try { // 모듈 초기화 await this.initializeModules(); // URL 파라미터 처리 this.parseUrlParameters(); // 문서 로드 await this.loadDocument(); console.log('✅ DocumentViewer 초기화 완료'); } catch (error) { console.error('❌ DocumentViewer 초기화 실패:', error); this.error = error.message; this.loading = false; } }, // ==================== 모듈 초기화 (지연 로딩 + 폴백) ==================== async initializeModules() { console.log('🔧 모듈 초기화 시작 (지연 로딩)'); // API 및 캐시 초기화 this.api = new DocumentServerAPI(); // 토큰 설정 (인증 확인) const token = localStorage.getItem('access_token'); if (token) { this.api.setToken(token); console.log('🔐 API 토큰 설정 완료'); } else { console.error('❌ 인증 토큰이 없습니다!'); throw new Error('인증이 필요합니다'); } this.cache = new CacheManager(); this.cachedApi = new CachedAPI(this.api, this.cache); // 직접 모듈 인스턴스 생성 (모든 모듈이 HTML에서 로드됨) if (window.DocumentLoader && window.UIManager && window.HighlightManager && window.LinkManager && window.BookmarkManager) { this.documentLoader = new window.DocumentLoader(this.cachedApi); this.uiManager = new window.UIManager(); this.highlightManager = new window.HighlightManager(this.cachedApi); this.linkManager = new window.LinkManager(this.cachedApi); this.bookmarkManager = new window.BookmarkManager(this.cachedApi); console.log('✅ 모든 모듈 직접 로드 성공'); } else { console.error('❌ 필수 모듈이 로드되지 않음'); console.log('사용 가능한 모듈:', { DocumentLoader: !!window.DocumentLoader, UIManager: !!window.UIManager, HighlightManager: !!window.HighlightManager, LinkManager: !!window.LinkManager, BookmarkManager: !!window.BookmarkManager }); throw new Error('필수 모듈을 로드할 수 없습니다.'); } // UI 상태를 UIManager와 동기화 (모달은 초기화 시 닫힌 상태로) this.syncUIState(); // 초기화 시 모든 모달을 명시적으로 닫기 this.closeAllModals(); // 나머지 모듈들은 백그라운드에서 프리로딩 (지연 로딩 가능한 경우만) if (window.moduleLoader) { window.moduleLoader.preloadModules(['HighlightManager', 'BookmarkManager', 'LinkManager']); } console.log('✅ 모듈 초기화 완료'); }, // ==================== UI 상태 동기화 ==================== syncUIState() { // UIManager의 상태를 Alpine.js 컴포넌트와 동기화 (getter/setter 방식) // 패널 상태 동기화 Object.defineProperty(this, 'showNotesPanel', { get: () => this.uiManager.showNotesPanel, set: (value) => { this.uiManager.showNotesPanel = value; } }); Object.defineProperty(this, 'showBookmarksPanel', { get: () => this.uiManager.showBookmarksPanel, set: (value) => { this.uiManager.showBookmarksPanel = value; } }); Object.defineProperty(this, 'showBacklinks', { get: () => this.uiManager.showBacklinks, set: (value) => { this.uiManager.showBacklinks = value; } }); Object.defineProperty(this, 'activePanel', { get: () => this.uiManager.activePanel, set: (value) => { this.uiManager.activePanel = value; } }); // 모달 상태 동기화 (UIManager와 실시간 연동) this.updateModalStates(); // 검색 상태 동기화 Object.defineProperty(this, 'noteSearchQuery', { get: () => this.uiManager.noteSearchQuery, set: (value) => { this.uiManager.updateNoteSearchQuery(value); } }); Object.defineProperty(this, 'filteredNotes', { get: () => this.uiManager.filteredNotes, set: (value) => { this.uiManager.filteredNotes = value; } }); // 모드 및 핸들러 상태 this.activeMode = null; this.textSelectionHandler = null; this.editingNote = null; this.editingBookmark = null; this.editingLink = null; this.noteLoading = false; this.bookmarkLoading = false; this.linkLoading = false; }, // ==================== 모달 상태 업데이트 ==================== updateModalStates() { // UIManager의 모달 상태를 ViewerCore의 속성에 반영 if (this.uiManager) { this.showLinksModal = this.uiManager.showLinksModal; this.showLinkModal = this.uiManager.showLinkModal; this.showNotesModal = this.uiManager.showNotesModal; this.showBookmarksModal = this.uiManager.showBookmarksModal; this.showBacklinksModal = this.uiManager.showBacklinksModal; this.activeFeatureMenu = this.uiManager.activeFeatureMenu; this.searchQuery = this.uiManager.searchQuery; } }, // ==================== 모든 모달 닫기 ==================== closeAllModals() { console.log('🔒 초기화 시 모든 모달 닫기'); this.showLinksModal = false; this.showLinkModal = false; this.showNotesModal = false; this.showBookmarksModal = false; this.showBacklinksModal = false; this.showNoteInputModal = false; // UIManager에도 반영 if (this.uiManager) { this.uiManager.closeAllModals(); } }, // ==================== URL 파라미터 처리 ==================== parseUrlParameters() { const urlParams = new URLSearchParams(window.location.search); this.documentId = urlParams.get('id'); this.contentType = urlParams.get('type') || 'document'; console.log('🔍 URL 파싱 결과:', { documentId: this.documentId, contentType: this.contentType }); if (!this.documentId) { throw new Error('문서 ID가 필요합니다.'); } }, // ==================== 문서 로드 ==================== async loadDocument() { console.log('📄 문서 로드 시작'); this.loading = true; try { // 문서 데이터 로드 if (this.contentType === 'note') { this.document = await this.documentLoader.loadNote(this.documentId); this.navigation = null; // 노트는 네비게이션 없음 } else { this.document = await this.documentLoader.loadDocument(this.documentId); // 네비게이션 별도 로드 this.navigation = await this.documentLoader.loadNavigation(this.documentId); } // 관련 데이터 병렬 로드 await this.loadDocumentData(); // 데이터를 모듈에 전달 this.distributeDataToModules(); // 렌더링 await this.renderAllFeatures(); // URL 하이라이트 처리 await this.handleUrlHighlight(); this.loading = false; console.log('✅ 문서 로드 완료'); } catch (error) { console.error('❌ 문서 로드 실패:', error); this.error = error.message; this.loading = false; } }, // ==================== 문서 데이터 로드 (지연 로딩) ==================== async loadDocumentData() { console.log('📊 문서 데이터 로드 시작'); const [highlights, notes, bookmarks, documentLinks, backlinks] = await Promise.all([ this.highlightManager.loadHighlights(this.documentId, this.contentType), this.highlightManager.loadNotes(this.documentId, this.contentType), this.bookmarkManager.loadBookmarks(this.documentId), this.linkManager.loadDocumentLinks(this.documentId), this.linkManager.loadBacklinks(this.documentId) ]); // 데이터 저장 및 모듈 동기화 this.highlights = highlights; this.notes = notes; this.bookmarks = bookmarks; this.documentLinks = documentLinks; this.backlinks = backlinks; // 모듈에 데이터 동기화 (중요!) this.linkManager.documentLinks = documentLinks; this.linkManager.backlinks = backlinks; console.log('📊 로드된 데이터:', { highlights: highlights.length, notes: notes.length, bookmarks: bookmarks.length, documentLinks: documentLinks.length, backlinks: backlinks.length }); console.log('🔄 모듈 데이터 동기화 완료:', { 'linkManager.documentLinks': this.linkManager.documentLinks?.length || 0, 'linkManager.backlinks': this.linkManager.backlinks?.length || 0 }); }, // ==================== 모듈 지연 로딩 보장 (폴백 포함) ==================== async ensureModulesLoaded(moduleNames) { const missingModules = []; for (const moduleName of moduleNames) { const propertyName = this.getModulePropertyName(moduleName); if (!this[propertyName]) { missingModules.push(moduleName); } } if (missingModules.length > 0) { console.log(`🔄 필요한 모듈 지연 로딩: ${missingModules.join(', ')}`); // 각 모듈을 개별적으로 로드 for (const moduleName of missingModules) { const propertyName = this.getModulePropertyName(moduleName); try { // 지연 로딩 시도 if (window.moduleLoader) { const ModuleClass = await window.moduleLoader.loadModule(moduleName); if (moduleName === 'UIManager') { this[propertyName] = new ModuleClass(); } else { this[propertyName] = new ModuleClass(this.cachedApi); } console.log(`✅ 지연 로딩 성공: ${moduleName}`); } else { throw new Error('ModuleLoader 없음'); } } catch (error) { console.warn(`⚠️ 지연 로딩 실패, 폴백 시도: ${moduleName}`, error); // 폴백: 전역 클래스 직접 사용 if (window[moduleName]) { if (moduleName === 'UIManager') { this[propertyName] = new window[moduleName](); } else { this[propertyName] = new window[moduleName](this.cachedApi); } console.log(`✅ 폴백 성공: ${moduleName}`); } else { console.error(`❌ 폴백도 실패: ${moduleName} - 전역 클래스 없음`); throw new Error(`모듈을 로드할 수 없습니다: ${moduleName}`); } } } } }, // ==================== 모듈명 → 속성명 변환 ==================== getModulePropertyName(moduleName) { const nameMap = { 'DocumentLoader': 'documentLoader', 'HighlightManager': 'highlightManager', 'BookmarkManager': 'bookmarkManager', 'LinkManager': 'linkManager', 'UIManager': 'uiManager' }; return nameMap[moduleName]; }, // ==================== 모듈에 데이터 분배 ==================== distributeDataToModules() { // HighlightManager에 데이터 전달 this.highlightManager.highlights = this.highlights; this.highlightManager.notes = this.notes; // BookmarkManager에 데이터 전달 this.bookmarkManager.bookmarks = this.bookmarks; // LinkManager에 데이터 전달 this.linkManager.documentLinks = this.documentLinks; this.linkManager.backlinks = this.backlinks; }, // ==================== 모든 기능 렌더링 ==================== async renderAllFeatures() { console.log('🎨 모든 기능 렌더링 시작'); // 하이라이트 렌더링 this.highlightManager.renderHighlights(); // 백링크 먼저 렌더링 (링크보다 먼저) this.linkManager.renderBacklinks(); // 문서 링크 렌더링 (백링크 후에 렌더링) this.linkManager.renderDocumentLinks(); console.log('✅ 모든 기능 렌더링 완료'); }, // ==================== URL 하이라이트 처리 ==================== async handleUrlHighlight() { const urlParams = new URLSearchParams(window.location.search); const highlightText = urlParams.get('highlight'); const startOffset = parseInt(urlParams.get('start_offset')); const endOffset = parseInt(urlParams.get('end_offset')); if (highlightText || (startOffset && endOffset)) { console.log('🎯 URL에서 하이라이트 요청:', { highlightText, startOffset, endOffset }); await this.documentLoader.highlightAndScrollToText({ text: highlightText, start_offset: startOffset, end_offset: endOffset }); } }, // ==================== 기능 모드 활성화 ==================== activateLinkMode() { console.log('🔗 링크 모드 활성화'); this.activeMode = 'link'; // 선택된 텍스트 확인 const selectedText = window.getSelection().toString().trim(); const selection = window.getSelection(); if (!selectedText || selection.rangeCount === 0) { alert('텍스트를 먼저 선택해주세요.'); return; } const selectedRange = selection.getRangeAt(0); this.linkManager.createLinkFromSelection(this.documentId, selectedText, selectedRange); }, activateNoteMode() { console.log('📝 메모 모드 활성화'); this.activeMode = 'memo'; this.highlightManager.activateNoteMode(); }, async loadBacklinks() { console.log('🔗 백링크 로드 시작'); if (this.linkManager) { await this.linkManager.loadBacklinks(this.documentId); // UI 상태 동기화 this.backlinks = this.linkManager.backlinks || []; } }, async loadAvailableBooks() { try { console.log('📚 서적 목록 로딩 시작...'); // 문서 목록에서 서적 정보 추출 const allDocuments = await this.api.getLinkableDocuments(this.documentId); console.log('📄 모든 문서들 (총 개수):', allDocuments.length); // 소스 문서의 서적 정보 찾기 const sourceBookInfo = this.getSourceBookInfo(allDocuments); console.log('📖 소스 문서 서적 정보:', sourceBookInfo); // 서적별로 그룹화 const bookMap = new Map(); allDocuments.forEach(doc => { if (doc.book_id && doc.book_title) { console.log('📖 문서 서적 정보:', { docId: doc.id, bookId: doc.book_id, bookTitle: doc.book_title }); bookMap.set(doc.book_id, { id: doc.book_id, title: doc.book_title }); } }); console.log('📚 그룹화된 모든 서적들:', Array.from(bookMap.values())); // 모든 서적 표시 (소스 서적 포함) this.availableBooks = Array.from(bookMap.values()); console.log('📚 최종 사용 가능한 서적들 (모든 서적):', this.availableBooks); console.log('📖 소스 서적 정보 (포함됨):', sourceBookInfo); } catch (error) { console.error('서적 목록 로드 실패:', error); this.availableBooks = []; } }, getSourceBookInfo(allDocuments = null) { // 여러 소스에서 현재 문서의 서적 정보 찾기 let sourceBookId = this.navigation?.book_info?.id || this.document?.book_id || this.document?.book_info?.id; let sourceBookTitle = this.navigation?.book_info?.title || this.document?.book_title || this.document?.book_info?.title; // allDocuments에서도 확인 (가장 확실한 방법) if (allDocuments) { const currentDoc = allDocuments.find(doc => doc.id === this.documentId); if (currentDoc) { sourceBookId = currentDoc.book_id; sourceBookTitle = currentDoc.book_title; } } return { id: sourceBookId, title: sourceBookTitle }; }, async loadSameBookDocuments() { try { const allDocuments = await this.api.getLinkableDocuments(this.documentId); // 소스 문서의 서적 정보 가져오기 const sourceBookInfo = this.getSourceBookInfo(allDocuments); console.log('📚 같은 서적 문서 로드 시작:', { sourceBookId: sourceBookInfo.id, sourceBookTitle: sourceBookInfo.title, totalDocs: allDocuments.length }); if (sourceBookInfo.id) { // 소스 문서와 같은 서적의 문서들만 필터링 (현재 문서 제외) this.filteredDocuments = allDocuments.filter(doc => doc.book_id === sourceBookInfo.id && doc.id !== this.documentId ); console.log('📚 같은 서적 문서들:', { count: this.filteredDocuments.length, bookTitle: sourceBookInfo.title, documents: this.filteredDocuments.map(doc => ({ id: doc.id, title: doc.title })) }); } else { console.warn('⚠️ 소스 문서의 서적 정보를 찾을 수 없습니다!'); this.filteredDocuments = []; } } catch (error) { console.error('같은 서적 문서 로드 실패:', error); this.filteredDocuments = []; } }, async loadSameBookDocumentsForSelected() { try { console.log('📚 선택한 문서 기준으로 같은 서적 문서 로드 시작'); const allDocuments = await this.api.getLinkableDocuments(this.documentId); // 선택한 대상 문서 찾기 const selectedDoc = allDocuments.find(doc => doc.id === this.linkForm.target_document_id); if (!selectedDoc) { console.error('❌ 선택한 문서를 찾을 수 없습니다:', this.linkForm.target_document_id); return; } console.log('🎯 선택한 문서 정보:', { id: selectedDoc.id, title: selectedDoc.title, bookId: selectedDoc.book_id, bookTitle: selectedDoc.book_title }); // 선택한 문서와 같은 서적의 모든 문서들 (소스 문서 제외) this.filteredDocuments = allDocuments.filter(doc => doc.book_id === selectedDoc.book_id && doc.id !== this.documentId ); console.log('📚 선택한 문서와 같은 서적 문서들:', { selectedBookTitle: selectedDoc.book_title, count: this.filteredDocuments.length, documents: this.filteredDocuments.map(doc => ({ id: doc.id, title: doc.title })) }); } catch (error) { console.error('선택한 문서 기준 같은 서적 로드 실패:', error); this.filteredDocuments = []; } }, async loadDocumentsFromBook() { try { if (this.linkForm.target_book_id) { // 선택된 서적의 문서들만 가져오기 const allDocuments = await this.api.getLinkableDocuments(this.documentId); this.filteredDocuments = allDocuments.filter(doc => doc.book_id === this.linkForm.target_book_id ); console.log('📚 선택된 서적 문서들:', this.filteredDocuments); } else { this.filteredDocuments = []; } // 문서 선택 초기화 this.linkForm.target_document_id = ''; } catch (error) { console.error('서적별 문서 로드 실패:', error); this.filteredDocuments = []; } }, resetTargetSelection() { console.log('🔄 대상 선택 초기화'); this.linkForm.target_book_id = ''; this.linkForm.target_document_id = ''; this.filteredDocuments = []; // 초기화 후 아무것도 하지 않음 (서적 선택 후 문서 로드) }, async onTargetDocumentChange() { console.log('📄 대상 문서 변경:', this.linkForm.target_document_id); // 대상 문서 변경 시 특별한 처리 없음 }, selectTextFromDocument() { console.log('🎯 대상 문서에서 텍스트 선택 시작'); if (!this.linkForm.target_document_id) { alert('대상 문서를 먼저 선택해주세요.'); return; } // 새 창에서 대상 문서 열기 (텍스트 선택 모드 전용 페이지) const targetUrl = `/text-selector.html?id=${this.linkForm.target_document_id}`; console.log('🚀 텍스트 선택 창 열기:', targetUrl); const popup = window.open(targetUrl, 'targetDocumentSelector', 'width=1200,height=800,scrollbars=yes,resizable=yes'); if (!popup) { console.error('❌ 팝업 창이 차단되었습니다!'); alert('팝업 창이 차단되었습니다. 브라우저 설정에서 팝업을 허용해주세요.'); } else { console.log('✅ 팝업 창이 성공적으로 열렸습니다'); } // 팝업에서 텍스트 선택 완료 시 메시지 수신 window.addEventListener('message', (event) => { if (event.data.type === 'TEXT_SELECTED') { this.linkForm.target_text = event.data.selectedText; this.linkForm.target_start_offset = event.data.startOffset; this.linkForm.target_end_offset = event.data.endOffset; console.log('🎯 대상 텍스트 선택됨:', event.data); popup.close(); } }, { once: true }); }, activateBookmarkMode() { console.log('🔖 북마크 모드 활성화'); this.activeMode = 'bookmark'; this.bookmarkManager.activateBookmarkMode(); }, // ==================== 하이라이트 기능 위임 ==================== createHighlightWithColor(color) { console.log('🎨 하이라이트 생성 요청:', color); // ViewerCore의 selectedHighlightColor도 동기화 this.selectedHighlightColor = color; console.log('🎨 ViewerCore 색상 동기화:', this.selectedHighlightColor); return this.highlightManager.createHighlightWithColor(color); }, // ==================== 메모 입력 모달 관련 ==================== openNoteInputModal() { console.log('📝 메모 입력 모달 열기'); this.showNoteInputModal = true; // 폼 초기화 this.noteForm.content = ''; this.noteForm.tags = ''; // 포커스를 textarea로 이동 (다음 틱에서) this.$nextTick(() => { const textarea = document.querySelector('textarea[x-model="noteForm.content"]'); if (textarea) textarea.focus(); }); }, closeNoteInputModal() { console.log('📝 메모 입력 모달 닫기'); this.showNoteInputModal = false; this.noteForm.content = ''; this.noteForm.tags = ''; // 선택된 텍스트 정리 this.selectedText = ''; this.selectedRange = null; }, async createNoteForHighlight() { console.log('📝 하이라이트에 메모 생성'); if (!this.noteForm.content.trim()) { alert('메모 내용을 입력해주세요.'); return; } try { // 현재 생성된 하이라이트 정보가 필요함 if (this.highlightManager.lastCreatedHighlight) { await this.highlightManager.createNoteForHighlight( this.highlightManager.lastCreatedHighlight, this.noteForm.content.trim(), this.noteForm.tags.trim() ); this.closeNoteInputModal(); } else { alert('하이라이트 정보를 찾을 수 없습니다.'); } } catch (error) { console.error('메모 생성 실패:', error); alert('메모 생성에 실패했습니다: ' + error.message); } }, skipNoteForHighlight() { console.log('📝 메모 입력 건너뛰기'); this.closeNoteInputModal(); }, // ==================== UI 메서드 위임 ==================== toggleFeatureMenu(feature) { const result = this.uiManager.toggleFeatureMenu(feature); this.updateModalStates(); // 상태 동기화 return result; }, openNoteModal(highlight = null) { const result = this.uiManager.openNoteModal(highlight); this.updateModalStates(); // 상태 동기화 return result; }, closeNoteModal() { const result = this.uiManager.closeNoteModal(); this.updateModalStates(); // 상태 동기화 return result; }, closeLinkModal() { const result = this.uiManager.closeLinkModal(); this.updateModalStates(); // 상태 동기화 return result; }, closeBookmarkModal() { const result = this.uiManager.closeBookmarkModal(); this.updateModalStates(); // 상태 동기화 return result; }, highlightSearchResults(element, searchText) { return this.uiManager.highlightSearchResults(element, searchText); }, showSuccessMessage(message) { return this.uiManager.showSuccessMessage(message); }, showErrorMessage(message) { return this.uiManager.showErrorMessage(message); }, // ==================== 언어 전환 ==================== toggleLanguage() { this.isKorean = !this.isKorean; const lang = this.isKorean ? 'ko' : 'en'; console.log('🌐 언어 전환:', this.isKorean ? '한국어' : 'English'); // 문서에 내장된 언어 전환 기능 찾기 및 실행 this.findAndExecuteBuiltinLanguageToggle(); }, // 문서에 내장된 언어 전환 기능 찾기 findAndExecuteBuiltinLanguageToggle() { console.log('🔍 문서 내장 언어 전환 기능 찾기 시작'); const content = document.getElementById('document-content'); if (!content) { console.warn('❌ document-content 요소를 찾을 수 없습니다'); return; } // 1. 언어 전환 버튼 찾기 (다양한 패턴) const buttonSelectors = [ 'button[onclick*="toggleLanguage"]', 'button[onclick*="language"]', 'button[onclick*="Language"]', '.language-toggle', '.lang-toggle', 'button[id*="lang"]', 'button[class*="lang"]', 'input[type="button"][onclick*="language"]' ]; let foundButton = null; for (const selector of buttonSelectors) { const buttons = content.querySelectorAll(selector); if (buttons.length > 0) { foundButton = buttons[0]; console.log(`✅ 언어 전환 버튼 발견 (${selector}):`, foundButton.outerHTML.substring(0, 100)); break; } } // 2. 버튼이 있으면 클릭 if (foundButton) { console.log('🔘 내장 언어 전환 버튼 클릭'); try { foundButton.click(); console.log('✅ 언어 전환 버튼 클릭 완료'); return; } catch (error) { console.error('❌ 버튼 클릭 실패:', error); } } // 3. 버튼이 없으면 스크립트 함수 직접 호출 시도 this.tryDirectLanguageFunction(); }, // 직접 언어 전환 함수 호출 시도 tryDirectLanguageFunction() { console.log('🔧 직접 언어 전환 함수 호출 시도'); const functionNames = [ 'toggleLanguage', 'changeLanguage', 'switchLanguage', 'toggleLang', 'changeLang' ]; for (const funcName of functionNames) { if (typeof window[funcName] === 'function') { console.log(`✅ 전역 함수 발견: ${funcName}`); try { window[funcName](); console.log(`✅ ${funcName}() 호출 완료`); return; } catch (error) { console.error(`❌ ${funcName}() 호출 실패:`, error); } } } // 4. 문서 내 스크립트에서 함수 찾기 this.findLanguageFunctionInScripts(); }, // 문서 내 스크립트에서 언어 전환 함수 찾기 findLanguageFunctionInScripts() { console.log('📜 문서 내 스크립트에서 언어 함수 찾기'); const content = document.getElementById('document-content'); const scripts = content.querySelectorAll('script'); console.log(`📜 발견된 스크립트 태그: ${scripts.length}개`); scripts.forEach((script, index) => { const scriptContent = script.textContent || script.innerHTML; if (scriptContent.includes('language') || scriptContent.includes('Language') || scriptContent.includes('lang')) { console.log(`📜 스크립트 ${index + 1}에서 언어 관련 코드 발견:`, scriptContent.substring(0, 200)); // 함수 실행 시도 try { eval(scriptContent); console.log(`✅ 스크립트 ${index + 1} 실행 완료`); } catch (error) { console.log(`⚠️ 스크립트 ${index + 1} 실행 실패:`, error.message); } } }); console.log('⚠️ 내장 언어 전환 기능을 찾을 수 없습니다'); }, async downloadOriginalFile() { if (!this.document || !this.document.id) { console.warn('문서 정보가 없습니다'); return; } console.log('📕 PDF 다운로드 시도:', { id: this.document.id, matched_pdf_id: this.document.matched_pdf_id, pdf_path: this.document.pdf_path }); // 1. 현재 문서 자체가 PDF인 경우 if (this.document.pdf_path) { console.log('📄 현재 문서가 PDF - 직접 다운로드'); this.downloadPdfFile(this.document.pdf_path, this.document.title || 'document'); return; } // 2. 연결된 PDF가 있는지 확인 if (!this.document.matched_pdf_id) { alert('연결된 원본 PDF 파일이 없습니다.\n\n서적 편집 페이지에서 PDF 파일을 연결해주세요.'); return; } try { console.log('📕 연결된 PDF 다운로드 시작:', this.document.matched_pdf_id); // 연결된 PDF 문서 정보 가져오기 const pdfDocument = await this.api.getDocument(this.document.matched_pdf_id); if (!pdfDocument) { throw new Error('연결된 PDF 문서를 찾을 수 없습니다'); } // PDF 파일 다운로드 URL 생성 const downloadUrl = `/api/documents/${this.document.matched_pdf_id}/download`; // 인증 헤더 추가를 위해 fetch 사용 const response = await fetch(downloadUrl, { method: 'GET', headers: { 'Authorization': `Bearer ${localStorage.getItem('access_token')}` } }); if (!response.ok) { throw new Error('연결된 PDF 다운로드에 실패했습니다'); } // Blob으로 변환하여 다운로드 const blob = await response.blob(); const url = window.URL.createObjectURL(blob); // 다운로드 링크 생성 및 클릭 const link = document.createElement('a'); link.href = url; link.download = pdfDocument.original_filename || `${pdfDocument.title}.pdf`; document.body.appendChild(link); link.click(); document.body.removeChild(link); // URL 정리 window.URL.revokeObjectURL(url); console.log('📕 PDF 다운로드 완료:', pdfDocument.original_filename); } catch (error) { console.error('PDF 다운로드 오류:', error); alert('PDF 다운로드 중 오류가 발생했습니다: ' + error.message); } }, // PDF 파일 직접 다운로드 downloadPdfFile(pdfPath, filename) { try { console.log('📄 PDF 파일 직접 다운로드:', pdfPath); // PDF 파일 URL 생성 (상대 경로를 절대 경로로 변환) let pdfUrl = pdfPath; if (!pdfUrl.startsWith('http')) { // 상대 경로인 경우 현재 도메인 기준으로 절대 경로 생성 const baseUrl = window.location.origin; pdfUrl = pdfUrl.startsWith('/') ? baseUrl + pdfUrl : baseUrl + '/' + pdfUrl; } console.log('📄 PDF URL:', pdfUrl); // 다운로드 링크 생성 및 클릭 const link = document.createElement('a'); link.href = pdfUrl; link.download = filename.endsWith('.pdf') ? filename : filename + '.pdf'; link.target = '_blank'; // 새 탭에서 열기 (다운로드 실패 시 뷰어로 열림) document.body.appendChild(link); link.click(); document.body.removeChild(link); console.log('✅ PDF 다운로드 링크 클릭 완료'); } catch (error) { console.error('PDF 다운로드 오류:', error); alert('PDF 다운로드 중 오류가 발생했습니다: ' + error.message); } }, // ==================== 유틸리티 메서드 ==================== formatDate(dateString) { return new Date(dateString).toLocaleString('ko-KR'); }, formatShortDate(dateString) { return new Date(dateString).toLocaleDateString('ko-KR'); }, getColorName(color) { const colorNames = { '#FFFF00': '노란색', '#00FF00': '초록색', '#FF0000': '빨간색', '#0000FF': '파란색', '#FF00FF': '보라색', '#00FFFF': '청록색', '#FFA500': '주황색', '#FFC0CB': '분홍색' }; return colorNames[color] || '기타'; }, getSelectedBookTitle() { const selectedBook = this.availableBooks.find(book => book.id === this.linkForm.target_book_id); return selectedBook ? selectedBook.title : '서적을 선택하세요'; }, // ==================== 모듈 메서드 위임 ==================== // 하이라이트 관련 selectHighlight(highlightId) { return this.highlightManager.selectHighlight(highlightId); }, deleteHighlight(highlightId) { return this.highlightManager.deleteHighlight(highlightId); }, deleteHighlightsByColor(color, highlightIds) { return this.highlightManager.deleteHighlightsByColor(color, highlightIds); }, deleteAllOverlappingHighlights(highlightIds) { return this.highlightManager.deleteAllOverlappingHighlights(highlightIds); }, hideTooltip() { return this.highlightManager.hideTooltip(); }, showAddNoteForm(highlightId) { return this.highlightManager.showAddNoteForm(highlightId); }, deleteNote(noteId) { return this.highlightManager.deleteNote(noteId); }, // 링크 관련 navigateToLinkedDocument(documentId, linkData) { return this.linkManager.navigateToLinkedDocument(documentId, linkData); }, navigateToBacklinkDocument(documentId, backlinkData) { return this.linkManager.navigateToBacklinkDocument(documentId, backlinkData); }, // 북마크 관련 scrollToBookmark(bookmark) { return this.bookmarkManager.scrollToBookmark(bookmark); }, deleteBookmark(bookmarkId) { return this.bookmarkManager.deleteBookmark(bookmarkId); }, // ==================== 링크 생성 ==================== async createDocumentLink() { console.log('🔗 createDocumentLink 함수 실행'); console.log('📋 현재 linkForm 상태:', JSON.stringify(this.linkForm, null, 2)); try { // 링크 데이터 검증 if (!this.linkForm.target_document_id) { alert('대상 문서를 선택해주세요.'); return; } if (this.linkForm.link_type === 'text' && !this.linkForm.target_text) { alert('대상 문서에서 텍스트를 선택해주세요. "대상 문서에서 텍스트 선택" 버튼을 클릭하여 연결할 텍스트를 드래그해주세요.'); return; } // API 호출용 데이터 준비 (백엔드 필드명에 맞춤) const linkData = { target_document_id: this.linkForm.target_document_id, selected_text: this.linkForm.selected_text, // 백엔드: selected_text start_offset: this.linkForm.start_offset, // 백엔드: start_offset end_offset: this.linkForm.end_offset, // 백엔드: end_offset link_text: this.linkForm.link_text || this.linkForm.selected_text, description: this.linkForm.description, link_type: this.linkForm.link_type, target_text: this.linkForm.target_text || null, target_start_offset: this.linkForm.target_start_offset || null, target_end_offset: this.linkForm.target_end_offset || null }; console.log('📤 링크 생성 데이터:', linkData); console.log('📤 링크 생성 데이터 (JSON):', JSON.stringify(linkData, null, 2)); // 필수 필드 검증 const requiredFields = ['target_document_id', 'selected_text', 'start_offset', 'end_offset']; const missingFields = requiredFields.filter(field => linkData[field] === undefined || linkData[field] === null || linkData[field] === '' ); if (missingFields.length > 0) { console.error('❌ 필수 필드 누락:', missingFields); alert('필수 필드가 누락되었습니다: ' + missingFields.join(', ')); return; } console.log('✅ 모든 필수 필드 확인됨'); // API 호출 await this.api.createDocumentLink(this.documentId, linkData); console.log('✅ 링크 생성됨'); // 성공 알림 alert('링크가 성공적으로 생성되었습니다!'); // 모달 닫기 this.showLinkModal = false; // 캐시 무효화 (새 링크가 반영되도록) console.log('🗑️ 링크 캐시 무효화 시작...'); if (window.cachedApi && window.cachedApi.invalidateRelatedCache) { window.cachedApi.invalidateRelatedCache(`/documents/${this.documentId}/links`, ['links']); console.log('✅ 링크 캐시 무효화 완료'); } // 링크 목록 새로고침 console.log('🔄 링크 목록 새로고침 시작...'); await this.linkManager.loadDocumentLinks(this.documentId); this.documentLinks = this.linkManager.documentLinks || []; console.log('📊 로드된 링크 개수:', this.documentLinks.length); console.log('📊 링크 데이터:', this.documentLinks); // 링크 렌더링 console.log('🎨 링크 렌더링 시작...'); this.linkManager.renderDocumentLinks(); console.log('✅ 링크 렌더링 완료'); // 백링크도 다시 로드하고 렌더링 (새 링크가 다른 문서의 백링크가 될 수 있음) console.log('🔄 백링크 새로고침 시작...'); // 백링크 캐시도 무효화 if (window.cachedApi && window.cachedApi.invalidateRelatedCache) { window.cachedApi.invalidateRelatedCache(`/documents/${this.documentId}/backlinks`, ['links']); console.log('✅ 백링크 캐시도 무효화 완료'); } await this.linkManager.loadBacklinks(this.documentId); this.backlinks = this.linkManager.backlinks || []; this.linkManager.renderBacklinks(); console.log('✅ 백링크 새로고침 완료'); } catch (error) { console.error('링크 생성 실패:', error); console.error('에러 상세:', { message: error.message, stack: error.stack, response: error.response }); // 422 에러인 경우 상세 정보 표시 if (error.response && error.response.status === 422) { console.error('422 Validation Error Details:', error.response.data); alert('데이터 검증 실패: ' + JSON.stringify(error.response.data, null, 2)); } else { alert('링크 생성에 실패했습니다: ' + error.message); } } }, // 네비게이션 함수들 goBack() { console.log('🔙 뒤로가기'); window.history.back(); }, navigateToDocument(documentId) { if (!documentId) { console.warn('⚠️ 문서 ID가 없습니다'); return; } console.log('📄 문서로 이동:', documentId); window.location.href = `/viewer.html?id=${documentId}`; }, goToBookContents() { if (!this.navigation?.book_info?.id) { console.warn('⚠️ 서적 정보가 없습니다'); return; } console.log('📚 서적 목차로 이동:', this.navigation.book_info.id); window.location.href = `/book-documents.html?book_id=${this.navigation.book_info.id}`; } }); // Alpine.js 컴포넌트 등록 document.addEventListener('alpine:init', () => { console.log('🔧 Alpine.js 컴포넌트 로드됨'); // 전역 함수들 (말풍선에서 사용) window.cancelTextSelection = () => { if (window.documentViewerInstance && window.documentViewerInstance.linkManager) { window.documentViewerInstance.linkManager.cancelTextSelection(); } }; window.confirmTextSelection = (selectedText, startOffset, endOffset) => { if (window.documentViewerInstance && window.documentViewerInstance.linkManager) { window.documentViewerInstance.linkManager.confirmTextSelection(selectedText, startOffset, endOffset); } }; }); // Alpine.js Store 등록 document.addEventListener('alpine:init', () => { Alpine.store('documentViewer', { instance: null, init() { // DocumentViewer 인스턴스가 생성되면 저장 setTimeout(() => { this.instance = window.documentViewerInstance; }, 500); }, downloadOriginalFile() { console.log('🏪 Store downloadOriginalFile 호출'); if (this.instance) { return this.instance.downloadOriginalFile(); } else { console.warn('DocumentViewer 인스턴스가 없습니다'); } }, toggleLanguage() { console.log('🏪 Store toggleLanguage 호출'); if (this.instance) { return this.instance.toggleLanguage(); } else { console.warn('DocumentViewer 인스턴스가 없습니다'); } }, loadBacklinks() { console.log('🏪 Store loadBacklinks 호출'); if (this.instance) { return this.instance.loadBacklinks(); } else { console.warn('DocumentViewer 인스턴스가 없습니다'); } } }); });