/** * HighlightManager 모듈 * 하이라이트 및 메모 관리 */ class HighlightManager { constructor(api) { console.log('🎨 HighlightManager 초기화 시작'); this.api = api; // 캐싱된 API 사용 (사용 가능한 경우) this.cachedApi = window.cachedApi || api; this.highlights = []; this.notes = []; this.selectedHighlightColor = '#FFFF00'; this.selectedText = ''; this.selectedRange = null; // 텍스트 선택 이벤트 리스너 등록 this.textSelectionHandler = this.handleTextSelection.bind(this); document.addEventListener('mouseup', this.textSelectionHandler); console.log('✅ HighlightManager 텍스트 선택 이벤트 리스너 등록 완료'); } /** * 하이라이트 데이터 로드 */ async loadHighlights(documentId, contentType) { try { if (contentType === 'note') { this.highlights = await this.api.get(`/note/${documentId}/highlights`).catch(() => []); } else { this.highlights = await this.cachedApi.get('/highlights', { document_id: documentId, content_type: contentType }, { category: 'highlights' }).catch(() => []); } return this.highlights || []; } catch (error) { console.error('하이라이트 로드 실패:', error); return []; } } /** * 메모 데이터 로드 */ async loadNotes(documentId, contentType) { try { console.log('📝 메모 로드 시작:', { documentId, contentType }); if (contentType === 'note') { // 노트 문서의 하이라이트 메모 this.notes = await this.api.get(`/highlight-notes/`, { note_document_id: documentId }).catch(() => []); } else { // 일반 문서의 하이라이트 메모 this.notes = await this.api.get(`/highlight-notes/`, { document_id: documentId }).catch(() => []); } console.log('📝 메모 로드 완료:', this.notes.length, '개'); return this.notes || []; } catch (error) { console.error('❌ 메모 로드 실패:', error); return []; } } /** * 하이라이트 렌더링 (개선된 버전) */ renderHighlights() { const content = document.getElementById('document-content'); console.log('🎨 하이라이트 렌더링 호출됨'); console.log('📄 document-content 요소:', content ? '존재' : '없음'); console.log('📊 this.highlights:', this.highlights ? this.highlights.length + '개' : 'null/undefined'); if (!content || !this.highlights || this.highlights.length === 0) { console.log('❌ 하이라이트 렌더링 조건 미충족:', { content: !!content, highlights: !!this.highlights, length: this.highlights ? this.highlights.length : 0 }); return; } console.log('🎨 하이라이트 렌더링 시작:', this.highlights.length + '개'); // 기존 하이라이트 제거 const existingHighlights = content.querySelectorAll('.highlight-span'); existingHighlights.forEach(el => { const parent = el.parentNode; parent.replaceChild(document.createTextNode(el.textContent), el); parent.normalize(); }); // 위치별로 하이라이트 그룹화 const positionGroups = this.groupHighlightsByPosition(); // 각 그룹별로 하이라이트 적용 Object.keys(positionGroups).forEach(key => { const group = positionGroups[key]; this.applyHighlightGroup(group); }); console.log('✅ 하이라이트 렌더링 완료'); } /** * 위치별로 하이라이트 그룹화 */ groupHighlightsByPosition() { const groups = {}; console.log('📊 하이라이트 그룹화 시작:', this.highlights.length + '개'); console.log('📊 하이라이트 데이터:', this.highlights); this.highlights.forEach(highlight => { const key = `${highlight.start_offset}-${highlight.end_offset}`; if (!groups[key]) { groups[key] = { start_offset: highlight.start_offset, end_offset: highlight.end_offset, highlights: [] }; } groups[key].highlights.push(highlight); }); console.log('📊 그룹화 결과:', Object.keys(groups).length + '개 그룹'); console.log('📊 그룹 상세:', groups); return groups; } /** * 하이라이트 그룹 적용 */ applyHighlightGroup(group) { const content = document.getElementById('document-content'); const textContent = content.textContent; console.log('🎯 하이라이트 그룹 적용:', { start: group.start_offset, end: group.end_offset, text: textContent.substring(group.start_offset, group.end_offset), colors: group.highlights.map(h => h.highlight_color || h.color) }); if (group.start_offset >= textContent.length || group.end_offset > textContent.length) { console.warn('하이라이트 위치가 텍스트 범위를 벗어남:', group); return; } const targetText = textContent.substring(group.start_offset, group.end_offset); // 텍스트 노드 찾기 및 하이라이트 적용 const walker = document.createTreeWalker( content, NodeFilter.SHOW_TEXT, null, false ); let currentOffset = 0; let node; let found = false; while (node = walker.nextNode()) { const nodeLength = node.textContent.length; const nodeStart = currentOffset; const nodeEnd = currentOffset + nodeLength; // 하이라이트 범위와 겹치는지 확인 if (nodeEnd > group.start_offset && nodeStart < group.end_offset) { const highlightStart = Math.max(0, group.start_offset - nodeStart); const highlightEnd = Math.min(nodeLength, group.end_offset - nodeStart); if (highlightStart < highlightEnd) { console.log('✅ 하이라이트 적용 중:', { nodeText: node.textContent.substring(0, 50) + '...', highlightStart, highlightEnd, highlightText: node.textContent.substring(highlightStart, highlightEnd) }); this.highlightTextInNode(node, highlightStart, highlightEnd, group.highlights); found = true; break; } } currentOffset = nodeEnd; } if (!found) { console.warn('❌ 하이라이트 적용할 텍스트 노드를 찾지 못함:', targetText); } } /** * 텍스트 노드에 하이라이트 적용 */ highlightTextInNode(textNode, start, end, highlights) { const text = textNode.textContent; const beforeText = text.substring(0, start); const highlightText = text.substring(start, end); const afterText = text.substring(end); // 하이라이트 스팬 생성 const span = document.createElement('span'); span.className = 'highlight-span'; span.textContent = highlightText; // 첫 번째 하이라이트의 ID를 data 속성으로 설정 if (highlights.length > 0) { span.dataset.highlightId = highlights[0].id; } // 다중 색상 처리 if (highlights.length === 1) { console.log('🔍 하이라이트 데이터 구조:', highlights[0]); const color = highlights[0].highlight_color || highlights[0].color || '#FFFF00'; span.style.setProperty('background', color, 'important'); span.style.setProperty('background-color', color, 'important'); console.log('🎨 단일 하이라이트 색상 적용 (!important):', color); } else { // 여러 색상이 겹치는 경우 줄무늬(스트라이프) 적용 const colors = highlights.map(h => h.highlight_color || h.color || '#FFFF00'); const stripeSize = 100 / colors.length; // 각 색상의 비율 // 색상별로 동일한 크기의 줄무늬 생성 const stripes = colors.map((color, index) => { const start = index * stripeSize; const end = (index + 1) * stripeSize; return `${color} ${start}%, ${color} ${end}%`; }).join(', '); span.style.background = `linear-gradient(180deg, ${stripes})`; console.log('🎨 다중 하이라이트 색상 적용 (위아래 절반씩):', colors); } // 메모 툴팁 설정 const notesForHighlight = highlights.filter(h => h.note_content); if (notesForHighlight.length > 0) { span.title = notesForHighlight.map(h => h.note_content).join('\n---\n'); span.style.cursor = 'help'; } // 하이라이트 클릭 이벤트 추가 (통합 툴팁 사용) span.style.cursor = 'pointer'; span.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); console.log('🎨 하이라이트 클릭됨:', { text: span.textContent, highlightId: span.dataset.highlightId, classList: Array.from(span.classList) }); // 링크, 백링크, 하이라이트 모두 찾기 const overlappingElements = window.documentViewerInstance.getOverlappingElements(span); const totalElements = overlappingElements.links.length + overlappingElements.backlinks.length + overlappingElements.highlights.length; console.log('🎨 하이라이트 클릭 분석:', { links: overlappingElements.links.length, backlinks: overlappingElements.backlinks.length, highlights: overlappingElements.highlights.length, total: totalElements, selectedText: overlappingElements.selectedText }); if (totalElements > 1) { // 통합 툴팁 표시 (링크 + 백링크 + 하이라이트) console.log('🎯 통합 툴팁 표시 시작 (하이라이트에서)'); await window.documentViewerInstance.showUnifiedTooltip(overlappingElements, span); } else { // 단일 하이라이트 툴팁 console.log('🎨 단일 하이라이트 툴팁 표시'); // 클릭된 하이라이트 찾기 const clickedHighlightId = span.dataset.highlightId; const clickedHighlight = this.highlights.find(h => h.id === clickedHighlightId); if (clickedHighlight) { await this.showHighlightTooltip(clickedHighlight, span); } else { console.error('❌ 클릭된 하이라이트를 찾을 수 없음:', clickedHighlightId); } } }); // DOM 교체 const parent = textNode.parentNode; const fragment = document.createDocumentFragment(); if (beforeText) fragment.appendChild(document.createTextNode(beforeText)); fragment.appendChild(span); if (afterText) fragment.appendChild(document.createTextNode(afterText)); parent.replaceChild(fragment, textNode); } /** * 텍스트 선택 처리 */ handleTextSelection() { console.log('handleTextSelection called'); const selection = window.getSelection(); if (!selection.rangeCount || selection.isCollapsed) { return; } const range = selection.getRangeAt(0); const selectedText = selection.toString().trim(); if (!selectedText) { return; } console.log('Selected text:', selectedText); // 선택된 텍스트와 범위 저장 this.selectedText = selectedText; this.selectedRange = range.cloneRange(); // ViewerCore의 selectedText도 동기화 if (window.documentViewerInstance) { window.documentViewerInstance.selectedText = selectedText; window.documentViewerInstance.selectedRange = range.cloneRange(); } // 하이라이트 버튼 표시 this.showHighlightButton(range); } /** * 하이라이트 버튼 표시 */ showHighlightButton(range) { // 기존 버튼 제거 const existingButton = document.querySelector('.highlight-button'); if (existingButton) { existingButton.remove(); } const rect = range.getBoundingClientRect(); const button = document.createElement('button'); button.className = 'highlight-button'; button.innerHTML = '🖍️ 하이라이트'; button.style.cssText = ` position: fixed; top: ${rect.top - 40}px; left: ${rect.left}px; z-index: 1000; background: #4F46E5; color: white; border: none; padding: 8px 12px; border-radius: 6px; font-size: 12px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.2); `; document.body.appendChild(button); button.addEventListener('click', () => { this.createHighlight(); button.remove(); }); // 3초 후 자동 제거 setTimeout(() => { if (button.parentNode) { button.remove(); } }, 3000); } /** * 색상 버튼으로 하이라이트 생성 */ createHighlightWithColor(color) { console.log('🎨 createHighlightWithColor called with color:', color); console.log('🎨 이전 색상:', this.selectedHighlightColor); // 현재 선택된 텍스트가 있는지 확인 const selection = window.getSelection(); if (!selection.rangeCount || selection.isCollapsed) { console.log('선택된 텍스트가 없습니다'); return; } // 색상 설정 후 하이라이트 생성 this.selectedHighlightColor = color; console.log('🎨 색상 설정 완료:', this.selectedHighlightColor); this.handleTextSelection(); // 텍스트 선택 처리 // 바로 하이라이트 생성 (버튼 클릭 없이) setTimeout(() => { this.createHighlight(); }, 100); } /** * 하이라이트 생성 */ async createHighlight() { console.log('createHighlight called'); console.log('selectedText:', this.selectedText); console.log('selectedRange:', this.selectedRange); if (!this.selectedText || !this.selectedRange) { console.log('선택된 텍스트나 범위가 없습니다'); return; } try { // 문서 전체 텍스트에서 선택된 텍스트의 위치 계산 const documentContent = document.getElementById('document-content'); const fullText = documentContent.textContent; // 선택된 범위의 시작점을 문서 전체에서의 오프셋으로 변환 const startOffset = this.getTextOffset(documentContent, this.selectedRange.startContainer, this.selectedRange.startOffset); const endOffset = startOffset + this.selectedText.length; console.log('Calculated offsets:', { startOffset, endOffset, text: this.selectedText }); const highlightData = { selected_text: this.selectedText, start_offset: startOffset, end_offset: endOffset, highlight_color: this.selectedHighlightColor // 백엔드 API 스키마에 맞게 수정 }; console.log('🎨 하이라이트 데이터 전송:', highlightData); console.log('🎨 현재 선택된 색상:', this.selectedHighlightColor); let highlight; if (window.documentViewerInstance.contentType === 'note') { const noteHighlightData = { note_document_id: window.documentViewerInstance.documentId, ...highlightData }; highlight = await this.api.post('/note-highlights/', noteHighlightData); } else { // 문서 하이라이트의 경우 document_id 추가 const documentHighlightData = { document_id: window.documentViewerInstance.documentId, ...highlightData }; console.log('🔍 최종 전송 데이터:', documentHighlightData); highlight = await this.api.createHighlight(documentHighlightData); } console.log('🔍 생성된 하이라이트 응답:', highlight); console.log('🎨 응답에서 받은 색상:', highlight.highlight_color); this.highlights.push(highlight); // 마지막 생성된 하이라이트 저장 (메모 생성용) this.lastCreatedHighlight = highlight; // 하이라이트 렌더링 this.renderHighlights(); // 선택 해제 window.getSelection().removeAllRanges(); this.selectedText = ''; this.selectedRange = null; // ViewerCore의 selectedText도 동기화 (메모 모달에서 사용하기 전에는 유지) // 메모 모달이 열리기 전에는 selectedText를 유지해야 함 // 하이라이트 버튼 제거 const button = document.querySelector('.highlight-button'); if (button) { button.remove(); } console.log('✅ 하이라이트 생성 완료:', highlight); console.log('🔍 생성된 하이라이트 데이터 구조:', JSON.stringify(highlight, null, 2)); console.log('🔍 생성된 하이라이트 색상 필드들:', { color: highlight.color, highlight_color: highlight.highlight_color, background_color: highlight.background_color }); // 메모 입력 모달 열기 if (window.documentViewerInstance) { window.documentViewerInstance.openNoteInputModal(); } } catch (error) { console.error('하이라이트 생성 실패:', error); alert('하이라이트 생성에 실패했습니다: ' + error.message); } } /** * 하이라이트에 메모 생성 */ async createNoteForHighlight(highlight, content, tags = '') { try { console.log('📝 하이라이트에 메모 생성:', highlight.id, content); const noteData = { highlight_id: highlight.id, content: content, tags: tags }; // 노트 타입에 따라 다른 API 호출 if (window.documentViewerInstance.contentType === 'note') { noteData.note_document_id = window.documentViewerInstance.documentId; } else { noteData.document_id = window.documentViewerInstance.documentId; } const note = await this.api.createNote(noteData); // 메모 목록에 추가 if (!this.notes) this.notes = []; this.notes.push(note); console.log('✅ 메모 생성 완료:', note); // 메모 데이터 새로고침 (캐시 무효화) await this.loadNotes(window.documentViewerInstance.documentId, window.documentViewerInstance.contentType); } catch (error) { console.error('❌ 메모 생성 실패:', error); throw error; } } /** * 하이라이트 색상 변경 */ async updateHighlightColor(highlightId, newColor) { try { console.log('🎨 하이라이트 색상 업데이트:', highlightId, newColor); // API 호출 (구현 필요) await this.api.updateHighlight(highlightId, { highlight_color: newColor }); // 로컬 데이터 업데이트 const highlight = this.highlights.find(h => h.id === highlightId); if (highlight) { highlight.highlight_color = newColor; } // 하이라이트 다시 렌더링 this.renderHighlights(); this.hideTooltip(); console.log('✅ 하이라이트 색상 변경 완료'); } catch (error) { console.error('❌ 하이라이트 색상 변경 실패:', error); throw error; } } /** * 하이라이트 복사 */ async duplicateHighlight(highlightId) { try { console.log('📋 하이라이트 복사:', highlightId); const originalHighlight = this.highlights.find(h => h.id === highlightId); if (!originalHighlight) { throw new Error('원본 하이라이트를 찾을 수 없습니다.'); } // 새 하이라이트 데이터 생성 (약간 다른 위치에) const duplicateData = { document_id: originalHighlight.document_id, start_offset: originalHighlight.start_offset, end_offset: originalHighlight.end_offset, selected_text: originalHighlight.selected_text, highlight_color: originalHighlight.highlight_color, highlight_type: originalHighlight.highlight_type }; // API 호출 const newHighlight = await this.api.createHighlight(duplicateData); // 로컬 데이터에 추가 this.highlights.push(newHighlight); // 하이라이트 다시 렌더링 this.renderHighlights(); this.hideTooltip(); console.log('✅ 하이라이트 복사 완료:', newHighlight); } catch (error) { console.error('❌ 하이라이트 복사 실패:', error); throw error; } } /** * 메모 업데이트 */ async updateNote(noteId, newContent) { try { console.log('✏️ 메모 업데이트:', noteId, newContent); // API 호출 const apiToUse = this.cachedApi || this.api; await apiToUse.updateNote(noteId, { content: newContent }); // 로컬 데이터 업데이트 const note = this.notes.find(n => n.id === noteId); if (note) { note.content = newContent; } // 툴팁 새로고침 (현재 표시 중인 경우) const tooltip = document.getElementById('highlight-tooltip'); if (tooltip) { // 간단히 툴팁을 다시 로드하는 대신 텍스트만 업데이트 const noteElement = document.querySelector(`[data-note-id="${noteId}"] .text-gray-800`); if (noteElement) { noteElement.textContent = newContent; } } console.log('✅ 메모 업데이트 완료'); } catch (error) { console.error('❌ 메모 업데이트 실패:', error); throw error; } } /** * 텍스트 오프셋 계산 */ getTextOffset(root, node, offset) { let textOffset = 0; const walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, null, false ); let currentNode; while (currentNode = walker.nextNode()) { if (currentNode === node) { return textOffset + offset; } textOffset += currentNode.textContent.length; } return textOffset; } /** * 메모 저장 */ async saveNote() { const noteContent = window.documentViewerInstance.noteForm.content; const tags = window.documentViewerInstance.noteForm.tags; if (!noteContent.trim()) { alert('메모 내용을 입력해주세요.'); return; } try { window.documentViewerInstance.noteLoading = true; const noteData = { content: noteContent, tags: tags }; let savedNote; if (window.documentViewerInstance.contentType === 'note') { noteData.note_document_id = window.documentViewerInstance.documentId; savedNote = await this.api.post('/note-notes/', noteData); } else { savedNote = await this.api.createNote(noteData); } this.notes.push(savedNote); // 폼 초기화 window.documentViewerInstance.noteForm.content = ''; window.documentViewerInstance.noteForm.tags = ''; window.documentViewerInstance.showNoteModal = false; console.log('메모 저장 완료:', savedNote); } catch (error) { console.error('메모 저장 실패:', error); alert('메모 저장에 실패했습니다: ' + error.message); } finally { window.documentViewerInstance.noteLoading = false; } } /** * 메모 삭제 */ async deleteNote(noteId) { if (!confirm('이 메모를 삭제하시겠습니까?')) { return; } try { await this.api.deleteNote(noteId); this.notes = this.notes.filter(n => n.id !== noteId); // ViewerCore의 filterNotes 호출 if (window.documentViewerInstance.filterNotes) { window.documentViewerInstance.filterNotes(); } console.log('메모 삭제 완료:', noteId); } catch (error) { console.error('메모 삭제 실패:', error); alert('메모 삭제에 실패했습니다: ' + error.message); } } /** * 하이라이트 삭제 */ async deleteHighlight(highlightId) { try { await this.api.delete(`/highlights/${highlightId}`); // 하이라이트 배열에서 제거 this.highlights = this.highlights.filter(h => h.id !== highlightId); // 메모 배열에서도 해당 하이라이트의 메모들 제거 this.notes = this.notes.filter(note => note.highlight_id !== highlightId); // 캐시 무효화 (하이라이트와 메모 모두) if (window.documentViewerInstance && window.documentViewerInstance.cacheManager) { window.documentViewerInstance.cacheManager.invalidateCategory('highlights'); window.documentViewerInstance.cacheManager.invalidateCategory('notes'); console.log('🗑️ 하이라이트 삭제 후 캐시 무효화 완료'); } // 화면 다시 렌더링 this.renderHighlights(); console.log('하이라이트 삭제 완료:', highlightId); } catch (error) { console.error('하이라이트 삭제 실패:', error); alert('하이라이트 삭제에 실패했습니다: ' + error.message); } } /** * 선택된 텍스트로 메모 생성 */ async createNoteFromSelection(documentId, contentType) { if (!this.selectedText || !this.selectedRange) return; try { console.log('📝 메모 생성 시작:', this.selectedText); // 하이라이트 생성 await this.createHighlight(); // 생성된 하이라이트 찾기 (가장 최근 생성된 것) const highlightData = this.highlights[this.highlights.length - 1]; // 메모 내용 입력받기 const content = prompt('메모 내용을 입력하세요:', ''); if (content === null) { // 취소한 경우 하이라이트 제거 if (highlightData && highlightData.id) { await this.api.deleteHighlight(highlightData.id); this.highlights = this.highlights.filter(h => h.id !== highlightData.id); this.renderHighlights(); } return; } // 메모 생성 const noteData = { highlight_id: highlightData.id, content: content }; // 노트와 문서에 따라 다른 API 호출 let note; if (contentType === 'note') { noteData.note_id = documentId; // 노트 메모는 note_id 필요 note = await this.api.post('/note-notes/', noteData); } else { // 문서 메모는 document_id 필요 noteData.document_id = documentId; note = await this.api.createNote(noteData); } this.notes.push(note); console.log('✅ 메모 생성 완료:', note); alert('메모가 생성되었습니다.'); } catch (error) { console.error('메모 생성 실패:', error); alert('메모 생성에 실패했습니다: ' + error.message); } } /** * 하이라이트 클릭 시 모달 표시 */ showHighlightModal(highlights) { console.log('🔍 하이라이트 모달 표시:', highlights); // 첫 번째 하이라이트로 툴팁 표시 const firstHighlight = highlights[0]; const element = document.querySelector(`[data-highlight-id="${firstHighlight.id}"]`); if (element) { this.showHighlightTooltip(firstHighlight, element); } } /** * 동일한 텍스트 범위의 모든 하이라이트 찾기 */ findOverlappingHighlights(clickedHighlight) { const overlapping = []; this.highlights.forEach(highlight => { // 텍스트 범위가 겹치는지 확인 const isOverlapping = ( (highlight.start_offset <= clickedHighlight.end_offset && highlight.end_offset >= clickedHighlight.start_offset) || (clickedHighlight.start_offset <= highlight.end_offset && clickedHighlight.end_offset >= highlight.start_offset) ); if (isOverlapping) { overlapping.push(highlight); } }); // 시작 위치 순으로 정렬 return overlapping.sort((a, b) => a.start_offset - b.start_offset); } /** * 색상별로 하이라이트 그룹화 */ groupHighlightsByColor(highlights) { const colorGroups = {}; highlights.forEach(highlight => { const color = highlight.highlight_color || highlight.color || '#FFFF00'; if (!colorGroups[color]) { colorGroups[color] = []; } colorGroups[color].push(highlight); }); return colorGroups; } /** * 색상 이름 반환 */ getColorName(color) { const colorNames = { '#FFFF00': '노란색', '#90EE90': '초록색', '#FFCCCB': '분홍색', '#87CEEB': '파란색' }; return colorNames[color] || '기타'; } /** * 날짜 포맷팅 (상세) */ formatDate(dateString) { if (!dateString) return '알 수 없음'; const date = new Date(dateString); return date.toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } /** * 날짜 포맷팅 (간단) */ formatShortDate(dateString) { if (!dateString) return ''; const date = new Date(dateString); const now = new Date(); const diffTime = Math.abs(now - date); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); if (diffDays === 1) { return '오늘'; } else if (diffDays === 2) { return '어제'; } else if (diffDays <= 7) { return `${diffDays - 1}일 전`; } else { return date.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' }); } } /** * 하이라이트 툴팁 표시 */ async showHighlightTooltip(clickedHighlight, element) { // 기존 말풍선 제거 this.hideTooltip(); // 메모 데이터 다시 로드 (최신 상태 보장) console.log('📝 하이라이트 툴팁용 메모 로드 시작...'); const documentId = window.documentViewerInstance.documentId; const contentType = window.documentViewerInstance.contentType; console.log('📝 메모 로드 파라미터:', { documentId, contentType }); console.log('📝 기존 메모 개수:', this.notes ? this.notes.length : 'undefined'); await this.loadNotes(documentId, contentType); console.log('📝 메모 로드 완료:', this.notes.length, '개'); console.log('📝 로드된 메모 상세:', this.notes.map(n => ({ id: n.id, highlight_id: n.highlight_id, content: n.content, created_at: n.created_at }))); // 동일한 범위의 모든 하이라이트 찾기 const overlappingHighlights = this.findOverlappingHighlights(clickedHighlight); const colorGroups = this.groupHighlightsByColor(overlappingHighlights); console.log('🎨 겹치는 하이라이트:', overlappingHighlights.length, '개'); const tooltip = document.createElement('div'); tooltip.id = 'highlight-tooltip'; tooltip.className = 'absolute bg-white border border-gray-300 rounded-lg shadow-lg p-4 z-50 max-w-lg'; tooltip.style.minWidth = '350px'; // 선택된 텍스트 표시 (가장 긴 텍스트 사용) const longestText = overlappingHighlights.reduce((longest, current) => current.selected_text.length > longest.length ? current.selected_text : longest, '' ); let tooltipHTML = `