주요 수정사항:
- 노트 간 링크 네비게이션 수정 (target_note_id 우선 사용)
- 노트 백링크 네비게이션 수정 (source_note_id 우선 사용)
- 노트 링크 삭제 API 분기 처리 (/note-links vs /document-links)
- 하이라이트 삭제 시 메모 캐시 무효화 추가
- 하이라이트 메모 삭제 API 엔드포인트 추가 (DELETE /highlight-notes/{note_id})
- URL 파싱 개선 (null/undefined ID 감지 및 오류 처리)
- 노트 링크 생성 응답에 source_content_type, target_content_type 추가
- 통합 툴팁에서 노트 링크 제목 표시 수정 (target_note_title 사용)
- 링크 삭제 버튼에서 null 참조 오류 수정
수정된 파일:
- frontend: viewer-core.js, link-manager.js, highlight-manager.js, api.js, cached-api.js
- backend: note_links.py, notes.py
- 브라우저 캐시 무효화: 버전 v=2025012623
1310 lines
52 KiB
JavaScript
1310 lines
52 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<div class="mb-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="text-lg font-semibold text-blue-800 flex items-center">
|
|
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
|
|
</svg>
|
|
하이라이트 정보
|
|
</div>
|
|
<div class="text-xs text-gray-500">${overlappingHighlights.length}개 하이라이트</div>
|
|
</div>
|
|
|
|
<div class="font-medium text-gray-900 bg-blue-50 px-4 py-3 rounded-lg border-l-4 border-blue-500">
|
|
<div class="text-sm text-blue-700 mb-1">선택된 텍스트</div>
|
|
<div class="text-base">"${longestText}"</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 색상별로 메모 표시
|
|
tooltipHTML += '<div class="space-y-4">';
|
|
|
|
Object.entries(colorGroups).forEach(([color, highlights]) => {
|
|
const colorName = this.getColorName(color);
|
|
|
|
// 각 하이라이트에 대한 메모 찾기 (디버깅 로그 추가)
|
|
const allNotes = highlights.flatMap(h => {
|
|
const notesForHighlight = this.notes.filter(note => note.highlight_id === h.id);
|
|
console.log(`📝 하이라이트 ${h.id}에 대한 메모:`, notesForHighlight.length, '개');
|
|
if (notesForHighlight.length > 0) {
|
|
console.log('📝 메모 내용:', notesForHighlight.map(n => n.content));
|
|
}
|
|
return notesForHighlight;
|
|
});
|
|
|
|
console.log(`🎨 ${colorName} 하이라이트의 총 메모:`, allNotes.length, '개');
|
|
|
|
const createdDate = highlights[0].created_at ? this.formatDate(highlights[0].created_at) : '알 수 없음';
|
|
|
|
tooltipHTML += `
|
|
<div class="border rounded-lg p-4 bg-gradient-to-r from-gray-50 to-gray-100" style="border-left: 4px solid ${color}">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="flex items-center space-x-3">
|
|
<div class="w-4 h-4 rounded-full shadow-sm" style="background-color: ${color}"></div>
|
|
<div>
|
|
<span class="text-sm font-semibold text-gray-800">${colorName} 하이라이트</span>
|
|
<div class="text-xs text-gray-600">${createdDate} 생성</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<button onclick="window.documentViewerInstance.highlightManager.changeHighlightColor('${highlights[0].id}')"
|
|
class="text-xs bg-gray-500 text-white px-2 py-1 rounded hover:bg-gray-600 transition-colors"
|
|
title="색상 변경">
|
|
🎨
|
|
</button>
|
|
<button onclick="window.documentViewerInstance.highlightManager.showAddNoteForm('${highlights[0].id}')"
|
|
class="text-xs bg-blue-500 text-white px-2 py-1 rounded hover:bg-blue-600 transition-colors">
|
|
📝 메모 추가
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 메모 목록 -->
|
|
<div class="mb-3">
|
|
<div class="text-sm font-medium text-gray-700 mb-2 flex items-center">
|
|
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"></path>
|
|
<path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 102 0V3a2 2 0 012 0v1a1 1 0 102 0V3a2 2 0 012 2v6.586A2 2 0 0115.414 13L13 15.586A2 2 0 0111.586 16H6a2 2 0 01-2-2V5zm8 4a1 1 0 10-2 0v2a1 1 0 102 0V9z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
메모 (${allNotes.length}개)
|
|
</div>
|
|
|
|
<div id="notes-list-${highlights[0].id}" class="space-y-2 max-h-40 overflow-y-auto">
|
|
${allNotes.length > 0 ?
|
|
allNotes.map(note => `
|
|
<div class="bg-white p-3 rounded-lg border shadow-sm group">
|
|
<div class="text-gray-800 text-sm leading-relaxed">${note.content}</div>
|
|
<div class="text-xs text-gray-500 mt-2 flex justify-between items-center">
|
|
<span class="flex items-center">
|
|
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
${this.formatShortDate(note.created_at)}
|
|
</span>
|
|
<div class="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<button onclick="window.documentViewerInstance.highlightManager.editNote('${note.id}', '${note.content.replace(/'/g, "\\'")}');"
|
|
class="text-blue-600 hover:text-blue-800 mr-2 text-xs"
|
|
title="메모 편집">
|
|
✏️
|
|
</button>
|
|
<button onclick="window.documentViewerInstance.highlightManager.deleteNote('${note.id}')"
|
|
class="text-red-600 hover:text-red-800 text-xs"
|
|
title="메모 삭제">
|
|
🗑️
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('') :
|
|
'<div class="text-sm text-gray-500 italic bg-white p-3 rounded-lg border">메모가 없습니다. 위의 "📝 메모 추가" 버튼을 클릭해보세요!</div>'
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
tooltipHTML += '</div>';
|
|
|
|
// 하이라이트 관리 버튼들
|
|
tooltipHTML += `
|
|
<div class="mt-4 pt-4 border-t border-gray-200">
|
|
<div class="flex items-center justify-between">
|
|
<div class="text-sm font-medium text-gray-700 flex items-center">
|
|
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
하이라이트 관리
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<button onclick="window.documentViewerInstance.highlightManager.duplicateHighlight('${clickedHighlight.id}')"
|
|
class="text-xs bg-green-500 text-white px-3 py-1 rounded hover:bg-green-600 transition-colors flex items-center"
|
|
title="하이라이트 복사">
|
|
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z"></path>
|
|
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z"></path>
|
|
</svg>
|
|
복사
|
|
</button>
|
|
<button onclick="window.documentViewerInstance.deleteHighlightWithConfirm('${clickedHighlight.id}')"
|
|
class="text-xs bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600 transition-colors flex items-center"
|
|
title="하이라이트 삭제">
|
|
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
</svg>
|
|
삭제
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
tooltipHTML += `
|
|
<div class="flex justify-end mt-3">
|
|
<button onclick="window.documentViewerInstance.highlightManager.hideTooltip()"
|
|
class="text-xs bg-gray-500 text-white px-3 py-1 rounded hover:bg-gray-600">
|
|
닫기
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
tooltip.innerHTML = tooltipHTML;
|
|
|
|
// 위치 계산
|
|
const rect = element.getBoundingClientRect();
|
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
|
|
|
document.body.appendChild(tooltip);
|
|
|
|
// 말풍선 위치 조정
|
|
const tooltipRect = tooltip.getBoundingClientRect();
|
|
let top = rect.bottom + scrollTop + 5;
|
|
let left = rect.left + scrollLeft;
|
|
|
|
// 화면 경계 체크
|
|
if (left + tooltipRect.width > window.innerWidth) {
|
|
left = window.innerWidth - tooltipRect.width - 10;
|
|
}
|
|
if (top + tooltipRect.height > window.innerHeight + scrollTop) {
|
|
top = rect.top + scrollTop - tooltipRect.height - 5;
|
|
}
|
|
|
|
tooltip.style.top = top + 'px';
|
|
tooltip.style.left = left + 'px';
|
|
|
|
// 외부 클릭 시 닫기
|
|
setTimeout(() => {
|
|
document.addEventListener('click', this.handleTooltipOutsideClick.bind(this));
|
|
}, 100);
|
|
}
|
|
|
|
/**
|
|
* 말풍선 숨기기
|
|
*/
|
|
hideTooltip() {
|
|
const highlightTooltip = document.getElementById('highlight-tooltip');
|
|
if (highlightTooltip) {
|
|
highlightTooltip.remove();
|
|
}
|
|
|
|
document.removeEventListener('click', this.handleTooltipOutsideClick.bind(this));
|
|
}
|
|
|
|
/**
|
|
* 말풍선 외부 클릭 처리
|
|
*/
|
|
handleTooltipOutsideClick(e) {
|
|
const highlightTooltip = document.getElementById('highlight-tooltip');
|
|
|
|
const isOutsideHighlightTooltip = highlightTooltip && !highlightTooltip.contains(e.target);
|
|
|
|
if (isOutsideHighlightTooltip) {
|
|
this.hideTooltip();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 메모 추가 폼 표시
|
|
*/
|
|
showAddNoteForm(highlightId) {
|
|
console.log('🔍 showAddNoteForm 호출됨, highlightId:', highlightId);
|
|
const tooltip = document.getElementById('highlight-tooltip');
|
|
if (!tooltip) return;
|
|
|
|
const notesList = document.getElementById(`notes-list-${highlightId}`);
|
|
if (!notesList) return;
|
|
|
|
// 기존 폼이 있으면 제거
|
|
const existingForm = document.getElementById('add-note-form');
|
|
if (existingForm) {
|
|
existingForm.remove();
|
|
}
|
|
|
|
const formHTML = `
|
|
<div id="add-note-form" class="mt-2 p-2 bg-blue-50 rounded border">
|
|
<textarea id="new-note-content"
|
|
placeholder="메모를 입력하세요..."
|
|
class="w-full p-2 border border-gray-300 rounded text-sm resize-none"
|
|
rows="3"></textarea>
|
|
<div class="flex justify-end mt-2 space-x-2">
|
|
<button onclick="window.documentViewerInstance.highlightManager.cancelAddNote('${highlightId}')"
|
|
class="text-xs bg-gray-500 text-white px-3 py-1 rounded hover:bg-gray-600">
|
|
취소
|
|
</button>
|
|
<button onclick="window.documentViewerInstance.highlightManager.saveNewNote('${highlightId}')"
|
|
class="text-xs bg-blue-500 text-white px-3 py-1 rounded hover:bg-blue-600">
|
|
저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
notesList.insertAdjacentHTML('afterend', formHTML);
|
|
|
|
// 텍스트 영역에 포커스
|
|
setTimeout(() => {
|
|
document.getElementById('new-note-content').focus();
|
|
}, 100);
|
|
}
|
|
|
|
/**
|
|
* 메모 추가 취소
|
|
*/
|
|
cancelAddNote(highlightId) {
|
|
const form = document.getElementById('add-note-form');
|
|
if (form) {
|
|
form.remove();
|
|
}
|
|
|
|
// 툴팁 다시 표시
|
|
const highlight = this.highlights.find(h => h.id === highlightId);
|
|
if (highlight) {
|
|
const element = document.querySelector(`[data-highlight-id="${highlightId}"]`);
|
|
if (element) {
|
|
this.showHighlightTooltip(highlight, element);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 새 메모 저장
|
|
*/
|
|
async saveNewNote(highlightId) {
|
|
const content = document.getElementById('new-note-content').value.trim();
|
|
if (!content) {
|
|
alert('메모 내용을 입력해주세요');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const noteData = {
|
|
highlight_id: highlightId,
|
|
content: content
|
|
};
|
|
|
|
// 문서 타입에 따라 다른 API 호출
|
|
let note;
|
|
if (window.documentViewerInstance.contentType === 'note') {
|
|
noteData.note_id = window.documentViewerInstance.documentId;
|
|
note = await this.api.post('/note-notes/', noteData);
|
|
} else {
|
|
noteData.document_id = window.documentViewerInstance.documentId;
|
|
note = await this.api.createNote(noteData);
|
|
}
|
|
|
|
this.notes.push(note);
|
|
|
|
// 폼 제거
|
|
const form = document.getElementById('add-note-form');
|
|
if (form) {
|
|
form.remove();
|
|
}
|
|
|
|
// 툴팁 다시 표시
|
|
const highlight = this.highlights.find(h => h.id === highlightId);
|
|
if (highlight) {
|
|
const element = document.querySelector(`[data-highlight-id="${highlightId}"]`);
|
|
if (element) {
|
|
this.showHighlightTooltip(highlight, element);
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Failed to save note:', error);
|
|
alert('메모 저장에 실패했습니다');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 날짜 포맷팅 (짧은 형식)
|
|
*/
|
|
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 <= 7) {
|
|
return `${diffDays}일 전`;
|
|
} else {
|
|
return date.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 텍스트 선택 리스너 제거 (정리용)
|
|
*/
|
|
removeTextSelectionListener() {
|
|
if (this.textSelectionHandler) {
|
|
document.removeEventListener('mouseup', this.textSelectionHandler);
|
|
this.textSelectionHandler = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 전역으로 내보내기
|
|
window.HighlightManager = HighlightManager;
|