Files
document-server/frontend/static/js/viewer/viewer-core.js
Hyungi Ahn 43e7466195 스토리 뷰 페이지 헤더 z-index 충돌 문제 해결
- 메인 컨테이너 padding-top을 pt-4에서 pt-20으로 증가
- 드롭다운 z-index를 z-[60]으로 설정하여 헤더보다 높은 우선순위 부여
- 스토리 선택 드롭다운이 정상적으로 작동하도록 수정
- 디버깅용 코드 정리
2025-09-04 10:22:43 +09:00

2373 lines
100 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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, '&quot;')}], 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, '&quot;')})">
<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, '&quot;')})">
<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 인스턴스가 없습니다');
}
}
});
});