diff --git a/backend/src/core/config.py b/backend/src/core/config.py
index 275bfed..2c07451 100644
--- a/backend/src/core/config.py
+++ b/backend/src/core/config.py
@@ -28,6 +28,7 @@ class Settings(BaseSettings):
# CORS 설정
ALLOWED_HOSTS: List[str] = ["http://localhost:24100", "http://127.0.0.1:24100"]
+ ALLOWED_ORIGINS: List[str] = ["http://localhost:24100", "http://127.0.0.1:24100"]
# 파일 업로드 설정
UPLOAD_DIR: str = "uploads"
diff --git a/backend/src/main.py b/backend/src/main.py
index 1fa1439..9d32862 100644
--- a/backend/src/main.py
+++ b/backend/src/main.py
@@ -30,12 +30,12 @@ app = FastAPI(
lifespan=lifespan,
)
-# CORS 설정 (개발용 - 더 관대한 설정)
+# CORS 설정
app.add_middleware(
CORSMiddleware,
- allow_origins=["*"], # 개발용으로 모든 오리진 허용
+ allow_origins=settings.ALLOWED_ORIGINS if hasattr(settings, 'ALLOWED_ORIGINS') else ["*"],
allow_credentials=True,
- allow_methods=["*"],
+ allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"],
)
diff --git a/frontend/static/js/todos.js b/frontend/static/js/todos.js
index a541bb5..e0e5885 100644
--- a/frontend/static/js/todos.js
+++ b/frontend/static/js/todos.js
@@ -233,11 +233,7 @@ function todosApp() {
// 할일 일정 설정
async scheduleTodo() {
- console.log('🔧 scheduleTodo 호출됨:', this.currentTodo, this.scheduleForm);
- if (!this.currentTodo || !this.scheduleForm.start_date) {
- console.log('❌ 필수 데이터 누락:', { currentTodo: this.currentTodo, start_date: this.scheduleForm.start_date });
- return;
- }
+ if (!this.currentTodo || !this.scheduleForm.start_date) return;
try {
// 선택한 날짜의 총 시간 체크
@@ -273,17 +269,14 @@ function todosApp() {
let response;
// 이미 일정이 설정된 할일인지 확인
- console.log('📋 할일 상태 확인:', this.currentTodo.status);
if (this.currentTodo.status === 'draft') {
// 새로 일정 설정
- console.log('📅 새로 일정 설정 API 호출');
response = await window.api.post(`/todos/${this.currentTodo.id}/schedule`, {
start_date: startDate.toISOString(),
estimated_minutes: newMinutes
});
} else {
// 기존 일정 지연 (active 상태의 할일)
- console.log('🔄 기존 일정 지연 API 호출');
response = await window.api.put(`/todos/${this.currentTodo.id}/delay`, {
delayed_until: startDate.toISOString()
});
diff --git a/frontend/static/js/viewer-test.js b/frontend/static/js/viewer-test.js
deleted file mode 100644
index a00eb61..0000000
--- a/frontend/static/js/viewer-test.js
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * 간단한 테스트용 documentViewer
- */
-window.documentViewer = () => ({
- // 기본 상태
- loading: false,
- error: null,
-
- // 네비게이션
- navigation: null,
-
- // 검색
- searchQuery: '',
-
- // 데이터
- notes: [],
- bookmarks: [],
- documentLinks: [],
- backlinks: [],
-
- // UI 상태
- activeFeatureMenu: null,
- selectedHighlightColor: '#FFFF00',
-
- // 모달 상태
- showLinksModal: false,
- showLinkModal: false,
- showNotesModal: false,
- showBookmarksModal: false,
- showBacklinksModal: false,
-
- // 폼 데이터
- linkForm: {
- target_document_id: '',
- selected_text: '',
- book_scope: 'same',
- target_book_id: '',
- link_type: 'document',
- target_text: '',
- description: ''
- },
-
- // 기타 데이터
- availableBooks: [],
- filteredDocuments: [],
-
- // 초기화
- init() {
- console.log('🔧 간단한 documentViewer 로드됨');
- this.documentId = new URLSearchParams(window.location.search).get('id');
- console.log('📋 문서 ID:', this.documentId);
- },
-
- // 뒤로가기
- goBack() {
- console.log('🔙 뒤로가기 클릭됨');
- const urlParams = new URLSearchParams(window.location.search);
- const fromPage = urlParams.get('from');
-
- if (fromPage === 'index') {
- window.location.href = '/index.html';
- } else if (fromPage === 'hierarchy') {
- window.location.href = '/hierarchy.html';
- } else {
- window.location.href = '/index.html';
- }
- },
-
- // 기본 함수들
- toggleFeatureMenu(feature) {
- console.log('🎯 기능 메뉴 토글:', feature);
- this.activeFeatureMenu = this.activeFeatureMenu === feature ? null : feature;
- },
-
- searchInDocument() {
- console.log('🔍 문서 검색:', this.searchQuery);
- },
-
- // 빈 함수들 (오류 방지용)
- navigateToDocument() { console.log('네비게이션 함수 호출됨'); },
- goToBookContents() { console.log('목차로 이동 함수 호출됨'); },
- createHighlightWithColor() { console.log('하이라이트 생성 함수 호출됨'); },
- resetTargetSelection() { console.log('타겟 선택 리셋 함수 호출됨'); },
- loadDocumentsFromBook() { console.log('서적 문서 로드 함수 호출됨'); },
- onTargetDocumentChange() { console.log('타겟 문서 변경 함수 호출됨'); },
- openTargetDocumentSelector() { console.log('타겟 문서 선택기 열기 함수 호출됨'); },
- saveDocumentLink() { console.log('문서 링크 저장 함수 호출됨'); },
- closeLinkModal() { console.log('링크 모달 닫기 함수 호출됨'); },
- getSelectedBookTitle() { return '테스트 서적'; }
-});
-
-console.log('✅ 테스트용 documentViewer 정의됨');
diff --git a/frontend/static/js/viewer.js.backup b/frontend/static/js/viewer.js.backup
deleted file mode 100644
index c2e9f43..0000000
--- a/frontend/static/js/viewer.js.backup
+++ /dev/null
@@ -1,3656 +0,0 @@
-/**
- * 문서 뷰어 Alpine.js 컴포넌트
- */
-window.documentViewer = () => ({
- // 상태
- loading: true,
- error: null,
- document: null,
- documentId: null,
- contentType: 'document', // 'document' 또는 'note'
- navigation: null, // 네비게이션 정보
-
- // 하이라이트 및 메모
- highlights: [],
- notes: [],
- selectedHighlightColor: '#FFFF00',
- selectedText: '',
- selectedRange: null,
-
- // 책갈피
- bookmarks: [],
-
- // 문서 링크
- documentLinks: [],
- linkableDocuments: [],
- backlinks: [],
-
- // 텍스트 선택 모드 플래그
- textSelectorUISetup: false,
-
- // UI 상태
- showNotesPanel: false,
- showBookmarksPanel: false,
- showBacklinks: false,
- activePanel: 'notes',
-
- // 검색
- searchQuery: '',
- noteSearchQuery: '',
- filteredNotes: [],
-
- // 언어 전환
- isKorean: false,
-
- // 모달
- showNoteModal: false,
- showBookmarkModal: false,
- showLinkModal: false,
- showNotesModal: false,
- showBookmarksModal: false,
- showLinksModal: false,
- showBacklinksModal: false,
-
- // 기능 메뉴 상태
- activeFeatureMenu: null,
-
- // 링크 관련 데이터
- availableBooks: [], // 사용 가능한 서적 목록
- filteredDocuments: [], // 필터링된 문서 목록
-
- // 모드 및 핸들러
- activeMode: null, // 'link', 'memo', 'bookmark' 등
- textSelectionHandler: null,
- editingNote: null,
- editingBookmark: null,
- editingLink: null,
- noteLoading: false,
- bookmarkLoading: false,
- linkLoading: false,
-
- // 폼 데이터
- noteForm: {
- content: '',
- tags: ''
- },
- bookmarkForm: {
- title: '',
- description: ''
- },
- linkForm: {
- 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,
- // 서적 범위 선택
- book_scope: 'same', // 'same' 또는 'other'
- target_book_id: ''
- },
-
- // 초기화
- async init() {
- // 중복 초기화 방지
- if (this._initialized) {
- console.log('⚠️ 이미 초기화됨, 중복 실행 방지');
- return;
- }
- this._initialized = true;
-
- console.log('🚀 DocumentViewer 초기화 시작');
-
- // 전역 인스턴스 설정 (말풍선에서 함수 호출용)
- window.documentViewerInstance = this;
-
- // 모듈 초기화
- this.documentLoader = new DocumentLoader(api);
- this.highlightManager = new HighlightManager(api);
- this.bookmarkManager = new BookmarkManager(api);
- this.linkManager = new LinkManager(api);
-
- // URL에서 문서 ID 추출
- const urlParams = new URLSearchParams(window.location.search);
- this.documentId = urlParams.get('id');
- this.contentType = urlParams.get('type') || 'document'; // 'document' 또는 'note'
- const mode = urlParams.get('mode');
- const isParentWindow = urlParams.get('parent_window') === 'true';
-
- console.log('🔍 URL 파싱 결과:', {
- documentId: this.documentId,
- mode: mode,
- parent_window: urlParams.get('parent_window'),
- isParentWindow: isParentWindow,
- fullUrl: window.location.href
- });
-
- // 함수들이 제대로 바인딩되었는지 확인
- console.log('🔧 Alpine.js 컴포넌트 로드됨');
- console.log('🔗 activateLinkMode 함수:', typeof this.activateLinkMode);
- console.log('📝 activateNoteMode 함수:', typeof this.activateNoteMode);
- console.log('🔖 activateBookmarkMode 함수:', typeof this.activateBookmarkMode);
- console.log('🎯 toggleFeatureMenu 함수:', typeof this.toggleFeatureMenu);
-
- if (!this.documentId) {
- this.error = '문서 ID가 없습니다';
- this.loading = false;
- return;
- }
-
- // 텍스트 선택 모드인 경우 특별 처리
- console.log('🔍 URL 파라미터 확인:', { mode, isParentWindow, documentId: this.documentId });
- if (mode === 'text_selector') {
- console.log('🎯 텍스트 선택 모드로 진입');
- await this.initTextSelectorMode();
- return;
- }
-
- // 인증 확인
- if (!api.token) {
- window.location.href = '/';
- return;
- }
-
- try {
- if (this.contentType === 'note') {
- this.document = await this.documentLoader.loadNote(this.documentId);
- } else {
- this.document = await this.documentLoader.loadDocument(this.documentId);
- this.navigation = await this.documentLoader.loadNavigation(this.documentId);
- }
- await this.loadDocumentData();
-
- // URL 파라미터 확인해서 특정 텍스트로 스크롤
- this.documentLoader.checkForTextHighlight();
-
- } catch (error) {
- console.error('Failed to load document:', error);
- this.error = error.message;
- } finally {
- this.loading = false;
- }
-
- // 초기 필터링
- this.filterNotes();
- },
-
-
-
-
-
-
-
- // 문서 관련 데이터 로드
- async loadDocumentData() {
- try {
- console.log('Loading document data for:', this.documentId, 'type:', this.contentType);
-
- if (this.contentType === 'note') {
- // 노트의 경우: HighlightManager 사용
- console.log('📝 노트 데이터 로드 중...');
- const [highlights, notes] = await Promise.all([
- this.highlightManager.loadHighlights(this.documentId, this.contentType),
- this.highlightManager.loadNotes(this.documentId, this.contentType)
- ]);
-
- this.highlights = highlights;
- this.notes = notes;
- this.bookmarks = []; // 노트에서는 북마크 미지원
- this.documentLinks = []; // 노트에서는 링크 미지원 (향후 구현 예정)
- this.backlinks = [];
-
- console.log('📝 노트 데이터 로드됨:', { highlights: this.highlights.length, notes: this.notes.length });
-
- // HighlightManager에 데이터 동기화
- this.highlightManager.highlights = this.highlights;
- this.highlightManager.notes = this.notes;
-
- // 하이라이트 렌더링
- this.highlightManager.renderHighlights();
- return;
- }
-
- // 문서의 경우: 모듈별 로딩
- 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.linkManager.loadBacklinks(this.documentId)
- ]);
-
- this.highlights = highlights;
- this.notes = notes;
- this.bookmarks = bookmarks || [];
- this.documentLinks = documentLinks || [];
- this.backlinks = backlinks || [];
-
- console.log('Loaded data:', { highlights: this.highlights.length, notes: this.notes.length, bookmarks: this.bookmarks.length });
-
- // 모듈에 데이터 동기화
- this.highlightManager.highlights = this.highlights;
- this.highlightManager.notes = this.notes;
- this.bookmarkManager.bookmarks = this.bookmarks;
- this.linkManager.documentLinks = this.documentLinks;
- this.linkManager.backlinks = this.backlinks;
-
- // 하이라이트 렌더링
- this.highlightManager.renderHighlights();
-
- // 백링크 렌더링 (먼저 렌더링)
- this.linkManager.renderBacklinks();
-
- // 문서 링크 렌더링 (백링크 후에 렌더링)
- this.linkManager.renderDocumentLinks();
-
- // 백링크 배너 숫자 업데이트
- this.updateBacklinkBanner();
-
- } catch (error) {
- console.warn('Some document data failed to load, continuing with empty data:', error);
- this.highlights = [];
- this.notes = [];
- this.bookmarks = [];
- }
- },
-
-
-
- // 하이라이트 그룹 적용 (여러 색상 지원)
- applyHighlightGroup(highlightGroup) {
- if (highlightGroup.length === 0) return;
-
- // 그룹의 전체 범위 계산
- const minStart = Math.min(...highlightGroup.map(h => h.start_offset));
- const maxEnd = Math.max(...highlightGroup.map(h => h.end_offset));
-
- const content = document.getElementById('document-content');
- const walker = document.createTreeWalker(
- content,
- NodeFilter.SHOW_TEXT,
- null,
- false
- );
-
- let currentOffset = 0;
- let node;
-
- while (node = walker.nextNode()) {
- const nodeLength = node.textContent.length;
- const nodeStart = currentOffset;
- const nodeEnd = currentOffset + nodeLength;
-
- // 그룹 범위와 겹치는지 확인
- if (nodeStart < maxEnd && nodeEnd > minStart) {
- const startInNode = Math.max(0, minStart - nodeStart);
- const endInNode = Math.min(nodeLength, maxEnd - nodeStart);
-
- if (startInNode < endInNode) {
- // 텍스트 노드를 분할하고 하이라이트 적용
- const beforeText = node.textContent.substring(0, startInNode);
- const highlightText = node.textContent.substring(startInNode, endInNode);
- const afterText = node.textContent.substring(endInNode);
-
- const parent = node.parentNode;
-
- // 하이라이트 요소 생성
- const highlightEl = this.createMultiColorHighlight(highlightGroup, highlightText, minStart + startInNode);
-
- // 노드 교체
- if (beforeText) {
- parent.insertBefore(document.createTextNode(beforeText), node);
- }
- parent.insertBefore(highlightEl, node);
- if (afterText) {
- parent.insertBefore(document.createTextNode(afterText), node);
- }
- parent.removeChild(node);
- }
- }
-
- currentOffset = nodeEnd;
- }
- },
-
- // 다중 색상 하이라이트 요소 생성
- createMultiColorHighlight(highlightGroup, text, textOffset) {
- const container = document.createElement('span');
-
- if (highlightGroup.length === 1) {
- // 단일 색상
- const highlight = highlightGroup[0];
- container.className = 'highlight';
- container.style.backgroundColor = highlight.highlight_color;
- container.textContent = text;
- container.dataset.highlightId = highlight.id;
-
- // 클릭 이벤트
- container.addEventListener('click', (e) => {
- e.stopPropagation();
- this.showHighlightTooltip(highlight, e.target);
- });
- } else {
- // 다중 색상 - 그라데이션 또는 스트라이프 효과
- container.className = 'multi-highlight';
- container.textContent = text;
-
- // 색상들 수집
- const colors = highlightGroup.map(h => h.highlight_color);
- const uniqueColors = [...new Set(colors)];
-
- if (uniqueColors.length === 2) {
- // 2색상: 위아래 분할
- container.style.background = `linear-gradient(to bottom, ${uniqueColors[0]} 50%, ${uniqueColors[1]} 50%)`;
- } else if (uniqueColors.length === 3) {
- // 3색상: 3등분
- container.style.background = `linear-gradient(to bottom, ${uniqueColors[0]} 33%, ${uniqueColors[1]} 33% 66%, ${uniqueColors[2]} 66%)`;
- } else {
- // 4색상 이상: 스트라이프 패턴
- const stripeSize = 100 / uniqueColors.length;
- const gradientStops = uniqueColors.map((color, index) => {
- const start = index * stripeSize;
- const end = (index + 1) * stripeSize;
- return `${color} ${start}% ${end}%`;
- }).join(', ');
- container.style.background = `linear-gradient(to bottom, ${gradientStops})`;
- }
-
- // 테두리 추가로 더 명확하게
- container.style.border = '1px solid rgba(0,0,0,0.2)';
- container.style.borderRadius = '2px';
-
- // 모든 하이라이트 ID 저장
- container.dataset.highlightIds = JSON.stringify(highlightGroup.map(h => h.id));
-
- // 클릭 이벤트 - 첫 번째 하이라이트로 툴팁 표시
- container.addEventListener('click', (e) => {
- e.stopPropagation();
- this.showHighlightTooltip(highlightGroup[0], e.target);
- });
- }
-
- return container;
- },
-
- // 개별 하이라이트 적용
- applyHighlight(highlight) {
- const content = document.getElementById('document-content');
- const walker = document.createTreeWalker(
- content,
- NodeFilter.SHOW_TEXT,
- null,
- false
- );
-
- let currentOffset = 0;
- let node;
-
- while (node = walker.nextNode()) {
- const nodeLength = node.textContent.length;
- const nodeStart = currentOffset;
- const nodeEnd = currentOffset + nodeLength;
-
- // 하이라이트 범위와 겹치는지 확인
- if (nodeStart < highlight.end_offset && nodeEnd > highlight.start_offset) {
- const startInNode = Math.max(0, highlight.start_offset - nodeStart);
- const endInNode = Math.min(nodeLength, highlight.end_offset - nodeStart);
-
- if (startInNode < endInNode) {
- // 텍스트 노드를 분할하고 하이라이트 적용
- const beforeText = node.textContent.substring(0, startInNode);
- const highlightText = node.textContent.substring(startInNode, endInNode);
- const afterText = node.textContent.substring(endInNode);
-
- const parent = node.parentNode;
-
- // 하이라이트 요소 생성
- const highlightEl = document.createElement('span');
- highlightEl.className = 'highlight';
- highlightEl.style.backgroundColor = highlight.highlight_color;
- highlightEl.textContent = highlightText;
- highlightEl.dataset.highlightId = highlight.id;
-
- // 클릭 이벤트 추가 - 말풍선 표시
- highlightEl.addEventListener('click', (e) => {
- e.stopPropagation();
- this.showHighlightTooltip(highlight, e.target);
- });
-
- // 노드 교체
- if (beforeText) {
- parent.insertBefore(document.createTextNode(beforeText), node);
- }
- parent.insertBefore(highlightEl, node);
- if (afterText) {
- parent.insertBefore(document.createTextNode(afterText), node);
- }
- parent.removeChild(node);
- }
- }
-
- currentOffset = nodeEnd;
- }
- },
-
- // 텍스트 선택 처리 (HighlightManager로 위임)
- handleTextSelection() {
- this.highlightManager.handleTextSelection();
- // 상태 동기화
- this.selectedText = this.highlightManager.selectedText;
- this.selectedRange = this.highlightManager.selectedRange;
- },
-
- // 하이라이트 버튼 표시
- showHighlightButton(selection) {
- // 기존 버튼 제거
- const existingButton = document.querySelector('.highlight-button');
- if (existingButton) {
- existingButton.remove();
- }
-
- const range = selection.getRangeAt(0);
- const rect = range.getBoundingClientRect();
-
- const button = document.createElement('button');
- button.className = 'highlight-button fixed z-50 bg-blue-600 text-white px-4 py-2 rounded shadow-lg text-sm font-medium border-2 border-blue-700';
- button.style.left = `${rect.left + window.scrollX}px`;
- button.style.top = `${rect.bottom + window.scrollY + 10}px`;
- button.innerHTML = '🖍️ 하이라이트';
-
- console.log('Highlight button created at:', button.style.left, button.style.top);
-
- button.addEventListener('click', () => {
- this.createHighlight();
- button.remove();
- });
-
- document.body.appendChild(button);
-
- // 3초 후 자동 제거
- setTimeout(() => {
- if (button.parentNode) {
- button.remove();
- }
- }, 3000);
- },
-
- // 색상 버튼으로 하이라이트 생성
- // HighlightManager로 위임
- createHighlightWithColor(color) {
- this.highlightManager.selectedHighlightColor = color;
- this.selectedHighlightColor = color;
- this.highlightManager.createHighlightWithColor(color);
- // 상태 동기화
- this.highlights = this.highlightManager.highlights;
- },
-
- // 하이라이트 생성
- async createHighlight() {
- console.log('createHighlight called');
- console.log('selectedText:', this.selectedText);
- console.log('selectedRange:', this.selectedRange);
-
- if (!this.selectedText || !this.selectedRange) {
- console.log('No selected text or range');
- return;
- }
-
- try {
- console.log('Starting highlight creation...');
- // 텍스트 오프셋 계산
- const content = document.getElementById('document-content');
- const { startOffset, endOffset } = this.calculateTextOffsets(this.selectedRange, content);
- console.log('Text offsets:', startOffset, endOffset);
-
- const highlightData = {
- document_id: this.documentId,
- start_offset: startOffset,
- end_offset: endOffset,
- selected_text: this.selectedText,
- highlight_color: this.selectedHighlightColor,
- highlight_type: 'highlight'
- };
-
- // 노트와 문서에 따라 다른 API 호출
- let highlight;
- if (this.contentType === 'note') {
- // 노트용 하이라이트 API 호출 (document_id를 note_id로 변경)
- const noteHighlightData = {
- ...highlightData,
- note_id: highlightData.document_id
- };
- delete noteHighlightData.document_id;
- console.log('📝 노트 하이라이트 데이터:', noteHighlightData);
- highlight = await api.post('/note-highlights/', noteHighlightData);
- } else {
- highlight = await api.createHighlight(highlightData);
- }
- this.highlights.push(highlight);
-
- // 하이라이트 렌더링
- this.highlightManager.renderHighlights();
-
- // 선택 해제
- window.getSelection().removeAllRanges();
- this.selectedText = '';
- this.selectedRange = null;
-
- // 메모 추가 여부 확인
- if (confirm('이 하이라이트에 메모를 추가하시겠습니까?')) {
- // 노트와 문서 모두 동일한 방식으로 처리
- this.createMemoForHighlight(highlight);
- }
-
- } catch (error) {
- console.error('Failed to create highlight:', error);
- alert('하이라이트 생성에 실패했습니다');
- }
- },
-
- // 하이라이트 메모 생성 (노트/문서 통합)
- async createMemoForHighlight(highlight) {
- try {
- // 메모 내용 입력받기
- const content = prompt('메모 내용을 입력하세요:', '');
- if (content === null || content.trim() === '') {
- return; // 취소하거나 빈 내용인 경우
- }
-
- // 메모 생성 데이터
- const noteData = {
- highlight_id: highlight.id,
- content: content.trim()
- };
-
- let note;
- if (this.contentType === 'note') {
- // 노트용 메모 API
- noteData.note_id = this.documentId;
- console.log('📝 노트 메모 생성 데이터:', noteData);
- note = await api.post('/note-notes/', noteData);
- } else {
- // 문서용 메모 API
- noteData.is_private = false;
- noteData.tags = [];
- console.log('📝 문서 메모 생성 데이터:', noteData);
- note = await api.createNote(noteData);
- }
-
- // 메모 목록에 추가
- this.notes.push(note);
-
- console.log('✅ 메모 생성 완료:', note);
-
- } catch (error) {
- console.error('❌ 메모 생성 실패:', error);
- alert('메모 생성에 실패했습니다.');
- }
- },
-
- // 텍스트 오프셋 계산
- calculateTextOffsets(range, container) {
- const walker = document.createTreeWalker(
- container,
- NodeFilter.SHOW_TEXT,
- null,
- false
- );
-
- let currentOffset = 0;
- let startOffset = -1;
- let endOffset = -1;
- let node;
-
- while (node = walker.nextNode()) {
- const nodeLength = node.textContent.length;
-
- if (range.startContainer === node) {
- startOffset = currentOffset + range.startOffset;
- }
-
- if (range.endContainer === node) {
- endOffset = currentOffset + range.endOffset;
- break;
- }
-
- currentOffset += nodeLength;
- }
-
- return { startOffset, endOffset };
- },
-
- // 하이라이트 선택
- selectHighlight(highlightId) {
- // 모든 하이라이트에서 selected 클래스 제거
- document.querySelectorAll('.highlight').forEach(el => {
- el.classList.remove('selected');
- });
-
- // 선택된 하이라이트에 selected 클래스 추가
- const highlightEl = document.querySelector(`[data-highlight-id="${highlightId}"]`);
- if (highlightEl) {
- highlightEl.classList.add('selected');
- }
-
- // 해당 하이라이트의 메모 찾기
- const note = this.notes.find(n => n.highlight.id === highlightId);
- if (note) {
- this.editNote(note);
- } else {
- // 메모가 없으면 새로 생성
- const highlight = this.highlights.find(h => h.id === highlightId);
- if (highlight) {
- this.openNoteModal(highlight);
- }
- }
- },
-
- // 하이라이트로 스크롤
- scrollToHighlight(highlightId) {
- const highlightEl = document.querySelector(`[data-highlight-id="${highlightId}"]`);
- if (highlightEl) {
- highlightEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
- highlightEl.classList.add('selected');
-
- // 2초 후 선택 해제
- setTimeout(() => {
- highlightEl.classList.remove('selected');
- }, 2000);
- }
- },
-
- // 메모 모달 열기
- openNoteModal(highlight = null) {
- this.editingNote = null;
- this.noteForm = {
- content: '',
- tags: ''
- };
-
- if (highlight) {
- this.selectedHighlight = highlight;
- this.selectedText = highlight.selected_text;
- }
-
- this.showNoteModal = true;
- },
-
- // 메모 편집
- editNote(note) {
- this.editingNote = note;
- this.noteForm = {
- content: note.content,
- tags: note.tags ? note.tags.join(', ') : ''
- };
- this.selectedText = note.highlight.selected_text;
- this.showNoteModal = true;
- },
-
- // 메모 저장 (HighlightManager로 위임)
- async saveNote() {
- await this.highlightManager.saveNote();
- // 상태 동기화
- this.notes = this.highlightManager.notes;
- this.noteLoading = false;
-
- try {
- const noteData = {
- content: this.noteForm.content,
- tags: this.noteForm.tags ? this.noteForm.tags.split(',').map(t => t.trim()).filter(t => t) : []
- };
-
- if (this.editingNote) {
- // 메모 수정
- const updatedNote = await api.updateNote(this.editingNote.id, noteData);
- const index = this.notes.findIndex(n => n.id === this.editingNote.id);
- if (index !== -1) {
- this.notes[index] = updatedNote;
- }
- } else {
- // 새 메모 생성
- noteData.highlight_id = this.selectedHighlight.id;
- const newNote = await api.createNote(noteData);
- this.notes.push(newNote);
- }
-
- this.filterNotes();
- this.closeNoteModal();
-
- } catch (error) {
- console.error('Failed to save note:', error);
- alert('메모 저장에 실패했습니다');
- } finally {
- this.noteLoading = false;
- }
- },
-
- // 메모 삭제 (HighlightManager로 위임)
- async deleteNote(noteId) {
- try {
- await this.highlightManager.deleteNote(noteId);
- // 상태 동기화
- this.notes = this.highlightManager.notes;
- this.filterNotes();
- } catch (error) {
- console.error('Failed to delete note:', error);
- alert('메모 삭제에 실패했습니다');
- }
- },
-
- // 메모 모달 닫기
- closeNoteModal() {
- this.showNoteModal = false;
- this.editingNote = null;
- this.selectedHighlight = null;
- this.selectedText = '';
- this.noteForm = { content: '', tags: '' };
- },
-
- // 메모 필터링
- filterNotes() {
- if (!this.noteSearchQuery.trim()) {
- this.filteredNotes = [...this.notes];
- } else {
- const query = this.noteSearchQuery.toLowerCase();
- this.filteredNotes = this.notes.filter(note =>
- note.content.toLowerCase().includes(query) ||
- note.highlight.selected_text.toLowerCase().includes(query) ||
- (note.tags && note.tags.some(tag => tag.toLowerCase().includes(query)))
- );
- }
- },
-
- // 책갈피 추가 (BookmarkManager로 위임)
- async addBookmark() {
- await this.bookmarkManager.addBookmark(this.document);
- // 상태 동기화
- this.bookmarkForm = this.bookmarkManager.bookmarkForm;
- this.currentScrollPosition = this.bookmarkManager.currentScrollPosition;
- },
-
- // 책갈피 편집 (BookmarkManager로 위임)
- editBookmark(bookmark) {
- this.bookmarkManager.editBookmark(bookmark);
- // 상태 동기화
- this.editingBookmark = this.bookmarkManager.editingBookmark;
- this.bookmarkForm = this.bookmarkManager.bookmarkForm;
- },
-
- // 책갈피 저장 (BookmarkManager로 위임)
- async saveBookmark() {
- // BookmarkManager의 폼 데이터 동기화
- this.bookmarkManager.bookmarkForm = this.bookmarkForm;
- this.bookmarkManager.editingBookmark = this.editingBookmark;
- this.bookmarkManager.currentScrollPosition = this.currentScrollPosition;
-
- await this.bookmarkManager.saveBookmark(this.documentId);
-
- // 상태 동기화
- this.bookmarks = this.bookmarkManager.bookmarks;
- this.editingBookmark = this.bookmarkManager.editingBookmark;
- this.bookmarkForm = this.bookmarkManager.bookmarkForm;
- this.currentScrollPosition = this.bookmarkManager.currentScrollPosition;
-
- try {
- const bookmarkData = {
- title: this.bookmarkForm.title,
- description: this.bookmarkForm.description,
- scroll_position: this.currentScrollPosition || 0
- };
-
- if (this.editingBookmark) {
- // 책갈피 수정
- const updatedBookmark = await api.updateBookmark(this.editingBookmark.id, bookmarkData);
- const index = this.bookmarks.findIndex(b => b.id === this.editingBookmark.id);
- if (index !== -1) {
- this.bookmarks[index] = updatedBookmark;
- }
- } else {
- // 새 책갈피 생성
- bookmarkData.document_id = this.documentId;
- const newBookmark = await api.createBookmark(bookmarkData);
- this.bookmarks.push(newBookmark);
- }
-
- this.closeBookmarkModal();
-
- } catch (error) {
- console.error('Failed to save bookmark:', error);
- alert('책갈피 저장에 실패했습니다');
- } finally {
- this.bookmarkLoading = false;
- }
- },
-
- // 책갈피 삭제 (BookmarkManager로 위임)
- async deleteBookmark(bookmarkId) {
- await this.bookmarkManager.deleteBookmark(bookmarkId);
- // 상태 동기화
- this.bookmarks = this.bookmarkManager.bookmarks;
- },
-
- // 책갈피로 스크롤 (BookmarkManager로 위임)
- scrollToBookmark(bookmark) {
- this.bookmarkManager.scrollToBookmark(bookmark);
- },
-
- // 책갈피 모달 닫기 (BookmarkManager로 위임)
- closeBookmarkModal() {
- this.bookmarkManager.closeBookmarkModal();
- // 상태 동기화
- this.editingBookmark = this.bookmarkManager.editingBookmark;
- this.bookmarkForm = this.bookmarkManager.bookmarkForm;
- this.currentScrollPosition = this.bookmarkManager.currentScrollPosition;
- },
-
- // 문서 내 검색
- searchInDocument() {
- // 기존 검색 하이라이트 제거
- document.querySelectorAll('.search-highlight').forEach(el => {
- const parent = el.parentNode;
- parent.replaceChild(document.createTextNode(el.textContent), el);
- parent.normalize();
- });
-
- if (!this.searchQuery.trim()) {
- return;
- }
-
- // 새 검색 하이라이트 적용
- const content = document.getElementById('document-content');
- this.highlightSearchResults(content, this.searchQuery);
- },
-
- // 검색 결과 하이라이트
- highlightSearchResults(element, searchText) {
- const walker = document.createTreeWalker(
- element,
- NodeFilter.SHOW_TEXT,
- null,
- false
- );
-
- const textNodes = [];
- let node;
- while (node = walker.nextNode()) {
- textNodes.push(node);
- }
-
- textNodes.forEach(textNode => {
- const text = textNode.textContent;
- const regex = new RegExp(`(${searchText})`, 'gi');
-
- if (regex.test(text)) {
- const parent = textNode.parentNode;
- const highlightedHTML = text.replace(regex, '$1');
-
- const tempDiv = document.createElement('div');
- tempDiv.innerHTML = highlightedHTML;
-
- while (tempDiv.firstChild) {
- parent.insertBefore(tempDiv.firstChild, textNode);
- }
- parent.removeChild(textNode);
- }
- });
- },
-
- // 문서 클릭 처리
- handleDocumentClick(event) {
- // 하이라이트 버튼 제거
- const button = document.querySelector('.highlight-button');
- if (button && !button.contains(event.target)) {
- button.remove();
- }
-
- // 하이라이트 선택 해제
- document.querySelectorAll('.highlight.selected').forEach(el => {
- el.classList.remove('selected');
- });
- },
-
- // 뒤로가기 - 문서 관리 페이지로 이동
- goBack() {
- // 1. URL 파라미터에서 type과 from 확인
- const urlParams = new URLSearchParams(window.location.search);
- const fromPage = urlParams.get('from');
-
- // 노트인 경우 노트 목록으로 이동
- if (this.contentType === 'note') {
- window.location.href = 'notes.html';
- return;
- }
-
- // 2. 세션 스토리지에서 이전 페이지 확인
- const previousPage = sessionStorage.getItem('previousPage');
-
- // 3. referrer 확인
- const referrer = document.referrer;
-
- let targetPage = 'index.html'; // 기본값: 그리드 뷰
-
- // 우선순위: URL 파라미터 > 세션 스토리지 > referrer
- if (fromPage === 'hierarchy') {
- targetPage = 'hierarchy.html';
- } else if (previousPage === 'hierarchy.html') {
- targetPage = 'hierarchy.html';
- } else if (referrer && referrer.includes('hierarchy.html')) {
- targetPage = 'hierarchy.html';
- }
-
- console.log(`🔙 뒤로가기: ${targetPage}로 이동`);
- window.location.href = targetPage;
- },
-
- // 날짜 포맷팅
- formatDate(dateString) {
- const date = new Date(dateString);
- return date.toLocaleDateString('ko-KR', {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit'
- });
- },
-
- // 동일한 텍스트 범위의 모든 하이라이트 찾기
- findOverlappingHighlights(clickedHighlight) {
- const overlapping = [];
-
- this.highlights.forEach(highlight => {
- // 텍스트 범위가 겹치는지 확인
- const isOverlapping = (
- (highlight.start_offset <= clickedHighlight.end_offset &&
- highlight.end_offset >= clickedHighlight.start_offset) ||
- (clickedHighlight.start_offset <= highlight.end_offset &&
- clickedHighlight.end_offset >= highlight.start_offset)
- );
-
- if (isOverlapping) {
- overlapping.push(highlight);
- }
- });
-
- // 시작 위치 순으로 정렬
- return overlapping.sort((a, b) => a.start_offset - b.start_offset);
- },
-
- // 색상별로 하이라이트 그룹화
- groupHighlightsByColor(highlights) {
- const colorGroups = {};
-
- highlights.forEach(highlight => {
- const color = highlight.highlight_color || highlight.color || '#FFB6C1';
- if (!colorGroups[color]) {
- colorGroups[color] = [];
- }
- colorGroups[color].push(highlight);
- });
-
- return colorGroups;
- },
-
- // 색상 이름 매핑 (더 많은 색상 지원)
- getColorName(color) {
- const colorNames = {
- // 기본 색상들
- '#FFB6C1': '핑크',
- '#FFFF99': '노랑',
- '#FFFF00': '노랑',
- '#YELLOW': '노랑',
- '#98FB98': '연두',
- '#90EE90': '연두',
- '#LIGHTGREEN': '연두',
- '#87CEEB': '하늘',
- '#ADD8E6': '하늘',
- '#LIGHTBLUE': '하늘',
- '#DDA0DD': '보라',
- '#DA70D6': '보라',
- '#ORCHID': '보라',
- '#FFA500': '주황',
- '#ORANGE': '주황',
- // RGB 형식
- 'rgb(255, 255, 0)': '노랑',
- 'rgb(255, 255, 153)': '노랑',
- 'rgb(152, 251, 152)': '연두',
- 'rgb(144, 238, 144)': '연두',
- 'rgb(135, 206, 235)': '하늘',
- 'rgb(173, 216, 230)': '하늘',
- 'rgb(255, 182, 193)': '핑크',
- 'rgb(255, 165, 0)': '주황'
- };
-
- // 대소문자 구분 없이 매칭
- const normalizedColor = color?.toUpperCase();
- const exactMatch = colorNames[color] || colorNames[normalizedColor];
-
- if (exactMatch) {
- return exactMatch;
- }
-
- // RGB 값으로 색상 추정
- if (color?.includes('rgb')) {
- const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
- if (rgbMatch) {
- const [, r, g, b] = rgbMatch.map(Number);
-
- // 색상 범위로 판단
- if (r > 200 && g > 200 && b < 100) return '노랑';
- if (r < 200 && g > 200 && b < 200) return '연두';
- if (r < 200 && g < 200 && b > 200) return '하늘';
- if (r > 200 && g < 200 && b > 200) return '핑크';
- if (r > 200 && g > 100 && b < 100) return '주황';
- if (r > 150 && g < 150 && b > 150) return '보라';
- }
- }
-
- console.log('🎨 알 수 없는 색상:', color);
- return '기타';
- },
-
- // 하이라이트 말풍선 표시 (HighlightManager로 위임)
- showHighlightTooltip(clickedHighlight, element) {
- if (this.highlightManager) {
- this.highlightManager.showHighlightTooltip(clickedHighlight, element);
- }
- },
-
- // 말풍선 외부 클릭 처리
- handleTooltipOutsideClick(e) {
- const highlightTooltip = document.getElementById('highlight-tooltip');
- const linkTooltip = document.getElementById('link-tooltip');
- const backlinkTooltip = document.getElementById('backlink-tooltip');
-
- const isOutsideHighlightTooltip = highlightTooltip && !highlightTooltip.contains(e.target) && !e.target.classList.contains('highlight');
- const isOutsideLinkTooltip = linkTooltip && !linkTooltip.contains(e.target) && !e.target.classList.contains('document-link');
- const isOutsideBacklinkTooltip = backlinkTooltip && !backlinkTooltip.contains(e.target) && !e.target.classList.contains('backlink-highlight');
-
- if (isOutsideHighlightTooltip || isOutsideLinkTooltip || isOutsideBacklinkTooltip) {
- this.hideTooltip();
- }
- },
-
- // 짧은 날짜 형식
- formatShortDate(dateString) {
- const date = new Date(dateString);
- const now = new Date();
- const diffTime = Math.abs(now - date);
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
-
- if (diffDays === 1) {
- return '오늘';
- } else if (diffDays === 2) {
- return '어제';
- } else if (diffDays <= 7) {
- return `${diffDays-1}일 전`;
- } else {
- return date.toLocaleDateString('ko-KR', {
- year: '2-digit',
- month: '2-digit',
- day: '2-digit'
- });
- }
- },
-
- // 메모 추가 폼 표시 (HighlightManager로 위임)
- showAddNoteForm(highlightId) {
- if (this.highlightManager) {
- this.highlightManager.showAddNoteForm(highlightId);
- }
- },
-
- // 동일한 범위의 모든 하이라이트 찾기
- const overlappingHighlights = this.findOverlappingHighlights(clickedHighlight);
- const colorGroups = this.groupHighlightsByColor(overlappingHighlights);
-
- console.log('🎨 겹치는 하이라이트:', overlappingHighlights.length, '개');
- console.log('🎨 하이라이트 상세:', overlappingHighlights.map(h => ({
- id: h.id,
- color: h.highlight_color,
- colorName: this.getColorName(h.highlight_color),
- text: h.selected_text
- })));
- console.log('🎨 색상 그룹:', Object.keys(colorGroups));
-
- // 각 색상별 메모 개수 디버깅
- Object.entries(colorGroups).forEach(([color, highlights]) => {
- const noteCount = highlights.flatMap(h =>
- this.notes.filter(note => note.highlight_id === h.id)
- ).length;
- console.log(`🎨 ${this.getColorName(color)} (${color}): ${highlights.length}개 하이라이트, ${noteCount}개 메모`);
- });
-
- const tooltip = document.createElement('div');
- tooltip.id = 'highlight-tooltip';
- tooltip.className = 'absolute bg-white border border-gray-300 rounded-lg shadow-lg p-4 z-50 max-w-lg';
- tooltip.style.minWidth = '350px';
-
- // 선택된 텍스트 표시 (가장 긴 텍스트 사용)
- const longestText = overlappingHighlights.reduce((longest, current) =>
- current.selected_text.length > longest.length ? current.selected_text : longest, ''
- );
-
- let tooltipHTML = `
-
-
선택된 텍스트
-
- "${longestText}"
-
-
- `;
-
- // 하이라이트가 여러 개인 경우 색상별로 표시
- if (overlappingHighlights.length > 1) {
- tooltipHTML += `
-
-
- 하이라이트 색상 (${overlappingHighlights.length}개)
-
-
- ${Object.keys(colorGroups).map(color => `
-
-
-
${this.getColorName(color)} (${colorGroups[color].length})
-
- `).join('')}
-
-
- `;
- }
-
- // 색상별로 메모 표시
- tooltipHTML += '';
-
- Object.entries(colorGroups).forEach(([color, highlights]) => {
- const colorName = this.getColorName(color);
- const allNotes = highlights.flatMap(h =>
- this.notes.filter(note => note.highlight_id === h.id)
- );
-
- tooltipHTML += `
-
-
-
-
-
${colorName} 메모 (${allNotes.length})
-
-
-
-
-
- ${allNotes.length > 0 ?
- allNotes.map(note => `
-
-
${note.content}
-
- ${this.formatShortDate(note.created_at)} · Administrator
-
-
-
- `).join('') :
- '
메모가 없습니다
'
- }
-
-
- `;
- });
-
- tooltipHTML += '
';
-
- // 하이라이트 삭제 버튼들
- if (overlappingHighlights.length > 1) {
- tooltipHTML += `
-
-
하이라이트 삭제
-
- ${Object.entries(colorGroups).map(([color, highlights]) => `
-
- `).join('')}
-
-
-
- `;
- } else {
- tooltipHTML += `
-
-
-
- `;
- }
-
- tooltipHTML += `
-
-
-
- `;
-
- tooltip.innerHTML = tooltipHTML;
-
- // 위치 계산
- const rect = element.getBoundingClientRect();
- const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
- const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
-
- document.body.appendChild(tooltip);
-
- // 말풍선 위치 조정
- const tooltipRect = tooltip.getBoundingClientRect();
- let top = rect.bottom + scrollTop + 5;
- let left = rect.left + scrollLeft;
-
- // 화면 경계 체크
- if (left + tooltipRect.width > window.innerWidth) {
- left = window.innerWidth - tooltipRect.width - 10;
- }
- if (top + tooltipRect.height > window.innerHeight + scrollTop) {
- top = rect.top + scrollTop - tooltipRect.height - 5;
- }
-
- tooltip.style.top = top + 'px';
- tooltip.style.left = left + 'px';
-
- // 외부 클릭 시 닫기
- setTimeout(() => {
- document.addEventListener('click', this.handleTooltipOutsideClick.bind(this));
- }, 100);
- },
-
- // 말풍선 숨기기 (HighlightManager로 위임)
- hideTooltip() {
- if (this.highlightManager) {
- this.highlightManager.hideTooltip();
- }
- },
-
-
-
- // 말풍선 외부 클릭 처리
- handleTooltipOutsideClick(e) {
- const highlightTooltip = document.getElementById('highlight-tooltip');
- const linkTooltip = document.getElementById('link-tooltip');
- const backlinkTooltip = document.getElementById('backlink-tooltip');
-
- const isOutsideHighlightTooltip = highlightTooltip && !highlightTooltip.contains(e.target) && !e.target.classList.contains('highlight');
- const isOutsideLinkTooltip = linkTooltip && !linkTooltip.contains(e.target) && !e.target.classList.contains('document-link');
- const isOutsideBacklinkTooltip = backlinkTooltip && !backlinkTooltip.contains(e.target) && !e.target.classList.contains('backlink-highlight');
-
- if (isOutsideHighlightTooltip || isOutsideLinkTooltip || isOutsideBacklinkTooltip) {
- this.hideTooltip();
- }
- },
-
- // 짧은 날짜 형식
- formatShortDate(dateString) {
- const date = new Date(dateString);
- const now = new Date();
- const diffTime = Math.abs(now - date);
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
-
- if (diffDays === 1) {
- return '오늘';
- } else if (diffDays === 2) {
- return '어제';
- } else if (diffDays <= 7) {
- return `${diffDays-1}일 전`;
- } else {
- return date.toLocaleDateString('ko-KR', {
- year: '2-digit',
- month: '2-digit',
- day: '2-digit'
- });
- }
- },
-
- // 메모 추가 폼 표시 (HighlightManager로 위임)
- showAddNoteForm(highlightId) {
- if (this.highlightManager) {
- this.highlightManager.showAddNoteForm(highlightId);
- }
- },
-
-
-
- // 메모 추가 취소
- cancelAddNote(highlightId) {
- // 말풍선 다시 표시
- const highlight = this.highlights.find(h => h.id === highlightId);
- if (highlight) {
- const element = document.querySelector(`[data-highlight-id="${highlightId}"]`);
- if (element) {
- this.showHighlightTooltip(highlight, element);
- }
- }
- },
-
- // 새 메모 저장 (HighlightManager로 위임)
- async saveNewNote(highlightId) {
- if (this.highlightManager) {
- await this.highlightManager.saveNewNote(highlightId);
- // 상태 동기화
- this.notes = this.highlightManager.notes;
- }
- },
-
-
-
- // 하이라이트 삭제
- async deleteHighlight(highlightId) {
- if (!confirm('이 하이라이트를 삭제하시겠습니까? 연결된 메모도 함께 삭제됩니다.')) {
- return;
- }
-
- try {
- await api.deleteHighlight(highlightId);
-
- // 로컬 데이터에서 제거
- this.highlights = this.highlights.filter(h => h.id !== highlightId);
- this.notes = this.notes.filter(n => n.highlight_id !== highlightId);
-
- // UI 업데이트
- this.hideTooltip();
- this.highlightManager.renderHighlights();
-
- } catch (error) {
- console.error('Failed to delete highlight:', error);
- alert('하이라이트 삭제에 실패했습니다');
- }
- },
-
- // 특정 색상의 하이라이트들 삭제
- async deleteHighlightsByColor(color, highlightIds) {
- const colorName = this.getColorName(color);
- if (!confirm(`${colorName} 색상의 하이라이트를 모두 삭제하시겠습니까? 연결된 메모도 함께 삭제됩니다.`)) {
- return;
- }
-
- try {
- // 각 하이라이트를 개별적으로 삭제
- for (const highlightId of highlightIds) {
- await api.deleteHighlight(highlightId);
- }
-
- // 로컬 데이터에서 제거
- this.highlights = this.highlights.filter(h => !highlightIds.includes(h.id));
- this.notes = this.notes.filter(n => !highlightIds.includes(n.highlight_id));
-
- // UI 업데이트
- this.hideTooltip();
- this.highlightManager.renderHighlights();
-
- } catch (error) {
- console.error('Failed to delete highlights:', error);
- alert('하이라이트 삭제에 실패했습니다');
- }
- },
-
- // 겹치는 모든 하이라이트 삭제
- async deleteAllOverlappingHighlights(highlightIds) {
- if (!confirm(`겹치는 모든 하이라이트를 삭제하시겠습니까? (${highlightIds.length}개) 연결된 메모도 함께 삭제됩니다.`)) {
- return;
- }
-
- try {
- // 각 하이라이트를 개별적으로 삭제
- for (const highlightId of highlightIds) {
- await api.deleteHighlight(highlightId);
- }
-
- // 로컬 데이터에서 제거
- this.highlights = this.highlights.filter(h => !highlightIds.includes(h.id));
- this.notes = this.notes.filter(n => !highlightIds.includes(n.highlight_id));
-
- // UI 업데이트
- this.hideTooltip();
- this.highlightManager.renderHighlights();
-
- } catch (error) {
- console.error('Failed to delete highlights:', error);
- alert('하이라이트 삭제에 실패했습니다');
- }
- },
-
- // 언어 전환 함수
- toggleLanguage() {
- this.isKorean = !this.isKorean;
-
- console.log(`🌐 언어 전환 시작 (isKorean: ${this.isKorean})`);
-
- // 문서 내 언어별 요소 토글 (HTML, HEAD, BODY 태그 제외)
- const primaryLangElements = document.querySelectorAll('[lang="ko"]:not(html):not(head):not(body), .korean, .kr, .primary-lang');
- const secondaryLangElements = document.querySelectorAll('[lang="en"]:not(html):not(head):not(body), .english, .en, [lang="ja"]:not(html):not(head):not(body), .japanese, .jp, [lang="zh"]:not(html):not(head):not(body), .chinese, .cn, .secondary-lang');
-
- // 디버깅: 찾은 요소 수 출력
- console.log(`🔍 Primary 요소 수: ${primaryLangElements.length}`);
- console.log(`🔍 Secondary 요소 수: ${secondaryLangElements.length}`);
-
- // 언어별 요소가 있는 경우에만 토글 적용
- if (primaryLangElements.length > 0 || secondaryLangElements.length > 0) {
- console.log('✅ 언어별 요소 발견, 토글 적용 중...');
-
- primaryLangElements.forEach((el, index) => {
- const oldDisplay = el.style.display || getComputedStyle(el).display;
- const newDisplay = this.isKorean ? 'block' : 'none';
- console.log(`Primary 요소 ${index + 1}: ${el.tagName}#${el.id || 'no-id'}.${el.className || 'no-class'}`);
- console.log(` - 이전 display: ${oldDisplay}`);
- console.log(` - 새로운 display: ${newDisplay}`);
- console.log(` - 요소 내용 미리보기: "${el.textContent?.substring(0, 50) || 'no-text'}..."`);
- console.log(` - 요소 위치:`, el.getBoundingClientRect());
- el.style.display = newDisplay;
- console.log(` - 적용 후 display: ${el.style.display}`);
- });
-
- secondaryLangElements.forEach((el, index) => {
- const oldDisplay = el.style.display || getComputedStyle(el).display;
- const newDisplay = this.isKorean ? 'none' : 'block';
- console.log(`Secondary 요소 ${index + 1}: ${el.tagName}#${el.id || 'no-id'}.${el.className || 'no-class'}`);
- console.log(` - 이전 display: ${oldDisplay}`);
- console.log(` - 새로운 display: ${newDisplay}`);
- console.log(` - 요소 내용 미리보기: "${el.textContent?.substring(0, 50) || 'no-text'}..."`);
- console.log(` - 요소 위치:`, el.getBoundingClientRect());
- el.style.display = newDisplay;
- console.log(` - 적용 후 display: ${el.style.display}`);
- });
- } else {
- // 문서 내 콘텐츠에서 언어별 요소를 더 광범위하게 찾기
- console.log('⚠️ 기본 언어별 요소를 찾을 수 없습니다. 문서 내 콘텐츠를 분석합니다.');
-
- // 문서 콘텐츠 영역에서 언어별 요소 찾기
- const contentArea = document.querySelector('#document-content, .document-content, main, .content, #content');
-
- if (contentArea) {
- console.log('📄 문서 콘텐츠 영역 발견:', contentArea.tagName, contentArea.id || contentArea.className);
-
- // 콘텐츠 영역의 구조 분석
- console.log('📋 콘텐츠 영역 내 모든 자식 요소들:');
- const allChildren = contentArea.querySelectorAll('*');
- const childrenInfo = Array.from(allChildren).slice(0, 10).map(el => {
- return `${el.tagName}${el.id ? '#' + el.id : ''}${el.className ? '.' + el.className.split(' ').join('.') : ''} [lang="${el.lang || 'none'}"]`;
- });
- console.log(childrenInfo);
-
- // 콘텐츠 영역 내에서 언어별 요소 재검색
- const contentPrimary = contentArea.querySelectorAll('[lang="ko"], .korean, .kr, .primary-lang');
- const contentSecondary = contentArea.querySelectorAll('[lang="en"], .english, .en, [lang="ja"], .japanese, .jp, [lang="zh"], .chinese, .cn, .secondary-lang');
-
- console.log(`📄 콘텐츠 내 Primary 요소: ${contentPrimary.length}개`);
- console.log(`📄 콘텐츠 내 Secondary 요소: ${contentSecondary.length}개`);
-
- if (contentPrimary.length > 0 || contentSecondary.length > 0) {
- // 콘텐츠 영역 내 요소들에 토글 적용
- contentPrimary.forEach(el => {
- el.style.display = this.isKorean ? 'block' : 'none';
- });
- contentSecondary.forEach(el => {
- el.style.display = this.isKorean ? 'none' : 'block';
- });
- console.log('✅ 콘텐츠 영역 내 언어 토글 적용됨');
- return;
- } else {
- // 실제 문서 내용에서 언어 패턴 찾기
- console.log('🔍 문서 내용에서 언어 패턴을 찾습니다...');
-
- // 문서의 실제 텍스트 내용 확인
- const textContent = contentArea.textContent || '';
- const hasKorean = /[가-힣]/.test(textContent);
- const hasEnglish = /[a-zA-Z]/.test(textContent);
-
- console.log(`📝 문서 언어 분석: 한국어=${hasKorean}, 영어=${hasEnglish}`);
- console.log(`📝 문서 내용 미리보기: "${textContent.substring(0, 100)}..."`);
-
- if (!hasKorean && !hasEnglish) {
- console.log('❌ 텍스트 콘텐츠를 찾을 수 없습니다.');
- }
- }
- }
-
- // ID 기반 토글 시도
- console.log('🔍 ID 기반 토글을 시도합니다.');
- const koreanContent = document.getElementById('korean-content');
- const englishContent = document.getElementById('english-content');
-
- if (koreanContent && englishContent) {
- koreanContent.style.display = this.isKorean ? 'block' : 'none';
- englishContent.style.display = this.isKorean ? 'none' : 'block';
- console.log('✅ ID 기반 토글 적용됨');
- } else {
- console.log('❌ 언어 전환 가능한 요소를 찾을 수 없습니다.');
- console.log('📋 이 문서는 단일 언어 문서이거나 언어 구분이 없습니다.');
-
- // 단일 언어 문서의 경우 아무것도 하지 않음 (흰색 페이지 방지)
- console.log('🔄 언어 전환을 되돌립니다.');
- this.isKorean = !this.isKorean; // 상태를 원래대로 되돌림
- return;
- }
- }
-
- console.log(`🌐 언어 전환 완료 (Primary: ${this.isKorean ? '표시' : '숨김'})`);
- },
-
- // 매칭된 PDF 다운로드
- async downloadMatchedPDF() {
- if (!this.document.matched_pdf_id) {
- console.warn('매칭된 PDF가 없습니다');
- return;
- }
-
- try {
- console.log('📕 PDF 다운로드 시작:', this.document.matched_pdf_id);
-
- // PDF 문서 정보 가져오기
- const pdfDocument = await window.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 다운로드 완료');
-
- } catch (error) {
- console.error('❌ PDF 다운로드 실패:', error);
- alert('PDF 다운로드에 실패했습니다: ' + error.message);
- }
- },
-
- // 원본 파일 다운로드 (연결된 PDF 파일)
- async downloadOriginalFile() {
- if (!this.document || !this.document.id) {
- console.warn('문서 정보가 없습니다');
- return;
- }
-
- // 연결된 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 window.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 다운로드 완료');
-
- } catch (error) {
- console.error('❌ 연결된 PDF 다운로드 실패:', error);
- alert('연결된 PDF 다운로드에 실패했습니다: ' + error.message);
- }
- },
-
-
-
- // 다른 문서로 네비게이션
- navigateToDocument(documentId) {
- if (!documentId) return;
-
- const currentUrl = new URL(window.location);
- currentUrl.searchParams.set('id', documentId);
- window.location.href = currentUrl.toString();
- },
-
- // 서적 목차로 이동
- goToBookContents() {
- if (!this.navigation?.book_info) return;
-
- window.location.href = `/book-documents.html?bookId=${this.navigation.book_info.id}`;
- },
-
- // === 문서 링크 관련 함수들 ===
-
- // 문서 링크 생성
- async createDocumentLink() {
- console.log('🔗 createDocumentLink 함수 실행');
-
- // 이미 설정된 selectedText와 selectedRange 사용
- let selectedText = this.selectedText;
- let range = this.selectedRange;
-
- // 설정되지 않은 경우 현재 선택 확인
- if (!selectedText || !range) {
- console.log('📝 기존 선택 없음, 현재 선택 확인');
- const selection = window.getSelection();
- if (!selection.rangeCount || selection.isCollapsed) {
- alert('텍스트를 선택한 후 링크를 생성해주세요.');
- return;
- }
- range = selection.getRangeAt(0);
- selectedText = selection.toString().trim();
- }
-
- console.log('✅ 선택된 텍스트:', selectedText);
-
- if (selectedText.length === 0) {
- alert('텍스트를 선택한 후 링크를 생성해주세요.');
- return;
- }
-
- // 선택된 텍스트의 위치 계산
- const documentContent = document.getElementById('document-content');
- const startOffset = this.getTextOffset(documentContent, range.startContainer, range.startOffset);
- const endOffset = startOffset + selectedText.length;
-
- console.log('📍 텍스트 위치:', { startOffset, endOffset });
-
- // 폼 데이터 설정
- this.linkForm = {
- target_document_id: '',
- selected_text: selectedText,
- start_offset: startOffset,
- end_offset: endOffset,
- link_text: '',
- description: '',
- link_type: 'document', // 기본값: 문서 전체 링크
- target_text: '',
- target_start_offset: null,
- target_end_offset: null,
- book_scope: 'same', // 기본값: 같은 서적
- target_book_id: ''
- };
-
- console.log('📋 링크 폼 데이터:', this.linkForm);
-
- // 링크 가능한 문서 목록 로드
- await this.loadLinkableDocuments();
-
- // 모달 열기
- console.log('🔗 링크 모달 열기');
- console.log('🔗 showLinksModal 설정 전:', this.showLinksModal);
- this.showLinksModal = true;
- this.showLinkModal = true; // 기존 호환성
- console.log('🔗 showLinksModal 설정 후:', this.showLinksModal);
- this.editingLink = null;
- },
-
- // 링크 가능한 문서 목록 로드
- async loadLinkableDocuments() {
- try {
- if (this.contentType === 'note') {
- // 노트의 경우: 다른 노트들과 문서들 모두 로드
- console.log('📝 노트 링크 대상 로드 중...');
-
- // 임시: 빈 배열로 설정 (나중에 노트-문서 간 링크 API 구현 시 수정)
- this.linkableDocuments = [];
- this.availableBooks = [];
- this.filteredDocuments = [];
-
- console.warn('📝 노트 간 링크는 아직 지원되지 않습니다.');
- return;
- } else {
- // 문서의 경우: 기존 로직
- this.linkableDocuments = await api.getLinkableDocuments(this.documentId);
- console.log('🔗 링크 가능한 문서들:', this.linkableDocuments);
-
- // 서적 목록도 함께 로드
- await this.loadAvailableBooks();
-
- // 기본적으로 같은 서적 문서들 로드
- await this.loadSameBookDocuments();
- }
- } catch (error) {
- console.error('❌ 링크 가능한 문서 로드 실패:', error);
- this.linkableDocuments = [];
- }
- },
-
- // 문서 링크 저장 (고급 기능 포함)
- async saveDocumentLink() {
- if (!this.linkForm.target_document_id || !this.linkForm.selected_text) {
- alert('필수 정보가 누락되었습니다.');
- return;
- }
-
- // 대상 텍스트가 선택되지 않았으면 경고
- if (!this.linkForm.target_text) {
- alert('대상 문서에서 텍스트를 선택해주세요. "대상 문서에서 텍스트 선택" 버튼을 클릭하여 연결할 텍스트를 드래그해주세요.');
- return;
- }
-
- this.linkLoading = true;
-
- try {
- const linkData = {
- target_document_id: this.linkForm.target_document_id,
- selected_text: this.linkForm.selected_text,
- start_offset: this.linkForm.start_offset,
- end_offset: this.linkForm.end_offset,
- link_text: this.linkForm.link_text || null,
- description: this.linkForm.description || null,
- 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
- };
-
- if (this.editingLink) {
- await window.api.updateDocumentLink(this.editingLink.id, linkData);
- console.log('✅ 링크 수정됨');
- } else {
- await window.api.createDocumentLink(this.documentId, linkData);
- console.log('✅ 링크 생성됨');
- }
-
- // 데이터 새로고침
- await this.loadDocumentData();
- // 링크 렌더링은 loadDocumentData에서 처리됨
- this.closeLinkModal();
- } catch (error) {
- console.error('❌ 링크 저장 실패:', error);
- alert('링크 저장에 실패했습니다: ' + error.message);
- } finally {
- this.linkLoading = false;
- }
- },
-
- // 링크 모달 닫기 (고급 기능 포함)
- closeLinkModal() {
- this.showLinksModal = false;
- this.showLinkModal = false;
- this.editingLink = null;
- this.linkForm = {
- target_document_id: '',
- selected_text: '',
- start_offset: 0,
- end_offset: 0,
- link_text: '',
- description: '',
- link_type: 'document',
- target_text: '',
- target_start_offset: 0,
- target_end_offset: 0,
- book_scope: 'same',
- target_book_id: ''
- };
-
- // 필터링된 문서 목록 초기화
- this.filteredDocuments = [];
- },
-
- // 문서 링크 렌더링 (LinkManager로 위임)
- renderDocumentLinks() {
- if (this.linkManager) {
- this.linkManager.renderDocumentLinks();
- }
- },
-
- // 백링크 하이라이트 렌더링 (LinkManager로 위임)
- async renderBacklinkHighlights() {
- if (this.linkManager) {
- this.linkManager.renderBacklinks();
- }
- },
-
- // 백링크 배너 업데이트 (LinkManager 데이터 사용)
- updateBacklinkBanner() {
- const backlinkCount = this.backlinks ? this.backlinks.length : 0;
- const backlinkBanner = document.getElementById('backlink-banner');
- if (backlinkBanner) {
- const countElement = backlinkBanner.querySelector('.backlink-count');
- if (countElement) {
- countElement.textContent = backlinkCount;
- }
- }
- },
- existingLinks.forEach(link => {
- // 백링크는 제거하지 않음
- if (!link.classList.contains('backlink-highlight')) {
- const parent = link.parentNode;
- parent.replaceChild(document.createTextNode(link.textContent), link);
- parent.normalize();
- }
- });
-
- // 백링크도 보호 (별도 클래스)
- const existingBacklinks = documentContent.querySelectorAll('.backlink-highlight');
- console.log(`🔒 백링크 보호: ${existingBacklinks.length}개 백링크 발견`);
- // 백링크는 건드리지 않음 (보호만 함)
-
- // 새 링크 적용
- console.log(`🔗 링크 렌더링 시작 - 총 ${this.documentLinks.length}개`);
- this.documentLinks.forEach((link, index) => {
- console.log(`🔗 링크 ${index + 1}:`, link);
- console.log(` - selected_text: "${link.selected_text}"`);
- console.log(` - start_offset: ${link.start_offset}`);
- console.log(` - end_offset: ${link.end_offset}`);
-
- const span = this.highlightTextRange(
- documentContent,
- link.start_offset,
- link.end_offset,
- 'document-link',
- {
- 'data-link-id': link.id,
- 'data-target-document': link.target_document_id,
- 'data-target-title': link.target_document_title,
- 'title': `링크: ${link.target_document_title}${link.description ? '\n' + link.description : ''}`,
- 'style': 'color: #7C3AED; text-decoration: underline; cursor: pointer; background-color: rgba(124, 58, 237, 0.1);'
- }
- );
-
- if (span) {
- console.log(`✅ 링크 렌더링 성공: "${link.selected_text}"`);
- } else {
- console.log(`❌ 링크 렌더링 실패: "${link.selected_text}"`);
- }
- });
-
- // 링크 클릭 이벤트 추가
- const linkElements = documentContent.querySelectorAll('.document-link');
- linkElements.forEach(linkEl => {
- linkEl.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
-
- // 클릭된 위치의 모든 링크 찾기
- const clickedText = linkEl.textContent;
- const overlappingLinks = this.getOverlappingLinks(linkEl);
-
- // 링크 툴팁 표시
- this.showLinkTooltip(overlappingLinks, linkEl, clickedText);
- });
- });
- },
-
- // 겹치는 링크들 찾기
- getOverlappingLinks(clickedElement) {
- const clickedLinkId = clickedElement.getAttribute('data-link-id');
- const clickedText = clickedElement.textContent;
-
- // 동일한 텍스트 범위에 있는 모든 링크 찾기
- const overlappingLinks = this.documentLinks.filter(link => {
- // 클릭된 링크와 텍스트가 겹치는지 확인
- const linkElement = document.querySelector(`[data-link-id="${link.id}"]`);
- if (!linkElement) return false;
-
- // 텍스트가 겹치는지 확인 (간단한 방법: 텍스트 내용 비교)
- return linkElement.textContent === clickedText;
- });
-
- return overlappingLinks;
- },
-
- // 링크 툴팁 표시
- showLinkTooltip(links, element, selectedText) {
- // 기존 툴팁 제거
- this.hideTooltip();
-
- const tooltip = document.createElement('div');
- tooltip.id = 'link-tooltip';
- tooltip.className = 'absolute bg-white border border-gray-300 rounded-lg shadow-lg p-4 z-50 max-w-lg';
- tooltip.style.minWidth = '350px';
-
- let tooltipHTML = `
-
-
선택된 텍스트
-
- "${selectedText}"
-
-
- `;
-
- if (links.length > 1) {
- tooltipHTML += `
-
-
- 연결된 링크 (${links.length}개)
-
-
- `;
- }
-
- tooltipHTML += '';
-
- links.forEach(link => {
- tooltipHTML += `
-
-
-
-
${link.target_document_title}
- ${link.description ? `
${link.description}
` : ''}
-
- ${link.link_type === 'text_fragment' ? '텍스트 조각 링크' : '문서 링크'}
-
-
-
-
-
- `;
- });
-
- tooltipHTML += '
';
-
- tooltip.innerHTML = tooltipHTML;
-
- // 위치 계산 및 표시
- const rect = element.getBoundingClientRect();
- tooltip.style.top = (rect.bottom + window.scrollY + 10) + 'px';
- tooltip.style.left = Math.max(10, rect.left + window.scrollX - 175) + 'px';
-
- document.body.appendChild(tooltip);
-
- // 외부 클릭 시 툴팁 숨기기
- setTimeout(() => {
- document.addEventListener('click', this.handleTooltipOutsideClick.bind(this));
- }, 100);
- },
-
- // 백링크 하이라이트 렌더링 (LinkManager로 위임)
- async renderBacklinkHighlights() {
- if (this.linkManager) {
- this.linkManager.renderBacklinks();
- }
- },
-
- // 텍스트 범위 하이라이트 (하이라이트와 동일한 로직, 클래스명만 다름)
- highlightTextRange(container, startOffset, endOffset, className, attributes = {}) {
- console.log(`🎯 highlightTextRange 호출: ${startOffset}-${endOffset}, 클래스: ${className}`);
- const walker = document.createTreeWalker(
- container,
- NodeFilter.SHOW_TEXT,
- null,
- false
- );
-
- let currentOffset = 0;
- let startNode = null;
- let startNodeOffset = 0;
- let endNode = null;
- let endNodeOffset = 0;
-
- let node;
- while (node = walker.nextNode()) {
- const nodeLength = node.textContent.length;
- const nodeStart = currentOffset;
- const nodeEnd = currentOffset + nodeLength;
-
- // 시작 노드 찾기
- if (!startNode && nodeEnd > startOffset) {
- startNode = node;
- startNodeOffset = startOffset - nodeStart;
- }
-
- // 끝 노드 찾기
- if (!endNode && nodeEnd >= endOffset) {
- endNode = node;
- endNodeOffset = endOffset - nodeStart;
- break;
- }
-
- currentOffset += nodeLength;
- }
-
- if (!startNode || !endNode) return;
-
- try {
- // DOM 변경 전에 Range 유효성 검사
- if (!startNode.parentNode || !endNode.parentNode ||
- startNodeOffset < 0 || endNodeOffset < 0) {
- console.warn(`❌ 유효하지 않은 노드 또는 오프셋 (${className})`);
- return null;
- }
-
- const range = document.createRange();
-
- // Range 설정 시 예외 처리
- try {
- range.setStart(startNode, startNodeOffset);
- range.setEnd(endNode, endNodeOffset);
- } catch (rangeError) {
- console.warn(`❌ Range 설정 실패 (${className}):`, rangeError);
- return null;
- }
-
- // 빈 범위 체크
- if (range.collapsed) {
- console.warn(`❌ 빈 범위 (${className})`);
- range.detach();
- return null;
- }
-
- const span = document.createElement('span');
- span.className = className;
-
- // 속성 추가
- Object.entries(attributes).forEach(([key, value]) => {
- span.setAttribute(key, value);
- });
-
- // 더 안전한 하이라이트 적용 방식
- try {
- // 범위가 단일 텍스트 노드인지 확인
- if (startNode === endNode && startNode.nodeType === Node.TEXT_NODE) {
- // 단일 텍스트 노드인 경우 직접 분할
- const text = startNode.textContent;
- const beforeText = text.substring(0, startNodeOffset);
- const highlightText = text.substring(startNodeOffset, endNodeOffset);
- const afterText = text.substring(endNodeOffset);
-
- // 새로운 노드들 생성
- const parent = startNode.parentNode;
- const fragment = document.createDocumentFragment();
-
- if (beforeText) {
- fragment.appendChild(document.createTextNode(beforeText));
- }
-
- span.textContent = highlightText;
- fragment.appendChild(span);
-
- if (afterText) {
- fragment.appendChild(document.createTextNode(afterText));
- }
-
- // 원본 노드를 새로운 fragment로 교체
- parent.replaceChild(fragment, startNode);
- console.log(`✅ 안전한 하이라이트 적용: "${highlightText}" (${className})`);
- return span;
- } else {
- // 복잡한 경우 surroundContents 시도
- range.surroundContents(span);
- console.log(`✅ surroundContents 성공: "${span.textContent}" (${className})`);
- return span;
- }
- } catch (error) {
- console.warn(`❌ 하이라이트 적용 실패 (${className}):`, error);
- // 실패 시 범위만 표시하고 실제 DOM은 건드리지 않음
- return null;
- }
- } catch (error) {
- console.warn(`❌ highlightTextRange 실패 (${className}):`, error);
- return null;
- }
- },
-
- try {
- // 백링크 정보 가져오기
- console.log('🔍 백링크 API 호출 시작 - 문서 ID:', this.documentId);
- const backlinks = await api.getDocumentBacklinks(this.documentId);
- console.log('🔗 백링크 API 응답:', backlinks);
- console.log('🔗 백링크 개수:', backlinks.length);
-
- if (backlinks.length === 0) {
- console.log('⚠️ 백링크가 없습니다. 이 문서를 참조하는 링크가 없거나 권한이 없을 수 있습니다.');
- }
-
- const documentContent = document.getElementById('document-content');
- if (!documentContent) return;
-
- // 기존 백링크 하이라이트 제거
- const existingBacklinks = documentContent.querySelectorAll('.backlink-highlight');
- existingBacklinks.forEach(el => {
- const parent = el.parentNode;
- parent.replaceChild(document.createTextNode(el.textContent), el);
- parent.normalize();
- });
-
- // 백링크 하이라이트 적용 (간단한 방법)
- console.log(`🔗 백링크 렌더링 시작 - 총 ${backlinks.length}개`);
- console.log(`📄 현재 문서 내용 (처음 200자):`, documentContent.textContent.substring(0, 200));
- console.log(`📄 현재 문서 전체 길이:`, documentContent.textContent.length);
-
- // 백링크 렌더링 전략 결정
- console.log(`🎯 백링크 렌더링 전략:`);
- backlinks.forEach((backlink, index) => {
- console.log(`🔍 백링크 ${index + 1}:`);
- console.log(` - 타입: ${backlink.link_type}`);
- console.log(` - target_text: "${backlink.target_text || 'null'}"`);
- console.log(` - selected_text: "${backlink.selected_text}"`);
-
- if (backlink.link_type === 'document') {
- // 문서 레벨 백링크: 문서 제목이나 첫 번째 헤딩 찾기
- const titleElement = documentContent.querySelector('h1, h2, .title, title');
- if (titleElement) {
- console.log(`✅ 문서 레벨 백링크 - 제목 요소 발견: "${titleElement.textContent.trim()}"`);
- } else {
- console.log(`⚠️ 문서 레벨 백링크 - 제목 요소 없음`);
- }
- } else if (backlink.link_type === 'text_fragment') {
- // 텍스트 프래그먼트 백링크: selected_text가 현재 문서에 있는지 확인
- const searchText = backlink.selected_text;
- const found = documentContent.textContent.includes(searchText);
- console.log(`${found ? '✅' : '❌'} 텍스트 프래그먼트 백링크 - "${searchText}" 존재: ${found}`);
- }
- });
-
- if (backlinks.length === 0) {
- console.log(`⚠️ 백링크가 없어서 렌더링하지 않음`);
-
- // 테스트용: 강제로 백링크 표시 (디버깅용)
- console.log(`🧪 테스트용 백링크 강제 생성...`);
- const testText = "pressure vessel";
- if (documentContent.textContent.includes(testText)) {
- console.log(`🎯 테스트 텍스트 발견: "${testText}"`);
-
- // 간단한 텍스트 하이라이트
- const walker = document.createTreeWalker(
- documentContent,
- NodeFilter.SHOW_TEXT,
- null,
- false
- );
-
- let node;
- while (node = walker.nextNode()) {
- const text = node.textContent;
- const index = text.indexOf(testText);
-
- if (index !== -1) {
- console.log(`🎯 테스트 텍스트 노드에서 발견!`);
-
- try {
- const beforeText = text.substring(0, index);
- const matchText = text.substring(index, index + testText.length);
- const afterText = text.substring(index + testText.length);
-
- const parent = node.parentNode;
- const fragment = document.createDocumentFragment();
-
- if (beforeText) {
- fragment.appendChild(document.createTextNode(beforeText));
- }
-
- const span = document.createElement('span');
- span.className = 'backlink-highlight';
- span.textContent = matchText;
- span.style.cssText = 'color: #EA580C !important; text-decoration: underline !important; cursor: pointer !important; background-color: rgba(234, 88, 12, 0.2) !important; border: 2px solid #EA580C !important; border-radius: 4px !important; padding: 4px 6px !important; font-weight: bold !important; box-shadow: 0 2px 4px rgba(234, 88, 12, 0.3) !important;';
- span.setAttribute('title', '테스트 백링크');
-
- fragment.appendChild(span);
-
- if (afterText) {
- fragment.appendChild(document.createTextNode(afterText));
- }
-
- parent.replaceChild(fragment, node);
-
- console.log(`✅ 테스트 백링크 렌더링 성공: "${matchText}"`);
- break;
-
- } catch (error) {
- console.error(`❌ 테스트 백링크 렌더링 실패:`, error);
- }
- }
- }
- }
-
- return;
- }
-
- backlinks.forEach((backlink, index) => {
- console.log(`🔗 백링크 ${index + 1}:`, backlink);
-
- let searchText = null;
- let renderStrategy = null;
-
- if (backlink.link_type === 'document') {
- // 문서 레벨 백링크: 제목 요소 찾기
- const titleElement = documentContent.querySelector('h1, h2, .title, title');
- if (titleElement) {
- searchText = titleElement.textContent.trim();
- renderStrategy = 'title';
- console.log(`📋 문서 레벨 백링크 - 제목으로 렌더링: "${searchText}"`);
- }
- } else if (backlink.link_type === 'text_fragment') {
- // 텍스트 프래그먼트 백링크: selected_text 사용
- searchText = backlink.selected_text;
- renderStrategy = 'text';
- console.log(`📋 텍스트 프래그먼트 백링크 - 텍스트로 렌더링: "${searchText}"`);
- }
- if (!searchText) {
- console.log(`❌ selected_text가 없음`);
- return;
- }
-
- console.log(`🔍 텍스트 검색: "${searchText}"`);
- console.log(`📊 문서에 해당 텍스트 포함 여부:`, documentContent.textContent.includes(searchText));
-
- // 기존 링크 요소들 확인
- const existingLinks = documentContent.querySelectorAll('.document-link');
- console.log(`🔗 기존 링크 요소 개수:`, existingLinks.length);
- existingLinks.forEach((link, i) => {
- console.log(` 링크 ${i + 1}: "${link.textContent}"`);
- if (link.textContent.includes(searchText)) {
- console.log(` ⚠️ 이 링크가 백링크 텍스트를 포함하고 있음!`);
- }
- });
-
- // DOM에서 직접 텍스트 찾기 (링크 요소 포함)
- let found = false;
-
- // 1. 일반 텍스트 노드에서 찾기
- const walker = document.createTreeWalker(
- documentContent,
- NodeFilter.SHOW_TEXT,
- null,
- false
- );
-
- let node;
- while (node = walker.nextNode()) {
- const text = node.textContent;
- const index = text.indexOf(searchText);
-
- if (index !== -1) {
- console.log(`🎯 일반 텍스트에서 발견! 노드: "${text.substring(0, 50)}..."`);
-
- try {
- // 텍스트를 3부분으로 나누기
- const beforeText = text.substring(0, index);
- const matchText = text.substring(index, index + searchText.length);
- const afterText = text.substring(index + searchText.length);
-
- // 새로운 요소들 생성
- const parent = node.parentNode;
- const fragment = document.createDocumentFragment();
-
- if (beforeText) {
- fragment.appendChild(document.createTextNode(beforeText));
- }
-
- // 백링크 스팬 생성
- const span = document.createElement('span');
- span.className = 'backlink-highlight';
- span.textContent = matchText;
- span.style.cssText = 'color: #EA580C !important; text-decoration: underline !important; cursor: pointer !important; background-color: rgba(234, 88, 12, 0.2) !important; border: 2px solid #EA580C !important; border-radius: 4px !important; padding: 4px 6px !important; font-weight: bold !important; box-shadow: 0 2px 4px rgba(234, 88, 12, 0.3) !important;';
- span.setAttribute('data-backlink-id', backlink.id);
- span.setAttribute('data-source-document', backlink.source_document_id);
- span.setAttribute('data-source-title', backlink.source_document_title);
- span.setAttribute('title', `백링크: ${backlink.source_document_title}에서 참조`);
-
- // 백링크 클릭 이벤트 추가
- span.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- console.log(`🔗 백링크 클릭됨: ${backlink.source_document_title}`);
- this.showBacklinkTooltip(e, [backlink]);
- });
-
- fragment.appendChild(span);
-
- if (afterText) {
- fragment.appendChild(document.createTextNode(afterText));
- }
-
- // DOM에 교체
- parent.replaceChild(fragment, node);
-
- console.log(`✅ 백링크 렌더링 성공: "${matchText}"`);
-
- // 강제로 스타일 확인
- setTimeout(() => {
- const renderedBacklink = documentContent.querySelector('.backlink-highlight');
- if (renderedBacklink) {
- console.log(`🎯 백링크 DOM 확인됨:`, renderedBacklink);
- console.log(`🎯 백링크 스타일:`, renderedBacklink.style.cssText);
- console.log(`🎯 백링크 클래스:`, renderedBacklink.className);
- } else {
- console.error(`❌ 백링크 DOM에서 사라짐!`);
- }
- }, 100);
-
- found = true;
- break;
-
- } catch (error) {
- console.error(`❌ 백링크 렌더링 실패:`, error);
- }
- }
- }
-
- // 2. 링크 요소 내부에서 찾기 (일반 텍스트에서 못 찾은 경우)
- if (!found) {
- console.log(`🔍 링크 요소 내부에서 검색 시도...`);
-
- existingLinks.forEach((linkEl, i) => {
- if (linkEl.textContent.includes(searchText) && !found) {
- console.log(`🎯 링크 ${i + 1} 내부에서 발견: "${linkEl.textContent}"`);
-
- // 이미 백링크인지 확인
- if (linkEl.classList.contains('backlink-highlight')) {
- console.log(`⚠️ 이미 백링크로 렌더링됨: "${searchText}"`);
- found = true;
- return;
- }
-
- // 링크 요소를 백링크로 변경
- linkEl.className = 'backlink-highlight document-link'; // 두 클래스 모두 유지
- linkEl.style.cssText = 'color: #EA580C; text-decoration: underline; cursor: pointer; background-color: rgba(234, 88, 12, 0.1); border-left: 3px solid #EA580C; padding-left: 2px;';
- linkEl.setAttribute('data-backlink-id', backlink.id);
- linkEl.setAttribute('data-source-document', backlink.source_document_id);
- linkEl.setAttribute('data-source-title', backlink.source_document_title);
- linkEl.setAttribute('title', `백링크: ${backlink.source_document_title}에서 참조`);
-
- console.log(`✅ 링크를 백링크로 변경 성공: "${searchText}"`);
- found = true;
- }
- });
- }
-
- if (!found) {
- console.log(`❌ 텍스트를 찾을 수 없음: "${searchText}"`);
- }
- });
-
- // 백링크 클릭 이벤트 추가
- const backlinkElements = documentContent.querySelectorAll('.backlink-highlight');
- backlinkElements.forEach(backlinkEl => {
- backlinkEl.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
-
- // 클릭된 위치의 모든 백링크 찾기
- const clickedText = backlinkEl.textContent;
- const overlappingBacklinks = this.getOverlappingBacklinks(backlinkEl);
-
- // 백링크 툴팁 표시
- this.showBacklinkTooltip(overlappingBacklinks, backlinkEl, clickedText);
- });
- });
-
- } catch (error) {
- console.warn('백링크 하이라이트 렌더링 실패:', error);
- }
- },
-
- // 문서에서 특정 텍스트 찾기
- findTextInDocument(container, searchText) {
- const textNodes = [];
- const walker = document.createTreeWalker(
- container,
- NodeFilter.SHOW_TEXT,
- null,
- false
- );
-
- let node;
- while (node = walker.nextNode()) {
- if (node.textContent.includes(searchText)) {
- textNodes.push(node);
- }
- }
-
- return textNodes;
- },
-
- // 텍스트 노드를 스타일로 감싸기 (간단 버전)
- wrapTextNode(textNode, className, attributes = {}) {
- try {
- const parent = textNode.parentNode;
- const span = document.createElement('span');
-
- // span 설정
- span.className = className;
- span.textContent = textNode.textContent;
-
- // 속성 설정
- Object.keys(attributes).forEach(key => {
- if (key === 'style') {
- span.style.cssText = attributes[key];
- } else {
- span.setAttribute(key, attributes[key]);
- }
- });
-
- // DOM에 교체
- parent.replaceChild(span, textNode);
-
- return span;
- } catch (error) {
- console.error('텍스트 노드 감싸기 실패:', error);
- return null;
- }
- },
-
- // 겹치는 백링크들 찾기
- getOverlappingBacklinks(clickedElement) {
- const clickedBacklinkId = clickedElement.getAttribute('data-backlink-id');
- const clickedText = clickedElement.textContent;
-
- // 동일한 텍스트 범위에 있는 모든 백링크 찾기
- const overlappingBacklinks = this.backlinks.filter(backlink => {
- // 클릭된 백링크와 텍스트가 겹치는지 확인
- const backlinkElement = document.querySelector(`[data-backlink-id="${backlink.id}"]`);
- if (!backlinkElement) return false;
-
- // 텍스트가 겹치는지 확인 (간단한 방법: 텍스트 내용 비교)
- return backlinkElement.textContent === clickedText;
- });
-
- return overlappingBacklinks;
- },
-
- // 백링크 툴팁 표시
- showBacklinkTooltip(backlinks, element, selectedText) {
- // 기존 툴팁 제거
- this.hideTooltip();
-
- const tooltip = document.createElement('div');
- tooltip.id = 'backlink-tooltip';
- tooltip.className = 'absolute bg-white border border-gray-300 rounded-lg shadow-lg p-4 z-50 max-w-lg';
- tooltip.style.minWidth = '350px';
-
- let tooltipHTML = `
-
-
참조된 텍스트
-
- "${selectedText}"
-
-
- `;
-
- if (backlinks.length > 1) {
- tooltipHTML += `
-
-
- 이 텍스트를 참조하는 문서 (${backlinks.length}개)
-
-
- `;
- } else {
- tooltipHTML += `
-
- `;
- }
-
- tooltipHTML += '';
-
- backlinks.forEach(backlink => {
- tooltipHTML += `
-
-
-
-
${backlink.source_document_title}
- ${backlink.description ? `
${backlink.description}
` : ''}
-
- 원본 텍스트: "${backlink.selected_text}"
-
-
-
-
-
- `;
- });
-
- tooltipHTML += '
';
-
- tooltip.innerHTML = tooltipHTML;
-
- // 위치 계산 및 표시
- const rect = element.getBoundingClientRect();
- tooltip.style.top = (rect.bottom + window.scrollY + 10) + 'px';
- tooltip.style.left = Math.max(10, rect.left + window.scrollX - 175) + 'px';
-
- document.body.appendChild(tooltip);
-
- // 외부 클릭 시 툴팁 숨기기
- setTimeout(() => {
- document.addEventListener('click', this.handleTooltipOutsideClick.bind(this));
- }, 100);
- },
-
- // 백링크 문서로 이동
- navigateToBacklinkDocument(sourceDocumentId, backlinkInfo) {
- console.log('🔗 백링크로 이동:', sourceDocumentId, backlinkInfo);
-
- // 툴팁 숨기기
- this.hideTooltip();
-
- // 백링크 문서로 이동
- window.location.href = `/viewer.html?id=${sourceDocumentId}`;
- },
-
- // 링크된 문서로 이동 (특정 텍스트 위치 포함)
- navigateToLinkedDocument(targetDocumentId, linkInfo) {
- let targetUrl = `/viewer.html?id=${targetDocumentId}`;
-
- // 특정 텍스트 위치가 있는 경우 URL에 추가
- if (linkInfo && linkInfo.link_type === 'text_fragment' && linkInfo.target_text) {
- const params = new URLSearchParams({
- highlight_text: linkInfo.target_text,
- start_offset: linkInfo.target_start_offset,
- end_offset: linkInfo.target_end_offset
- });
- targetUrl += `&${params.toString()}`;
- }
-
- console.log('🔗 링크된 문서로 이동:', targetUrl);
- window.location.href = targetUrl;
- },
-
- // 기존 navigateToDocument 함수 (백워드 호환성)
- navigateToDocument(documentId) {
- window.location.href = `/viewer.html?id=${documentId}`;
- },
-
-
-
- // 특정 텍스트를 하이라이트하고 스크롤
- highlightAndScrollToText(targetText, startOffset, endOffset) {
- console.log('🎯 highlightAndScrollToText 호출됨:', { targetText, startOffset, endOffset });
-
- const documentContent = document.getElementById('document-content');
- if (!documentContent) {
- console.error('❌ document-content 요소를 찾을 수 없습니다');
- return;
- }
-
- // 백링크는 LinkManager가 관리하므로 별도 처리 불필요
- console.log('🔗 LinkManager가 백링크를 관리 중');
-
- console.log('📄 문서 내용 길이:', documentContent.textContent.length);
-
- try {
- // 임시 하이라이트 적용
- console.log('🎨 하이라이트 적용 시작...');
- const highlightElement = this.highlightTextRange(
- documentContent,
- startOffset,
- endOffset,
- 'linked-text-highlight',
- {
- 'style': 'background-color: #FEF3C7 !important; border: 2px solid #F59E0B; border-radius: 4px; padding: 2px;'
- }
- );
-
- console.log('🔍 하이라이트 요소 결과:', highlightElement);
-
- if (highlightElement) {
- console.log('📐 하이라이트 요소 위치:', highlightElement.getBoundingClientRect());
-
- // 해당 요소로 스크롤
- highlightElement.scrollIntoView({
- behavior: 'smooth',
- block: 'center'
- });
-
- console.log('✅ 링크된 텍스트로 스크롤 완료');
- } else {
- console.warn('⚠️ 링크된 텍스트를 찾을 수 없습니다');
- console.log('🔍 전체 텍스트 미리보기:', documentContent.textContent.substring(startOffset - 50, endOffset + 50));
- }
-
- // 5초 후 하이라이트 제거 및 백링크 복원 (하이라이트 성공 여부와 관계없이)
- const self = this;
- setTimeout(() => {
- const tempHighlight = document.querySelector('.linked-text-highlight');
- if (tempHighlight) {
- const parent = tempHighlight.parentNode;
- parent.replaceChild(document.createTextNode(tempHighlight.textContent), tempHighlight);
- parent.normalize();
- console.log('🗑️ 임시 하이라이트 제거됨');
- }
-
- // 백링크는 LinkManager가 관리하므로 별도 재렌더링 불필요
- console.log('✅ 임시 하이라이트 제거 완료 - 백링크는 LinkManager가 유지 관리');
- }, 5000);
-
- } catch (error) {
- console.error('❌ 텍스트 하이라이트 실패:', error);
- }
- },
-
- // 텍스트 오프셋 계산 (하이라이트와 동일한 로직)
- getTextOffset(container, node, offset) {
- let textOffset = 0;
- const walker = document.createTreeWalker(
- container,
- NodeFilter.SHOW_TEXT,
- null,
- false
- );
-
- let currentNode;
- while (currentNode = walker.nextNode()) {
- if (currentNode === node) {
- return textOffset + offset;
- }
- textOffset += currentNode.textContent.length;
- }
- return textOffset;
- },
-
- // 텍스트 범위 하이라이트 (하이라이트와 동일한 로직, 클래스명만 다름)
- highlightTextRange(container, startOffset, endOffset, className, attributes = {}) {
- console.log(`🎯 highlightTextRange 호출: ${startOffset}-${endOffset}, 클래스: ${className}`);
- const walker = document.createTreeWalker(
- container,
- NodeFilter.SHOW_TEXT,
- null,
- false
- );
-
- let currentOffset = 0;
- let startNode = null;
- let startNodeOffset = 0;
- let endNode = null;
- let endNodeOffset = 0;
-
- // 시작과 끝 노드 찾기
- let node;
- while (node = walker.nextNode()) {
- const nodeLength = node.textContent.length;
-
- if (!startNode && currentOffset + nodeLength > startOffset) {
- startNode = node;
- startNodeOffset = startOffset - currentOffset;
- }
-
- if (currentOffset + nodeLength >= endOffset) {
- endNode = node;
- endNodeOffset = endOffset - currentOffset;
- break;
- }
-
- currentOffset += nodeLength;
- }
-
- if (!startNode || !endNode) return;
-
- try {
- // DOM 변경 전에 Range 유효성 검사
- if (!startNode.parentNode || !endNode.parentNode ||
- startNodeOffset < 0 || endNodeOffset < 0) {
- console.warn(`❌ 유효하지 않은 노드 또는 오프셋 (${className})`);
- return null;
- }
-
- const range = document.createRange();
-
- // Range 설정 시 예외 처리
- try {
- range.setStart(startNode, startNodeOffset);
- range.setEnd(endNode, endNodeOffset);
- } catch (rangeError) {
- console.warn(`❌ Range 설정 실패 (${className}):`, rangeError);
- return null;
- }
-
- // 빈 범위 체크
- if (range.collapsed) {
- console.warn(`❌ 빈 범위 (${className})`);
- range.detach();
- return null;
- }
-
- const span = document.createElement('span');
- span.className = className;
-
- // 속성 추가
- Object.entries(attributes).forEach(([key, value]) => {
- span.setAttribute(key, value);
- });
-
- // 더 안전한 하이라이트 적용 방식
- try {
- // 범위가 단일 텍스트 노드인지 확인
- if (startNode === endNode && startNode.nodeType === Node.TEXT_NODE) {
- // 단일 텍스트 노드인 경우 직접 분할
- const text = startNode.textContent;
- const beforeText = text.substring(0, startNodeOffset);
- const highlightText = text.substring(startNodeOffset, endNodeOffset);
- const afterText = text.substring(endNodeOffset);
-
- // 새로운 노드들 생성
- const parent = startNode.parentNode;
- const fragment = document.createDocumentFragment();
-
- if (beforeText) {
- fragment.appendChild(document.createTextNode(beforeText));
- }
-
- span.textContent = highlightText;
- fragment.appendChild(span);
-
- if (afterText) {
- fragment.appendChild(document.createTextNode(afterText));
- }
-
- // 원본 노드를 새로운 fragment로 교체
- parent.replaceChild(fragment, startNode);
- console.log(`✅ 안전한 하이라이트 적용: "${highlightText}" (${className})`);
- return span;
- } else {
- // 복잡한 경우 surroundContents 시도
- range.surroundContents(span);
- console.log(`✅ surroundContents 성공: "${span.textContent}" (${className})`);
- return span;
- }
- } catch (error) {
- console.warn(`❌ 하이라이트 적용 실패 (${className}):`, error);
- // 실패 시 범위만 표시하고 실제 DOM은 건드리지 않음
- return null;
- }
- } catch (error) {
- console.warn(`❌ highlightTextRange 실패 (${className}):`, error);
- return null;
- }
- },
-
- // 백링크 배너 업데이트 (LinkManager 데이터 사용)
- updateBacklinkBanner() {
- const backlinkCount = this.backlinks ? this.backlinks.length : 0;
- const backlinkBanner = document.getElementById('backlink-banner');
- if (backlinkBanner) {
- const countElement = backlinkBanner.querySelector('.backlink-count');
- if (countElement) {
- countElement.textContent = backlinkCount;
- }
- }
- },
-
- // 특정 텍스트를 하이라이트하고 스크롤
- highlightAndScrollToText(targetText, startOffset, endOffset) {
- console.log('🎯 highlightAndScrollToText 호출됨:', { targetText, startOffset, endOffset });
-
- const documentContent = document.getElementById('document-content');
- if (!documentContent) {
- console.error('❌ document-content 요소를 찾을 수 없습니다');
- return;
- }
-
- // 백링크는 LinkManager가 관리하므로 별도 처리 불필요
- console.log('🔗 LinkManager가 백링크를 관리 중');
-
- console.log('📄 문서 내용 길이:', documentContent.textContent.length);
-
- try {
- // 임시 하이라이트 적용
- console.log('🎨 하이라이트 적용 시작...');
-
- const highlightElement = this.highlightTextRange(
- documentContent,
- startOffset,
- endOffset,
- 'linked-text-highlight',
- {
- 'style': 'background-color: #FEF3C7 !important; border: 2px solid #F59E0B; border-radius: 4px; padding: 2px;'
- }
- );
-
- console.log('🔍 하이라이트 요소 결과:', highlightElement);
-
- if (highlightElement) {
- // 요소 위치 가져오기
- const rect = highlightElement.getBoundingClientRect();
- console.log('📀 하이라이트 요소 위치:', rect);
-
- // 스크롤
- const scrollTop = window.pageYOffset + rect.top - window.innerHeight / 2;
- window.scrollTo({ top: scrollTop, behavior: 'smooth' });
- console.log('✅ 링크된 텍스트로 스크롤 완료');
- } else {
- console.warn('⚠️ 링크된 텍스트를 찾을 수 없습니다');
- console.log('🔍 전체 텍스트 미리보기:', documentContent.textContent.substring(startOffset - 50, endOffset + 50));
- }
-
- // 5초 후 하이라이트 제거 및 백링크 복원 (하이라이트 성공 여부와 관계없이)
- const self = this;
- setTimeout(() => {
- const tempHighlight = document.querySelector('.linked-text-highlight');
- if (tempHighlight) {
- const parent = tempHighlight.parentNode;
- parent.replaceChild(document.createTextNode(tempHighlight.textContent), tempHighlight);
- parent.normalize();
- console.log('🗑️ 임시 하이라이트 제거됨');
- }
-
- // 백링크는 LinkManager가 관리하므로 별도 재렌더링 불필요
- console.log('✅ 임시 하이라이트 제거 완료 - 백링크는 LinkManager가 유지 관리');
- }, 5000);
-
- } catch (error) {
- console.error('❌ 텍스트 하이라이트 실패:', error);
- }
- },
-
- try {
- console.log('🔗 백링크 로드 중...');
- console.log('📋 현재 문서 ID:', this.documentId);
- console.log('📋 현재 문서 제목:', this.documentTitle);
-
- this.backlinks = await window.api.getDocumentBacklinks(this.documentId);
- console.log(`✅ 백링크 ${this.backlinks.length}개 로드됨:`, this.backlinks);
-
- // 각 백링크의 상세 정보 출력
- this.backlinks.forEach((backlink, index) => {
- console.log(`🔗 백링크 ${index + 1}:`);
- console.log(` - 소스 문서: ${backlink.source_document_title}`);
- console.log(` - 타겟 문서: ${backlink.target_document_title} (현재 문서와 일치해야 함)`);
- console.log(` - 선택된 텍스트: "${backlink.selected_text}"`);
- console.log(` - 링크 타입: ${backlink.link_type}`);
-
- // 현재 문서와 일치하는지 확인
- if (backlink.target_document_id !== this.documentId) {
- console.warn(`⚠️ 백링크 타겟 문서 ID 불일치!`);
- console.warn(` - 백링크 타겟: ${backlink.target_document_id}`);
- console.warn(` - 현재 문서: ${this.documentId}`);
- }
- });
-
- // Alpine.js 상태 업데이트 강제
- if (window.Alpine && window.Alpine.store) {
- console.log('🔄 Alpine.js 상태 업데이트 시도...');
- }
-
- } catch (error) {
- console.error('백링크 로드 실패:', error);
- this.backlinks = [];
- }
- },
-
- navigateToBacklink(backlink) {
- console.log('🔗 배너에서 백링크로 이동:', backlink);
-
- // 백링크의 출발 문서로 이동
- const url = `/viewer.html?id=${backlink.source_document_id}`;
- console.log('🔗 이동할 URL:', url);
-
- window.location.href = url;
- },
-
- // 링크 배너에서 링크로 이동
- navigateToLink(link) {
- console.log('🔗 배너에서 링크로 이동:', link);
-
- // 링크의 대상 문서로 이동
- const url = `/viewer.html?id=${link.target_document_id}`;
- console.log('🔗 이동할 URL:', url);
-
- // 텍스트 조각 링크인 경우 해당 위치로 스크롤
- if (link.link_type === 'text_fragment' && link.target_text) {
- const urlWithFragment = `${url}&highlight=${link.target_start_offset}-${link.target_end_offset}&text=${encodeURIComponent(link.target_text)}`;
- window.location.href = urlWithFragment;
- } else {
- window.location.href = url;
- }
- },
-
- // 고급 링크 기능 메서드들
- onTargetDocumentChange() {
- // 대상 문서가 변경되면 target_text 초기화
- this.linkForm.target_text = '';
- this.linkForm.target_start_offset = 0;
- this.linkForm.target_end_offset = 0;
- },
-
- openTargetDocumentSelector() {
- console.log('🎯 openTargetDocumentSelector 함수 호출됨!');
- console.log('📋 현재 linkForm.target_document_id:', this.linkForm.target_document_id);
-
- if (!this.linkForm.target_document_id) {
- alert('먼저 대상 문서를 선택해주세요.');
- return;
- }
-
- // 새 창에서 대상 문서 열기 (텍스트 선택 모드 전용 페이지)
- const targetUrl = `/text-selector.html?id=${this.linkForm.target_document_id}`;
- console.log('🚀 텍스트 선택 창 열기:', targetUrl);
- 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 });
- },
-
- // 텍스트 선택 모드 초기화
- async initTextSelectorMode() {
- console.log('🎯 텍스트 선택 모드로 초기화 중...');
-
- // Alpine.js 완전 차단
- window.Alpine = {
- start: () => console.log('Alpine.js 초기화 차단됨'),
- data: () => ({}),
- directive: () => {},
- magic: () => {},
- store: () => ({}),
- version: '3.0.0'
- };
-
- // 기존 Alpine 인스턴스 제거
- if (window.Alpine && window.Alpine.stop) {
- window.Alpine.stop();
- }
-
- // 인증 확인
- if (!api.token) {
- window.location.href = '/';
- return;
- }
-
- try {
- // 문서만 로드 (다른 데이터는 불필요)
- await this.loadDocument();
-
- // UI 설정
- console.log('🔧 텍스트 선택 모드 UI 설정 시작');
- this.setupTextSelectorUI();
- console.log('✅ 텍스트 선택 모드 UI 설정 완료');
-
- } catch (error) {
- console.error('텍스트 선택 모드 초기화 실패:', error);
- this.error = '문서를 불러올 수 없습니다: ' + error.message;
- } finally {
- this.loading = false;
- }
- },
-
- // 텍스트 선택 모드 UI 설정
- setupTextSelectorUI() {
- console.log('🔧 setupTextSelectorUI 함수 실행됨');
-
- // 이미 설정되었는지 확인
- if (this.textSelectorUISetup) {
- console.log('⚠️ 텍스트 선택 모드 UI가 이미 설정됨 - 중복 실행 방지');
- return;
- }
-
- // 헤더를 텍스트 선택 모드용으로 변경
- const header = document.querySelector('header');
- console.log('📋 헤더 요소 찾기:', header);
-
- if (header) {
- console.log('🎨 헤더 HTML 교체 중...');
-
- // 기존 Alpine 속성 제거
- header.removeAttribute('x-data');
- header.removeAttribute('x-init');
-
- header.innerHTML = `
-
-
-
-
-
-
텍스트 선택 모드
-
연결하고 싶은 텍스트를 선택하세요
-
-
-
-
-
-
-
-
- `;
-
- // 헤더가 다시 변경되지 않도록 보호
- header.setAttribute('data-text-selector-mode', 'true');
- console.log('🔒 헤더 보호 설정 완료');
-
- // 실제 헤더 내용 확인
- console.log('📄 헤더 HTML 확인:', header.innerHTML.substring(0, 200) + '...');
-
- // 언어전환 버튼 확인
- const langBtn = header.querySelector('#language-toggle-selector');
- console.log('🌐 언어전환 버튼 찾기:', langBtn);
-
- // 취소 버튼 확인
- const closeBtn = header.querySelector('button[onclick*="window.close"]');
- console.log('❌ 취소 버튼 찾기:', closeBtn);
-
- // 헤더 변경 감지
- const observer = new MutationObserver((mutations) => {
- mutations.forEach((mutation) => {
- if (mutation.type === 'childList' || mutation.type === 'attributes') {
- console.log('⚠️ 헤더가 변경되었습니다!', mutation);
- console.log('🔍 변경 후 헤더 내용:', header.innerHTML.substring(0, 100) + '...');
- }
- });
- });
-
- observer.observe(header, {
- childList: true,
- subtree: true,
- attributes: true,
- attributeOldValue: true
- });
- }
-
- // 사이드 패널 숨기기
- const aside = document.querySelector('aside');
- if (aside) {
- aside.style.display = 'none';
- }
-
- // 메인 컨텐츠 영역 조정
- const main = document.querySelector('main');
- if (main) {
- main.style.marginRight = '0';
- main.classList.add('text-selector-mode');
- }
-
- // 문서 콘텐츠에 텍스트 선택 이벤트 추가
- const documentContent = document.getElementById('document-content');
- if (documentContent) {
- documentContent.addEventListener('mouseup', this.handleTextSelectionForLinking.bind(this));
-
- // 선택 가능한 영역임을 시각적으로 표시
- documentContent.style.cursor = 'crosshair';
- documentContent.style.userSelect = 'text';
-
- // 안내 메시지 추가
- const guideDiv = document.createElement('div');
- guideDiv.className = 'bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6';
- guideDiv.innerHTML = `
-
-
-
-
텍스트 선택 방법
-
- 마우스로 연결하고 싶은 텍스트를 드래그하여 선택하세요.
- 선택이 완료되면 자동으로 부모 창으로 전달됩니다.
-
-
-
- `;
- documentContent.parentNode.insertBefore(guideDiv, documentContent);
- }
-
- // Alpine.js 컴포넌트 비활성화 (텍스트 선택 모드에서는 불필요)
- const alpineElements = document.querySelectorAll('[x-data]');
- alpineElements.forEach(el => {
- el.removeAttribute('x-data');
- });
-
- // 설정 완료 플래그
- this.textSelectorUISetup = true;
- console.log('✅ 텍스트 선택 모드 UI 설정 완료');
- },
-
- // 텍스트 선택 모드에서의 텍스트 선택 처리
- handleTextSelectionForLinking() {
- const selection = window.getSelection();
- if (!selection.rangeCount || selection.isCollapsed) return;
-
- const range = selection.getRangeAt(0);
- const selectedText = selection.toString().trim();
-
- if (selectedText.length < 3) {
- alert('최소 3글자 이상 선택해주세요.');
- return;
- }
-
- if (selectedText.length > 500) {
- alert('선택된 텍스트가 너무 깁니다. 500자 이하로 선택해주세요.');
- return;
- }
-
- // 텍스트 오프셋 계산
- const documentContent = document.getElementById('document-content');
- const { startOffset, endOffset } = this.getTextOffset(documentContent, range);
-
- console.log('🎯 텍스트 선택됨:', {
- selectedText,
- startOffset,
- endOffset
- });
-
- // 선택 확인 UI 표시
- this.showTextSelectionConfirm(selectedText, startOffset, endOffset);
- },
-
- // 텍스트 선택 확인 UI
- showTextSelectionConfirm(selectedText, startOffset, endOffset) {
- // 기존 확인 UI 제거
- const existingConfirm = document.querySelector('.text-selection-confirm');
- if (existingConfirm) {
- existingConfirm.remove();
- }
-
- // 확인 UI 생성
- const confirmDiv = document.createElement('div');
- confirmDiv.className = 'text-selection-confirm fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-white rounded-lg shadow-2xl border p-6 max-w-md z-50';
-
- // 텍스트 미리보기 (안전하게 처리)
- const previewText = selectedText.length > 100 ? selectedText.substring(0, 100) + '...' : selectedText;
-
- confirmDiv.innerHTML = `
-
-
-
-
-
텍스트가 선택되었습니다
-
-
-
-
-
-
- `;
-
- // 텍스트 안전하게 설정
- const previewElement = confirmDiv.querySelector('#selected-text-preview');
- previewElement.textContent = `"${previewText}"`;
-
- // 이벤트 리스너 추가
- const reselectBtn = confirmDiv.querySelector('#reselect-btn');
- const confirmBtn = confirmDiv.querySelector('#confirm-selection-btn');
-
- reselectBtn.addEventListener('click', () => {
- confirmDiv.remove();
- });
-
- confirmBtn.addEventListener('click', () => {
- this.confirmTextSelection(selectedText, startOffset, endOffset);
- });
-
- document.body.appendChild(confirmDiv);
-
- // 10초 후 자동 제거 (사용자가 선택하지 않은 경우)
- setTimeout(() => {
- if (document.contains(confirmDiv)) {
- confirmDiv.remove();
- }
- }, 10000);
- },
-
- // 텍스트 선택 확정
- confirmTextSelection(selectedText, startOffset, endOffset) {
- // 부모 창에 선택된 텍스트 정보 전달
- if (window.opener) {
- window.opener.postMessage({
- type: 'TEXT_SELECTED',
- selectedText: selectedText,
- startOffset: startOffset,
- endOffset: endOffset
- }, '*');
-
- console.log('✅ 부모 창에 텍스트 선택 정보 전달됨');
-
- // 성공 메시지 표시 후 창 닫기
- const confirmDiv = document.querySelector('.text-selection-confirm');
- if (confirmDiv) {
- confirmDiv.innerHTML = `
-
-
-
-
-
선택 완료!
-
창이 자동으로 닫힙니다...
-
- `;
-
- setTimeout(() => {
- window.close();
- }, 1500);
- }
- } else {
- alert('부모 창을 찾을 수 없습니다.');
- }
- },
-
- // 기능 메뉴 토글
- toggleFeatureMenu(feature) {
- if (this.activeFeatureMenu === feature) {
- this.activeFeatureMenu = null;
- } else {
- this.activeFeatureMenu = feature;
-
- // 해당 기능의 모달 표시
- switch(feature) {
- case 'link':
- this.showLinksModal = true;
- break;
- case 'memo':
- this.showNotesModal = true;
- break;
- case 'bookmark':
- this.showBookmarksModal = true;
- break;
- case 'backlink':
- this.showBacklinksModal = true;
- break;
- }
- }
- },
-
- // 링크 모드 활성화
- activateLinkMode() {
- if (this.contentType === 'note') {
- alert('📝 노트에서는 링크 기능이 향후 지원 예정입니다.');
- return;
- }
-
- console.log('🔗 링크 모드 활성화 - activateLinkMode 함수 실행됨');
-
- // 이미 선택된 텍스트가 있는지 확인
- const selection = window.getSelection();
- console.log('📝 현재 선택 상태:', {
- rangeCount: selection.rangeCount,
- isCollapsed: selection.isCollapsed,
- selectedText: selection.toString()
- });
-
- if (selection.rangeCount > 0 && !selection.isCollapsed) {
- const selectedText = selection.toString().trim();
- if (selectedText.length > 0) {
- console.log('✅ 선택된 텍스트 발견:', selectedText);
- this.selectedText = selectedText;
- this.selectedRange = selection.getRangeAt(0);
- console.log('🔗 LinkManager로 링크 생성 위임');
- this.linkManager.createLinkFromSelection(this.documentId, selectedText, selection.getRangeAt(0));
- return;
- }
- }
-
- // 선택된 텍스트가 없으면 선택 모드 활성화
- console.log('📝 텍스트 선택 모드 활성화');
- this.activeMode = 'link';
- this.showSelectionMessage('텍스트를 선택하세요.');
-
- // 기존 리스너 제거 후 새로 추가
- this.removeTextSelectionListener();
- this.textSelectionHandler = this.handleTextSelection.bind(this);
- document.addEventListener('mouseup', this.textSelectionHandler);
- },
-
- // 메모 모드 활성화
- activateNoteMode() {
- console.log('📝 메모 모드 활성화');
-
- // 이미 선택된 텍스트가 있는지 확인
- const selection = window.getSelection();
- if (selection.rangeCount > 0 && !selection.isCollapsed) {
- const selectedText = selection.toString().trim();
- if (selectedText.length > 0) {
- console.log('✅ 선택된 텍스트 발견:', selectedText);
- this.selectedText = selectedText;
- this.selectedRange = selection.getRangeAt(0);
- this.createNoteFromSelection();
- return;
- }
- }
-
- // 선택된 텍스트가 없으면 선택 모드 활성화
- console.log('📝 텍스트 선택 모드 활성화');
- this.activeMode = 'memo';
- this.showSelectionMessage('텍스트를 선택하세요.');
-
- // 기존 리스너 제거 후 새로 추가
- this.removeTextSelectionListener();
- this.textSelectionHandler = this.handleTextSelection.bind(this);
- document.addEventListener('mouseup', this.textSelectionHandler);
- },
-
- // 책갈피 모드 활성화
- activateBookmarkMode() {
- console.log('🔖 책갈피 모드 활성화');
-
- // 이미 선택된 텍스트가 있는지 확인
- const selection = window.getSelection();
- if (selection.rangeCount > 0 && !selection.isCollapsed) {
- const selectedText = selection.toString().trim();
- if (selectedText.length > 0) {
- console.log('✅ 선택된 텍스트 발견:', selectedText);
- this.selectedText = selectedText;
- this.selectedRange = selection.getRangeAt(0);
- this.createBookmarkFromSelection();
- return;
- }
- }
-
- // 선택된 텍스트가 없으면 선택 모드 활성화
- console.log('📝 텍스트 선택 모드 활성화');
- this.activeMode = 'bookmark';
- this.showSelectionMessage('텍스트를 선택하세요.');
-
- // 기존 리스너 제거 후 새로 추가
- this.removeTextSelectionListener();
- this.textSelectionHandler = this.handleTextSelection.bind(this);
- document.addEventListener('mouseup', this.textSelectionHandler);
- },
-
- // 텍스트 선택 리스너 제거
- removeTextSelectionListener() {
- if (this.textSelectionHandler) {
- document.removeEventListener('mouseup', this.textSelectionHandler);
- this.textSelectionHandler = null;
- }
- },
-
- // 텍스트 선택 처리
- handleTextSelection(event) {
- console.log('🎯 텍스트 선택 이벤트 발생');
-
- const selection = window.getSelection();
- if (selection.rangeCount > 0 && !selection.isCollapsed) {
- const range = selection.getRangeAt(0);
- const selectedText = selection.toString().trim();
-
- console.log('📝 선택된 텍스트:', selectedText);
-
- if (selectedText.length > 0) {
- this.selectedText = selectedText;
- this.selectedRange = range;
-
- // 선택 모드에 따라 다른 동작
- console.log('🎯 현재 모드:', this.activeMode);
-
- if (this.activeMode === 'link') {
- console.log('🔗 링크 생성 실행');
- this.createDocumentLink();
- } else if (this.activeMode === 'memo') {
- console.log('📝 메모 생성 실행');
- this.createNoteFromSelection();
- } else if (this.activeMode === 'bookmark') {
- console.log('🔖 책갈피 생성 실행');
- this.createBookmarkFromSelection();
- }
-
- // 모드 해제
- this.activeMode = null;
- this.hideSelectionMessage();
- this.removeTextSelectionListener();
- }
- }
- },
-
- // 선택 메시지 표시
- showSelectionMessage(message) {
- // 기존 메시지 제거
- const existingMessage = document.querySelector('.selection-message');
- if (existingMessage) {
- existingMessage.remove();
- }
-
- // 새 메시지 생성
- const messageDiv = document.createElement('div');
- messageDiv.className = 'selection-message fixed top-20 left-1/2 transform -translate-x-1/2 bg-blue-500 text-white px-4 py-2 rounded-lg shadow-lg z-50';
- messageDiv.textContent = message;
- document.body.appendChild(messageDiv);
- },
-
- // 선택 메시지 숨기기
- hideSelectionMessage() {
- const existingMessage = document.querySelector('.selection-message');
- if (existingMessage) {
- existingMessage.remove();
- }
- },
-
- // 링크 생성 UI 표시
- showLinkCreationUI() {
- this.createDocumentLink();
- },
-
- // 선택된 텍스트로 메모 생성 (HighlightManager로 위임)
- async createNoteFromSelection() {
- if (!this.selectedText || !this.selectedRange) return;
-
- try {
- // HighlightManager의 상태 설정
- this.highlightManager.selectedText = this.selectedText;
- this.highlightManager.selectedRange = this.selectedRange;
- this.highlightManager.selectedHighlightColor = '#FFFF00';
-
- // HighlightManager의 createNoteFromSelection 호출
- await this.highlightManager.createNoteFromSelection(this.documentId, this.contentType);
-
- // 상태 동기화
- this.highlights = this.highlightManager.highlights;
- this.notes = this.highlightManager.notes;
-
- return;
-
- } catch (error) {
- console.error('메모 생성 실패:', error);
- alert('메모 생성에 실패했습니다.');
- }
- },
-
- // 선택된 텍스트로 책갈피 생성
- async createBookmarkFromSelection() {
- if (!this.selectedText || !this.selectedRange) return;
-
- try {
- // 하이라이트 생성 (책갈피는 주황색)
- const highlightData = await this.createHighlight(this.selectedText, this.selectedRange, '#FFA500');
-
- // 책갈피 생성
- const bookmarkData = {
- highlight_id: highlightData.id,
- title: this.selectedText.substring(0, 50) + (this.selectedText.length > 50 ? '...' : '')
- };
-
- const bookmark = await api.createBookmark(this.documentId, bookmarkData);
-
- // 데이터 새로고침
- await this.loadBookmarks();
-
- alert('책갈피가 생성되었습니다.');
-
- } catch (error) {
- console.error('책갈피 생성 실패:', error);
- alert('책갈피 생성에 실패했습니다.');
- }
- },
-
- // 대상 선택 초기화
- resetTargetSelection() {
- this.linkForm.target_book_id = '';
- this.linkForm.target_document_id = '';
- this.linkForm.target_text = '';
- this.linkForm.target_start_offset = null;
- this.linkForm.target_end_offset = null;
- this.filteredDocuments = [];
-
- // 같은 서적인 경우 현재 서적의 문서들 로드
- if (this.linkForm.book_scope === 'same') {
- this.loadSameBookDocuments();
- }
- },
-
- // 같은 서적의 문서들 로드
- async loadSameBookDocuments() {
- try {
- if (this.navigation?.book_info?.id) {
- // 현재 서적의 문서들만 가져오기
- const allDocuments = await api.getLinkableDocuments(this.documentId);
- this.filteredDocuments = allDocuments.filter(doc =>
- doc.book_id === this.navigation.book_info.id && doc.id !== this.documentId
- );
- console.log('📚 같은 서적 문서들:', this.filteredDocuments);
- } else {
- // 서적 정보가 없으면 모든 문서
- this.filteredDocuments = await api.getLinkableDocuments(this.documentId);
- }
- } catch (error) {
- console.error('같은 서적 문서 로드 실패:', error);
- this.filteredDocuments = [];
- }
- },
-
- // 서적별 문서 로드
- async loadDocumentsFromBook() {
- try {
- if (this.linkForm.target_book_id) {
- // 선택된 서적의 문서들만 가져오기
- const allDocuments = await 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 = [];
- }
- },
-
- // 사용 가능한 서적 목록 로드
- async loadAvailableBooks() {
- try {
- console.log('📚 서적 목록 로딩 시작...');
-
- // 문서 목록에서 서적 정보 추출
- const allDocuments = await api.getLinkableDocuments(this.documentId);
- console.log('📄 모든 문서들:', allDocuments);
-
- // 서적별로 그룹화
- 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
- });
- }
- });
-
- // 현재 서적 제외
- const currentBookId = this.navigation?.book_info?.id;
- if (currentBookId) {
- bookMap.delete(currentBookId);
- }
-
- this.availableBooks = Array.from(bookMap.values());
- console.log('📚 사용 가능한 서적들:', this.availableBooks);
- console.log('🔍 현재 서적 ID:', currentBookId);
- } catch (error) {
- console.error('서적 목록 로드 실패:', error);
- this.availableBooks = [];
- }
- },
-
- // 선택된 서적 제목 가져오기
- getSelectedBookTitle() {
- const selectedBook = this.availableBooks.find(book => book.id === this.linkForm.target_book_id);
- return selectedBook ? selectedBook.title : '';
- }
-});
diff --git a/frontend/todos.html b/frontend/todos.html
index fb1697b..95bc096 100644
--- a/frontend/todos.html
+++ b/frontend/todos.html
@@ -226,12 +226,28 @@
-
-
-
+
+
+
할일관리
-
간편한 할일 관리
+
효율적인 일정 관리와 생산성 향상
+
+
+
+
+
+ 검토필요 개
+
+
+
+ 진행중 개
+
+
+
+ 완료 개
+
+