주요 수정사항: - 하이라이트 생성 시 color → highlight_color 필드명 수정으로 색상 전달 문제 해결 - 분홍색을 더 연하게 변경하여 글씨 가독성 향상 - 다중 하이라이트 렌더링을 위아래 균등 분할로 개선 - CSS highlight-span 클래스 추가 및 색상 적용 강화 - 하이라이트 생성/렌더링 과정에 상세한 디버깅 로그 추가 UI 개선: - 단일 하이라이트: 선택한 색상으로 정확히 표시 - 다중 하이라이트: 위아래로 균등하게 색상 분할 표시 - 메모 입력 모달에서 선택된 텍스트 표시 개선 버그 수정: - 프론트엔드-백엔드 API 스키마 불일치 해결 - CSS 스타일 우선순위 문제 해결 - 하이라이트 색상이 노랑색으로만 표시되던 문제 해결
414 lines
12 KiB
JavaScript
414 lines
12 KiB
JavaScript
/**
|
|
* 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;
|