- 하이라이트: '메모 보기' 버튼으로 하이라이트 툴팁 표시 - 링크: '문서로 이동' 버튼으로 바로 연결된 문서로 이동 - 백링크: '원본으로 이동' 버튼으로 바로 원본 문서로 이동 - 각 항목에 '상세보기' 버튼으로 상세 정보 툴팁 표시 - 메뉴 UI 개선: 크기 확대, 아이콘 추가, 레이아웃 개선 - 링크/백링크 툴팁을 메모처럼 상세하게 개선 (날짜, 아이콘, 그라데이션) - 겹침 감지 로직 디버깅 및 안정화
1147 lines
48 KiB
JavaScript
1147 lines
48 KiB
JavaScript
/**
|
|
* LinkManager 모듈
|
|
* 문서 링크 및 백링크 통합 관리
|
|
*/
|
|
class LinkManager {
|
|
constructor(api) {
|
|
console.log('🔗 LinkManager 초기화 시작');
|
|
this.api = api;
|
|
// 캐싱된 API 사용 (사용 가능한 경우)
|
|
this.cachedApi = window.cachedApi || api;
|
|
this.documentLinks = [];
|
|
this.backlinks = [];
|
|
|
|
// 안전한 초기화 확인
|
|
console.log('🔧 LinkManager 초기화 - backlinks 타입:', typeof this.backlinks, Array.isArray(this.backlinks));
|
|
this.selectedText = '';
|
|
this.selectedRange = null;
|
|
this.availableBooks = [];
|
|
this.filteredDocuments = [];
|
|
|
|
console.log('✅ LinkManager 초기화 완료');
|
|
}
|
|
|
|
/**
|
|
* 문서 링크 데이터 로드
|
|
*/
|
|
async loadDocumentLinks(documentId) {
|
|
try {
|
|
console.log('📡 링크 API 호출:', `/documents/${documentId}/links`);
|
|
console.log('📡 사용 중인 documentId:', documentId);
|
|
console.log('📡 cachedApi 객체:', this.cachedApi);
|
|
const response = await this.cachedApi.get(`/documents/${documentId}/links`, {}, { category: 'links' });
|
|
console.log('📡 원본 API 응답:', response);
|
|
console.log('📡 응답 타입:', typeof response);
|
|
console.log('📡 응답이 배열인가?', Array.isArray(response));
|
|
console.log('📡 응답 JSON 문자열:', JSON.stringify(response, null, 2));
|
|
console.log('📡 응답 키들:', Object.keys(response || {}));
|
|
|
|
// API 응답이 객체일 경우 처리
|
|
if (Array.isArray(response)) {
|
|
this.documentLinks = response;
|
|
} else if (response && typeof response === 'object') {
|
|
// 객체에서 배열 추출 시도
|
|
if (response.data && Array.isArray(response.data)) {
|
|
this.documentLinks = response.data;
|
|
} else if (response.links && Array.isArray(response.links)) {
|
|
this.documentLinks = response.links;
|
|
} else {
|
|
console.warn('⚠️ 예상치 못한 API 응답 구조:', response);
|
|
this.documentLinks = [];
|
|
}
|
|
} else {
|
|
this.documentLinks = [];
|
|
}
|
|
|
|
console.log('📡 최종 링크 데이터:', this.documentLinks);
|
|
console.log('📡 최종 링크 개수:', this.documentLinks.length);
|
|
return this.documentLinks;
|
|
} catch (error) {
|
|
console.error('❌ 문서 링크 로드 실패:', error);
|
|
this.documentLinks = [];
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 백링크 데이터 로드
|
|
*/
|
|
async loadBacklinks(documentId) {
|
|
try {
|
|
console.log('📡 백링크 API 호출:', `/documents/${documentId}/backlinks`);
|
|
const response = await this.cachedApi.get(`/documents/${documentId}/backlinks`, {}, { category: 'links' });
|
|
console.log('📡 원본 백링크 응답:', response);
|
|
console.log('📡 백링크 응답 타입:', typeof response);
|
|
console.log('📡 백링크 응답이 배열인가?', Array.isArray(response));
|
|
console.log('📡 백링크 응답 JSON 문자열:', JSON.stringify(response, null, 2));
|
|
console.log('📡 백링크 응답 키들:', Object.keys(response || {}));
|
|
|
|
// API 응답이 객체일 경우 처리
|
|
if (Array.isArray(response)) {
|
|
this.backlinks = response;
|
|
} else if (response && typeof response === 'object') {
|
|
// 객체에서 배열 추출 시도
|
|
if (response.data && Array.isArray(response.data)) {
|
|
this.backlinks = response.data;
|
|
} else if (response.backlinks && Array.isArray(response.backlinks)) {
|
|
this.backlinks = response.backlinks;
|
|
} else {
|
|
console.warn('⚠️ 예상치 못한 백링크 API 응답 구조:', response);
|
|
this.backlinks = [];
|
|
}
|
|
} else {
|
|
this.backlinks = [];
|
|
}
|
|
|
|
console.log('📡 최종 백링크 데이터:', this.backlinks);
|
|
console.log('📡 최종 백링크 개수:', this.backlinks.length);
|
|
return this.backlinks;
|
|
} catch (error) {
|
|
console.error('❌ 백링크 로드 실패:', error);
|
|
this.backlinks = [];
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 문서 링크 렌더링
|
|
*/
|
|
renderDocumentLinks() {
|
|
const documentContent = document.getElementById('document-content');
|
|
if (!documentContent) {
|
|
console.error('❌ document-content 요소를 찾을 수 없습니다');
|
|
return;
|
|
}
|
|
|
|
// 안전한 링크 초기화
|
|
if (!Array.isArray(this.documentLinks)) {
|
|
console.warn('⚠️ this.documentLinks가 배열이 아닙니다. 빈 배열로 초기화합니다.');
|
|
console.log('🔍 기존 this.documentLinks:', typeof this.documentLinks, this.documentLinks);
|
|
this.documentLinks = [];
|
|
}
|
|
|
|
console.log('🔗 링크 렌더링 시작 - 총', this.documentLinks.length, '개');
|
|
console.log('🔗 링크 데이터 상세:', this.documentLinks);
|
|
|
|
// 기존 링크 제거 (항상 실행)
|
|
const existingLinks = documentContent.querySelectorAll('.document-link');
|
|
console.log('🔍 기존 링크 제거:', existingLinks.length, '개');
|
|
existingLinks.forEach(el => {
|
|
const parent = el.parentNode;
|
|
parent.replaceChild(document.createTextNode(el.textContent), el);
|
|
parent.normalize();
|
|
});
|
|
|
|
// 링크 데이터가 없으면 여기서 종료
|
|
if (this.documentLinks.length === 0) {
|
|
console.log('📝 링크 데이터가 없습니다.');
|
|
return;
|
|
}
|
|
|
|
// 각 링크 렌더링
|
|
if (Array.isArray(this.documentLinks)) {
|
|
this.documentLinks.forEach(link => {
|
|
this.renderSingleLink(link);
|
|
});
|
|
} else {
|
|
console.warn('⚠️ this.documentLinks가 배열이 아닙니다:', typeof this.documentLinks, this.documentLinks);
|
|
}
|
|
|
|
console.log('✅ 링크 렌더링 완료');
|
|
}
|
|
|
|
/**
|
|
* 개별 링크 렌더링
|
|
*/
|
|
renderSingleLink(link) {
|
|
console.log('🔗 renderSingleLink 시작:', link.id, link.selected_text);
|
|
const content = document.getElementById('document-content');
|
|
const textContent = content.textContent;
|
|
|
|
console.log('📝 문서 텍스트 길이:', textContent.length);
|
|
console.log('📍 링크 위치:', link.start_offset, '-', link.end_offset);
|
|
console.log('📝 예상 텍스트:', textContent.substring(link.start_offset, link.end_offset));
|
|
|
|
if (link.start_offset >= textContent.length || link.end_offset > textContent.length) {
|
|
console.warn('❌ 링크 위치가 텍스트 범위를 벗어남:', link);
|
|
console.log('📝 텍스트 미리보기:', textContent.substring(0, 200));
|
|
console.log('📝 링크 주변 텍스트:', textContent.substring(Math.max(0, link.start_offset - 50), link.start_offset + 50));
|
|
return;
|
|
}
|
|
|
|
// 실제 텍스트와 선택된 텍스트가 일치하는지 확인
|
|
const actualText = textContent.substring(link.start_offset, link.end_offset);
|
|
if (actualText !== link.selected_text) {
|
|
console.warn('⚠️ 오프셋 위치의 텍스트가 선택된 텍스트와 다름');
|
|
console.log('📍 예상:', link.selected_text);
|
|
console.log('📍 실제:', actualText);
|
|
|
|
// 텍스트 검색으로 대체 시도
|
|
const searchIndex = textContent.indexOf(link.selected_text);
|
|
if (searchIndex !== -1) {
|
|
console.log('✅ 텍스트 검색으로 위치 찾음:', searchIndex);
|
|
link.start_offset = searchIndex;
|
|
link.end_offset = searchIndex + link.selected_text.length;
|
|
} else {
|
|
console.error('❌ 링크 텍스트를 찾을 수 없음:', link.selected_text);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const walker = document.createTreeWalker(
|
|
content,
|
|
NodeFilter.SHOW_TEXT,
|
|
null,
|
|
false
|
|
);
|
|
|
|
let currentOffset = 0;
|
|
let node;
|
|
|
|
while (node = walker.nextNode()) {
|
|
const nodeLength = node.textContent.length;
|
|
const nodeStart = currentOffset;
|
|
const nodeEnd = currentOffset + nodeLength;
|
|
|
|
// 링크 범위와 겹치는지 확인
|
|
if (nodeEnd > link.start_offset && nodeStart < link.end_offset) {
|
|
const linkStart = Math.max(0, link.start_offset - nodeStart);
|
|
const linkEnd = Math.min(nodeLength, link.end_offset - nodeStart);
|
|
|
|
if (linkStart < linkEnd) {
|
|
console.log('✅ 링크 적용:', linkStart, '-', linkEnd, '텍스트:', node.textContent.substring(linkStart, linkEnd));
|
|
this.applyLinkToNode(node, linkStart, linkEnd, link);
|
|
break;
|
|
}
|
|
}
|
|
|
|
currentOffset = nodeEnd;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 텍스트 노드에 링크 적용
|
|
*/
|
|
applyLinkToNode(textNode, start, end, link) {
|
|
console.log('🎨 applyLinkToNode 시작:', start, end, link.id);
|
|
const text = textNode.textContent;
|
|
const beforeText = text.substring(0, start);
|
|
const linkText = text.substring(start, end);
|
|
const afterText = text.substring(end);
|
|
|
|
console.log('📝 링크 텍스트:', linkText);
|
|
|
|
// 링크 스팬 생성
|
|
const span = document.createElement('span');
|
|
span.className = 'document-link';
|
|
span.textContent = linkText;
|
|
span.dataset.linkId = link.id;
|
|
|
|
// 링크 스타일 (보라색) - 레이아웃 안전
|
|
span.style.cssText = `
|
|
color: #7C3AED !important;
|
|
text-decoration: underline !important;
|
|
cursor: pointer !important;
|
|
background-color: rgba(124, 58, 237, 0.1) !important;
|
|
border-radius: 2px !important;
|
|
padding: 0 1px !important;
|
|
display: inline !important;
|
|
line-height: inherit !important;
|
|
vertical-align: baseline !important;
|
|
margin: 0 !important;
|
|
box-sizing: border-box !important;
|
|
`;
|
|
|
|
// 클릭 이벤트 추가 (겹치는 요소 감지)
|
|
span.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// 겹치는 하이라이트나 백링크가 있는지 확인
|
|
const overlappingElements = this.findOverlappingElements(span);
|
|
if (overlappingElements.length > 0) {
|
|
this.showOverlapMenu(e, span, overlappingElements, 'link');
|
|
} else {
|
|
this.showLinkTooltip(link, span);
|
|
}
|
|
});
|
|
|
|
// DOM 교체
|
|
const parent = textNode.parentNode;
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
if (beforeText) fragment.appendChild(document.createTextNode(beforeText));
|
|
fragment.appendChild(span);
|
|
if (afterText) fragment.appendChild(document.createTextNode(afterText));
|
|
|
|
parent.replaceChild(fragment, textNode);
|
|
console.log('✅ 링크 DOM 교체 완료:', linkText);
|
|
}
|
|
|
|
/**
|
|
* 백링크 렌더링 (링크와 동일한 방식)
|
|
*/
|
|
renderBacklinks() {
|
|
const documentContent = document.getElementById('document-content');
|
|
if (!documentContent) {
|
|
console.error('❌ document-content 요소를 찾을 수 없습니다');
|
|
return;
|
|
}
|
|
|
|
// 안전한 백링크 초기화
|
|
if (!Array.isArray(this.backlinks)) {
|
|
console.warn('⚠️ this.backlinks가 배열이 아닙니다. 빈 배열로 초기화합니다.');
|
|
console.log('🔍 기존 this.backlinks:', typeof this.backlinks, this.backlinks);
|
|
this.backlinks = [];
|
|
}
|
|
|
|
console.log('🔗 백링크 렌더링 시작 - 총', this.backlinks.length, '개');
|
|
console.log('🔗 백링크 데이터 상세:', this.backlinks);
|
|
|
|
// 기존 백링크 확인 (제거하지 않고 중복 체크만)
|
|
const existingBacklinks = documentContent.querySelectorAll('.backlink-highlight');
|
|
console.log(`🔍 기존 백링크 ${existingBacklinks.length}개 발견`);
|
|
|
|
// 백링크 데이터가 없으면 여기서 종료
|
|
if (this.backlinks.length === 0) {
|
|
console.log('📝 백링크 데이터가 없습니다.');
|
|
return;
|
|
}
|
|
|
|
// 각 백링크 렌더링 (중복되지 않는 것만)
|
|
if (Array.isArray(this.backlinks)) {
|
|
this.backlinks.forEach(backlink => {
|
|
// 이미 렌더링된 백링크인지 확인
|
|
const existingBacklink = Array.from(existingBacklinks).find(el =>
|
|
el.dataset.backlinkId === backlink.id.toString()
|
|
);
|
|
|
|
if (!existingBacklink) {
|
|
console.log(`🆕 새로운 백링크 렌더링: ${backlink.id}`);
|
|
this.renderSingleBacklink(backlink);
|
|
} else {
|
|
console.log(`✅ 백링크 이미 존재: ${backlink.id}`);
|
|
}
|
|
});
|
|
} else {
|
|
console.warn('⚠️ this.backlinks가 배열이 아닙니다:', typeof this.backlinks, this.backlinks);
|
|
}
|
|
|
|
console.log('✅ 백링크 렌더링 완료');
|
|
}
|
|
|
|
/**
|
|
* 개별 백링크 렌더링
|
|
*/
|
|
renderSingleBacklink(backlink) {
|
|
console.log('🔗 renderSingleBacklink 시작:', backlink.id, backlink.target_text);
|
|
const content = document.getElementById('document-content');
|
|
if (!content) {
|
|
console.error('❌ document-content 요소를 찾을 수 없습니다');
|
|
return;
|
|
}
|
|
|
|
// 원본 문서 내용 사용 (오프셋 정확성을 위해)
|
|
const textContent = content.textContent || content.innerText || '';
|
|
console.log('📝 문서 텍스트 길이:', textContent.length);
|
|
|
|
// target_start_offset과 target_end_offset이 있으면 직접 사용
|
|
let textIndex, searchText, searchLength;
|
|
|
|
if (backlink.target_start_offset !== undefined && backlink.target_end_offset !== undefined) {
|
|
// 오프셋 정보가 있으면 직접 사용 (더 정확함)
|
|
textIndex = backlink.target_start_offset;
|
|
searchLength = backlink.target_end_offset - backlink.target_start_offset;
|
|
searchText = textContent.substring(textIndex, textIndex + searchLength);
|
|
console.log('✅ 오프셋으로 백링크 텍스트 찾음:', searchText, '위치:', textIndex);
|
|
} else {
|
|
// 오프셋 정보가 없으면 텍스트 검색 사용
|
|
searchText = backlink.target_text || backlink.selected_text;
|
|
if (!searchText) {
|
|
console.warn('❌ 백링크 텍스트 정보가 없음:', backlink);
|
|
return;
|
|
}
|
|
|
|
// 텍스트 검색 (대소문자 무시, 공백 정규화)
|
|
const normalizedContent = textContent.replace(/\s+/g, ' ').trim();
|
|
const normalizedSearchText = searchText.replace(/\s+/g, ' ').trim();
|
|
|
|
textIndex = normalizedContent.indexOf(normalizedSearchText);
|
|
if (textIndex === -1) {
|
|
// 부분 검색 시도
|
|
const words = normalizedSearchText.split(' ');
|
|
if (words.length > 1) {
|
|
const firstWord = words[0];
|
|
const lastWord = words[words.length - 1];
|
|
const partialPattern = firstWord + '.*' + lastWord;
|
|
const regex = new RegExp(partialPattern, 'i');
|
|
const match = normalizedContent.match(regex);
|
|
if (match) {
|
|
textIndex = match.index;
|
|
console.log('✅ 부분 매칭으로 백링크 텍스트 찾음:', searchText);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (textIndex === -1) {
|
|
console.warn('❌ 백링크 텍스트를 찾을 수 없음:', searchText);
|
|
console.log('📝 검색 대상 텍스트 미리보기:', normalizedContent.substring(0, 200));
|
|
console.log('📝 전체 텍스트 길이:', normalizedContent.length);
|
|
return;
|
|
}
|
|
|
|
searchLength = searchText.length;
|
|
console.log('✅ 텍스트 검색으로 백링크 텍스트 찾음:', searchText, '위치:', textIndex);
|
|
}
|
|
|
|
const walker = document.createTreeWalker(
|
|
content,
|
|
NodeFilter.SHOW_TEXT,
|
|
null,
|
|
false
|
|
);
|
|
|
|
let currentOffset = 0;
|
|
let node;
|
|
|
|
while (node = walker.nextNode()) {
|
|
const nodeLength = node.textContent.length;
|
|
const nodeStart = currentOffset;
|
|
const nodeEnd = currentOffset + nodeLength;
|
|
|
|
// 백링크 범위와 겹치는지 확인
|
|
if (nodeEnd > textIndex && nodeStart < textIndex + searchLength) {
|
|
const backlinkStart = Math.max(0, textIndex - nodeStart);
|
|
const backlinkEnd = Math.min(nodeLength, textIndex + searchLength - nodeStart);
|
|
|
|
if (backlinkStart < backlinkEnd) {
|
|
console.log('✅ 백링크 적용:', backlinkStart, '-', backlinkEnd, '텍스트:', node.textContent.substring(backlinkStart, backlinkEnd));
|
|
this.applyBacklinkToNode(node, backlinkStart, backlinkEnd, backlink);
|
|
break;
|
|
}
|
|
}
|
|
|
|
currentOffset = nodeEnd;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 텍스트 노드에 백링크 적용
|
|
*/
|
|
applyBacklinkToNode(textNode, start, end, backlink) {
|
|
console.log('🎨 applyBacklinkToNode 시작:', start, end, backlink.id);
|
|
const text = textNode.textContent;
|
|
const beforeText = text.substring(0, start);
|
|
const backlinkText = text.substring(start, end);
|
|
const afterText = text.substring(end);
|
|
|
|
console.log('📝 백링크 텍스트:', backlinkText);
|
|
|
|
// 백링크 스팬 생성
|
|
const span = document.createElement('span');
|
|
span.className = 'backlink-highlight';
|
|
span.textContent = backlinkText;
|
|
span.dataset.backlinkId = backlink.id;
|
|
|
|
// 백링크 스타일 (주황색) - 레이아웃 안전
|
|
span.style.cssText = `
|
|
color: #EA580C !important;
|
|
text-decoration: underline !important;
|
|
cursor: pointer !important;
|
|
background-color: rgba(234, 88, 12, 0.2) !important;
|
|
border: 1px solid #EA580C !important;
|
|
border-radius: 3px !important;
|
|
padding: 0 2px !important;
|
|
font-weight: bold !important;
|
|
display: inline !important;
|
|
line-height: inherit !important;
|
|
vertical-align: baseline !important;
|
|
margin: 0 !important;
|
|
box-sizing: border-box !important;
|
|
`;
|
|
|
|
// 클릭 이벤트 추가 (겹치는 요소 감지)
|
|
span.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// 겹치는 하이라이트나 링크가 있는지 확인
|
|
const overlappingElements = this.findOverlappingElements(span);
|
|
if (overlappingElements.length > 0) {
|
|
this.showOverlapMenu(e, span, overlappingElements, 'backlink');
|
|
} else {
|
|
this.showBacklinkTooltip(backlink, span);
|
|
}
|
|
});
|
|
|
|
// DOM 교체
|
|
const parent = textNode.parentNode;
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
if (beforeText) fragment.appendChild(document.createTextNode(beforeText));
|
|
fragment.appendChild(span);
|
|
if (afterText) fragment.appendChild(document.createTextNode(afterText));
|
|
|
|
parent.replaceChild(fragment, textNode);
|
|
console.log('✅ 백링크 DOM 교체 완료:', backlinkText);
|
|
}
|
|
|
|
/**
|
|
* 링크 툴팁 표시
|
|
*/
|
|
showLinkTooltip(link, element) {
|
|
this.hideTooltip();
|
|
|
|
const tooltip = document.createElement('div');
|
|
tooltip.id = 'link-tooltip';
|
|
tooltip.className = 'absolute bg-white border border-gray-300 rounded-lg shadow-lg p-5 z-50 max-w-2xl';
|
|
tooltip.style.minWidth = '450px';
|
|
tooltip.style.maxHeight = '80vh';
|
|
tooltip.style.overflowY = 'auto';
|
|
|
|
// 생성 날짜 포맷팅
|
|
const createdDate = link.created_at ? this.formatDate(link.created_at) : '알 수 없음';
|
|
|
|
const tooltipHTML = `
|
|
<div class="mb-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="text-lg font-semibold text-purple-800 flex items-center">
|
|
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
링크 정보
|
|
</div>
|
|
<div class="text-xs text-gray-500">${createdDate}</div>
|
|
</div>
|
|
|
|
<div class="font-medium text-purple-900 bg-purple-50 px-4 py-3 rounded-lg border-l-4 border-purple-500">
|
|
<div class="text-sm text-purple-700 mb-1">선택된 텍스트</div>
|
|
<div class="text-base">"${link.selected_text}"</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-5">
|
|
<div class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
|
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
연결된 문서
|
|
</div>
|
|
<div class="bg-gradient-to-r from-gray-50 to-gray-100 p-4 rounded-lg border">
|
|
<div class="font-semibold text-gray-900 mb-2">${link.target_document_title}</div>
|
|
${link.target_text ? `
|
|
<div class="bg-white p-3 rounded border-l-3 border-blue-400">
|
|
<div class="text-xs text-gray-600 mb-1">대상 텍스트</div>
|
|
<div class="text-sm text-gray-800">"${link.target_text}"</div>
|
|
</div>
|
|
` : ''}
|
|
${link.description ? `
|
|
<div class="mt-3 p-3 bg-blue-50 rounded border-l-3 border-blue-300">
|
|
<div class="text-xs text-blue-700 mb-1">설명</div>
|
|
<div class="text-sm text-blue-800">${link.description}</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col sm:flex-row gap-3 mb-4">
|
|
<button onclick="window.documentViewerInstance.linkManager.navigateToLinkedDocument('${link.target_document_id}', ${JSON.stringify(link).replace(/"/g, '"')})"
|
|
class="flex-1 bg-purple-500 text-white px-4 py-2 rounded-lg hover:bg-purple-600 transition-colors flex items-center justify-center">
|
|
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
문서로 이동
|
|
</button>
|
|
<button onclick="window.documentViewerInstance.linkManager.deleteLink('${link.id}')"
|
|
class="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600 transition-colors flex items-center justify-center">
|
|
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" clip-rule="evenodd"></path>
|
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414L7.586 12l-1.293 1.293a1 1 0 101.414 1.414L9 13.414l2.293 2.293a1 1 0 001.414-1.414L11.414 12l1.293-1.293z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
삭제
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex justify-end pt-3 border-t border-gray-200">
|
|
<button onclick="window.documentViewerInstance.linkManager.hideTooltip()"
|
|
class="text-sm bg-gray-500 text-white px-4 py-2 rounded-lg hover:bg-gray-600 transition-colors">
|
|
닫기
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
tooltip.innerHTML = tooltipHTML;
|
|
this.positionTooltip(tooltip, element);
|
|
}
|
|
|
|
/**
|
|
* 백링크 툴팁 표시
|
|
*/
|
|
showBacklinkTooltip(backlink, element) {
|
|
this.hideTooltip();
|
|
|
|
const tooltip = document.createElement('div');
|
|
tooltip.id = 'backlink-tooltip';
|
|
tooltip.className = 'absolute bg-white border border-gray-300 rounded-lg shadow-lg p-5 z-50 max-w-2xl';
|
|
tooltip.style.minWidth = '450px';
|
|
tooltip.style.maxHeight = '80vh';
|
|
tooltip.style.overflowY = 'auto';
|
|
|
|
// 생성 날짜 포맷팅
|
|
const createdDate = backlink.created_at ? this.formatDate(backlink.created_at) : '알 수 없음';
|
|
|
|
const tooltipHTML = `
|
|
<div class="mb-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="text-lg font-semibold text-orange-800 flex items-center">
|
|
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M7.707 3.293a1 1 0 010 1.414L5.414 7H11a7 7 0 017 7v2a1 1 0 11-2 0v-2a5 5 0 00-5-5H5.414l2.293 2.293a1 1 0 11-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
백링크 정보
|
|
</div>
|
|
<div class="text-xs text-gray-500">${createdDate}</div>
|
|
</div>
|
|
|
|
<div class="font-medium text-orange-900 bg-orange-50 px-4 py-3 rounded-lg border-l-4 border-orange-500">
|
|
<div class="text-sm text-orange-700 mb-1">현재 문서의 텍스트</div>
|
|
<div class="text-base">"${backlink.target_text || backlink.selected_text}"</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-5">
|
|
<div class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
|
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
참조하는 문서
|
|
</div>
|
|
<div class="bg-gradient-to-r from-orange-50 to-orange-100 p-4 rounded-lg border">
|
|
<div class="font-semibold text-gray-900 mb-2">${backlink.source_document_title}</div>
|
|
<div class="bg-white p-3 rounded border-l-3 border-orange-400">
|
|
<div class="text-xs text-gray-600 mb-1">원본 텍스트</div>
|
|
<div class="text-sm text-gray-800">"${backlink.selected_text}"</div>
|
|
</div>
|
|
${backlink.description ? `
|
|
<div class="mt-3 p-3 bg-orange-50 rounded border-l-3 border-orange-300">
|
|
<div class="text-xs text-orange-700 mb-1">설명</div>
|
|
<div class="text-sm text-orange-800">${backlink.description}</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col sm:flex-row gap-3 mb-4">
|
|
<button onclick="window.documentViewerInstance.linkManager.navigateToSourceDocument('${backlink.source_document_id}', ${JSON.stringify(backlink).replace(/"/g, '"')})"
|
|
class="flex-1 bg-orange-500 text-white px-4 py-2 rounded-lg hover:bg-orange-600 transition-colors flex items-center justify-center">
|
|
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
원본 문서로 이동
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex justify-end pt-3 border-t border-gray-200">
|
|
<button onclick="window.documentViewerInstance.linkManager.hideTooltip()"
|
|
class="text-sm bg-gray-500 text-white px-4 py-2 rounded-lg hover:bg-gray-600 transition-colors">
|
|
닫기
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
tooltip.innerHTML = tooltipHTML;
|
|
this.positionTooltip(tooltip, element);
|
|
}
|
|
|
|
/**
|
|
* 날짜 포맷팅
|
|
*/
|
|
formatDate(dateString) {
|
|
if (!dateString) return '알 수 없음';
|
|
|
|
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 <= 7) {
|
|
return `${diffDays}일 전`;
|
|
} else if (diffDays <= 30) {
|
|
return `${Math.ceil(diffDays / 7)}주 전`;
|
|
} else {
|
|
return date.toLocaleDateString('ko-KR', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 툴팁 위치 설정
|
|
*/
|
|
positionTooltip(tooltip, element) {
|
|
const rect = element.getBoundingClientRect();
|
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
|
|
|
document.body.appendChild(tooltip);
|
|
|
|
// 툴팁 위치 조정
|
|
const tooltipRect = tooltip.getBoundingClientRect();
|
|
let top = rect.bottom + scrollTop + 5;
|
|
let left = rect.left + scrollLeft;
|
|
|
|
// 화면 경계 체크
|
|
if (left + tooltipRect.width > window.innerWidth) {
|
|
left = window.innerWidth - tooltipRect.width - 10;
|
|
}
|
|
if (top + tooltipRect.height > window.innerHeight + scrollTop) {
|
|
top = rect.top + scrollTop - tooltipRect.height - 5;
|
|
}
|
|
|
|
tooltip.style.top = top + 'px';
|
|
tooltip.style.left = left + 'px';
|
|
|
|
// 외부 클릭 시 닫기
|
|
setTimeout(() => {
|
|
document.addEventListener('click', this.handleTooltipOutsideClick.bind(this));
|
|
}, 100);
|
|
}
|
|
|
|
/**
|
|
* 툴팁 숨기기
|
|
*/
|
|
hideTooltip() {
|
|
const linkTooltip = document.getElementById('link-tooltip');
|
|
if (linkTooltip) {
|
|
linkTooltip.remove();
|
|
}
|
|
|
|
const backlinkTooltip = document.getElementById('backlink-tooltip');
|
|
if (backlinkTooltip) {
|
|
backlinkTooltip.remove();
|
|
}
|
|
|
|
const overlapMenu = document.getElementById('overlap-menu');
|
|
if (overlapMenu) {
|
|
overlapMenu.remove();
|
|
}
|
|
|
|
document.removeEventListener('click', this.handleTooltipOutsideClick.bind(this));
|
|
}
|
|
|
|
/**
|
|
* 툴팁 외부 클릭 처리
|
|
*/
|
|
handleTooltipOutsideClick(e) {
|
|
const linkTooltip = document.getElementById('link-tooltip');
|
|
const backlinkTooltip = document.getElementById('backlink-tooltip');
|
|
const overlapMenu = document.getElementById('overlap-menu');
|
|
|
|
const isOutsideLinkTooltip = linkTooltip && !linkTooltip.contains(e.target);
|
|
const isOutsideBacklinkTooltip = backlinkTooltip && !backlinkTooltip.contains(e.target);
|
|
const isOutsideOverlapMenu = overlapMenu && !overlapMenu.contains(e.target);
|
|
|
|
if (isOutsideLinkTooltip || isOutsideBacklinkTooltip || isOutsideOverlapMenu) {
|
|
this.hideTooltip();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 링크된 문서로 이동
|
|
*/
|
|
navigateToLinkedDocument(targetDocumentId, linkInfo) {
|
|
let targetUrl = `/viewer.html?id=${targetDocumentId}`;
|
|
|
|
// 특정 텍스트 위치가 있는 경우 URL에 추가
|
|
if (linkInfo.target_text && linkInfo.target_start_offset !== undefined) {
|
|
targetUrl += `&highlight_text=${encodeURIComponent(linkInfo.target_text)}`;
|
|
targetUrl += `&start_offset=${linkInfo.target_start_offset}`;
|
|
targetUrl += `&end_offset=${linkInfo.target_end_offset}`;
|
|
}
|
|
|
|
window.location.href = targetUrl;
|
|
}
|
|
|
|
/**
|
|
* 원본 문서로 이동 (백링크)
|
|
*/
|
|
navigateToSourceDocument(sourceDocumentId, backlinkInfo) {
|
|
let targetUrl = `/viewer.html?id=${sourceDocumentId}`;
|
|
|
|
// 원본 텍스트 위치가 있는 경우 URL에 추가
|
|
if (backlinkInfo.selected_text && backlinkInfo.start_offset !== undefined) {
|
|
targetUrl += `&highlight_text=${encodeURIComponent(backlinkInfo.selected_text)}`;
|
|
targetUrl += `&start_offset=${backlinkInfo.start_offset}`;
|
|
targetUrl += `&end_offset=${backlinkInfo.end_offset}`;
|
|
}
|
|
|
|
window.location.href = targetUrl;
|
|
}
|
|
|
|
/**
|
|
* 링크 삭제
|
|
*/
|
|
async deleteLink(linkId) {
|
|
if (!confirm('이 링크를 삭제하시겠습니까?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.api.delete(`/document-links/${linkId}`);
|
|
this.documentLinks = this.documentLinks.filter(l => l.id !== linkId);
|
|
|
|
this.hideTooltip();
|
|
this.renderDocumentLinks();
|
|
|
|
console.log('링크 삭제 완료:', linkId);
|
|
} catch (error) {
|
|
console.error('링크 삭제 실패:', error);
|
|
alert('링크 삭제에 실패했습니다');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 선택된 텍스트로 링크 생성
|
|
*/
|
|
async createLinkFromSelection(documentId = null, selectedText = null, selectedRange = null) {
|
|
// 매개변수가 없으면 현재 선택된 텍스트 사용
|
|
if (!selectedText || !selectedRange) {
|
|
selectedText = window.getSelection().toString().trim();
|
|
const selection = window.getSelection();
|
|
|
|
if (!selectedText || selection.rangeCount === 0) {
|
|
alert('텍스트를 먼저 선택해주세요.');
|
|
return;
|
|
}
|
|
|
|
selectedRange = selection.getRangeAt(0);
|
|
}
|
|
|
|
if (!documentId && window.documentViewerInstance) {
|
|
documentId = window.documentViewerInstance.documentId;
|
|
}
|
|
|
|
try {
|
|
console.log('🔗 링크 생성 시작:', selectedText);
|
|
|
|
// ViewerCore의 링크 생성 모달 표시
|
|
if (window.documentViewerInstance) {
|
|
window.documentViewerInstance.selectedText = selectedText;
|
|
window.documentViewerInstance.selectedRange = selectedRange;
|
|
window.documentViewerInstance.showLinkModal = true;
|
|
window.documentViewerInstance.linkForm.selected_text = selectedText;
|
|
|
|
// 서적 목록 로드
|
|
await window.documentViewerInstance.loadAvailableBooks();
|
|
|
|
// 기본적으로 같은 서적 문서들 로드
|
|
await window.documentViewerInstance.loadSameBookDocuments();
|
|
|
|
// 텍스트 오프셋 계산
|
|
const documentContent = document.getElementById('document-content');
|
|
const fullText = documentContent.textContent;
|
|
const startOffset = this.getTextOffset(documentContent, selectedRange.startContainer, selectedRange.startOffset);
|
|
const endOffset = startOffset + selectedText.length;
|
|
|
|
window.documentViewerInstance.linkForm.start_offset = startOffset;
|
|
window.documentViewerInstance.linkForm.end_offset = endOffset;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('링크 생성 실패:', error);
|
|
alert('링크 생성에 실패했습니다: ' + error.message);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* 겹치는 요소 찾기 (하이라이트, 링크, 백링크)
|
|
*/
|
|
findOverlappingElements(element) {
|
|
const overlapping = [];
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
// 같은 위치에 있는 모든 요소 찾기
|
|
const elementsAtPoint = document.elementsFromPoint(
|
|
rect.left + rect.width / 2,
|
|
rect.top + rect.height / 2
|
|
);
|
|
|
|
elementsAtPoint.forEach(el => {
|
|
if (el !== element) {
|
|
// 하이라이트 요소
|
|
if (el.classList.contains('highlight')) {
|
|
overlapping.push({ type: 'highlight', element: el });
|
|
}
|
|
// 링크 요소
|
|
if (el.classList.contains('document-link')) {
|
|
overlapping.push({ type: 'link', element: el });
|
|
}
|
|
// 백링크 요소
|
|
if (el.classList.contains('backlink-highlight')) {
|
|
overlapping.push({ type: 'backlink', element: el });
|
|
}
|
|
}
|
|
});
|
|
|
|
return overlapping;
|
|
}
|
|
|
|
/**
|
|
* 겹침 메뉴 표시
|
|
*/
|
|
showOverlapMenu(event, clickedElement, overlappingElements, clickedType) {
|
|
this.hideTooltip();
|
|
|
|
const menu = document.createElement('div');
|
|
menu.id = 'overlap-menu';
|
|
menu.className = 'absolute bg-white border border-gray-300 rounded-lg shadow-lg p-4 z-50';
|
|
menu.style.minWidth = '320px';
|
|
menu.style.maxWidth = '400px';
|
|
|
|
let menuHTML = `
|
|
<div class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
|
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
겹치는 요소들
|
|
</div>
|
|
<div class="space-y-2">
|
|
`;
|
|
|
|
// 클릭된 요소 추가
|
|
menuHTML += this.getMenuItemHTML(clickedType, clickedElement, true);
|
|
|
|
// 겹치는 요소들 추가
|
|
overlappingElements.forEach(item => {
|
|
menuHTML += this.getMenuItemHTML(item.type, item.element, false);
|
|
});
|
|
|
|
menuHTML += `
|
|
</div>
|
|
<div class="mt-2 pt-2 border-t border-gray-200">
|
|
<button onclick="window.documentViewerInstance.linkManager.hideTooltip()"
|
|
class="text-xs bg-gray-500 text-white px-2 py-1 rounded hover:bg-gray-600 w-full">
|
|
닫기
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
menu.innerHTML = menuHTML;
|
|
|
|
// 메뉴 위치 설정
|
|
const rect = clickedElement.getBoundingClientRect();
|
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
|
|
|
document.body.appendChild(menu);
|
|
|
|
let top = rect.bottom + scrollTop + 5;
|
|
let left = rect.left + scrollLeft;
|
|
|
|
// 화면 경계 체크
|
|
const menuRect = menu.getBoundingClientRect();
|
|
if (left + menuRect.width > window.innerWidth) {
|
|
left = window.innerWidth - menuRect.width - 10;
|
|
}
|
|
if (top + menuRect.height > window.innerHeight + scrollTop) {
|
|
top = rect.top + scrollTop - menuRect.height - 5;
|
|
}
|
|
|
|
menu.style.top = top + 'px';
|
|
menu.style.left = left + 'px';
|
|
|
|
// 외부 클릭 시 닫기
|
|
setTimeout(() => {
|
|
document.addEventListener('click', this.handleTooltipOutsideClick.bind(this));
|
|
}, 100);
|
|
}
|
|
|
|
/**
|
|
* 메뉴 아이템 HTML 생성
|
|
*/
|
|
getMenuItemHTML(type, element, isClicked) {
|
|
const icons = {
|
|
highlight: '🖍️',
|
|
link: '🔗',
|
|
backlink: '🔙'
|
|
};
|
|
|
|
const labels = {
|
|
highlight: '하이라이트',
|
|
link: '링크',
|
|
backlink: '백링크'
|
|
};
|
|
|
|
const colors = {
|
|
highlight: 'bg-yellow-50 border-yellow-200 text-yellow-800',
|
|
link: 'bg-purple-50 border-purple-200 text-purple-800',
|
|
backlink: 'bg-orange-50 border-orange-200 text-orange-800'
|
|
};
|
|
|
|
const clickedClass = isClicked ? 'ring-2 ring-blue-300' : '';
|
|
const elementId = element.dataset.highlightId || element.dataset.linkId || element.dataset.backlinkId || '';
|
|
|
|
|
|
|
|
// 각 타입별 액션 버튼들
|
|
let actionButtons = '';
|
|
if (type === 'highlight') {
|
|
actionButtons = `
|
|
<div class="flex gap-1 mt-2">
|
|
<button onclick="window.documentViewerInstance.linkManager.handleOverlapMenuClick('${type}', '${elementId}')"
|
|
class="text-xs bg-yellow-500 text-white px-2 py-1 rounded hover:bg-yellow-600 transition-colors">
|
|
메모 보기
|
|
</button>
|
|
</div>
|
|
`;
|
|
} else if (type === 'link') {
|
|
actionButtons = `
|
|
<div class="flex gap-1 mt-2">
|
|
<button onclick="window.documentViewerInstance.linkManager.navigateToLinkedDocumentFromMenu('${elementId}')"
|
|
class="text-xs bg-purple-500 text-white px-2 py-1 rounded hover:bg-purple-600 transition-colors">
|
|
문서로 이동
|
|
</button>
|
|
<button onclick="window.documentViewerInstance.linkManager.handleOverlapMenuClick('${type}', '${elementId}')"
|
|
class="text-xs bg-gray-500 text-white px-2 py-1 rounded hover:bg-gray-600 transition-colors">
|
|
상세보기
|
|
</button>
|
|
</div>
|
|
`;
|
|
} else if (type === 'backlink') {
|
|
actionButtons = `
|
|
<div class="flex gap-1 mt-2">
|
|
<button onclick="window.documentViewerInstance.linkManager.navigateToSourceDocumentFromMenu('${elementId}')"
|
|
class="text-xs bg-orange-500 text-white px-2 py-1 rounded hover:bg-orange-600 transition-colors">
|
|
원본으로 이동
|
|
</button>
|
|
<button onclick="window.documentViewerInstance.linkManager.handleOverlapMenuClick('${type}', '${elementId}')"
|
|
class="text-xs bg-gray-500 text-white px-2 py-1 rounded hover:bg-gray-600 transition-colors">
|
|
상세보기
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return `
|
|
<div class="w-full p-3 rounded border ${colors[type].replace('text-yellow-800', '').replace('text-purple-800', '').replace('text-orange-800', '')} ${clickedClass}">
|
|
<div class="flex items-center space-x-2">
|
|
<span class="text-lg">${icons[type]}</span>
|
|
<div class="flex-1">
|
|
<div class="font-medium text-gray-800">${labels[type]}</div>
|
|
<div class="text-xs text-gray-600">${element.textContent.substring(0, 40)}${element.textContent.length > 40 ? '...' : ''}</div>
|
|
</div>
|
|
${isClicked ? '<span class="text-xs bg-blue-500 text-white px-1 rounded">클릭됨</span>' : ''}
|
|
</div>
|
|
${actionButtons}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 메뉴에서 바로 링크된 문서로 이동
|
|
*/
|
|
navigateToLinkedDocumentFromMenu(elementId) {
|
|
this.hideTooltip();
|
|
|
|
const link = this.documentLinks.find(l => l.id === elementId);
|
|
if (link) {
|
|
this.navigateToLinkedDocument(link.target_document_id, link);
|
|
} else {
|
|
console.warn('링크 데이터를 찾을 수 없음:', elementId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 메뉴에서 바로 소스 문서로 이동
|
|
*/
|
|
navigateToSourceDocumentFromMenu(elementId) {
|
|
this.hideTooltip();
|
|
|
|
const backlink = this.backlinks.find(b => b.id === elementId);
|
|
if (backlink) {
|
|
this.navigateToSourceDocument(backlink.source_document_id, backlink);
|
|
} else {
|
|
console.warn('백링크 데이터를 찾을 수 없음:', elementId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 겹침 메뉴 클릭 처리
|
|
*/
|
|
handleOverlapMenuClick(type, elementId) {
|
|
|
|
this.hideTooltip();
|
|
|
|
// 해당 요소 찾기
|
|
let element;
|
|
if (type === 'highlight') {
|
|
element = document.querySelector(`[data-highlight-id="${elementId}"]`);
|
|
if (element && window.documentViewerInstance.highlightManager) {
|
|
// 하이라이트 클릭 처리 (HighlightManager에 위임)
|
|
const highlightId = elementId;
|
|
|
|
// 해당 하이라이트 찾기
|
|
const highlight = window.documentViewerInstance.highlightManager.highlights.find(h => h.id === highlightId);
|
|
|
|
if (highlight) {
|
|
// 하이라이트 툴팁 표시 (메모 작성/편집 기능 포함)
|
|
window.documentViewerInstance.highlightManager.showHighlightTooltip(highlight, element);
|
|
} else {
|
|
console.warn('하이라이트를 찾을 수 없음:', highlightId);
|
|
}
|
|
}
|
|
} else if (type === 'link') {
|
|
element = document.querySelector(`[data-link-id="${elementId}"]`);
|
|
|
|
if (element) {
|
|
// 링크 데이터 찾기
|
|
const link = this.documentLinks.find(l => l.id === elementId);
|
|
if (link) {
|
|
this.showLinkTooltip(link, element);
|
|
}
|
|
}
|
|
} else if (type === 'backlink') {
|
|
element = document.querySelector(`[data-backlink-id="${elementId}"]`);
|
|
|
|
if (element) {
|
|
// 백링크 데이터 찾기
|
|
const backlink = this.backlinks.find(b => b.id === elementId);
|
|
if (backlink) {
|
|
this.showBacklinkTooltip(backlink, element);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 텍스트 오프셋 계산
|
|
*/
|
|
getTextOffset(root, node, offset) {
|
|
let textOffset = 0;
|
|
const walker = document.createTreeWalker(
|
|
root,
|
|
NodeFilter.SHOW_TEXT,
|
|
null,
|
|
false
|
|
);
|
|
|
|
let currentNode;
|
|
while (currentNode = walker.nextNode()) {
|
|
if (currentNode === node) {
|
|
return textOffset + offset;
|
|
}
|
|
textOffset += currentNode.textContent.length;
|
|
}
|
|
|
|
return textOffset;
|
|
}
|
|
}
|
|
|
|
// 전역으로 내보내기
|
|
window.LinkManager = LinkManager;
|