feat: 겹침 메뉴에 각 기능별 액션 버튼 추가

- 하이라이트: '메모 보기' 버튼으로 하이라이트 툴팁 표시
- 링크: '문서로 이동' 버튼으로 바로 연결된 문서로 이동
- 백링크: '원본으로 이동' 버튼으로 바로 원본 문서로 이동
- 각 항목에 '상세보기' 버튼으로 상세 정보 툴팁 표시
- 메뉴 UI 개선: 크기 확대, 아이콘 추가, 레이아웃 개선
- 링크/백링크 툴팁을 메모처럼 상세하게 개선 (날짜, 아이콘, 그라데이션)
- 겹침 감지 로직 디버깅 및 안정화
This commit is contained in:
Hyungi Ahn
2025-09-02 15:11:36 +09:00
parent 397c63979d
commit f711998ce9

View File

@@ -494,39 +494,77 @@ class LinkManager {
const tooltip = document.createElement('div'); const tooltip = document.createElement('div');
tooltip.id = 'link-tooltip'; 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.className = 'absolute bg-white border border-gray-300 rounded-lg shadow-lg p-5 z-50 max-w-2xl';
tooltip.style.minWidth = '350px'; tooltip.style.minWidth = '450px';
tooltip.style.maxHeight = '80vh';
tooltip.style.overflowY = 'auto';
// 생성 날짜 포맷팅
const createdDate = link.created_at ? this.formatDate(link.created_at) : '알 수 없음';
const tooltipHTML = ` const tooltipHTML = `
<div class="mb-4"> <div class="mb-4">
<div class="text-sm text-gray-600 mb-2">링크 정보</div> <div class="flex items-center justify-between mb-3">
<div class="font-medium text-purple-900 bg-purple-50 px-3 py-2 rounded border-l-4 border-purple-500"> <div class="text-lg font-semibold text-purple-800 flex items-center">
"${link.selected_text}" <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> </div>
<div class="mb-4"> <div class="mb-5">
<div class="text-sm text-gray-600 mb-2">연결된 문서</div> <div class="text-sm font-medium text-gray-700 mb-3 flex items-center">
<div class="bg-gray-50 p-3 rounded"> <svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<div class="font-medium text-gray-900">${link.target_document_title}</div> <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
${link.target_text ? `<div class="text-sm text-gray-600 mt-1">대상 텍스트: "${link.target_text}"</div>` : ''} </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> </div>
<div class="flex justify-between items-center"> <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, '&quot;')})" <button onclick="window.documentViewerInstance.linkManager.navigateToLinkedDocument('${link.target_document_id}', ${JSON.stringify(link).replace(/"/g, '&quot;')})"
class="text-sm bg-purple-500 text-white px-3 py-2 rounded hover:bg-purple-600"> 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>
<button onclick="window.documentViewerInstance.linkManager.deleteLink('${link.id}')" <button onclick="window.documentViewerInstance.linkManager.deleteLink('${link.id}')"
class="text-sm bg-red-500 text-white px-3 py-2 rounded hover:bg-red-600"> 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> </button>
</div> </div>
<div class="flex justify-end mt-3"> <div class="flex justify-end pt-3 border-t border-gray-200">
<button onclick="window.documentViewerInstance.linkManager.hideTooltip()" <button onclick="window.documentViewerInstance.linkManager.hideTooltip()"
class="text-xs bg-gray-500 text-white px-3 py-1 rounded hover:bg-gray-600"> class="text-sm bg-gray-500 text-white px-4 py-2 rounded-lg hover:bg-gray-600 transition-colors">
닫기 닫기
</button> </button>
</div> </div>
@@ -544,35 +582,67 @@ class LinkManager {
const tooltip = document.createElement('div'); const tooltip = document.createElement('div');
tooltip.id = 'backlink-tooltip'; 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.className = 'absolute bg-white border border-gray-300 rounded-lg shadow-lg p-5 z-50 max-w-2xl';
tooltip.style.minWidth = '350px'; tooltip.style.minWidth = '450px';
tooltip.style.maxHeight = '80vh';
tooltip.style.overflowY = 'auto';
// 생성 날짜 포맷팅
const createdDate = backlink.created_at ? this.formatDate(backlink.created_at) : '알 수 없음';
const tooltipHTML = ` const tooltipHTML = `
<div class="mb-4"> <div class="mb-4">
<div class="text-sm text-gray-600 mb-2">백링크 정보</div> <div class="flex items-center justify-between mb-3">
<div class="font-medium text-orange-900 bg-orange-50 px-3 py-2 rounded border-l-4 border-orange-500"> <div class="text-lg font-semibold text-orange-800 flex items-center">
"${backlink.target_text || backlink.selected_text}" <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> </div>
<div class="mb-4"> <div class="mb-5">
<div class="text-sm text-gray-600 mb-2">참조 문서</div> <div class="text-sm font-medium text-gray-700 mb-3 flex items-center">
<div class="bg-gray-50 p-3 rounded"> <svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<div class="font-medium text-gray-900">${backlink.source_document_title}</div> <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
<div class="text-sm text-gray-600 mt-1">원본 텍스트: "${backlink.selected_text}"</div> </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> </div>
<div class="flex justify-between items-center"> <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, '&quot;')})" <button onclick="window.documentViewerInstance.linkManager.navigateToSourceDocument('${backlink.source_document_id}', ${JSON.stringify(backlink).replace(/"/g, '&quot;')})"
class="text-sm bg-orange-500 text-white px-3 py-2 rounded hover:bg-orange-600"> 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> </button>
</div> </div>
<div class="flex justify-end mt-3"> <div class="flex justify-end pt-3 border-t border-gray-200">
<button onclick="window.documentViewerInstance.linkManager.hideTooltip()" <button onclick="window.documentViewerInstance.linkManager.hideTooltip()"
class="text-xs bg-gray-500 text-white px-3 py-1 rounded hover:bg-gray-600"> class="text-sm bg-gray-500 text-white px-4 py-2 rounded-lg hover:bg-gray-600 transition-colors">
닫기 닫기
</button> </button>
</div> </div>
@@ -582,6 +652,32 @@ class LinkManager {
this.positionTooltip(tooltip, element); 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'
});
}
}
/** /**
* 툴팁 위치 설정 * 툴팁 위치 설정
*/ */
@@ -803,12 +899,18 @@ class LinkManager {
const menu = document.createElement('div'); const menu = document.createElement('div');
menu.id = 'overlap-menu'; menu.id = 'overlap-menu';
menu.className = 'absolute bg-white border border-gray-300 rounded-lg shadow-lg p-2 z-50'; menu.className = 'absolute bg-white border border-gray-300 rounded-lg shadow-lg p-4 z-50';
menu.style.minWidth = '200px'; menu.style.minWidth = '320px';
menu.style.maxWidth = '400px';
let menuHTML = ` let menuHTML = `
<div class="text-sm font-medium text-gray-700 mb-2">여러 요소가 겹쳐있습니다</div> <div class="text-sm font-medium text-gray-700 mb-3 flex items-center">
<div class="space-y-1"> <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">
`; `;
// 클릭된 요소 추가 // 클릭된 요소 추가
@@ -884,25 +986,95 @@ class LinkManager {
const clickedClass = isClicked ? 'ring-2 ring-blue-300' : ''; const clickedClass = isClicked ? 'ring-2 ring-blue-300' : '';
const elementId = element.dataset.highlightId || element.dataset.linkId || element.dataset.backlinkId || ''; 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 ` return `
<button onclick="window.documentViewerInstance.linkManager.handleOverlapMenuClick('${type}', '${elementId}')" <div class="w-full p-3 rounded border ${colors[type].replace('text-yellow-800', '').replace('text-purple-800', '').replace('text-orange-800', '')} ${clickedClass}">
class="w-full text-left px-3 py-2 rounded border ${colors[type]} ${clickedClass} hover:opacity-80 transition-opacity">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span class="text-lg">${icons[type]}</span> <span class="text-lg">${icons[type]}</span>
<div class="flex-1"> <div class="flex-1">
<div class="font-medium">${labels[type]}</div> <div class="font-medium text-gray-800">${labels[type]}</div>
<div class="text-xs opacity-75">${element.textContent.substring(0, 30)}${element.textContent.length > 30 ? '...' : ''}</div> <div class="text-xs text-gray-600">${element.textContent.substring(0, 40)}${element.textContent.length > 40 ? '...' : ''}</div>
</div> </div>
${isClicked ? '<span class="text-xs bg-blue-500 text-white px-1 rounded">클릭됨</span>' : ''} ${isClicked ? '<span class="text-xs bg-blue-500 text-white px-1 rounded">클릭됨</span>' : ''}
</div> </div>
</button> ${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) { handleOverlapMenuClick(type, elementId) {
this.hideTooltip(); this.hideTooltip();
// 해당 요소 찾기 // 해당 요소 찾기
@@ -913,20 +1085,19 @@ class LinkManager {
// 하이라이트 클릭 처리 (HighlightManager에 위임) // 하이라이트 클릭 처리 (HighlightManager에 위임)
const highlightId = elementId; const highlightId = elementId;
// 해당 하이라이트 그룹 찾기 // 해당 하이라이트 찾기
const highlightGroups = window.documentViewerInstance.highlightManager.highlightGroups || []; const highlight = window.documentViewerInstance.highlightManager.highlights.find(h => h.id === highlightId);
const targetGroup = highlightGroups.find(group =>
group.some(h => h.id === highlightId)
);
if (targetGroup) { if (highlight) {
window.documentViewerInstance.highlightManager.showHighlightModal(targetGroup); // 하이라이트 툴팁 표시 (메모 작성/편집 기능 포함)
window.documentViewerInstance.highlightManager.showHighlightTooltip(highlight, element);
} else { } else {
console.warn('하이라이트 그룹을 찾을 수 없음:', highlightId); console.warn('하이라이트 찾을 수 없음:', highlightId);
} }
} }
} else if (type === 'link') { } else if (type === 'link') {
element = document.querySelector(`[data-link-id="${elementId}"]`); element = document.querySelector(`[data-link-id="${elementId}"]`);
if (element) { if (element) {
// 링크 데이터 찾기 // 링크 데이터 찾기
const link = this.documentLinks.find(l => l.id === elementId); const link = this.documentLinks.find(l => l.id === elementId);
@@ -936,6 +1107,7 @@ class LinkManager {
} }
} else if (type === 'backlink') { } else if (type === 'backlink') {
element = document.querySelector(`[data-backlink-id="${elementId}"]`); element = document.querySelector(`[data-backlink-id="${elementId}"]`);
if (element) { if (element) {
// 백링크 데이터 찾기 // 백링크 데이터 찾기
const backlink = this.backlinks.find(b => b.id === elementId); const backlink = this.backlinks.find(b => b.id === elementId);