- 메인 컨테이너 padding-top을 pt-4에서 pt-20으로 증가 - 드롭다운 z-index를 z-[60]으로 설정하여 헤더보다 높은 우선순위 부여 - 스토리 선택 드롭다운이 정상적으로 작동하도록 수정 - 디버깅용 코드 정리
2373 lines
100 KiB
JavaScript
2373 lines
100 KiB
JavaScript
/**
|
||
* ViewerCore - 문서 뷰어 Alpine.js 컴포넌트
|
||
* 모든 모듈을 통합하고 Alpine.js 컴포넌트를 관리합니다.
|
||
*/
|
||
window.documentViewer = () => ({
|
||
// ==================== 기본 상태 ====================
|
||
loading: true,
|
||
error: null,
|
||
document: null,
|
||
documentId: null,
|
||
contentType: 'document', // 'document' 또는 'note'
|
||
navigation: null,
|
||
|
||
// ==================== PDF 뷰어 상태 ====================
|
||
pdfSrc: '',
|
||
pdfLoading: false,
|
||
pdfError: false,
|
||
pdfLoaded: false,
|
||
|
||
// ==================== PDF 검색 상태 ====================
|
||
showPdfSearchModal: false,
|
||
pdfSearchQuery: '',
|
||
pdfSearchResults: [],
|
||
pdfSearchLoading: false,
|
||
|
||
// ==================== PDF.js 뷰어 상태 ====================
|
||
pdfDocument: null,
|
||
currentPage: 1,
|
||
totalPages: 0,
|
||
pdfScale: 1.0,
|
||
pdfCanvas: null,
|
||
pdfContext: null,
|
||
pdfTextContent: [],
|
||
|
||
// ==================== 데이터 상태 ====================
|
||
highlights: [],
|
||
notes: [],
|
||
bookmarks: [],
|
||
documentLinks: [],
|
||
linkableDocuments: [],
|
||
backlinks: [],
|
||
|
||
// ==================== 선택 상태 ====================
|
||
selectedHighlightColor: '#FFFF00',
|
||
selectedText: '',
|
||
selectedRange: null,
|
||
|
||
// ==================== 폼 데이터 ====================
|
||
noteForm: {
|
||
content: '',
|
||
tags: ''
|
||
},
|
||
bookmarkForm: {
|
||
title: '',
|
||
description: ''
|
||
},
|
||
linkForm: {
|
||
target_type: 'document', // 'document' 또는 'note'
|
||
target_document_id: '',
|
||
selected_text: '',
|
||
start_offset: 0,
|
||
end_offset: 0,
|
||
link_text: '',
|
||
description: '',
|
||
link_type: 'text_fragment', // 무조건 텍스트 선택만 지원
|
||
target_text: '',
|
||
target_start_offset: 0,
|
||
target_end_offset: 0,
|
||
|
||
target_book_id: ''
|
||
},
|
||
|
||
// ==================== 언어 및 기타 ====================
|
||
isKorean: false,
|
||
|
||
// ==================== UI 상태 (Alpine.js 바인딩용) ====================
|
||
searchQuery: '',
|
||
activeFeatureMenu: null,
|
||
showLinksModal: false,
|
||
showLinkModal: false,
|
||
showNotesModal: false,
|
||
showBookmarksModal: false,
|
||
showBacklinksModal: false,
|
||
showNoteInputModal: false,
|
||
availableBooks: [],
|
||
filteredDocuments: [],
|
||
|
||
// ==================== 모듈 인스턴스 ====================
|
||
documentLoader: null,
|
||
highlightManager: null,
|
||
bookmarkManager: null,
|
||
linkManager: null,
|
||
uiManager: null,
|
||
|
||
// ==================== 초기화 플래그 ====================
|
||
_initialized: false,
|
||
|
||
// ==================== 초기화 ====================
|
||
async init() {
|
||
// 중복 초기화 방지
|
||
if (this._initialized) {
|
||
console.log('⚠️ 이미 초기화됨, 중복 실행 방지');
|
||
return;
|
||
}
|
||
this._initialized = true;
|
||
|
||
console.log('🚀 DocumentViewer 초기화 시작');
|
||
|
||
// 전역 인스턴스 설정 (말풍선에서 함수 호출용)
|
||
window.documentViewerInstance = this;
|
||
|
||
try {
|
||
// 모듈 초기화
|
||
await this.initializeModules();
|
||
|
||
// URL 파라미터 처리
|
||
this.parseUrlParameters();
|
||
|
||
// 문서 로드
|
||
await this.loadDocument();
|
||
|
||
console.log('✅ DocumentViewer 초기화 완료');
|
||
} catch (error) {
|
||
console.error('❌ DocumentViewer 초기화 실패:', error);
|
||
this.error = error.message;
|
||
this.loading = false;
|
||
}
|
||
},
|
||
|
||
// ==================== 모듈 초기화 (지연 로딩 + 폴백) ====================
|
||
async initializeModules() {
|
||
console.log('🔧 모듈 초기화 시작 (지연 로딩)');
|
||
|
||
// API 및 캐시 초기화
|
||
this.api = new DocumentServerAPI();
|
||
|
||
// 토큰 설정 (인증 확인)
|
||
const token = localStorage.getItem('access_token');
|
||
if (token) {
|
||
this.api.setToken(token);
|
||
console.log('🔐 API 토큰 설정 완료');
|
||
} else {
|
||
console.error('❌ 인증 토큰이 없습니다!');
|
||
throw new Error('인증이 필요합니다');
|
||
}
|
||
|
||
this.cache = new CacheManager();
|
||
this.cachedApi = new CachedAPI(this.api, this.cache);
|
||
|
||
// 직접 모듈 인스턴스 생성 (모든 모듈이 HTML에서 로드됨)
|
||
if (window.DocumentLoader && window.UIManager && window.HighlightManager &&
|
||
window.LinkManager && window.BookmarkManager) {
|
||
|
||
this.documentLoader = new window.DocumentLoader(this.cachedApi);
|
||
this.uiManager = new window.UIManager();
|
||
this.highlightManager = new window.HighlightManager(this.cachedApi);
|
||
this.linkManager = new window.LinkManager(this.cachedApi);
|
||
this.bookmarkManager = new window.BookmarkManager(this.cachedApi);
|
||
|
||
console.log('✅ 모든 모듈 직접 로드 성공');
|
||
} else {
|
||
console.error('❌ 필수 모듈이 로드되지 않음');
|
||
console.log('사용 가능한 모듈:', {
|
||
DocumentLoader: !!window.DocumentLoader,
|
||
UIManager: !!window.UIManager,
|
||
HighlightManager: !!window.HighlightManager,
|
||
LinkManager: !!window.LinkManager,
|
||
BookmarkManager: !!window.BookmarkManager
|
||
});
|
||
throw new Error('필수 모듈을 로드할 수 없습니다.');
|
||
}
|
||
|
||
// UI 상태를 UIManager와 동기화 (모달은 초기화 시 닫힌 상태로)
|
||
this.syncUIState();
|
||
|
||
// 초기화 시 모든 모달을 명시적으로 닫기
|
||
this.closeAllModals();
|
||
|
||
// 나머지 모듈들은 백그라운드에서 프리로딩 (지연 로딩 가능한 경우만)
|
||
if (window.moduleLoader) {
|
||
window.moduleLoader.preloadModules(['HighlightManager', 'BookmarkManager', 'LinkManager']);
|
||
}
|
||
|
||
console.log('✅ 모듈 초기화 완료');
|
||
},
|
||
|
||
// ==================== UI 상태 동기화 ====================
|
||
syncUIState() {
|
||
// UIManager의 상태를 Alpine.js 컴포넌트와 동기화 (getter/setter 방식)
|
||
|
||
// 패널 상태 동기화
|
||
Object.defineProperty(this, 'showNotesPanel', {
|
||
get: () => this.uiManager.showNotesPanel,
|
||
set: (value) => { this.uiManager.showNotesPanel = value; }
|
||
});
|
||
|
||
Object.defineProperty(this, 'showBookmarksPanel', {
|
||
get: () => this.uiManager.showBookmarksPanel,
|
||
set: (value) => { this.uiManager.showBookmarksPanel = value; }
|
||
});
|
||
|
||
Object.defineProperty(this, 'showBacklinks', {
|
||
get: () => this.uiManager.showBacklinks,
|
||
set: (value) => { this.uiManager.showBacklinks = value; }
|
||
});
|
||
|
||
Object.defineProperty(this, 'activePanel', {
|
||
get: () => this.uiManager.activePanel,
|
||
set: (value) => { this.uiManager.activePanel = value; }
|
||
});
|
||
|
||
// 모달 상태 동기화 (UIManager와 실시간 연동)
|
||
this.updateModalStates();
|
||
|
||
// 검색 상태 동기화
|
||
Object.defineProperty(this, 'noteSearchQuery', {
|
||
get: () => this.uiManager.noteSearchQuery,
|
||
set: (value) => { this.uiManager.updateNoteSearchQuery(value); }
|
||
});
|
||
|
||
Object.defineProperty(this, 'filteredNotes', {
|
||
get: () => this.uiManager.filteredNotes,
|
||
set: (value) => { this.uiManager.filteredNotes = value; }
|
||
});
|
||
|
||
// 모드 및 핸들러 상태
|
||
this.activeMode = null;
|
||
this.textSelectionHandler = null;
|
||
this.editingNote = null;
|
||
this.editingBookmark = null;
|
||
this.editingLink = null;
|
||
this.noteLoading = false;
|
||
this.bookmarkLoading = false;
|
||
this.linkLoading = false;
|
||
},
|
||
|
||
// ==================== 모달 상태 업데이트 ====================
|
||
updateModalStates() {
|
||
// UIManager의 모달 상태를 ViewerCore의 속성에 반영
|
||
if (this.uiManager) {
|
||
this.showLinksModal = this.uiManager.showLinksModal;
|
||
this.showLinkModal = this.uiManager.showLinkModal;
|
||
this.showNotesModal = this.uiManager.showNotesModal;
|
||
this.showBookmarksModal = this.uiManager.showBookmarksModal;
|
||
this.showBacklinksModal = this.uiManager.showBacklinksModal;
|
||
this.activeFeatureMenu = this.uiManager.activeFeatureMenu;
|
||
this.searchQuery = this.uiManager.searchQuery;
|
||
}
|
||
},
|
||
|
||
// ==================== 모든 모달 닫기 ====================
|
||
closeAllModals() {
|
||
console.log('🔒 초기화 시 모든 모달 닫기');
|
||
this.showLinksModal = false;
|
||
this.showLinkModal = false;
|
||
this.showNotesModal = false;
|
||
this.showBookmarksModal = false;
|
||
this.showBacklinksModal = false;
|
||
this.showNoteInputModal = false;
|
||
|
||
// UIManager에도 반영
|
||
if (this.uiManager) {
|
||
this.uiManager.closeAllModals();
|
||
}
|
||
},
|
||
|
||
// ==================== URL 파라미터 처리 ====================
|
||
parseUrlParameters() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const rawId = urlParams.get('id');
|
||
|
||
// null 문자열이나 빈 값 처리
|
||
if (!rawId || rawId === 'null' || rawId === 'undefined' || rawId.trim() === '') {
|
||
console.error('❌ 유효하지 않은 문서 ID:', rawId);
|
||
throw new Error('유효한 문서 ID가 필요합니다. URL을 확인해주세요.');
|
||
}
|
||
|
||
this.documentId = rawId;
|
||
// contentType 파라미터를 올바르게 가져오기 (type과 contentType 둘 다 지원)
|
||
this.contentType = urlParams.get('contentType') || urlParams.get('type') || 'document';
|
||
|
||
console.log('🔍 URL 파싱 결과:', {
|
||
rawId: rawId,
|
||
documentId: this.documentId,
|
||
contentType: this.contentType,
|
||
fullURL: window.location.href
|
||
});
|
||
},
|
||
|
||
// ==================== 문서 로드 ====================
|
||
async loadDocument() {
|
||
console.log('📄 문서 로드 시작');
|
||
this.loading = true;
|
||
|
||
try {
|
||
// 문서 데이터 로드
|
||
if (this.contentType === 'note') {
|
||
this.document = await this.documentLoader.loadNote(this.documentId);
|
||
this.navigation = null; // 노트는 네비게이션 없음
|
||
} else {
|
||
this.document = await this.documentLoader.loadDocument(this.documentId);
|
||
// 네비게이션 별도 로드
|
||
this.navigation = await this.documentLoader.loadNavigation(this.documentId);
|
||
|
||
// PDF 문서인 경우 PDF 뷰어 준비
|
||
if (this.document && this.document.pdf_path) {
|
||
await this.loadPdfViewer();
|
||
}
|
||
}
|
||
|
||
// 관련 데이터 병렬 로드
|
||
await this.loadDocumentData();
|
||
|
||
// 데이터를 모듈에 전달
|
||
this.distributeDataToModules();
|
||
|
||
// 렌더링
|
||
await this.renderAllFeatures();
|
||
|
||
// URL 하이라이트 처리
|
||
await this.handleUrlHighlight();
|
||
|
||
this.loading = false;
|
||
console.log('✅ 문서 로드 완료');
|
||
|
||
} catch (error) {
|
||
console.error('❌ 문서 로드 실패:', error);
|
||
this.error = error.message;
|
||
this.loading = false;
|
||
}
|
||
},
|
||
|
||
// ==================== 문서 데이터 로드 (지연 로딩) ====================
|
||
async loadDocumentData() {
|
||
console.log('📊 문서 데이터 로드 시작');
|
||
|
||
const [highlights, notes, bookmarks, documentLinks, backlinks] = await Promise.all([
|
||
this.highlightManager.loadHighlights(this.documentId, this.contentType),
|
||
this.highlightManager.loadNotes(this.documentId, this.contentType),
|
||
this.bookmarkManager.loadBookmarks(this.documentId),
|
||
this.linkManager.loadDocumentLinks(this.documentId, this.contentType),
|
||
this.linkManager.loadBacklinks(this.documentId, this.contentType)
|
||
]);
|
||
|
||
// 데이터 저장 및 모듈 동기화
|
||
this.highlights = highlights;
|
||
this.notes = notes;
|
||
this.bookmarks = bookmarks;
|
||
this.documentLinks = documentLinks;
|
||
this.backlinks = backlinks;
|
||
|
||
// 모듈에 데이터 동기화 (중요!)
|
||
this.linkManager.documentLinks = documentLinks;
|
||
this.linkManager.backlinks = backlinks;
|
||
|
||
console.log('📊 로드된 데이터:', {
|
||
highlights: highlights.length,
|
||
notes: notes.length,
|
||
bookmarks: bookmarks.length,
|
||
documentLinks: documentLinks.length,
|
||
backlinks: backlinks.length
|
||
});
|
||
|
||
console.log('🔄 모듈 데이터 동기화 완료:', {
|
||
'linkManager.documentLinks': this.linkManager.documentLinks?.length || 0,
|
||
'linkManager.backlinks': this.linkManager.backlinks?.length || 0
|
||
});
|
||
},
|
||
|
||
// ==================== 모듈 지연 로딩 보장 (폴백 포함) ====================
|
||
async ensureModulesLoaded(moduleNames) {
|
||
const missingModules = [];
|
||
|
||
for (const moduleName of moduleNames) {
|
||
const propertyName = this.getModulePropertyName(moduleName);
|
||
if (!this[propertyName]) {
|
||
missingModules.push(moduleName);
|
||
}
|
||
}
|
||
|
||
if (missingModules.length > 0) {
|
||
console.log(`🔄 필요한 모듈 지연 로딩: ${missingModules.join(', ')}`);
|
||
|
||
// 각 모듈을 개별적으로 로드
|
||
for (const moduleName of missingModules) {
|
||
const propertyName = this.getModulePropertyName(moduleName);
|
||
|
||
try {
|
||
// 지연 로딩 시도
|
||
if (window.moduleLoader) {
|
||
const ModuleClass = await window.moduleLoader.loadModule(moduleName);
|
||
|
||
if (moduleName === 'UIManager') {
|
||
this[propertyName] = new ModuleClass();
|
||
} else {
|
||
this[propertyName] = new ModuleClass(this.cachedApi);
|
||
}
|
||
|
||
console.log(`✅ 지연 로딩 성공: ${moduleName}`);
|
||
} else {
|
||
throw new Error('ModuleLoader 없음');
|
||
}
|
||
} catch (error) {
|
||
console.warn(`⚠️ 지연 로딩 실패, 폴백 시도: ${moduleName}`, error);
|
||
|
||
// 폴백: 전역 클래스 직접 사용
|
||
if (window[moduleName]) {
|
||
if (moduleName === 'UIManager') {
|
||
this[propertyName] = new window[moduleName]();
|
||
} else {
|
||
this[propertyName] = new window[moduleName](this.cachedApi);
|
||
}
|
||
|
||
console.log(`✅ 폴백 성공: ${moduleName}`);
|
||
} else {
|
||
console.error(`❌ 폴백도 실패: ${moduleName} - 전역 클래스 없음`);
|
||
throw new Error(`모듈을 로드할 수 없습니다: ${moduleName}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
// ==================== 모듈명 → 속성명 변환 ====================
|
||
getModulePropertyName(moduleName) {
|
||
const nameMap = {
|
||
'DocumentLoader': 'documentLoader',
|
||
'HighlightManager': 'highlightManager',
|
||
'BookmarkManager': 'bookmarkManager',
|
||
'LinkManager': 'linkManager',
|
||
'UIManager': 'uiManager'
|
||
};
|
||
return nameMap[moduleName];
|
||
},
|
||
|
||
// ==================== 모듈에 데이터 분배 ====================
|
||
distributeDataToModules() {
|
||
// HighlightManager에 데이터 전달
|
||
this.highlightManager.highlights = this.highlights;
|
||
this.highlightManager.notes = this.notes;
|
||
|
||
// BookmarkManager에 데이터 전달
|
||
this.bookmarkManager.bookmarks = this.bookmarks;
|
||
|
||
// LinkManager에 데이터 전달
|
||
this.linkManager.documentLinks = this.documentLinks;
|
||
this.linkManager.backlinks = this.backlinks;
|
||
},
|
||
|
||
// ==================== 모든 기능 렌더링 ====================
|
||
async renderAllFeatures() {
|
||
console.log('🎨 모든 기능 렌더링 시작');
|
||
|
||
// 하이라이트 렌더링
|
||
this.highlightManager.renderHighlights();
|
||
|
||
// 백링크 먼저 렌더링 (링크보다 먼저)
|
||
this.linkManager.renderBacklinks();
|
||
|
||
// 문서 링크 렌더링 (백링크 후에 렌더링)
|
||
this.linkManager.renderDocumentLinks();
|
||
|
||
console.log('✅ 모든 기능 렌더링 완료');
|
||
},
|
||
|
||
// ==================== URL 하이라이트 처리 ====================
|
||
async handleUrlHighlight() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const highlightText = urlParams.get('highlight');
|
||
const startOffset = parseInt(urlParams.get('start_offset'));
|
||
const endOffset = parseInt(urlParams.get('end_offset'));
|
||
|
||
if (highlightText || (startOffset && endOffset)) {
|
||
console.log('🎯 URL에서 하이라이트 요청:', { highlightText, startOffset, endOffset });
|
||
await this.documentLoader.highlightAndScrollToText({
|
||
text: highlightText,
|
||
start_offset: startOffset,
|
||
end_offset: endOffset
|
||
});
|
||
}
|
||
},
|
||
|
||
// ==================== 기능 모드 활성화 ====================
|
||
activateLinkMode() {
|
||
console.log('🔗 링크 모드 활성화');
|
||
this.activeMode = 'link';
|
||
|
||
// 선택된 텍스트 확인
|
||
const selectedText = window.getSelection().toString().trim();
|
||
const selection = window.getSelection();
|
||
|
||
if (!selectedText || selection.rangeCount === 0) {
|
||
alert('텍스트를 먼저 선택해주세요.');
|
||
return;
|
||
}
|
||
|
||
const selectedRange = selection.getRangeAt(0);
|
||
this.linkManager.createLinkFromSelection(this.documentId, selectedText, selectedRange);
|
||
},
|
||
|
||
|
||
|
||
activateNoteMode() {
|
||
console.log('📝 메모 모드 활성화');
|
||
this.activeMode = 'memo';
|
||
this.highlightManager.activateNoteMode();
|
||
},
|
||
|
||
async loadBacklinks() {
|
||
console.log('🔗 백링크 로드 시작');
|
||
if (this.linkManager) {
|
||
await this.linkManager.loadBacklinks(this.documentId, this.contentType);
|
||
// UI 상태 동기화
|
||
this.backlinks = this.linkManager.backlinks || [];
|
||
}
|
||
},
|
||
|
||
// 링크 대상 타입 변경 시 호출
|
||
async onTargetTypeChange() {
|
||
console.log('🔄 링크 대상 타입 변경:', this.linkForm.target_type);
|
||
|
||
// 기존 선택 초기화
|
||
this.linkForm.target_book_id = '';
|
||
this.linkForm.target_document_id = '';
|
||
this.availableBooks = [];
|
||
this.filteredDocuments = [];
|
||
|
||
// 선택된 타입에 따라 데이터 로드
|
||
if (this.linkForm.target_type === 'note') {
|
||
await this.loadNotebooks();
|
||
} else {
|
||
await this.loadBooks();
|
||
}
|
||
},
|
||
|
||
// 노트북 목록 로드
|
||
async loadNotebooks() {
|
||
try {
|
||
console.log('📚 노트북 목록 로딩 시작...');
|
||
|
||
const notebooks = await this.api.get('/notebooks/', { active_only: true });
|
||
this.availableBooks = notebooks.map(notebook => ({
|
||
id: notebook.id,
|
||
title: notebook.title
|
||
})) || [];
|
||
|
||
console.log('📚 로드된 노트북 목록:', this.availableBooks.length, '개');
|
||
} catch (error) {
|
||
console.error('노트북 목록 로드 실패:', error);
|
||
this.availableBooks = [];
|
||
}
|
||
},
|
||
|
||
// 서적 목록 로드
|
||
async loadBooks() {
|
||
try {
|
||
console.log('📚 서적 목록 로딩 시작...');
|
||
|
||
let allDocuments;
|
||
|
||
// contentType에 따라 다른 API 사용
|
||
if (this.contentType === 'note') {
|
||
// 노트의 경우 전체 문서 목록에서 서적 정보 추출
|
||
console.log('📝 노트 모드: 전체 문서 목록에서 서적 정보 추출');
|
||
allDocuments = await this.api.getDocuments();
|
||
console.log('📄 전체 문서들 (총 개수):', allDocuments.length);
|
||
} else {
|
||
// 일반 문서의 경우 linkable-documents API 사용
|
||
console.log('📄 문서 모드: linkable-documents API 사용');
|
||
allDocuments = await this.api.getLinkableDocuments(this.documentId);
|
||
console.log('📄 링크 가능한 문서들 (총 개수):', allDocuments.length);
|
||
}
|
||
|
||
// 서적별로 그룹화
|
||
const bookMap = new Map();
|
||
allDocuments.forEach(doc => {
|
||
if (doc.book_id && doc.book_title) {
|
||
bookMap.set(doc.book_id, {
|
||
id: doc.book_id,
|
||
title: doc.book_title
|
||
});
|
||
}
|
||
});
|
||
|
||
this.availableBooks = Array.from(bookMap.values());
|
||
console.log('📚 로드된 서적 목록:', this.availableBooks.length, '개');
|
||
} catch (error) {
|
||
console.error('서적 목록 로드 실패:', error);
|
||
this.availableBooks = [];
|
||
}
|
||
},
|
||
|
||
async loadAvailableBooks() {
|
||
try {
|
||
// 기본값으로 문서 타입 설정 (기존 호환성)
|
||
if (this.linkForm.target_type === 'note') {
|
||
await this.loadNotebooks();
|
||
} else {
|
||
await this.loadBooks();
|
||
}
|
||
} catch (error) {
|
||
console.error('목록 로드 실패:', error);
|
||
this.availableBooks = [];
|
||
}
|
||
},
|
||
|
||
getSourceBookInfo(allDocuments = null) {
|
||
// 여러 소스에서 현재 문서의 서적 정보 찾기
|
||
let sourceBookId = this.navigation?.book_info?.id ||
|
||
this.document?.book_id ||
|
||
this.document?.book_info?.id;
|
||
|
||
let sourceBookTitle = this.navigation?.book_info?.title ||
|
||
this.document?.book_title ||
|
||
this.document?.book_info?.title;
|
||
|
||
// allDocuments에서도 확인 (가장 확실한 방법)
|
||
if (allDocuments) {
|
||
const currentDoc = allDocuments.find(doc => doc.id === this.documentId);
|
||
if (currentDoc) {
|
||
sourceBookId = currentDoc.book_id;
|
||
sourceBookTitle = currentDoc.book_title;
|
||
}
|
||
}
|
||
|
||
return {
|
||
id: sourceBookId,
|
||
title: sourceBookTitle
|
||
};
|
||
},
|
||
|
||
async loadSameBookDocuments() {
|
||
try {
|
||
if (this.contentType === 'note') {
|
||
console.log('📚 같은 노트북의 노트들 로드 시작...');
|
||
|
||
// 현재 노트의 노트북 정보 가져오기
|
||
const currentNote = this.document;
|
||
const notebookId = currentNote?.notebook_id;
|
||
|
||
if (notebookId) {
|
||
// 같은 노트북의 노트들 로드 (현재 노트 제외)
|
||
const notes = await this.api.getNotesInNotebook(notebookId);
|
||
|
||
this.filteredDocuments = notes.filter(note => note.id !== this.documentId);
|
||
console.log('📚 같은 노트북 노트들:', {
|
||
count: this.filteredDocuments.length,
|
||
notebookId: notebookId,
|
||
notes: this.filteredDocuments.map(note => ({ id: note.id, title: note.title }))
|
||
});
|
||
} else {
|
||
console.warn('⚠️ 현재 노트의 노트북 정보를 찾을 수 없습니다');
|
||
this.filteredDocuments = [];
|
||
}
|
||
return;
|
||
}
|
||
|
||
const allDocuments = await this.api.getLinkableDocuments(this.documentId);
|
||
|
||
// 소스 문서의 서적 정보 가져오기
|
||
const sourceBookInfo = this.getSourceBookInfo(allDocuments);
|
||
|
||
console.log('📚 같은 서적 문서 로드 시작:', {
|
||
sourceBookId: sourceBookInfo.id,
|
||
sourceBookTitle: sourceBookInfo.title,
|
||
totalDocs: allDocuments.length
|
||
});
|
||
|
||
if (sourceBookInfo.id) {
|
||
// 소스 문서와 같은 서적의 문서들만 필터링 (현재 문서 제외)
|
||
this.filteredDocuments = allDocuments.filter(doc =>
|
||
doc.book_id === sourceBookInfo.id && doc.id !== this.documentId
|
||
);
|
||
console.log('📚 같은 서적 문서들:', {
|
||
count: this.filteredDocuments.length,
|
||
bookTitle: sourceBookInfo.title,
|
||
documents: this.filteredDocuments.map(doc => ({ id: doc.id, title: doc.title }))
|
||
});
|
||
} else {
|
||
console.warn('⚠️ 소스 문서의 서적 정보를 찾을 수 없습니다!');
|
||
this.filteredDocuments = [];
|
||
}
|
||
} catch (error) {
|
||
console.error('같은 서적/노트북 문서 로드 실패:', error);
|
||
this.filteredDocuments = [];
|
||
}
|
||
},
|
||
|
||
async loadSameBookDocumentsForSelected() {
|
||
try {
|
||
console.log('📚 선택한 문서 기준으로 같은 서적 문서 로드 시작');
|
||
|
||
const allDocuments = await this.api.getLinkableDocuments(this.documentId);
|
||
|
||
// 선택한 대상 문서 찾기
|
||
const selectedDoc = allDocuments.find(doc => doc.id === this.linkForm.target_document_id);
|
||
|
||
if (!selectedDoc) {
|
||
console.error('❌ 선택한 문서를 찾을 수 없습니다:', this.linkForm.target_document_id);
|
||
return;
|
||
}
|
||
|
||
console.log('🎯 선택한 문서 정보:', {
|
||
id: selectedDoc.id,
|
||
title: selectedDoc.title,
|
||
bookId: selectedDoc.book_id,
|
||
bookTitle: selectedDoc.book_title
|
||
});
|
||
|
||
// 선택한 문서와 같은 서적의 모든 문서들 (소스 문서 제외)
|
||
this.filteredDocuments = allDocuments.filter(doc =>
|
||
doc.book_id === selectedDoc.book_id && doc.id !== this.documentId
|
||
);
|
||
|
||
console.log('📚 선택한 문서와 같은 서적 문서들:', {
|
||
selectedBookTitle: selectedDoc.book_title,
|
||
count: this.filteredDocuments.length,
|
||
documents: this.filteredDocuments.map(doc => ({ id: doc.id, title: doc.title }))
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('선택한 문서 기준 같은 서적 로드 실패:', error);
|
||
this.filteredDocuments = [];
|
||
}
|
||
},
|
||
|
||
async loadDocumentsFromBook() {
|
||
try {
|
||
if (this.linkForm.target_book_id) {
|
||
if (this.linkForm.target_type === 'note') {
|
||
// 노트북 선택: 선택된 노트북의 노트들 가져오기
|
||
const notes = await this.api.getNotesInNotebook(this.linkForm.target_book_id);
|
||
this.filteredDocuments = notes.filter(note => note.id !== this.documentId);
|
||
console.log('📚 선택된 노트북 노트들:', this.filteredDocuments);
|
||
} else {
|
||
// 서적 선택: 선택된 서적의 문서들만 가져오기
|
||
let allDocuments;
|
||
|
||
if (this.contentType === 'note') {
|
||
// 노트에서 서적 문서를 선택하는 경우: 전체 문서 목록에서 필터링
|
||
console.log('📝 노트에서 서적 문서 선택: 전체 문서 목록 사용');
|
||
allDocuments = await this.api.getDocuments();
|
||
} else {
|
||
// 일반 문서에서 서적 문서를 선택하는 경우: linkable-documents API 사용
|
||
console.log('📄 문서에서 서적 문서 선택: linkable-documents API 사용');
|
||
allDocuments = await this.api.getLinkableDocuments(this.documentId);
|
||
}
|
||
|
||
this.filteredDocuments = allDocuments.filter(doc =>
|
||
doc.book_id === this.linkForm.target_book_id
|
||
);
|
||
console.log('📚 선택된 서적 문서들:', this.filteredDocuments);
|
||
}
|
||
} else {
|
||
this.filteredDocuments = [];
|
||
}
|
||
|
||
// 문서 선택 초기화
|
||
this.linkForm.target_document_id = '';
|
||
} catch (error) {
|
||
console.error('서적/노트북별 문서 로드 실패:', error);
|
||
this.filteredDocuments = [];
|
||
}
|
||
},
|
||
|
||
resetTargetSelection() {
|
||
console.log('🔄 대상 선택 초기화');
|
||
this.linkForm.target_book_id = '';
|
||
this.linkForm.target_document_id = '';
|
||
this.filteredDocuments = [];
|
||
|
||
// 초기화 후 아무것도 하지 않음 (서적 선택 후 문서 로드)
|
||
},
|
||
|
||
async onTargetDocumentChange() {
|
||
console.log('📄 대상 문서 변경:', this.linkForm.target_document_id);
|
||
|
||
// 대상 문서 변경 시 특별한 처리 없음
|
||
},
|
||
|
||
selectTextFromDocument() {
|
||
console.log('🎯 대상 문서에서 텍스트 선택 시작');
|
||
|
||
if (!this.linkForm.target_document_id) {
|
||
alert('대상 문서를 먼저 선택해주세요.');
|
||
return;
|
||
}
|
||
|
||
// 새 창에서 대상 문서 열기 (텍스트 선택 모드 전용 페이지)
|
||
const targetContentType = this.linkForm.target_type === 'note' ? 'note' : 'document';
|
||
const targetUrl = `/text-selector.html?id=${this.linkForm.target_document_id}&contentType=${targetContentType}`;
|
||
console.log('🚀 텍스트 선택 창 열기:', targetUrl, 'contentType:', targetContentType);
|
||
const popup = window.open(targetUrl, 'targetDocumentSelector', 'width=1200,height=800,scrollbars=yes,resizable=yes');
|
||
|
||
if (!popup) {
|
||
console.error('❌ 팝업 창이 차단되었습니다!');
|
||
alert('팝업 창이 차단되었습니다. 브라우저 설정에서 팝업을 허용해주세요.');
|
||
} else {
|
||
console.log('✅ 팝업 창이 성공적으로 열렸습니다');
|
||
}
|
||
|
||
// 팝업에서 텍스트 선택 완료 시 메시지 수신
|
||
window.addEventListener('message', (event) => {
|
||
if (event.data.type === 'TEXT_SELECTED') {
|
||
this.linkForm.target_text = event.data.selectedText;
|
||
this.linkForm.target_start_offset = event.data.startOffset;
|
||
this.linkForm.target_end_offset = event.data.endOffset;
|
||
console.log('🎯 대상 텍스트 선택됨:', event.data);
|
||
popup.close();
|
||
}
|
||
}, { once: true });
|
||
},
|
||
|
||
activateBookmarkMode() {
|
||
console.log('🔖 북마크 모드 활성화');
|
||
this.activeMode = 'bookmark';
|
||
this.bookmarkManager.activateBookmarkMode();
|
||
},
|
||
|
||
// ==================== 하이라이트 기능 위임 ====================
|
||
createHighlightWithColor(color) {
|
||
console.log('🎨 하이라이트 생성 요청:', color);
|
||
// ViewerCore의 selectedHighlightColor도 동기화
|
||
this.selectedHighlightColor = color;
|
||
console.log('🎨 ViewerCore 색상 동기화:', this.selectedHighlightColor);
|
||
return this.highlightManager.createHighlightWithColor(color);
|
||
},
|
||
|
||
// ==================== 메모 입력 모달 관련 ====================
|
||
openNoteInputModal() {
|
||
console.log('📝 메모 입력 모달 열기');
|
||
this.showNoteInputModal = true;
|
||
// 폼 초기화
|
||
this.noteForm.content = '';
|
||
this.noteForm.tags = '';
|
||
// 포커스를 textarea로 이동 (다음 틱에서)
|
||
this.$nextTick(() => {
|
||
const textarea = document.querySelector('textarea[x-model="noteForm.content"]');
|
||
if (textarea) textarea.focus();
|
||
});
|
||
},
|
||
|
||
closeNoteInputModal() {
|
||
console.log('📝 메모 입력 모달 닫기');
|
||
this.showNoteInputModal = false;
|
||
this.noteForm.content = '';
|
||
this.noteForm.tags = '';
|
||
// 선택된 텍스트 정리
|
||
this.selectedText = '';
|
||
this.selectedRange = null;
|
||
},
|
||
|
||
async createNoteForHighlight() {
|
||
console.log('📝 하이라이트에 메모 생성');
|
||
if (!this.noteForm.content.trim()) {
|
||
alert('메모 내용을 입력해주세요.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 현재 생성된 하이라이트 정보가 필요함
|
||
if (this.highlightManager.lastCreatedHighlight) {
|
||
await this.highlightManager.createNoteForHighlight(
|
||
this.highlightManager.lastCreatedHighlight,
|
||
this.noteForm.content.trim(),
|
||
this.noteForm.tags.trim()
|
||
);
|
||
this.closeNoteInputModal();
|
||
} else {
|
||
alert('하이라이트 정보를 찾을 수 없습니다.');
|
||
}
|
||
} catch (error) {
|
||
console.error('메모 생성 실패:', error);
|
||
alert('메모 생성에 실패했습니다: ' + error.message);
|
||
}
|
||
},
|
||
|
||
skipNoteForHighlight() {
|
||
console.log('📝 메모 입력 건너뛰기');
|
||
this.closeNoteInputModal();
|
||
},
|
||
|
||
// ==================== UI 메서드 위임 ====================
|
||
toggleFeatureMenu(feature) {
|
||
const result = this.uiManager.toggleFeatureMenu(feature);
|
||
this.updateModalStates(); // 상태 동기화
|
||
return result;
|
||
},
|
||
|
||
openNoteModal(highlight = null) {
|
||
const result = this.uiManager.openNoteModal(highlight);
|
||
this.updateModalStates(); // 상태 동기화
|
||
return result;
|
||
},
|
||
|
||
closeNoteModal() {
|
||
const result = this.uiManager.closeNoteModal();
|
||
this.updateModalStates(); // 상태 동기화
|
||
return result;
|
||
},
|
||
|
||
closeLinkModal() {
|
||
const result = this.uiManager.closeLinkModal();
|
||
this.updateModalStates(); // 상태 동기화
|
||
return result;
|
||
},
|
||
|
||
closeBookmarkModal() {
|
||
const result = this.uiManager.closeBookmarkModal();
|
||
this.updateModalStates(); // 상태 동기화
|
||
return result;
|
||
},
|
||
|
||
highlightSearchResults(element, searchText) {
|
||
return this.uiManager.highlightSearchResults(element, searchText);
|
||
},
|
||
|
||
showSuccessMessage(message) {
|
||
return this.uiManager.showSuccessMessage(message);
|
||
},
|
||
|
||
showErrorMessage(message) {
|
||
return this.uiManager.showErrorMessage(message);
|
||
},
|
||
|
||
// ==================== 언어 전환 ====================
|
||
toggleLanguage() {
|
||
this.isKorean = !this.isKorean;
|
||
const lang = this.isKorean ? 'ko' : 'en';
|
||
console.log('🌐 언어 전환:', this.isKorean ? '한국어' : 'English');
|
||
|
||
// 문서에 내장된 언어 전환 기능 찾기 및 실행
|
||
this.findAndExecuteBuiltinLanguageToggle();
|
||
},
|
||
|
||
// 문서에 내장된 언어 전환 기능 찾기
|
||
findAndExecuteBuiltinLanguageToggle() {
|
||
console.log('🔍 문서 내장 언어 전환 기능 찾기 시작');
|
||
|
||
const content = document.getElementById('document-content');
|
||
if (!content) {
|
||
console.warn('❌ document-content 요소를 찾을 수 없습니다');
|
||
return;
|
||
}
|
||
|
||
// 1. 언어 전환 버튼 찾기 (다양한 패턴)
|
||
const buttonSelectors = [
|
||
'button[onclick*="toggleLanguage"]',
|
||
'button[onclick*="language"]',
|
||
'button[onclick*="Language"]',
|
||
'.language-toggle',
|
||
'.lang-toggle',
|
||
'button[id*="lang"]',
|
||
'button[class*="lang"]',
|
||
'input[type="button"][onclick*="language"]'
|
||
];
|
||
|
||
let foundButton = null;
|
||
for (const selector of buttonSelectors) {
|
||
const buttons = content.querySelectorAll(selector);
|
||
if (buttons.length > 0) {
|
||
foundButton = buttons[0];
|
||
console.log(`✅ 언어 전환 버튼 발견 (${selector}):`, foundButton.outerHTML.substring(0, 100));
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 2. 버튼이 있으면 클릭
|
||
if (foundButton) {
|
||
console.log('🔘 내장 언어 전환 버튼 클릭');
|
||
try {
|
||
foundButton.click();
|
||
console.log('✅ 언어 전환 버튼 클릭 완료');
|
||
return;
|
||
} catch (error) {
|
||
console.error('❌ 버튼 클릭 실패:', error);
|
||
}
|
||
}
|
||
|
||
// 3. 버튼이 없으면 스크립트 함수 직접 호출 시도
|
||
this.tryDirectLanguageFunction();
|
||
},
|
||
|
||
// 직접 언어 전환 함수 호출 시도
|
||
tryDirectLanguageFunction() {
|
||
console.log('🔧 직접 언어 전환 함수 호출 시도');
|
||
|
||
const functionNames = [
|
||
'toggleLanguage',
|
||
'changeLanguage',
|
||
'switchLanguage',
|
||
'toggleLang',
|
||
'changeLang'
|
||
];
|
||
|
||
for (const funcName of functionNames) {
|
||
if (typeof window[funcName] === 'function') {
|
||
console.log(`✅ 전역 함수 발견: ${funcName}`);
|
||
try {
|
||
window[funcName]();
|
||
console.log(`✅ ${funcName}() 호출 완료`);
|
||
return;
|
||
} catch (error) {
|
||
console.error(`❌ ${funcName}() 호출 실패:`, error);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 4. 문서 내 스크립트에서 함수 찾기
|
||
this.findLanguageFunctionInScripts();
|
||
},
|
||
|
||
// 문서 내 스크립트에서 언어 전환 함수 찾기
|
||
findLanguageFunctionInScripts() {
|
||
console.log('📜 문서 내 스크립트에서 언어 함수 찾기');
|
||
|
||
const content = document.getElementById('document-content');
|
||
const scripts = content.querySelectorAll('script');
|
||
|
||
console.log(`📜 발견된 스크립트 태그: ${scripts.length}개`);
|
||
|
||
scripts.forEach((script, index) => {
|
||
const scriptContent = script.textContent || script.innerHTML;
|
||
if (scriptContent.includes('language') || scriptContent.includes('Language') || scriptContent.includes('lang')) {
|
||
console.log(`📜 스크립트 ${index + 1}에서 언어 관련 코드 발견:`, scriptContent.substring(0, 200));
|
||
|
||
// 함수 실행 시도
|
||
try {
|
||
eval(scriptContent);
|
||
console.log(`✅ 스크립트 ${index + 1} 실행 완료`);
|
||
} catch (error) {
|
||
console.log(`⚠️ 스크립트 ${index + 1} 실행 실패:`, error.message);
|
||
}
|
||
}
|
||
});
|
||
|
||
console.log('⚠️ 내장 언어 전환 기능을 찾을 수 없습니다');
|
||
},
|
||
|
||
|
||
|
||
async downloadOriginalFile() {
|
||
if (!this.document || !this.document.id) {
|
||
console.warn('문서 정보가 없습니다');
|
||
return;
|
||
}
|
||
|
||
console.log('📕 PDF 다운로드 시도:', {
|
||
id: this.document.id,
|
||
matched_pdf_id: this.document.matched_pdf_id,
|
||
pdf_path: this.document.pdf_path
|
||
});
|
||
|
||
// 캐시를 무시하고 최신 문서 정보를 다시 가져오기
|
||
console.log('🔄 최신 문서 정보 재로드 중...');
|
||
try {
|
||
const freshDocument = await this.api.getDocument(this.document.id);
|
||
console.log('📄 최신 문서 정보:', {
|
||
id: freshDocument.id,
|
||
matched_pdf_id: freshDocument.matched_pdf_id,
|
||
pdf_path: freshDocument.pdf_path
|
||
});
|
||
|
||
// 최신 정보로 업데이트
|
||
if (freshDocument.matched_pdf_id !== this.document.matched_pdf_id) {
|
||
console.log('🔄 PDF 매칭 정보 업데이트:', {
|
||
old: this.document.matched_pdf_id,
|
||
new: freshDocument.matched_pdf_id
|
||
});
|
||
this.document.matched_pdf_id = freshDocument.matched_pdf_id;
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 최신 문서 정보 로드 실패:', error);
|
||
}
|
||
|
||
// 1. 현재 문서 자체가 PDF인 경우
|
||
if (this.document.pdf_path) {
|
||
console.log('📄 현재 문서가 PDF - 직접 다운로드');
|
||
this.downloadPdfFile(this.document.pdf_path, this.document.title || 'document');
|
||
return;
|
||
}
|
||
|
||
// 2. 연결된 PDF가 있는지 확인
|
||
if (!this.document.matched_pdf_id) {
|
||
alert('연결된 원본 PDF 파일이 없습니다.\n\n서적 편집 페이지에서 PDF 파일을 연결해주세요.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
console.log('📕 연결된 PDF 다운로드 시작:', this.document.matched_pdf_id);
|
||
|
||
// 연결된 PDF 문서 정보 가져오기
|
||
const pdfDocument = await this.api.getDocument(this.document.matched_pdf_id);
|
||
|
||
if (!pdfDocument) {
|
||
throw new Error('연결된 PDF 문서를 찾을 수 없습니다');
|
||
}
|
||
|
||
// PDF 파일 다운로드 URL 생성
|
||
const downloadUrl = `/api/documents/${this.document.matched_pdf_id}/download`;
|
||
|
||
// 인증 헤더 추가를 위해 fetch 사용
|
||
const response = await fetch(downloadUrl, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('연결된 PDF 다운로드에 실패했습니다');
|
||
}
|
||
|
||
// Blob으로 변환하여 다운로드
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
|
||
// 다운로드 링크 생성 및 클릭
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.download = pdfDocument.original_filename || `${pdfDocument.title}.pdf`;
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
|
||
// URL 정리
|
||
window.URL.revokeObjectURL(url);
|
||
|
||
console.log('📕 PDF 다운로드 완료:', pdfDocument.original_filename);
|
||
|
||
} catch (error) {
|
||
console.error('PDF 다운로드 오류:', error);
|
||
alert('PDF 다운로드 중 오류가 발생했습니다: ' + error.message);
|
||
}
|
||
},
|
||
|
||
// PDF 파일 직접 다운로드
|
||
downloadPdfFile(pdfPath, filename) {
|
||
try {
|
||
console.log('📄 PDF 파일 직접 다운로드:', pdfPath);
|
||
|
||
// PDF 파일 URL 생성 (상대 경로를 절대 경로로 변환)
|
||
let pdfUrl = pdfPath;
|
||
if (!pdfUrl.startsWith('http')) {
|
||
// 상대 경로인 경우 현재 도메인 기준으로 절대 경로 생성
|
||
const baseUrl = window.location.origin;
|
||
pdfUrl = pdfUrl.startsWith('/') ? baseUrl + pdfUrl : baseUrl + '/' + pdfUrl;
|
||
}
|
||
|
||
console.log('📄 PDF URL:', pdfUrl);
|
||
|
||
// 다운로드 링크 생성 및 클릭
|
||
const link = document.createElement('a');
|
||
link.href = pdfUrl;
|
||
link.download = filename.endsWith('.pdf') ? filename : filename + '.pdf';
|
||
link.target = '_blank'; // 새 탭에서 열기 (다운로드 실패 시 뷰어로 열림)
|
||
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
|
||
console.log('✅ PDF 다운로드 링크 클릭 완료');
|
||
|
||
} catch (error) {
|
||
console.error('PDF 다운로드 오류:', error);
|
||
alert('PDF 다운로드 중 오류가 발생했습니다: ' + error.message);
|
||
}
|
||
},
|
||
|
||
// ==================== 통합 툴팁 처리 ====================
|
||
/**
|
||
* 클릭된 요소에서 링크, 백링크, 하이라이트 모두 찾기 (완전 개선 버전)
|
||
*/
|
||
getOverlappingElements(clickedElement) {
|
||
const selectedText = clickedElement.textContent.trim();
|
||
console.log('🔍 통합 요소 찾기 시작:', selectedText);
|
||
console.log('🔍 하이라이트 매니저 상태:', {
|
||
highlightManager: !!this.highlightManager,
|
||
highlightsCount: this.highlightManager?.highlights?.length || 0,
|
||
highlights: this.highlightManager?.highlights || []
|
||
});
|
||
|
||
// 결과 배열들
|
||
const overlappingLinks = [];
|
||
const overlappingBacklinks = [];
|
||
const overlappingHighlights = [];
|
||
|
||
// 1. 모든 링크 요소 찾기 (같은 텍스트)
|
||
const allLinkElements = document.querySelectorAll('.document-link');
|
||
allLinkElements.forEach(linkEl => {
|
||
if (linkEl.textContent.trim() === selectedText) {
|
||
const linkId = linkEl.dataset.linkId;
|
||
const link = this.linkManager.documentLinks.find(l => l.id === linkId);
|
||
if (link && !overlappingLinks.find(l => l.id === link.id)) {
|
||
overlappingLinks.push(link);
|
||
const linkTitle = link.target_note_title || link.target_document_title || 'Unknown';
|
||
console.log('✅ 겹치는 링크 발견:', linkTitle);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 2. 모든 백링크 요소 찾기 (같은 텍스트)
|
||
const allBacklinkElements = document.querySelectorAll('.backlink-highlight');
|
||
allBacklinkElements.forEach(backlinkEl => {
|
||
if (backlinkEl.textContent.trim() === selectedText) {
|
||
const backlinkId = backlinkEl.dataset.backlinkId;
|
||
const backlink = this.linkManager.backlinks.find(b => b.id === backlinkId);
|
||
if (backlink && !overlappingBacklinks.find(b => b.id === backlink.id)) {
|
||
overlappingBacklinks.push(backlink);
|
||
console.log('✅ 겹치는 백링크 발견:', backlink.source_document_title);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 3. 모든 하이라이트 요소 찾기 (같은 텍스트)
|
||
const allHighlightElements = document.querySelectorAll('.highlight-span');
|
||
console.log('🔍 페이지의 모든 하이라이트 요소:', allHighlightElements.length, '개');
|
||
allHighlightElements.forEach(highlightEl => {
|
||
const highlightText = highlightEl.textContent.trim();
|
||
|
||
// 텍스트가 정확히 일치하거나 포함 관계인 경우
|
||
if (highlightText === selectedText ||
|
||
highlightText.includes(selectedText) ||
|
||
selectedText.includes(highlightText)) {
|
||
|
||
const highlightId = highlightEl.dataset.highlightId;
|
||
console.log('🔍 하이라이트 요소 확인:', {
|
||
element: highlightEl,
|
||
highlightId: highlightId,
|
||
text: highlightText,
|
||
selectedText: selectedText
|
||
});
|
||
|
||
const highlight = this.highlightManager.highlights.find(h => h.id === highlightId);
|
||
if (highlight && !overlappingHighlights.find(h => h.id === highlight.id)) {
|
||
overlappingHighlights.push(highlight);
|
||
console.log('✅ 겹치는 하이라이트 발견:', {
|
||
id: highlight.id,
|
||
text: highlightText,
|
||
color: highlight.highlight_color
|
||
});
|
||
} else if (!highlight) {
|
||
console.log('❌ 하이라이트 데이터를 찾을 수 없음:', highlightId);
|
||
}
|
||
}
|
||
});
|
||
|
||
console.log('📊 최종 발견된 요소들:', {
|
||
links: overlappingLinks.length,
|
||
backlinks: overlappingBacklinks.length,
|
||
highlights: overlappingHighlights.length,
|
||
selectedText: selectedText
|
||
});
|
||
|
||
return {
|
||
links: overlappingLinks,
|
||
backlinks: overlappingBacklinks,
|
||
highlights: overlappingHighlights,
|
||
selectedText: selectedText
|
||
};
|
||
},
|
||
|
||
/**
|
||
* 통합 툴팁 표시 (링크 + 하이라이트 + 백링크)
|
||
*/
|
||
async showUnifiedTooltip(overlappingElements, element) {
|
||
const { links = [], highlights = [], backlinks = [], selectedText } = overlappingElements;
|
||
|
||
console.log('🎯 통합 툴팁 표시:', {
|
||
links: links.length,
|
||
highlights: highlights.length,
|
||
backlinks: backlinks.length
|
||
});
|
||
|
||
// 하이라이트가 있으면 메모 데이터 로드
|
||
if (highlights.length > 0) {
|
||
console.log('📝 통합 툴팁용 메모 로드 시작...');
|
||
const documentId = this.documentId;
|
||
const contentType = this.contentType;
|
||
await this.highlightManager.loadNotes(documentId, contentType);
|
||
console.log('📝 통합 툴팁용 메모 로드 완료:', this.highlightManager.notes.length, '개');
|
||
}
|
||
|
||
// 기존 툴팁들 숨기기
|
||
this.linkManager.hideTooltip();
|
||
this.highlightManager.hideTooltip();
|
||
|
||
// 툴팁 컨테이너 생성
|
||
const tooltip = document.createElement('div');
|
||
tooltip.id = 'unified-tooltip';
|
||
tooltip.className = 'fixed z-50 bg-white rounded-xl shadow-2xl border border-gray-200';
|
||
tooltip.style.cssText = `
|
||
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||
max-width: 90vw;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
z-index: 9999;
|
||
`;
|
||
|
||
const totalElements = links.length + highlights.length + backlinks.length;
|
||
|
||
let tooltipHTML = `
|
||
<div class="p-6">
|
||
<div class="mb-4">
|
||
<div class="flex items-center justify-between mb-3">
|
||
<div class="text-lg font-semibold text-gray-800 flex items-center">
|
||
<svg class="w-5 h-5 mr-2 text-purple-600" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"/>
|
||
겹치는 요소들
|
||
</div>
|
||
<div class="text-xs text-gray-500">${totalElements}개 요소</div>
|
||
</div>
|
||
|
||
<div class="font-medium text-gray-900 bg-purple-50 px-4 py-3 rounded-lg border-l-4 border-purple-500">
|
||
<div class="text-sm text-purple-700 mb-1">선택된 텍스트</div>
|
||
<div class="text-base">"${selectedText}"</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 하이라이트 섹션
|
||
if (highlights.length > 0) {
|
||
tooltipHTML += `
|
||
<div class="mb-6">
|
||
<div class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||
<svg class="w-4 h-4 mr-2 text-yellow-600" 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"/>
|
||
하이라이트 (${highlights.length}개)
|
||
</div>
|
||
<div class="space-y-2">
|
||
`;
|
||
|
||
highlights.forEach(highlight => {
|
||
const colorName = this.highlightManager.getColorName(highlight.highlight_color);
|
||
const createdDate = this.formatDate(highlight.created_at);
|
||
const notes = this.highlightManager.notes.filter(note => note.highlight_id === highlight.id);
|
||
|
||
console.log(`📝 통합 툴팁 - 하이라이트 ${highlight.id}의 메모:`, notes.length, '개');
|
||
if (notes.length > 0) {
|
||
console.log('📝 메모 내용:', notes.map(n => n.content));
|
||
}
|
||
|
||
tooltipHTML += `
|
||
<div class="border rounded-lg p-3 bg-gradient-to-r from-yellow-50 to-orange-50 cursor-pointer hover:shadow-md transition-shadow duration-200"
|
||
onclick="window.documentViewerInstance.highlightManager.showHighlightTooltip([${JSON.stringify(highlight).replace(/"/g, '"')}], this.parentElement)">
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex items-center space-x-2">
|
||
<div class="w-3 h-3 rounded-full" style="background-color: ${highlight.highlight_color}"></div>
|
||
<span class="text-sm font-medium text-gray-800">${colorName}</span>
|
||
<span class="text-xs text-gray-500">${createdDate}</span>
|
||
</div>
|
||
<div class="text-xs text-gray-600">${notes.length}개 메모</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
tooltipHTML += `
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 링크 섹션
|
||
if (links.length > 0) {
|
||
tooltipHTML += `
|
||
<div class="mb-4">
|
||
<div class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||
<svg class="w-4 h-4 mr-2 text-purple-600" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd"/>
|
||
</svg>
|
||
링크 (${links.length}개)
|
||
</div>
|
||
<div class="space-y-2">
|
||
`;
|
||
|
||
links.forEach(link => {
|
||
const isNote = link.target_content_type === 'note';
|
||
const bgClass = isNote ? 'from-green-50 to-emerald-50' : 'from-purple-50 to-indigo-50';
|
||
const iconClass = isNote ? 'text-green-600' : 'text-purple-600';
|
||
const createdDate = this.formatDate(link.created_at);
|
||
|
||
tooltipHTML += `
|
||
<div class="border rounded-lg p-3 bg-gradient-to-r ${bgClass} transition-colors duration-200 relative group">
|
||
<div class="flex items-start justify-between">
|
||
<div class="flex-1 cursor-pointer" onclick="window.documentViewerInstance.navigateToLink(${JSON.stringify(link).replace(/"/g, '"')})">
|
||
<div class="flex items-center mb-2">
|
||
<svg class="w-4 h-4 mr-2 ${iconClass}" fill="currentColor" viewBox="0 0 20 20">
|
||
${isNote ?
|
||
'<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><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 fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/>'
|
||
}
|
||
</svg>
|
||
<span class="font-medium ${iconClass}">${link.target_note_title || link.target_document_title}</span>
|
||
</div>
|
||
|
||
${link.target_text ? `
|
||
<div class="mb-2 p-2 bg-gray-50 rounded border-l-3 ${isNote ? 'border-green-400' : 'border-purple-400'}">
|
||
<div class="text-xs ${isNote ? 'text-green-700' : 'text-purple-700'} mb-1">연결된 텍스트</div>
|
||
<div class="text-sm text-gray-800 font-medium">"${link.target_text}"</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
${link.description ? `
|
||
<div class="mb-2 p-2 bg-blue-50 rounded border-l-3 border-blue-400">
|
||
<div class="text-xs text-blue-700 mb-1">링크 설명</div>
|
||
<div class="text-sm text-blue-800">${link.description}</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div class="text-xs text-gray-500 flex items-center justify-between">
|
||
<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="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd"/>
|
||
${link.link_type === 'text_fragment' ? '텍스트 조각 링크' : '문서 링크'}
|
||
</span>
|
||
<span>${createdDate}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 삭제 버튼 -->
|
||
<button onclick="event.stopPropagation(); window.documentViewerInstance.deleteLinkWithConfirm('${link.id}', '${(link.target_note_title || link.target_document_title).replace(/'/g, "\\'")}');"
|
||
class="ml-3 p-1 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors duration-200 opacity-0 group-hover:opacity-100"
|
||
title="링크 삭제">
|
||
<svg class="w-4 h-4" 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"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
tooltipHTML += `
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 백링크 섹션
|
||
if (backlinks.length > 0) {
|
||
tooltipHTML += `
|
||
<div class="mb-4">
|
||
<div class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||
<svg class="w-4 h-4 mr-2 text-orange-600" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M7.707 3.293a1 1 0 010 1.414L5.414 7H11a7 7 0 017 7v2a1 1 0 11-2 0v-2a5 5 0 00-5-5H5.414l2.293 2.293a1 1 0 11-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||
</svg>
|
||
백링크 (${backlinks.length}개)
|
||
</div>
|
||
<div class="space-y-2">
|
||
`;
|
||
|
||
backlinks.forEach(backlink => {
|
||
const createdDate = this.formatDate(backlink.created_at);
|
||
|
||
tooltipHTML += `
|
||
<div class="border rounded-lg p-3 bg-gradient-to-r from-orange-50 to-red-50 transition-colors duration-200 relative group">
|
||
<div class="cursor-pointer" onclick="window.documentViewerInstance.navigateToBacklink(${JSON.stringify(backlink).replace(/"/g, '"')})">
|
||
<div class="flex items-center mb-2">
|
||
<svg class="w-4 h-4 mr-2 text-orange-600" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/>
|
||
<span class="font-medium text-orange-600">${backlink.source_document_title}</span>
|
||
</div>
|
||
|
||
<div class="mb-2 p-2 bg-white rounded border-l-3 border-orange-400">
|
||
<div class="text-xs text-orange-600 mb-1">원본 문서에서 링크로 설정한 텍스트</div>
|
||
<div class="text-sm text-gray-800 font-medium">"${backlink.selected_text}"</div>
|
||
</div>
|
||
|
||
${backlink.target_text ? `
|
||
<div class="mb-2 p-2 bg-white rounded border-l-3 border-blue-400">
|
||
<div class="text-xs text-blue-600 mb-1">현재 문서에서 연결된 구체적인 텍스트</div>
|
||
<div class="text-sm text-blue-800 font-medium">"${backlink.target_text}"</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
${backlink.description ? `
|
||
<div class="mb-2 p-2 bg-white rounded border-l-3 border-green-400">
|
||
<div class="text-xs text-green-600 mb-1">링크 설명</div>
|
||
<div class="text-sm text-green-800">${backlink.description}</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div class="text-xs text-gray-500 flex items-center justify-between">
|
||
<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="M7.707 3.293a1 1 0 010 1.414L5.414 7H11a7 7 0 017 7v2a1 1 0 11-2 0v-2a5 5 0 00-5-5H5.414l2.293 2.293a1 1 0 11-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||
백링크
|
||
</span>
|
||
<span>${createdDate}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
tooltipHTML += `
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
tooltipHTML += `
|
||
<div class="flex justify-end pt-4 border-t border-gray-200">
|
||
<button onclick="window.documentViewerInstance.hideUnifiedTooltip()"
|
||
class="text-xs bg-gray-500 text-white px-3 py-1 rounded hover:bg-gray-600 transition-colors">
|
||
닫기
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
tooltip.innerHTML = tooltipHTML;
|
||
document.body.appendChild(tooltip);
|
||
|
||
// 위치 조정
|
||
this.positionTooltip(tooltip, element);
|
||
},
|
||
|
||
/**
|
||
* 통합 툴팁 숨기기
|
||
*/
|
||
hideUnifiedTooltip() {
|
||
const tooltip = document.getElementById('unified-tooltip');
|
||
if (tooltip) {
|
||
tooltip.remove();
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 툴팁 위치 조정 (화면 밖으로 나가지 않도록 개선)
|
||
*/
|
||
positionTooltip(tooltip, element) {
|
||
const rect = element.getBoundingClientRect();
|
||
const tooltipRect = tooltip.getBoundingClientRect();
|
||
const viewportWidth = window.innerWidth;
|
||
const viewportHeight = window.innerHeight;
|
||
const scrollX = window.scrollX;
|
||
const scrollY = window.scrollY;
|
||
|
||
console.log('🎯 툴팁 위치 계산:', {
|
||
elementRect: rect,
|
||
tooltipSize: { width: tooltipRect.width, height: tooltipRect.height },
|
||
viewport: { width: viewportWidth, height: viewportHeight }
|
||
});
|
||
|
||
// 기본 위치: 요소 아래 중앙
|
||
let left = rect.left + scrollX + (rect.width / 2) - (tooltipRect.width / 2);
|
||
let top = rect.bottom + scrollY + 10;
|
||
|
||
// 좌우 경계 체크 및 조정
|
||
const margin = 20;
|
||
if (left < margin) {
|
||
left = margin;
|
||
console.log('🔧 좌측 경계 조정:', left);
|
||
} else if (left + tooltipRect.width > viewportWidth - margin) {
|
||
left = viewportWidth - tooltipRect.width - margin;
|
||
console.log('🔧 우측 경계 조정:', left);
|
||
}
|
||
|
||
// 상하 경계 체크 및 조정
|
||
if (top + tooltipRect.height > viewportHeight - margin) {
|
||
// 요소 위쪽에 표시
|
||
top = rect.top + scrollY - tooltipRect.height - 10;
|
||
console.log('🔧 상단으로 이동:', top);
|
||
|
||
// 위쪽에도 공간이 부족하면 뷰포트 내에 강제로 맞춤
|
||
if (top < margin) {
|
||
top = margin;
|
||
console.log('🔧 상단 경계 조정:', top);
|
||
}
|
||
}
|
||
|
||
// 최종 위치 설정
|
||
tooltip.style.position = 'fixed';
|
||
tooltip.style.left = `${left - scrollX}px`;
|
||
tooltip.style.top = `${top - scrollY}px`;
|
||
|
||
console.log('✅ 최종 툴팁 위치:', {
|
||
left: left - scrollX,
|
||
top: top - scrollY
|
||
});
|
||
},
|
||
|
||
// ==================== 유틸리티 메서드 ====================
|
||
formatDate(dateString) {
|
||
return new Date(dateString).toLocaleString('ko-KR');
|
||
},
|
||
|
||
formatShortDate(dateString) {
|
||
return new Date(dateString).toLocaleDateString('ko-KR');
|
||
},
|
||
|
||
getSelectedBookTitle() {
|
||
const selectedBook = this.availableBooks.find(book => book.id === this.linkForm.target_book_id);
|
||
return selectedBook ? selectedBook.title : '서적을 선택하세요';
|
||
},
|
||
|
||
// ==================== 모듈 메서드 위임 ====================
|
||
|
||
// 하이라이트 관련
|
||
selectHighlight(highlightId) {
|
||
return this.highlightManager.selectHighlight(highlightId);
|
||
},
|
||
|
||
deleteHighlight(highlightId) {
|
||
return this.highlightManager.deleteHighlight(highlightId);
|
||
},
|
||
|
||
deleteHighlightsByColor(color, highlightIds) {
|
||
return this.highlightManager.deleteHighlightsByColor(color, highlightIds);
|
||
},
|
||
|
||
deleteAllOverlappingHighlights(highlightIds) {
|
||
return this.highlightManager.deleteAllOverlappingHighlights(highlightIds);
|
||
},
|
||
|
||
hideTooltip() {
|
||
return this.highlightManager.hideTooltip();
|
||
},
|
||
|
||
showAddNoteForm(highlightId) {
|
||
return this.highlightManager.showAddNoteForm(highlightId);
|
||
},
|
||
|
||
deleteNote(noteId) {
|
||
return this.highlightManager.deleteNote(noteId);
|
||
},
|
||
|
||
// 링크 관련
|
||
navigateToLinkedDocument(documentId, linkData) {
|
||
return this.linkManager.navigateToLinkedDocument(documentId, linkData);
|
||
},
|
||
|
||
navigateToBacklinkDocument(documentId, backlinkData) {
|
||
return this.linkManager.navigateToBacklinkDocument(documentId, backlinkData);
|
||
},
|
||
|
||
// HTML에서 사용되는 링크 이동 함수들
|
||
navigateToLink(link) {
|
||
console.log('🔗 링크 클릭:', link);
|
||
console.log('📋 링크 상세 정보:', {
|
||
target_document_id: link.target_document_id,
|
||
target_note_id: link.target_note_id,
|
||
target_content_type: link.target_content_type,
|
||
target_document_title: link.target_document_title,
|
||
target_note_title: link.target_note_title
|
||
});
|
||
|
||
// target_content_type이 없으면 ID로 추론
|
||
let targetContentType = link.target_content_type;
|
||
if (!targetContentType) {
|
||
if (link.target_note_id) {
|
||
targetContentType = 'note';
|
||
} else if (link.target_document_id) {
|
||
targetContentType = 'document';
|
||
}
|
||
console.log('🔍 target_content_type 추론됨:', targetContentType);
|
||
}
|
||
|
||
const targetId = link.target_document_id || link.target_note_id;
|
||
if (!targetId) {
|
||
console.error('❌ 대상 문서/노트 ID가 없습니다!', link);
|
||
alert('링크 대상을 찾을 수 없습니다.');
|
||
return;
|
||
}
|
||
|
||
// 링크 객체에 추론된 타입 추가
|
||
const linkWithType = {
|
||
...link,
|
||
target_content_type: targetContentType
|
||
};
|
||
|
||
console.log('🚀 최종 링크 데이터:', linkWithType);
|
||
return this.linkManager.navigateToLinkedDocument(targetId, linkWithType);
|
||
},
|
||
|
||
navigateToBacklink(backlink) {
|
||
console.log('🔙 백링크 클릭:', backlink);
|
||
console.log('📋 백링크 상세 정보:', {
|
||
source_document_id: backlink.source_document_id,
|
||
source_note_id: backlink.source_note_id,
|
||
source_content_type: backlink.source_content_type,
|
||
source_document_title: backlink.source_document_title
|
||
});
|
||
|
||
// 소스 ID 찾기 (노트 백링크의 경우 source_note_id 우선)
|
||
const sourceId = backlink.source_note_id || backlink.source_document_id;
|
||
if (!sourceId) {
|
||
console.error('❌ 소스 문서/노트 ID가 없습니다!', backlink);
|
||
alert('백링크 소스를 찾을 수 없습니다.');
|
||
return;
|
||
}
|
||
|
||
console.log('✅ 백링크 소스 ID 발견:', sourceId);
|
||
return this.linkManager.navigateToSourceDocument(sourceId, backlink);
|
||
},
|
||
|
||
// 링크 삭제 (확인 후)
|
||
async deleteLinkWithConfirm(linkId, targetTitle) {
|
||
console.log('🗑️ 링크 삭제 요청:', { linkId, targetTitle });
|
||
|
||
const confirmed = confirm(`"${targetTitle}"로의 링크를 삭제하시겠습니까?`);
|
||
if (!confirmed) {
|
||
console.log('❌ 링크 삭제 취소됨');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
console.log('🗑️ 링크 삭제 시작:', linkId);
|
||
|
||
// 출발지 타입에 따라 다른 API 사용
|
||
if (this.contentType === 'note') {
|
||
// 노트에서 출발하는 링크: NoteLink API 사용
|
||
console.log('📝 노트 링크 삭제 API 호출');
|
||
await this.api.delete(`/note-links/${linkId}`);
|
||
} else {
|
||
// 문서에서 출발하는 링크: DocumentLink API 사용
|
||
console.log('📄 문서 링크 삭제 API 호출');
|
||
await this.api.deleteDocumentLink(linkId);
|
||
}
|
||
console.log('✅ 링크 삭제 성공');
|
||
|
||
// 툴팁 숨기기
|
||
this.linkManager.hideTooltip();
|
||
|
||
// 캐시 무효화
|
||
console.log('🗑️ 링크 캐시 무효화 시작...');
|
||
if (window.cachedApi && window.cachedApi.invalidateRelatedCache) {
|
||
if (this.contentType === 'note') {
|
||
window.cachedApi.invalidateRelatedCache(`/note-documents/${this.documentId}/links`, ['links']);
|
||
} else {
|
||
window.cachedApi.invalidateRelatedCache(`/documents/${this.documentId}/links`, ['links']);
|
||
}
|
||
console.log('✅ 링크 캐시 무효화 완료');
|
||
}
|
||
|
||
// 링크 목록 새로고침
|
||
console.log('🔄 링크 목록 새로고침 시작...');
|
||
await this.linkManager.loadDocumentLinks(this.documentId, this.contentType);
|
||
this.documentLinks = this.linkManager.documentLinks || [];
|
||
console.log('📊 새로고침된 링크 개수:', this.documentLinks.length);
|
||
|
||
// 링크 렌더링
|
||
console.log('🎨 링크 렌더링 시작...');
|
||
this.linkManager.renderDocumentLinks();
|
||
console.log('✅ 링크 렌더링 완료');
|
||
|
||
// 백링크도 다시 로드 (삭제된 링크가 다른 문서의 백링크였을 수 있음)
|
||
console.log('🔄 백링크 새로고침 시작...');
|
||
if (window.cachedApi && window.cachedApi.invalidateRelatedCache) {
|
||
if (this.contentType === 'note') {
|
||
window.cachedApi.invalidateRelatedCache(`/note-documents/${this.documentId}/backlinks`, ['links']);
|
||
} else {
|
||
window.cachedApi.invalidateRelatedCache(`/documents/${this.documentId}/backlinks`, ['links']);
|
||
}
|
||
console.log('✅ 백링크 캐시도 무효화 완료');
|
||
}
|
||
await this.linkManager.loadBacklinks(this.documentId, this.contentType);
|
||
this.backlinks = this.linkManager.backlinks || [];
|
||
this.linkManager.renderBacklinks();
|
||
console.log('✅ 백링크 새로고침 완료');
|
||
|
||
// 성공 메시지
|
||
this.showSuccessMessage('링크가 삭제되었습니다.');
|
||
|
||
} catch (error) {
|
||
console.error('❌ 링크 삭제 실패:', error);
|
||
alert('링크 삭제에 실패했습니다: ' + error.message);
|
||
}
|
||
},
|
||
|
||
// 성공 메시지 표시
|
||
showSuccessMessage(message) {
|
||
const toast = document.createElement('div');
|
||
toast.className = 'fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg z-50 transition-opacity duration-300';
|
||
toast.textContent = message;
|
||
|
||
document.body.appendChild(toast);
|
||
|
||
setTimeout(() => {
|
||
toast.style.opacity = '0';
|
||
setTimeout(() => {
|
||
if (toast.parentNode) {
|
||
toast.parentNode.removeChild(toast);
|
||
}
|
||
}, 300);
|
||
}, 2000);
|
||
},
|
||
|
||
// 하이라이트 관련 추가 기능들
|
||
async changeHighlightColor(highlightId) {
|
||
console.log('🎨 하이라이트 색상 변경:', highlightId);
|
||
|
||
const colors = [
|
||
{ name: '노란색', value: '#FFFF00' },
|
||
{ name: '초록색', value: '#00FF00' },
|
||
{ name: '파란색', value: '#00BFFF' },
|
||
{ name: '분홍색', value: '#FFB6C1' },
|
||
{ name: '주황색', value: '#FFA500' },
|
||
{ name: '보라색', value: '#DDA0DD' }
|
||
];
|
||
|
||
const colorOptions = colors.map(c => `${c.name} (${c.value})`).join('\n');
|
||
const selectedColor = prompt(`새로운 색상을 선택하세요:\n\n${colorOptions}\n\n색상 코드를 입력하세요 (예: #FFFF00):`);
|
||
|
||
if (selectedColor && selectedColor.match(/^#[0-9A-Fa-f]{6}$/)) {
|
||
try {
|
||
await this.highlightManager.updateHighlightColor(highlightId, selectedColor);
|
||
this.showSuccessMessage('하이라이트 색상이 변경되었습니다.');
|
||
} catch (error) {
|
||
console.error('❌ 색상 변경 실패:', error);
|
||
alert('색상 변경에 실패했습니다: ' + error.message);
|
||
}
|
||
} else if (selectedColor !== null) {
|
||
alert('올바른 색상 코드를 입력해주세요 (예: #FFFF00)');
|
||
}
|
||
},
|
||
|
||
async duplicateHighlight(highlightId) {
|
||
console.log('📋 하이라이트 복사:', highlightId);
|
||
|
||
try {
|
||
await this.highlightManager.duplicateHighlight(highlightId);
|
||
this.showSuccessMessage('하이라이트가 복사되었습니다.');
|
||
} catch (error) {
|
||
console.error('❌ 하이라이트 복사 실패:', error);
|
||
alert('하이라이트 복사에 실패했습니다: ' + error.message);
|
||
}
|
||
},
|
||
|
||
async deleteHighlightWithConfirm(highlightId) {
|
||
console.log('🗑️ 하이라이트 삭제 확인:', highlightId);
|
||
|
||
const confirmed = confirm('이 하이라이트를 삭제하시겠습니까?\n\n⚠️ 주의: 연결된 모든 메모도 함께 삭제됩니다.');
|
||
if (!confirmed) {
|
||
console.log('❌ 하이라이트 삭제 취소됨');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await this.highlightManager.deleteHighlight(highlightId);
|
||
this.highlightManager.hideTooltip();
|
||
this.showSuccessMessage('하이라이트가 삭제되었습니다.');
|
||
} catch (error) {
|
||
console.error('❌ 하이라이트 삭제 실패:', error);
|
||
alert('하이라이트 삭제에 실패했습니다: ' + error.message);
|
||
}
|
||
},
|
||
|
||
// ==================== PDF 뷰어 관련 ====================
|
||
async loadPdfViewer() {
|
||
console.log('📄 PDF 뷰어 로드 시작');
|
||
this.pdfLoading = true;
|
||
this.pdfError = false;
|
||
this.pdfLoaded = false;
|
||
|
||
try {
|
||
const token = localStorage.getItem('access_token');
|
||
if (!token || token === 'null' || token === null) {
|
||
throw new Error('인증 토큰이 없습니다. 다시 로그인해주세요.');
|
||
}
|
||
|
||
// PDF 뷰어 URL 설정 (토큰 포함)
|
||
this.pdfSrc = `/api/documents/${this.documentId}/pdf?_token=${encodeURIComponent(token)}`;
|
||
console.log('✅ PDF 뷰어 준비 완료:', this.pdfSrc);
|
||
|
||
// PDF.js로 PDF 로드
|
||
await this.loadPdfWithPdfJs();
|
||
|
||
} catch (error) {
|
||
console.error('❌ PDF 뷰어 로드 실패:', error);
|
||
this.pdfError = true;
|
||
} finally {
|
||
this.pdfLoading = false;
|
||
}
|
||
},
|
||
|
||
async loadPdfWithPdfJs() {
|
||
try {
|
||
// PDF.js 워커 설정
|
||
if (typeof pdfjsLib !== 'undefined') {
|
||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
||
|
||
console.log('📄 PDF.js로 PDF 로드 시작:', this.pdfSrc);
|
||
|
||
// PDF 문서 로드
|
||
const loadingTask = pdfjsLib.getDocument(this.pdfSrc);
|
||
this.pdfDocument = await loadingTask.promise;
|
||
|
||
this.totalPages = this.pdfDocument.numPages;
|
||
this.currentPage = 1;
|
||
|
||
console.log(`✅ PDF 로드 완료: ${this.totalPages} 페이지`);
|
||
|
||
// 캔버스 초기화
|
||
this.initPdfCanvas();
|
||
|
||
// 첫 페이지 렌더링
|
||
await this.renderPdfPage(1);
|
||
|
||
this.pdfLoaded = true;
|
||
} else {
|
||
throw new Error('PDF.js 라이브러리가 로드되지 않았습니다.');
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ PDF.js 로드 실패:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
initPdfCanvas() {
|
||
this.pdfCanvas = document.getElementById('pdf-canvas');
|
||
if (this.pdfCanvas) {
|
||
this.pdfContext = this.pdfCanvas.getContext('2d');
|
||
}
|
||
},
|
||
|
||
async renderPdfPage(pageNum) {
|
||
if (!this.pdfDocument || !this.pdfCanvas) return;
|
||
|
||
try {
|
||
console.log(`📄 페이지 ${pageNum} 렌더링 시작`);
|
||
|
||
const page = await this.pdfDocument.getPage(pageNum);
|
||
const viewport = page.getViewport({ scale: this.pdfScale });
|
||
|
||
// 캔버스 크기 설정
|
||
this.pdfCanvas.height = viewport.height;
|
||
this.pdfCanvas.width = viewport.width;
|
||
|
||
// 페이지 렌더링
|
||
const renderContext = {
|
||
canvasContext: this.pdfContext,
|
||
viewport: viewport
|
||
};
|
||
|
||
await page.render(renderContext).promise;
|
||
|
||
// 텍스트 내용 추출 (검색용)
|
||
const textContent = await page.getTextContent();
|
||
this.pdfTextContent[pageNum] = textContent.items.map(item => item.str).join(' ');
|
||
|
||
console.log(`✅ 페이지 ${pageNum} 렌더링 완료`);
|
||
|
||
} catch (error) {
|
||
console.error(`❌ 페이지 ${pageNum} 렌더링 실패:`, error);
|
||
}
|
||
},
|
||
|
||
handlePdfError() {
|
||
console.error('❌ PDF iframe 로드 오류');
|
||
this.pdfError = true;
|
||
this.pdfLoading = false;
|
||
},
|
||
|
||
async retryPdfLoad() {
|
||
console.log('🔄 PDF 재로드 시도');
|
||
await this.loadPdfViewer();
|
||
},
|
||
|
||
// ==================== PDF 검색 관련 ====================
|
||
openPdfSearchModal() {
|
||
this.showPdfSearchModal = true;
|
||
this.pdfSearchQuery = '';
|
||
this.pdfSearchResults = [];
|
||
|
||
// 모달이 열린 후 입력 필드에 포커스
|
||
setTimeout(() => {
|
||
const searchInput = document.querySelector('input[x-ref="searchInput"]');
|
||
if (searchInput) {
|
||
searchInput.focus();
|
||
searchInput.select();
|
||
}
|
||
}, 100);
|
||
},
|
||
|
||
async searchInPdf() {
|
||
if (!this.pdfSearchQuery.trim()) {
|
||
alert('검색어를 입력해주세요.');
|
||
return;
|
||
}
|
||
|
||
console.log('🔍 PDF 검색 시작:', this.pdfSearchQuery);
|
||
this.pdfSearchLoading = true;
|
||
this.pdfSearchResults = [];
|
||
|
||
try {
|
||
// 백엔드 API를 통해 PDF 내용 검색
|
||
const searchResults = await this.api.get(
|
||
`/documents/${this.documentId}/search-in-content?q=${encodeURIComponent(this.pdfSearchQuery)}`
|
||
);
|
||
|
||
console.log('✅ PDF 검색 결과:', searchResults);
|
||
|
||
if (searchResults.matches && searchResults.matches.length > 0) {
|
||
this.pdfSearchResults = searchResults.matches.map(match => ({
|
||
page: match.page || 1,
|
||
context: match.context || match.text || this.pdfSearchQuery,
|
||
position: match.position || 0
|
||
}));
|
||
|
||
console.log(`📄 ${this.pdfSearchResults.length}개의 검색 결과 발견`);
|
||
|
||
if (this.pdfSearchResults.length === 0) {
|
||
alert('검색 결과를 찾을 수 없습니다.');
|
||
}
|
||
} else {
|
||
alert('검색 결과를 찾을 수 없습니다.');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ PDF 검색 실패:', error);
|
||
alert('PDF 검색 중 오류가 발생했습니다: ' + error.message);
|
||
} finally {
|
||
this.pdfSearchLoading = false;
|
||
}
|
||
},
|
||
|
||
jumpToPdfResult(result) {
|
||
console.log('📍 PDF 결과로 이동:', result);
|
||
|
||
// PDF URL에 페이지 번호 추가하여 해당 페이지로 이동
|
||
const token = localStorage.getItem('access_token');
|
||
let newPdfSrc = `/api/documents/${this.documentId}/pdf?_token=${encodeURIComponent(token)}`;
|
||
|
||
// 페이지 번호가 있으면 URL 프래그먼트로 추가
|
||
if (result.page && result.page > 1) {
|
||
newPdfSrc += `#page=${result.page}`;
|
||
}
|
||
|
||
// PDF src 업데이트하여 해당 페이지로 이동
|
||
this.pdfSrc = newPdfSrc;
|
||
|
||
console.log(`📄 페이지 ${result.page}로 이동:`, newPdfSrc);
|
||
|
||
// 잠시 후 검색 기능 활성화
|
||
setTimeout(() => {
|
||
const iframe = document.querySelector('#pdf-viewer-iframe');
|
||
if (iframe && iframe.contentWindow) {
|
||
try {
|
||
iframe.contentWindow.focus();
|
||
|
||
// 브라우저 내장 검색 기능 활용
|
||
if (iframe.contentWindow.find) {
|
||
iframe.contentWindow.find(this.pdfSearchQuery);
|
||
} else {
|
||
// 대안: 사용자에게 수동 검색 안내
|
||
this.showSuccessMessage(`페이지 ${result.page}로 이동했습니다. Ctrl+F를 눌러 "${this.pdfSearchQuery}"를 검색하세요.`);
|
||
}
|
||
} catch (e) {
|
||
console.warn('PDF iframe 접근 제한:', e);
|
||
this.showSuccessMessage(`페이지 ${result.page}로 이동했습니다. Ctrl+F를 눌러 "${this.pdfSearchQuery}"를 검색하세요.`);
|
||
}
|
||
}
|
||
}, 1000);
|
||
|
||
// 모달 닫기
|
||
this.showPdfSearchModal = false;
|
||
},
|
||
|
||
async editNote(noteId, currentContent) {
|
||
console.log('✏️ 메모 편집:', noteId);
|
||
console.log('🔍 HighlightManager 상태:', this.highlightManager);
|
||
console.log('🔍 updateNote 함수 존재:', typeof this.highlightManager?.updateNote);
|
||
|
||
if (!this.highlightManager) {
|
||
console.error('❌ HighlightManager가 초기화되지 않음');
|
||
alert('하이라이트 매니저가 초기화되지 않았습니다.');
|
||
return;
|
||
}
|
||
|
||
if (typeof this.highlightManager.updateNote !== 'function') {
|
||
console.error('❌ updateNote 함수가 존재하지 않음');
|
||
alert('메모 업데이트 함수가 존재하지 않습니다.');
|
||
return;
|
||
}
|
||
|
||
const newContent = prompt('메모 내용을 수정하세요:', currentContent);
|
||
if (newContent !== null && newContent.trim() !== currentContent) {
|
||
try {
|
||
await this.highlightManager.updateNote(noteId, newContent.trim());
|
||
this.showSuccessMessage('메모가 수정되었습니다.');
|
||
} catch (error) {
|
||
console.error('❌ 메모 수정 실패:', error);
|
||
alert('메모 수정에 실패했습니다: ' + error.message);
|
||
}
|
||
}
|
||
},
|
||
|
||
// 백링크 삭제 (확인 후)
|
||
async deleteBacklinkWithConfirm(backlinkId, sourceTitle) {
|
||
console.log('🗑️ 백링크 삭제 요청:', { backlinkId, sourceTitle });
|
||
|
||
const confirmed = confirm(`"${sourceTitle}"에서 오는 백링크를 삭제하시겠습니까?\n\n⚠️ 주의: 이는 원본 문서의 링크를 삭제합니다.`);
|
||
if (!confirmed) {
|
||
console.log('❌ 백링크 삭제 취소됨');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
console.log('🗑️ 백링크 삭제 시작:', backlinkId);
|
||
|
||
// 백링크 삭제는 실제로는 원본 링크를 삭제하는 것
|
||
await this.api.deleteDocumentLink(backlinkId);
|
||
console.log('✅ 백링크 삭제 성공');
|
||
|
||
// 툴팁 숨기기
|
||
this.linkManager.hideTooltip();
|
||
|
||
// 캐시 무효화 (현재 문서의 백링크 캐시)
|
||
console.log('🗑️ 백링크 캐시 무효화 시작...');
|
||
if (window.cachedApi && window.cachedApi.invalidateRelatedCache) {
|
||
if (this.contentType === 'note') {
|
||
window.cachedApi.invalidateRelatedCache(`/note-documents/${this.documentId}/backlinks`, ['links']);
|
||
} else {
|
||
window.cachedApi.invalidateRelatedCache(`/documents/${this.documentId}/backlinks`, ['links']);
|
||
}
|
||
console.log('✅ 백링크 캐시 무효화 완료');
|
||
}
|
||
|
||
// 백링크 목록 새로고침
|
||
console.log('🔄 백링크 목록 새로고침 시작...');
|
||
await this.linkManager.loadBacklinks(this.documentId, this.contentType);
|
||
this.backlinks = this.linkManager.backlinks || [];
|
||
console.log('📊 새로고침된 백링크 개수:', this.backlinks.length);
|
||
|
||
// 백링크 렌더링
|
||
console.log('🎨 백링크 렌더링 시작...');
|
||
this.linkManager.renderBacklinks();
|
||
console.log('✅ 백링크 렌더링 완료');
|
||
|
||
// 성공 메시지
|
||
this.showSuccessMessage('백링크가 삭제되었습니다.');
|
||
|
||
} catch (error) {
|
||
console.error('❌ 백링크 삭제 실패:', error);
|
||
alert('백링크 삭제에 실패했습니다: ' + error.message);
|
||
}
|
||
},
|
||
|
||
// 북마크 관련
|
||
scrollToBookmark(bookmark) {
|
||
return this.bookmarkManager.scrollToBookmark(bookmark);
|
||
},
|
||
|
||
deleteBookmark(bookmarkId) {
|
||
return this.bookmarkManager.deleteBookmark(bookmarkId);
|
||
},
|
||
|
||
// ==================== 링크 생성 ====================
|
||
async createDocumentLink() {
|
||
console.log('🔗 createDocumentLink 함수 실행');
|
||
console.log('📋 현재 linkForm 상태:', JSON.stringify(this.linkForm, null, 2));
|
||
|
||
try {
|
||
// 링크 데이터 검증
|
||
if (!this.linkForm.target_document_id) {
|
||
alert('대상 문서를 선택해주세요.');
|
||
return;
|
||
}
|
||
|
||
if (this.linkForm.link_type === 'text' && !this.linkForm.target_text) {
|
||
alert('대상 문서에서 텍스트를 선택해주세요. "대상 문서에서 텍스트 선택" 버튼을 클릭하여 연결할 텍스트를 드래그해주세요.');
|
||
return;
|
||
}
|
||
|
||
// API 호출용 데이터 준비 (백엔드 필드명에 맞춤)
|
||
const linkData = {
|
||
target_document_id: this.linkForm.target_document_id,
|
||
selected_text: this.linkForm.selected_text, // 백엔드: selected_text
|
||
start_offset: this.linkForm.start_offset, // 백엔드: start_offset
|
||
end_offset: this.linkForm.end_offset, // 백엔드: end_offset
|
||
link_text: this.linkForm.link_text || this.linkForm.selected_text,
|
||
description: this.linkForm.description,
|
||
link_type: this.linkForm.link_type,
|
||
target_text: this.linkForm.target_text || null,
|
||
target_start_offset: this.linkForm.target_start_offset || null,
|
||
target_end_offset: this.linkForm.target_end_offset || null
|
||
};
|
||
|
||
console.log('📤 링크 생성 데이터:', linkData);
|
||
console.log('📤 링크 생성 데이터 (JSON):', JSON.stringify(linkData, null, 2));
|
||
|
||
// 필수 필드 검증
|
||
const requiredFields = ['target_document_id', 'selected_text', 'start_offset', 'end_offset'];
|
||
const missingFields = requiredFields.filter(field =>
|
||
linkData[field] === undefined || linkData[field] === null || linkData[field] === ''
|
||
);
|
||
|
||
if (missingFields.length > 0) {
|
||
console.error('❌ 필수 필드 누락:', missingFields);
|
||
alert('필수 필드가 누락되었습니다: ' + missingFields.join(', '));
|
||
return;
|
||
}
|
||
|
||
console.log('✅ 모든 필수 필드 확인됨');
|
||
|
||
// API 호출 (출발지와 대상에 따라 다른 API 사용)
|
||
if (this.contentType === 'note') {
|
||
// 노트에서 출발하는 링크
|
||
if (this.linkForm.target_type === 'note') {
|
||
// 노트 → 노트: 노트 링크 API 사용
|
||
linkData.target_note_id = linkData.target_document_id;
|
||
delete linkData.target_document_id;
|
||
await this.api.post(`/note-documents/${this.documentId}/links`, linkData);
|
||
} else {
|
||
// 노트 → 문서: 노트 링크 API 사용 (target_document_id 유지)
|
||
await this.api.post(`/note-documents/${this.documentId}/links`, linkData);
|
||
}
|
||
} else {
|
||
// 문서에서 출발하는 링크
|
||
if (this.linkForm.target_type === 'note') {
|
||
// 문서 → 노트: 문서 링크 API에 노트 대상 지원 필요 (향후 구현)
|
||
// 현재는 기존 API 사용
|
||
await this.api.createDocumentLink(this.documentId, linkData);
|
||
} else {
|
||
// 문서 → 문서: 기존 문서 링크 API 사용
|
||
await this.api.createDocumentLink(this.documentId, linkData);
|
||
}
|
||
}
|
||
console.log('✅ 링크 생성됨');
|
||
|
||
// 성공 알림
|
||
alert('링크가 성공적으로 생성되었습니다!');
|
||
|
||
// 모달 닫기
|
||
this.showLinkModal = false;
|
||
|
||
// 캐시 무효화 (새 링크가 반영되도록)
|
||
console.log('🗑️ 링크 캐시 무효화 시작...');
|
||
if (window.cachedApi && window.cachedApi.invalidateRelatedCache) {
|
||
if (this.contentType === 'note') {
|
||
window.cachedApi.invalidateRelatedCache(`/note-documents/${this.documentId}/links`, ['links']);
|
||
} else {
|
||
window.cachedApi.invalidateRelatedCache(`/documents/${this.documentId}/links`, ['links']);
|
||
}
|
||
console.log('✅ 링크 캐시 무효화 완료');
|
||
}
|
||
|
||
// 링크 목록 새로고침
|
||
console.log('🔄 링크 목록 새로고침 시작...');
|
||
await this.linkManager.loadDocumentLinks(this.documentId, this.contentType);
|
||
this.documentLinks = this.linkManager.documentLinks || [];
|
||
console.log('📊 로드된 링크 개수:', this.documentLinks.length);
|
||
console.log('📊 링크 데이터:', this.documentLinks);
|
||
|
||
// 링크 렌더링
|
||
console.log('🎨 링크 렌더링 시작...');
|
||
this.linkManager.renderDocumentLinks();
|
||
console.log('✅ 링크 렌더링 완료');
|
||
|
||
// 백링크도 다시 로드하고 렌더링 (새 링크가 다른 문서의 백링크가 될 수 있음)
|
||
console.log('🔄 백링크 새로고침 시작...');
|
||
// 백링크 캐시도 무효화
|
||
if (window.cachedApi && window.cachedApi.invalidateRelatedCache) {
|
||
if (this.contentType === 'note') {
|
||
window.cachedApi.invalidateRelatedCache(`/note-documents/${this.documentId}/backlinks`, ['links']);
|
||
} else {
|
||
window.cachedApi.invalidateRelatedCache(`/documents/${this.documentId}/backlinks`, ['links']);
|
||
}
|
||
console.log('✅ 백링크 캐시도 무효화 완료');
|
||
}
|
||
await this.linkManager.loadBacklinks(this.documentId, this.contentType);
|
||
this.backlinks = this.linkManager.backlinks || [];
|
||
this.linkManager.renderBacklinks();
|
||
console.log('✅ 백링크 새로고침 완료');
|
||
|
||
} catch (error) {
|
||
console.error('링크 생성 실패:', error);
|
||
console.error('에러 상세:', {
|
||
message: error.message,
|
||
stack: error.stack,
|
||
response: error.response
|
||
});
|
||
|
||
// 422 에러인 경우 상세 정보 표시
|
||
if (error.response && error.response.status === 422) {
|
||
console.error('422 Validation Error Details:', error.response.data);
|
||
alert('데이터 검증 실패: ' + JSON.stringify(error.response.data, null, 2));
|
||
} else {
|
||
alert('링크 생성에 실패했습니다: ' + error.message);
|
||
}
|
||
}
|
||
},
|
||
|
||
// 네비게이션 함수들
|
||
goBack() {
|
||
console.log('🔙 뒤로가기');
|
||
window.history.back();
|
||
},
|
||
|
||
navigateToDocument(documentId) {
|
||
if (!documentId) {
|
||
console.warn('⚠️ 문서 ID가 없습니다');
|
||
return;
|
||
}
|
||
console.log('📄 문서로 이동:', documentId);
|
||
window.location.href = `/viewer.html?id=${documentId}`;
|
||
},
|
||
|
||
goToBookContents() {
|
||
if (!this.navigation?.book_info?.id) {
|
||
console.warn('⚠️ 서적 정보가 없습니다');
|
||
return;
|
||
}
|
||
console.log('📚 서적 목차로 이동:', this.navigation.book_info.id);
|
||
window.location.href = `/book-documents.html?book_id=${this.navigation.book_info.id}`;
|
||
}
|
||
});
|
||
|
||
// Alpine.js 컴포넌트 등록
|
||
document.addEventListener('alpine:init', () => {
|
||
console.log('🔧 Alpine.js 컴포넌트 로드됨');
|
||
|
||
// 전역 함수들 (말풍선에서 사용)
|
||
window.cancelTextSelection = () => {
|
||
if (window.documentViewerInstance && window.documentViewerInstance.linkManager) {
|
||
window.documentViewerInstance.linkManager.cancelTextSelection();
|
||
}
|
||
};
|
||
|
||
window.confirmTextSelection = (selectedText, startOffset, endOffset) => {
|
||
if (window.documentViewerInstance && window.documentViewerInstance.linkManager) {
|
||
window.documentViewerInstance.linkManager.confirmTextSelection(selectedText, startOffset, endOffset);
|
||
}
|
||
};
|
||
});
|
||
|
||
// Alpine.js Store 등록
|
||
document.addEventListener('alpine:init', () => {
|
||
Alpine.store('documentViewer', {
|
||
instance: null,
|
||
|
||
init() {
|
||
// DocumentViewer 인스턴스가 생성되면 저장
|
||
setTimeout(() => {
|
||
this.instance = window.documentViewerInstance;
|
||
}, 500);
|
||
},
|
||
|
||
downloadOriginalFile() {
|
||
console.log('🏪 Store downloadOriginalFile 호출');
|
||
if (this.instance) {
|
||
return this.instance.downloadOriginalFile();
|
||
} else {
|
||
console.warn('DocumentViewer 인스턴스가 없습니다');
|
||
}
|
||
},
|
||
|
||
toggleLanguage() {
|
||
console.log('🏪 Store toggleLanguage 호출');
|
||
if (this.instance) {
|
||
return this.instance.toggleLanguage();
|
||
} else {
|
||
console.warn('DocumentViewer 인스턴스가 없습니다');
|
||
}
|
||
},
|
||
|
||
loadBacklinks() {
|
||
console.log('🏪 Store loadBacklinks 호출');
|
||
if (this.instance) {
|
||
return this.instance.loadBacklinks();
|
||
} else {
|
||
console.warn('DocumentViewer 인스턴스가 없습니다');
|
||
}
|
||
}
|
||
});
|
||
});
|