하이라이트 색상 문제 해결 및 다중 하이라이트 렌더링 개선
주요 수정사항: - 하이라이트 생성 시 color → highlight_color 필드명 수정으로 색상 전달 문제 해결 - 분홍색을 더 연하게 변경하여 글씨 가독성 향상 - 다중 하이라이트 렌더링을 위아래 균등 분할로 개선 - CSS highlight-span 클래스 추가 및 색상 적용 강화 - 하이라이트 생성/렌더링 과정에 상세한 디버깅 로그 추가 UI 개선: - 단일 하이라이트: 선택한 색상으로 정확히 표시 - 다중 하이라이트: 위아래로 균등하게 색상 분할 표시 - 메모 입력 모달에서 선택된 텍스트 표시 개선 버그 수정: - 프론트엔드-백엔드 API 스키마 불일치 해결 - CSS 스타일 우선순위 문제 해결 - 하이라이트 색상이 노랑색으로만 표시되던 문제 해결
This commit is contained in:
268
frontend/static/js/viewer/features/bookmark-manager.js
Normal file
268
frontend/static/js/viewer/features/bookmark-manager.js
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* BookmarkManager 모듈
|
||||
* 북마크 관리
|
||||
*/
|
||||
class BookmarkManager {
|
||||
constructor(api) {
|
||||
this.api = api;
|
||||
// 캐싱된 API 사용 (사용 가능한 경우)
|
||||
this.cachedApi = window.cachedApi || api;
|
||||
this.bookmarks = [];
|
||||
this.bookmarkForm = {
|
||||
title: '',
|
||||
description: ''
|
||||
};
|
||||
this.editingBookmark = null;
|
||||
this.currentScrollPosition = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크 데이터 로드
|
||||
*/
|
||||
async loadBookmarks(documentId) {
|
||||
try {
|
||||
this.bookmarks = await this.cachedApi.get('/bookmarks', { document_id: documentId }, { category: 'bookmarks' }).catch(() => []);
|
||||
return this.bookmarks || [];
|
||||
} catch (error) {
|
||||
console.error('북마크 로드 실패:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크 추가
|
||||
*/
|
||||
async addBookmark(document) {
|
||||
const scrollPosition = window.scrollY;
|
||||
this.bookmarkForm = {
|
||||
title: `${document.title} - ${new Date().toLocaleString()}`,
|
||||
description: ''
|
||||
};
|
||||
this.currentScrollPosition = scrollPosition;
|
||||
|
||||
// ViewerCore의 모달 상태 업데이트
|
||||
if (window.documentViewerInstance) {
|
||||
window.documentViewerInstance.showBookmarkModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크 편집
|
||||
*/
|
||||
editBookmark(bookmark) {
|
||||
this.editingBookmark = bookmark;
|
||||
this.bookmarkForm = {
|
||||
title: bookmark.title,
|
||||
description: bookmark.description || ''
|
||||
};
|
||||
|
||||
// ViewerCore의 모달 상태 업데이트
|
||||
if (window.documentViewerInstance) {
|
||||
window.documentViewerInstance.showBookmarkModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크 저장
|
||||
*/
|
||||
async saveBookmark(documentId) {
|
||||
try {
|
||||
// ViewerCore의 로딩 상태 업데이트
|
||||
if (window.documentViewerInstance) {
|
||||
window.documentViewerInstance.bookmarkLoading = true;
|
||||
}
|
||||
|
||||
const bookmarkData = {
|
||||
title: this.bookmarkForm.title,
|
||||
description: this.bookmarkForm.description,
|
||||
scroll_position: this.currentScrollPosition || 0
|
||||
};
|
||||
|
||||
if (this.editingBookmark) {
|
||||
// 북마크 수정
|
||||
const updatedBookmark = await this.api.updateBookmark(this.editingBookmark.id, bookmarkData);
|
||||
const index = this.bookmarks.findIndex(b => b.id === this.editingBookmark.id);
|
||||
if (index !== -1) {
|
||||
this.bookmarks[index] = updatedBookmark;
|
||||
}
|
||||
} else {
|
||||
// 새 북마크 생성
|
||||
bookmarkData.document_id = documentId;
|
||||
const newBookmark = await this.api.createBookmark(bookmarkData);
|
||||
this.bookmarks.push(newBookmark);
|
||||
}
|
||||
|
||||
this.closeBookmarkModal();
|
||||
console.log('북마크 저장 완료');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to save bookmark:', error);
|
||||
alert('북마크 저장에 실패했습니다');
|
||||
} finally {
|
||||
// ViewerCore의 로딩 상태 업데이트
|
||||
if (window.documentViewerInstance) {
|
||||
window.documentViewerInstance.bookmarkLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크 삭제
|
||||
*/
|
||||
async deleteBookmark(bookmarkId) {
|
||||
if (!confirm('이 북마크를 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.api.deleteBookmark(bookmarkId);
|
||||
this.bookmarks = this.bookmarks.filter(b => b.id !== bookmarkId);
|
||||
console.log('북마크 삭제 완료:', bookmarkId);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete bookmark:', error);
|
||||
alert('북마크 삭제에 실패했습니다');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크로 스크롤
|
||||
*/
|
||||
scrollToBookmark(bookmark) {
|
||||
window.scrollTo({
|
||||
top: bookmark.scroll_position,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크 모달 닫기
|
||||
*/
|
||||
closeBookmarkModal() {
|
||||
this.editingBookmark = null;
|
||||
this.bookmarkForm = { title: '', description: '' };
|
||||
this.currentScrollPosition = null;
|
||||
|
||||
// ViewerCore의 모달 상태 업데이트
|
||||
if (window.documentViewerInstance) {
|
||||
window.documentViewerInstance.showBookmarkModal = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택된 텍스트로 북마크 생성
|
||||
*/
|
||||
async createBookmarkFromSelection(documentId, selectedText, selectedRange) {
|
||||
if (!selectedText || !selectedRange) return;
|
||||
|
||||
try {
|
||||
// 하이라이트 생성 (북마크는 주황색)
|
||||
const highlightData = await this.createHighlight(selectedText, selectedRange, '#FFA500');
|
||||
|
||||
// 북마크 생성
|
||||
const bookmarkData = {
|
||||
highlight_id: highlightData.id,
|
||||
title: selectedText.substring(0, 50) + (selectedText.length > 50 ? '...' : ''),
|
||||
description: `선택된 텍스트: "${selectedText}"`
|
||||
};
|
||||
|
||||
const bookmark = await this.api.createBookmark(documentId, bookmarkData);
|
||||
this.bookmarks.push(bookmark);
|
||||
|
||||
console.log('선택 텍스트 북마크 생성 완료:', bookmark);
|
||||
alert('북마크가 생성되었습니다.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('북마크 생성 실패:', error);
|
||||
alert('북마크 생성에 실패했습니다: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 하이라이트 생성 (북마크용)
|
||||
* HighlightManager와 연동
|
||||
*/
|
||||
async createHighlight(selectedText, selectedRange, color) {
|
||||
try {
|
||||
const viewerInstance = window.documentViewerInstance;
|
||||
if (viewerInstance && viewerInstance.highlightManager) {
|
||||
// HighlightManager의 상태 설정
|
||||
viewerInstance.highlightManager.selectedText = selectedText;
|
||||
viewerInstance.highlightManager.selectedRange = selectedRange;
|
||||
viewerInstance.highlightManager.selectedHighlightColor = color;
|
||||
|
||||
// ViewerCore의 상태도 동기화
|
||||
viewerInstance.selectedText = selectedText;
|
||||
viewerInstance.selectedRange = selectedRange;
|
||||
viewerInstance.selectedHighlightColor = color;
|
||||
|
||||
// HighlightManager의 createHighlight 호출
|
||||
await viewerInstance.highlightManager.createHighlight();
|
||||
|
||||
// 생성된 하이라이트 찾기 (가장 최근 생성된 것)
|
||||
const highlights = viewerInstance.highlightManager.highlights;
|
||||
if (highlights && highlights.length > 0) {
|
||||
return highlights[highlights.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
// 폴백: 간단한 하이라이트 데이터 반환
|
||||
console.warn('HighlightManager 연동 실패, 폴백 데이터 사용');
|
||||
return {
|
||||
id: Date.now().toString(),
|
||||
selected_text: selectedText,
|
||||
color: color,
|
||||
start_offset: 0,
|
||||
end_offset: selectedText.length
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('하이라이트 생성 실패:', error);
|
||||
|
||||
// 폴백: 간단한 하이라이트 데이터 반환
|
||||
return {
|
||||
id: Date.now().toString(),
|
||||
selected_text: selectedText,
|
||||
color: color,
|
||||
start_offset: 0,
|
||||
end_offset: selectedText.length
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크 모드 활성화
|
||||
*/
|
||||
activateBookmarkMode() {
|
||||
console.log('🔖 북마크 모드 활성화');
|
||||
|
||||
// 현재 선택된 텍스트가 있는지 확인
|
||||
const selection = window.getSelection();
|
||||
if (selection.rangeCount > 0 && !selection.isCollapsed) {
|
||||
const selectedText = selection.toString().trim();
|
||||
if (selectedText.length > 0) {
|
||||
// ViewerCore의 선택된 텍스트 상태 업데이트
|
||||
if (window.documentViewerInstance) {
|
||||
window.documentViewerInstance.selectedText = selectedText;
|
||||
window.documentViewerInstance.selectedRange = selection.getRangeAt(0);
|
||||
}
|
||||
this.createBookmarkFromSelection(
|
||||
window.documentViewerInstance?.documentId,
|
||||
selectedText,
|
||||
selection.getRangeAt(0)
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 텍스트 선택 모드 활성화
|
||||
console.log('📝 텍스트 선택 모드 활성화');
|
||||
if (window.documentViewerInstance) {
|
||||
window.documentViewerInstance.activeMode = 'bookmark';
|
||||
window.documentViewerInstance.showSelectionMessage('텍스트를 선택하세요.');
|
||||
window.documentViewerInstance.setupTextSelectionListener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역으로 내보내기
|
||||
window.BookmarkManager = BookmarkManager;
|
||||
1017
frontend/static/js/viewer/features/highlight-manager.js
Normal file
1017
frontend/static/js/viewer/features/highlight-manager.js
Normal file
File diff suppressed because it is too large
Load Diff
591
frontend/static/js/viewer/features/link-manager.js
Normal file
591
frontend/static/js/viewer/features/link-manager.js
Normal file
@@ -0,0 +1,591 @@
|
||||
/**
|
||||
* LinkManager 모듈
|
||||
* 문서 링크 및 백링크 통합 관리
|
||||
*/
|
||||
class LinkManager {
|
||||
constructor(api) {
|
||||
console.log('🔗 LinkManager 초기화 시작');
|
||||
this.api = api;
|
||||
// 캐싱된 API 사용 (사용 가능한 경우)
|
||||
this.cachedApi = window.cachedApi || api;
|
||||
this.documentLinks = [];
|
||||
this.backlinks = [];
|
||||
this.selectedText = '';
|
||||
this.selectedRange = null;
|
||||
this.availableBooks = [];
|
||||
this.filteredDocuments = [];
|
||||
|
||||
console.log('✅ LinkManager 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 링크 데이터 로드
|
||||
*/
|
||||
async loadDocumentLinks(documentId) {
|
||||
try {
|
||||
this.documentLinks = await this.cachedApi.get('/document-links', { document_id: documentId }, { category: 'links' }).catch(() => []);
|
||||
return this.documentLinks || [];
|
||||
} catch (error) {
|
||||
console.error('문서 링크 로드 실패:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 백링크 데이터 로드
|
||||
*/
|
||||
async loadBacklinks(documentId) {
|
||||
try {
|
||||
this.backlinks = await this.cachedApi.get('/document-links/backlinks', { target_document_id: documentId }, { category: 'links' }).catch(() => []);
|
||||
return this.backlinks || [];
|
||||
} catch (error) {
|
||||
console.error('백링크 로드 실패:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 링크 렌더링
|
||||
*/
|
||||
renderDocumentLinks() {
|
||||
const documentContent = document.getElementById('document-content');
|
||||
if (!documentContent) return;
|
||||
|
||||
console.log('🔗 링크 렌더링 시작 - 총', this.documentLinks.length, '개');
|
||||
|
||||
// 기존 링크 제거
|
||||
const existingLinks = documentContent.querySelectorAll('.document-link');
|
||||
existingLinks.forEach(el => {
|
||||
const parent = el.parentNode;
|
||||
parent.replaceChild(document.createTextNode(el.textContent), el);
|
||||
parent.normalize();
|
||||
});
|
||||
|
||||
// 각 링크 렌더링
|
||||
this.documentLinks.forEach(link => {
|
||||
this.renderSingleLink(link);
|
||||
});
|
||||
|
||||
console.log('✅ 링크 렌더링 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 개별 링크 렌더링
|
||||
*/
|
||||
renderSingleLink(link) {
|
||||
const content = document.getElementById('document-content');
|
||||
const textContent = content.textContent;
|
||||
|
||||
if (link.start_offset >= textContent.length || link.end_offset > textContent.length) {
|
||||
console.warn('링크 위치가 텍스트 범위를 벗어남:', link);
|
||||
return;
|
||||
}
|
||||
|
||||
const walker = document.createTreeWalker(
|
||||
content,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
let currentOffset = 0;
|
||||
let node;
|
||||
|
||||
while (node = walker.nextNode()) {
|
||||
const nodeLength = node.textContent.length;
|
||||
const nodeStart = currentOffset;
|
||||
const nodeEnd = currentOffset + nodeLength;
|
||||
|
||||
// 링크 범위와 겹치는지 확인
|
||||
if (nodeEnd > link.start_offset && nodeStart < link.end_offset) {
|
||||
const linkStart = Math.max(0, link.start_offset - nodeStart);
|
||||
const linkEnd = Math.min(nodeLength, link.end_offset - nodeStart);
|
||||
|
||||
if (linkStart < linkEnd) {
|
||||
this.applyLinkToNode(node, linkStart, linkEnd, link);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
currentOffset = nodeEnd;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 텍스트 노드에 링크 적용
|
||||
*/
|
||||
applyLinkToNode(textNode, start, end, link) {
|
||||
const text = textNode.textContent;
|
||||
const beforeText = text.substring(0, start);
|
||||
const linkText = text.substring(start, end);
|
||||
const afterText = text.substring(end);
|
||||
|
||||
// 링크 스팬 생성
|
||||
const span = document.createElement('span');
|
||||
span.className = 'document-link';
|
||||
span.textContent = linkText;
|
||||
span.dataset.linkId = link.id;
|
||||
|
||||
// 링크 스타일 (보라색) - 레이아웃 안전
|
||||
span.style.cssText = `
|
||||
color: #7C3AED !important;
|
||||
text-decoration: underline !important;
|
||||
cursor: pointer !important;
|
||||
background-color: rgba(124, 58, 237, 0.1) !important;
|
||||
border-radius: 2px !important;
|
||||
padding: 0 1px !important;
|
||||
display: inline !important;
|
||||
line-height: inherit !important;
|
||||
vertical-align: baseline !important;
|
||||
margin: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
`;
|
||||
|
||||
// 클릭 이벤트 추가
|
||||
span.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.showLinkTooltip(link, span);
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 백링크 렌더링 (링크와 동일한 방식)
|
||||
*/
|
||||
renderBacklinks() {
|
||||
const documentContent = document.getElementById('document-content');
|
||||
if (!documentContent) return;
|
||||
|
||||
console.log('🔗 백링크 렌더링 시작 - 총', this.backlinks.length, '개');
|
||||
|
||||
// 기존 백링크는 제거하지 않고 중복 체크만 함
|
||||
const existingBacklinks = documentContent.querySelectorAll('.backlink-highlight');
|
||||
console.log(`🔍 기존 백링크 ${existingBacklinks.length}개 발견 (유지)`);
|
||||
|
||||
// 각 백링크 렌더링 (중복되지 않는 것만)
|
||||
this.backlinks.forEach(backlink => {
|
||||
// 이미 렌더링된 백링크인지 확인
|
||||
const existingBacklink = Array.from(existingBacklinks).find(el =>
|
||||
el.dataset.backlinkId === backlink.id.toString()
|
||||
);
|
||||
|
||||
if (!existingBacklink) {
|
||||
console.log(`🆕 새로운 백링크 렌더링: ${backlink.id}`);
|
||||
this.renderSingleBacklink(backlink);
|
||||
} else {
|
||||
console.log(`✅ 백링크 이미 존재: ${backlink.id}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ 백링크 렌더링 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 개별 백링크 렌더링
|
||||
*/
|
||||
renderSingleBacklink(backlink) {
|
||||
const content = document.getElementById('document-content');
|
||||
if (!content) return;
|
||||
|
||||
// 실제 문서 내용만 추출 (CSS, 스크립트 제외)
|
||||
const contentClone = content.cloneNode(true);
|
||||
// 스타일 태그와 스크립트 태그 제거
|
||||
const styleTags = contentClone.querySelectorAll('style, script');
|
||||
styleTags.forEach(tag => tag.remove());
|
||||
|
||||
const textContent = contentClone.textContent || contentClone.innerText || '';
|
||||
|
||||
// target_text가 있으면 사용, 없으면 selected_text 사용
|
||||
const searchText = backlink.target_text || backlink.selected_text;
|
||||
if (!searchText) return;
|
||||
|
||||
// 텍스트 검색 (대소문자 무시, 공백 정규화)
|
||||
const normalizedContent = textContent.replace(/\s+/g, ' ').trim();
|
||||
const normalizedSearchText = searchText.replace(/\s+/g, ' ').trim();
|
||||
|
||||
let textIndex = normalizedContent.indexOf(normalizedSearchText);
|
||||
if (textIndex === -1) {
|
||||
// 부분 검색 시도
|
||||
const words = normalizedSearchText.split(' ');
|
||||
if (words.length > 1) {
|
||||
// 첫 번째와 마지막 단어로 검색
|
||||
const firstWord = words[0];
|
||||
const lastWord = words[words.length - 1];
|
||||
const partialPattern = firstWord + '.*' + lastWord;
|
||||
const regex = new RegExp(partialPattern, 'i');
|
||||
const match = normalizedContent.match(regex);
|
||||
if (match) {
|
||||
textIndex = match.index;
|
||||
console.log('✅ 부분 매칭으로 백링크 텍스트 찾음:', searchText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (textIndex === -1) {
|
||||
console.warn('백링크 텍스트를 찾을 수 없음:', searchText);
|
||||
console.log('검색 대상 텍스트 미리보기:', normalizedContent.substring(0, 200));
|
||||
return;
|
||||
}
|
||||
|
||||
const walker = document.createTreeWalker(
|
||||
content,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
let currentOffset = 0;
|
||||
let node;
|
||||
|
||||
while (node = walker.nextNode()) {
|
||||
const nodeLength = node.textContent.length;
|
||||
const nodeStart = currentOffset;
|
||||
const nodeEnd = currentOffset + nodeLength;
|
||||
|
||||
// 백링크 범위와 겹치는지 확인
|
||||
if (nodeEnd > textIndex && nodeStart < textIndex + searchText.length) {
|
||||
const backlinkStart = Math.max(0, textIndex - nodeStart);
|
||||
const backlinkEnd = Math.min(nodeLength, textIndex + searchText.length - nodeStart);
|
||||
|
||||
if (backlinkStart < backlinkEnd) {
|
||||
this.applyBacklinkToNode(node, backlinkStart, backlinkEnd, backlink);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
currentOffset = nodeEnd;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 텍스트 노드에 백링크 적용
|
||||
*/
|
||||
applyBacklinkToNode(textNode, start, end, backlink) {
|
||||
const text = textNode.textContent;
|
||||
const beforeText = text.substring(0, start);
|
||||
const backlinkText = text.substring(start, end);
|
||||
const afterText = text.substring(end);
|
||||
|
||||
// 백링크 스팬 생성
|
||||
const span = document.createElement('span');
|
||||
span.className = 'backlink-highlight';
|
||||
span.textContent = backlinkText;
|
||||
span.dataset.backlinkId = backlink.id;
|
||||
|
||||
// 백링크 스타일 (주황색) - 레이아웃 안전
|
||||
span.style.cssText = `
|
||||
color: #EA580C !important;
|
||||
text-decoration: underline !important;
|
||||
cursor: pointer !important;
|
||||
background-color: rgba(234, 88, 12, 0.2) !important;
|
||||
border: 1px solid #EA580C !important;
|
||||
border-radius: 3px !important;
|
||||
padding: 0 2px !important;
|
||||
font-weight: bold !important;
|
||||
display: inline !important;
|
||||
line-height: inherit !important;
|
||||
vertical-align: baseline !important;
|
||||
margin: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
`;
|
||||
|
||||
// 클릭 이벤트 추가
|
||||
span.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.showBacklinkTooltip(backlink, span);
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 링크 툴팁 표시
|
||||
*/
|
||||
showLinkTooltip(link, element) {
|
||||
this.hideTooltip();
|
||||
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.id = 'link-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 tooltipHTML = `
|
||||
<div class="mb-4">
|
||||
<div class="text-sm text-gray-600 mb-2">링크 정보</div>
|
||||
<div class="font-medium text-purple-900 bg-purple-50 px-3 py-2 rounded border-l-4 border-purple-500">
|
||||
"${link.selected_text}"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="text-sm text-gray-600 mb-2">연결된 문서</div>
|
||||
<div class="bg-gray-50 p-3 rounded">
|
||||
<div class="font-medium text-gray-900">${link.target_document_title}</div>
|
||||
${link.target_text ? `<div class="text-sm text-gray-600 mt-1">대상 텍스트: "${link.target_text}"</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<button onclick="window.documentViewerInstance.linkManager.navigateToLinkedDocument('${link.target_document_id}', ${JSON.stringify(link).replace(/"/g, '"')})"
|
||||
class="text-sm bg-purple-500 text-white px-3 py-2 rounded hover:bg-purple-600">
|
||||
문서로 이동
|
||||
</button>
|
||||
<button onclick="window.documentViewerInstance.linkManager.deleteLink('${link.id}')"
|
||||
class="text-sm bg-red-500 text-white px-3 py-2 rounded hover:bg-red-600">
|
||||
링크 삭제
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-3">
|
||||
<button onclick="window.documentViewerInstance.linkManager.hideTooltip()"
|
||||
class="text-xs bg-gray-500 text-white px-3 py-1 rounded hover:bg-gray-600">
|
||||
닫기
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
tooltip.innerHTML = tooltipHTML;
|
||||
this.positionTooltip(tooltip, element);
|
||||
}
|
||||
|
||||
/**
|
||||
* 백링크 툴팁 표시
|
||||
*/
|
||||
showBacklinkTooltip(backlink, element) {
|
||||
this.hideTooltip();
|
||||
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.id = 'backlink-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 tooltipHTML = `
|
||||
<div class="mb-4">
|
||||
<div class="text-sm text-gray-600 mb-2">백링크 정보</div>
|
||||
<div class="font-medium text-orange-900 bg-orange-50 px-3 py-2 rounded border-l-4 border-orange-500">
|
||||
"${backlink.target_text || backlink.selected_text}"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="text-sm text-gray-600 mb-2">참조 문서</div>
|
||||
<div class="bg-gray-50 p-3 rounded">
|
||||
<div class="font-medium text-gray-900">${backlink.source_document_title}</div>
|
||||
<div class="text-sm text-gray-600 mt-1">원본 텍스트: "${backlink.selected_text}"</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<button onclick="window.documentViewerInstance.linkManager.navigateToSourceDocument('${backlink.source_document_id}', ${JSON.stringify(backlink).replace(/"/g, '"')})"
|
||||
class="text-sm bg-orange-500 text-white px-3 py-2 rounded hover:bg-orange-600">
|
||||
원본 문서로 이동
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-3">
|
||||
<button onclick="window.documentViewerInstance.linkManager.hideTooltip()"
|
||||
class="text-xs bg-gray-500 text-white px-3 py-1 rounded hover:bg-gray-600">
|
||||
닫기
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
tooltip.innerHTML = tooltipHTML;
|
||||
this.positionTooltip(tooltip, element);
|
||||
}
|
||||
|
||||
/**
|
||||
* 툴팁 위치 설정
|
||||
*/
|
||||
positionTooltip(tooltip, element) {
|
||||
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 linkTooltip = document.getElementById('link-tooltip');
|
||||
if (linkTooltip) {
|
||||
linkTooltip.remove();
|
||||
}
|
||||
|
||||
const backlinkTooltip = document.getElementById('backlink-tooltip');
|
||||
if (backlinkTooltip) {
|
||||
backlinkTooltip.remove();
|
||||
}
|
||||
|
||||
document.removeEventListener('click', this.handleTooltipOutsideClick.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* 툴팁 외부 클릭 처리
|
||||
*/
|
||||
handleTooltipOutsideClick(e) {
|
||||
const linkTooltip = document.getElementById('link-tooltip');
|
||||
const backlinkTooltip = document.getElementById('backlink-tooltip');
|
||||
|
||||
const isOutsideLinkTooltip = linkTooltip && !linkTooltip.contains(e.target);
|
||||
const isOutsideBacklinkTooltip = backlinkTooltip && !backlinkTooltip.contains(e.target);
|
||||
|
||||
if (isOutsideLinkTooltip || isOutsideBacklinkTooltip) {
|
||||
this.hideTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 링크된 문서로 이동
|
||||
*/
|
||||
navigateToLinkedDocument(targetDocumentId, linkInfo) {
|
||||
let targetUrl = `/viewer.html?id=${targetDocumentId}`;
|
||||
|
||||
// 특정 텍스트 위치가 있는 경우 URL에 추가
|
||||
if (linkInfo.target_text && linkInfo.target_start_offset !== undefined) {
|
||||
targetUrl += `&highlight_text=${encodeURIComponent(linkInfo.target_text)}`;
|
||||
targetUrl += `&start_offset=${linkInfo.target_start_offset}`;
|
||||
targetUrl += `&end_offset=${linkInfo.target_end_offset}`;
|
||||
}
|
||||
|
||||
window.location.href = targetUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 원본 문서로 이동 (백링크)
|
||||
*/
|
||||
navigateToSourceDocument(sourceDocumentId, backlinkInfo) {
|
||||
let targetUrl = `/viewer.html?id=${sourceDocumentId}`;
|
||||
|
||||
// 원본 텍스트 위치가 있는 경우 URL에 추가
|
||||
if (backlinkInfo.selected_text && backlinkInfo.start_offset !== undefined) {
|
||||
targetUrl += `&highlight_text=${encodeURIComponent(backlinkInfo.selected_text)}`;
|
||||
targetUrl += `&start_offset=${backlinkInfo.start_offset}`;
|
||||
targetUrl += `&end_offset=${backlinkInfo.end_offset}`;
|
||||
}
|
||||
|
||||
window.location.href = targetUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 링크 삭제
|
||||
*/
|
||||
async deleteLink(linkId) {
|
||||
if (!confirm('이 링크를 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.api.delete(`/document-links/${linkId}`);
|
||||
this.documentLinks = this.documentLinks.filter(l => l.id !== linkId);
|
||||
|
||||
this.hideTooltip();
|
||||
this.renderDocumentLinks();
|
||||
|
||||
console.log('링크 삭제 완료:', linkId);
|
||||
} catch (error) {
|
||||
console.error('링크 삭제 실패:', error);
|
||||
alert('링크 삭제에 실패했습니다');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택된 텍스트로 링크 생성
|
||||
*/
|
||||
async createLinkFromSelection(documentId, selectedText, selectedRange) {
|
||||
if (!selectedText || !selectedRange) return;
|
||||
|
||||
try {
|
||||
console.log('🔗 링크 생성 시작:', selectedText);
|
||||
|
||||
// ViewerCore의 링크 생성 모달 표시
|
||||
if (window.documentViewerInstance) {
|
||||
window.documentViewerInstance.selectedText = selectedText;
|
||||
window.documentViewerInstance.selectedRange = selectedRange;
|
||||
window.documentViewerInstance.showLinkModal = true;
|
||||
window.documentViewerInstance.linkForm.selected_text = selectedText;
|
||||
|
||||
// 텍스트 오프셋 계산
|
||||
const documentContent = document.getElementById('document-content');
|
||||
const fullText = documentContent.textContent;
|
||||
const startOffset = this.getTextOffset(documentContent, selectedRange.startContainer, selectedRange.startOffset);
|
||||
const endOffset = startOffset + selectedText.length;
|
||||
|
||||
window.documentViewerInstance.linkForm.start_offset = startOffset;
|
||||
window.documentViewerInstance.linkForm.end_offset = endOffset;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('링크 생성 실패:', error);
|
||||
alert('링크 생성에 실패했습니다: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 텍스트 오프셋 계산
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 전역으로 내보내기
|
||||
window.LinkManager = LinkManager;
|
||||
413
frontend/static/js/viewer/features/ui-manager.js
Normal file
413
frontend/static/js/viewer/features/ui-manager.js
Normal file
@@ -0,0 +1,413 @@
|
||||
/**
|
||||
* UIManager 모듈
|
||||
* UI 컴포넌트 및 상태 관리
|
||||
*/
|
||||
class UIManager {
|
||||
constructor() {
|
||||
console.log('🎨 UIManager 초기화 시작');
|
||||
|
||||
// UI 상태
|
||||
this.showNotesPanel = false;
|
||||
this.showBookmarksPanel = false;
|
||||
this.showBacklinks = false;
|
||||
this.activePanel = 'notes';
|
||||
|
||||
// 모달 상태
|
||||
this.showNoteModal = false;
|
||||
this.showBookmarkModal = false;
|
||||
this.showLinkModal = false;
|
||||
this.showNotesModal = false;
|
||||
this.showBookmarksModal = false;
|
||||
this.showLinksModal = false;
|
||||
this.showBacklinksModal = false;
|
||||
|
||||
// 기능 메뉴 상태
|
||||
this.activeFeatureMenu = null;
|
||||
|
||||
// 검색 상태
|
||||
this.searchQuery = '';
|
||||
this.noteSearchQuery = '';
|
||||
this.filteredNotes = [];
|
||||
|
||||
// 텍스트 선택 모드
|
||||
this.textSelectorUISetup = false;
|
||||
|
||||
console.log('✅ UIManager 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 기능 메뉴 토글
|
||||
*/
|
||||
toggleFeatureMenu(feature) {
|
||||
if (this.activeFeatureMenu === feature) {
|
||||
this.activeFeatureMenu = null;
|
||||
} else {
|
||||
this.activeFeatureMenu = feature;
|
||||
|
||||
// 해당 기능의 모달 표시
|
||||
switch(feature) {
|
||||
case 'link':
|
||||
this.showLinksModal = true;
|
||||
break;
|
||||
case 'memo':
|
||||
this.showNotesModal = true;
|
||||
break;
|
||||
case 'bookmark':
|
||||
this.showBookmarksModal = true;
|
||||
break;
|
||||
case 'backlink':
|
||||
this.showBacklinksModal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 노트 모달 열기
|
||||
*/
|
||||
openNoteModal(highlight = null) {
|
||||
console.log('📝 노트 모달 열기');
|
||||
if (highlight) {
|
||||
console.log('🔍 하이라이트와 연결된 노트 모달:', highlight);
|
||||
}
|
||||
this.showNoteModal = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 노트 모달 닫기
|
||||
*/
|
||||
closeNoteModal() {
|
||||
this.showNoteModal = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 링크 모달 열기
|
||||
*/
|
||||
openLinkModal() {
|
||||
console.log('🔗 링크 모달 열기');
|
||||
console.log('🔗 showLinksModal 설정 전:', this.showLinksModal);
|
||||
this.showLinksModal = true;
|
||||
this.showLinkModal = true; // 기존 호환성
|
||||
console.log('🔗 showLinksModal 설정 후:', this.showLinksModal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 링크 모달 닫기
|
||||
*/
|
||||
closeLinkModal() {
|
||||
this.showLinksModal = false;
|
||||
this.showLinkModal = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크 모달 닫기
|
||||
*/
|
||||
closeBookmarkModal() {
|
||||
this.showBookmarkModal = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 검색 결과 하이라이트
|
||||
*/
|
||||
highlightSearchResults(element, searchText) {
|
||||
if (!searchText.trim()) return;
|
||||
|
||||
// 기존 검색 하이라이트 제거
|
||||
const existingHighlights = element.querySelectorAll('.search-highlight');
|
||||
existingHighlights.forEach(highlight => {
|
||||
const parent = highlight.parentNode;
|
||||
parent.replaceChild(document.createTextNode(highlight.textContent), highlight);
|
||||
parent.normalize();
|
||||
});
|
||||
|
||||
if (!searchText) return;
|
||||
|
||||
// 새로운 검색 하이라이트 적용
|
||||
const walker = document.createTreeWalker(
|
||||
element,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
const textNodes = [];
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
textNodes.push(node);
|
||||
}
|
||||
|
||||
const searchRegex = new RegExp(`(${searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
||||
|
||||
textNodes.forEach(textNode => {
|
||||
const text = textNode.textContent;
|
||||
if (searchRegex.test(text)) {
|
||||
const highlightedHTML = text.replace(searchRegex, '<span class="search-highlight bg-yellow-200">$1</span>');
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = highlightedHTML;
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
while (wrapper.firstChild) {
|
||||
fragment.appendChild(wrapper.firstChild);
|
||||
}
|
||||
|
||||
textNode.parentNode.replaceChild(fragment, textNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 노트 검색 필터링
|
||||
*/
|
||||
filterNotes(notes) {
|
||||
if (!this.noteSearchQuery.trim()) {
|
||||
this.filteredNotes = notes;
|
||||
return notes;
|
||||
}
|
||||
|
||||
const query = this.noteSearchQuery.toLowerCase();
|
||||
this.filteredNotes = notes.filter(note =>
|
||||
note.content.toLowerCase().includes(query) ||
|
||||
(note.tags && note.tags.some(tag => tag.toLowerCase().includes(query)))
|
||||
);
|
||||
|
||||
return this.filteredNotes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 텍스트 선택 모드 UI 설정
|
||||
*/
|
||||
setupTextSelectorUI() {
|
||||
console.log('🔧 setupTextSelectorUI 함수 실행됨');
|
||||
|
||||
// 중복 실행 방지
|
||||
if (this.textSelectorUISetup) {
|
||||
console.log('⚠️ 텍스트 선택 모드 UI가 이미 설정됨 - 중복 실행 방지');
|
||||
return;
|
||||
}
|
||||
|
||||
// 헤더 숨기기
|
||||
const header = document.querySelector('header');
|
||||
if (header) {
|
||||
header.style.display = 'none';
|
||||
}
|
||||
|
||||
// 안내 메시지 표시
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.id = 'text-selection-message';
|
||||
messageDiv.className = 'fixed top-4 left-1/2 transform -translate-x-1/2 bg-blue-500 text-white px-6 py-3 rounded-lg shadow-lg z-50';
|
||||
messageDiv.innerHTML = `
|
||||
<div class="flex items-center space-x-2">
|
||||
<i class="fas fa-mouse-pointer"></i>
|
||||
<span>연결할 텍스트를 드래그하여 선택해주세요</span>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(messageDiv);
|
||||
|
||||
this.textSelectorUISetup = true;
|
||||
console.log('✅ 텍스트 선택 모드 UI 설정 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 텍스트 선택 확인 UI 표시
|
||||
*/
|
||||
showTextSelectionConfirm(selectedText, startOffset, endOffset) {
|
||||
// 기존 확인 UI 제거
|
||||
const existingConfirm = document.getElementById('text-selection-confirm');
|
||||
if (existingConfirm) {
|
||||
existingConfirm.remove();
|
||||
}
|
||||
|
||||
// 확인 UI 생성
|
||||
const confirmDiv = document.createElement('div');
|
||||
confirmDiv.id = 'text-selection-confirm';
|
||||
confirmDiv.className = 'fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white border border-gray-300 rounded-lg shadow-xl p-6 z-50 max-w-md';
|
||||
confirmDiv.innerHTML = `
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-2">선택된 텍스트</h3>
|
||||
<div class="bg-blue-50 p-3 rounded border-l-4 border-blue-500">
|
||||
<p class="text-blue-800 font-medium">"${selectedText}"</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button onclick="window.cancelTextSelection()"
|
||||
class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors">
|
||||
취소
|
||||
</button>
|
||||
<button onclick="window.confirmTextSelection('${selectedText}', ${startOffset}, ${endOffset})"
|
||||
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
|
||||
이 텍스트로 링크 생성
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(confirmDiv);
|
||||
}
|
||||
|
||||
/**
|
||||
* 링크 생성 UI 표시
|
||||
*/
|
||||
showLinkCreationUI() {
|
||||
console.log('🔗 링크 생성 UI 표시');
|
||||
this.openLinkModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 성공 메시지 표시
|
||||
*/
|
||||
showSuccessMessage(message) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 flex items-center space-x-2';
|
||||
messageDiv.innerHTML = `
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>${message}</span>
|
||||
`;
|
||||
|
||||
document.body.appendChild(messageDiv);
|
||||
|
||||
// 3초 후 자동 제거
|
||||
setTimeout(() => {
|
||||
if (messageDiv.parentNode) {
|
||||
messageDiv.parentNode.removeChild(messageDiv);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 오류 메시지 표시
|
||||
*/
|
||||
showErrorMessage(message) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'fixed top-4 right-4 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 flex items-center space-x-2';
|
||||
messageDiv.innerHTML = `
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span>${message}</span>
|
||||
`;
|
||||
|
||||
document.body.appendChild(messageDiv);
|
||||
|
||||
// 5초 후 자동 제거
|
||||
setTimeout(() => {
|
||||
if (messageDiv.parentNode) {
|
||||
messageDiv.parentNode.removeChild(messageDiv);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 로딩 스피너 표시
|
||||
*/
|
||||
showLoadingSpinner(container, message = '로딩 중...') {
|
||||
const spinner = document.createElement('div');
|
||||
spinner.className = 'flex items-center justify-center py-8';
|
||||
spinner.innerHTML = `
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mr-3"></div>
|
||||
<span class="text-gray-600">${message}</span>
|
||||
`;
|
||||
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
container.appendChild(spinner);
|
||||
}
|
||||
|
||||
return spinner;
|
||||
}
|
||||
|
||||
/**
|
||||
* 로딩 스피너 제거
|
||||
*/
|
||||
hideLoadingSpinner(container) {
|
||||
if (container) {
|
||||
const spinner = container.querySelector('.animate-spin');
|
||||
if (spinner && spinner.parentElement) {
|
||||
spinner.parentElement.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모달 배경 클릭 시 닫기
|
||||
*/
|
||||
handleModalBackgroundClick(event, modalId) {
|
||||
if (event.target === event.currentTarget) {
|
||||
switch(modalId) {
|
||||
case 'notes':
|
||||
this.closeNoteModal();
|
||||
break;
|
||||
case 'bookmarks':
|
||||
this.closeBookmarkModal();
|
||||
break;
|
||||
case 'links':
|
||||
this.closeLinkModal();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 패널 토글
|
||||
*/
|
||||
togglePanel(panelType) {
|
||||
switch(panelType) {
|
||||
case 'notes':
|
||||
this.showNotesPanel = !this.showNotesPanel;
|
||||
if (this.showNotesPanel) {
|
||||
this.showBookmarksPanel = false;
|
||||
this.activePanel = 'notes';
|
||||
}
|
||||
break;
|
||||
case 'bookmarks':
|
||||
this.showBookmarksPanel = !this.showBookmarksPanel;
|
||||
if (this.showBookmarksPanel) {
|
||||
this.showNotesPanel = false;
|
||||
this.activePanel = 'bookmarks';
|
||||
}
|
||||
break;
|
||||
case 'backlinks':
|
||||
this.showBacklinks = !this.showBacklinks;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 검색 쿼리 업데이트
|
||||
*/
|
||||
updateSearchQuery(query) {
|
||||
this.searchQuery = query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 노트 검색 쿼리 업데이트
|
||||
*/
|
||||
updateNoteSearchQuery(query) {
|
||||
this.noteSearchQuery = query;
|
||||
}
|
||||
|
||||
/**
|
||||
* UI 상태 초기화
|
||||
*/
|
||||
resetUIState() {
|
||||
this.showNotesPanel = false;
|
||||
this.showBookmarksPanel = false;
|
||||
this.showBacklinks = false;
|
||||
this.activePanel = 'notes';
|
||||
this.activeFeatureMenu = null;
|
||||
this.searchQuery = '';
|
||||
this.noteSearchQuery = '';
|
||||
this.filteredNotes = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 모달 닫기
|
||||
*/
|
||||
closeAllModals() {
|
||||
this.showNoteModal = false;
|
||||
this.showBookmarkModal = false;
|
||||
this.showLinkModal = false;
|
||||
this.showNotesModal = false;
|
||||
this.showBookmarksModal = false;
|
||||
this.showLinksModal = false;
|
||||
this.showBacklinksModal = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 전역으로 내보내기
|
||||
window.UIManager = UIManager;
|
||||
Reference in New Issue
Block a user