Fix: 하이라이트 메모 표시 오류 수정
- highlight-manager.js에서 showHighlightTooltip 함수 호출 시 배열 대신 단일 객체 전달하도록 수정 - 하이라이트 클릭 시 메모가 0개로 표시되던 문제 해결 - getOverlappingElements 함수에 디버깅 로그 추가 - 하이라이트 매니저 상태 확인 로그 추가 - 브라우저 캐시 무효화를 위한 버전 업데이트 (v=2025012617)
This commit is contained in:
@@ -64,6 +64,7 @@ class DocumentLinkResponse(BaseModel):
|
|||||||
# 대상 문서 정보
|
# 대상 문서 정보
|
||||||
target_document_title: str
|
target_document_title: str
|
||||||
target_document_book_id: Optional[str]
|
target_document_book_id: Optional[str]
|
||||||
|
target_content_type: Optional[str] = "document" # "document" 또는 "note"
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
@@ -241,19 +242,51 @@ async def get_document_links(
|
|||||||
detail="Access denied"
|
detail="Access denied"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 링크 조회 (JOIN으로 대상 문서 정보도 함께)
|
# 모든 링크 조회 (문서→문서 + 문서→노트)
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(DocumentLink, Document)
|
select(DocumentLink)
|
||||||
.join(Document, DocumentLink.target_document_id == Document.id)
|
|
||||||
.where(DocumentLink.source_document_id == document_id)
|
.where(DocumentLink.source_document_id == document_id)
|
||||||
.order_by(DocumentLink.start_offset.asc())
|
.order_by(DocumentLink.start_offset.asc())
|
||||||
)
|
)
|
||||||
|
|
||||||
links_with_targets = result.all()
|
all_links = result.scalars().all()
|
||||||
|
print(f"🔍 문서 링크 조회 완료: {len(all_links)}개 발견")
|
||||||
|
|
||||||
# 응답 데이터 구성
|
# 응답 데이터 구성
|
||||||
response_links = []
|
response_links = []
|
||||||
for link, target_doc in links_with_targets:
|
for link in all_links:
|
||||||
|
print(f"🔗 링크 처리 중: {link.id} -> {link.target_document_id}")
|
||||||
|
|
||||||
|
# 대상이 문서인지 노트인지 확인
|
||||||
|
target_doc = None
|
||||||
|
target_note = None
|
||||||
|
|
||||||
|
# 먼저 Document 테이블에서 찾기
|
||||||
|
doc_result = await db.execute(select(Document).where(Document.id == link.target_document_id))
|
||||||
|
target_doc = doc_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if target_doc:
|
||||||
|
print(f"✅ 대상 문서 찾음: {target_doc.title}")
|
||||||
|
target_title = target_doc.title
|
||||||
|
target_book_id = str(target_doc.book_id) if target_doc.book_id else None
|
||||||
|
target_content_type = "document"
|
||||||
|
else:
|
||||||
|
# Document에서 찾지 못하면 NoteDocument에서 찾기
|
||||||
|
from ...models.note_document import NoteDocument
|
||||||
|
note_result = await db.execute(select(NoteDocument).where(NoteDocument.id == link.target_document_id))
|
||||||
|
target_note = note_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if target_note:
|
||||||
|
print(f"✅ 대상 노트 찾음: {target_note.title}")
|
||||||
|
target_title = f"📝 {target_note.title}" # 노트임을 표시
|
||||||
|
target_book_id = str(target_note.notebook_id) if target_note.notebook_id else None
|
||||||
|
target_content_type = "note"
|
||||||
|
else:
|
||||||
|
print(f"❌ 대상을 찾을 수 없음: {link.target_document_id}")
|
||||||
|
target_title = "Unknown Target"
|
||||||
|
target_book_id = None
|
||||||
|
target_content_type = "document" # 기본값
|
||||||
|
|
||||||
response_links.append(DocumentLinkResponse(
|
response_links.append(DocumentLinkResponse(
|
||||||
id=str(link.id),
|
id=str(link.id),
|
||||||
source_document_id=str(link.source_document_id),
|
source_document_id=str(link.source_document_id),
|
||||||
@@ -270,9 +303,10 @@ async def get_document_links(
|
|||||||
target_start_offset=getattr(link, 'target_start_offset', None),
|
target_start_offset=getattr(link, 'target_start_offset', None),
|
||||||
target_end_offset=getattr(link, 'target_end_offset', None),
|
target_end_offset=getattr(link, 'target_end_offset', None),
|
||||||
link_type=getattr(link, 'link_type', 'document'),
|
link_type=getattr(link, 'link_type', 'document'),
|
||||||
# 대상 문서 정보
|
# 대상 문서/노트 정보 추가
|
||||||
target_document_title=target_doc.title,
|
target_document_title=target_title,
|
||||||
target_document_book_id=str(target_doc.book_id) if target_doc.book_id else None
|
target_document_book_id=target_book_id,
|
||||||
|
target_content_type=target_content_type
|
||||||
))
|
))
|
||||||
|
|
||||||
return response_links
|
return response_links
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ router = APIRouter()
|
|||||||
# 용어 정의: 하이라이트에 달리는 짧은 코멘트
|
# 용어 정의: 하이라이트에 달리는 짧은 코멘트
|
||||||
|
|
||||||
@router.post("/")
|
@router.post("/")
|
||||||
async def create_note(
|
def create_note(
|
||||||
note_data: dict,
|
note_data: dict,
|
||||||
db: Session = Depends(get_sync_db),
|
db: Session = Depends(get_sync_db),
|
||||||
current_user: User = Depends(get_current_user)
|
current_user: User = Depends(get_current_user)
|
||||||
@@ -65,6 +65,39 @@ async def create_note(
|
|||||||
|
|
||||||
return note
|
return note
|
||||||
|
|
||||||
|
@router.put("/{note_id}")
|
||||||
|
def update_note(
|
||||||
|
note_id: str,
|
||||||
|
note_data: dict,
|
||||||
|
db: Session = Depends(get_sync_db),
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""하이라이트 메모 업데이트"""
|
||||||
|
from ...models.note import Note
|
||||||
|
from ...models.highlight import Highlight
|
||||||
|
|
||||||
|
# 메모 존재 및 소유권 확인
|
||||||
|
note = db.query(Note).join(Highlight).filter(
|
||||||
|
Note.id == note_id,
|
||||||
|
Highlight.user_id == current_user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not note:
|
||||||
|
raise HTTPException(status_code=404, detail="메모를 찾을 수 없습니다")
|
||||||
|
|
||||||
|
# 메모 업데이트
|
||||||
|
if 'content' in note_data:
|
||||||
|
note.content = note_data['content']
|
||||||
|
if 'tags' in note_data:
|
||||||
|
note.tags = note_data['tags']
|
||||||
|
|
||||||
|
note.updated_at = datetime.utcnow()
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(note)
|
||||||
|
|
||||||
|
return note
|
||||||
|
|
||||||
@router.get("/document/{document_id}")
|
@router.get("/document/{document_id}")
|
||||||
async def get_document_notes(
|
async def get_document_notes(
|
||||||
document_id: str,
|
document_id: str,
|
||||||
@@ -118,7 +151,7 @@ def calculate_word_count(content: str) -> int:
|
|||||||
|
|
||||||
return korean_chars + english_words
|
return korean_chars + english_words
|
||||||
|
|
||||||
@router.get("/", response_model=List[NoteDocumentListItem])
|
@router.get("/")
|
||||||
def get_notes(
|
def get_notes(
|
||||||
skip: int = Query(0, ge=0),
|
skip: int = Query(0, ge=0),
|
||||||
limit: int = Query(50, ge=1, le=100),
|
limit: int = Query(50, ge=1, le=100),
|
||||||
@@ -128,10 +161,34 @@ def get_notes(
|
|||||||
published_only: bool = Query(False),
|
published_only: bool = Query(False),
|
||||||
parent_id: Optional[str] = Query(None),
|
parent_id: Optional[str] = Query(None),
|
||||||
notebook_id: Optional[str] = Query(None), # 노트북 필터
|
notebook_id: Optional[str] = Query(None), # 노트북 필터
|
||||||
|
document_id: Optional[str] = Query(None), # 하이라이트 메모 조회용
|
||||||
|
note_document_id: Optional[str] = Query(None), # 노트 문서의 하이라이트 메모 조회용
|
||||||
db: Session = Depends(get_sync_db),
|
db: Session = Depends(get_sync_db),
|
||||||
current_user: User = Depends(get_current_user)
|
current_user: User = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""노트 목록 조회"""
|
"""노트 목록 조회 또는 하이라이트 메모 조회"""
|
||||||
|
|
||||||
|
# 하이라이트 메모 조회 요청인 경우
|
||||||
|
if document_id or note_document_id:
|
||||||
|
from ...models.note import Note
|
||||||
|
from ...models.highlight import Highlight
|
||||||
|
|
||||||
|
if document_id:
|
||||||
|
# 일반 문서의 하이라이트 메모 조회
|
||||||
|
notes = db.query(Note).join(Highlight).filter(
|
||||||
|
Highlight.document_id == document_id,
|
||||||
|
Highlight.user_id == current_user.id
|
||||||
|
).options(
|
||||||
|
selectinload(Note.highlight)
|
||||||
|
).all()
|
||||||
|
else:
|
||||||
|
# 노트 문서의 하이라이트 메모 조회 (note_document_id)
|
||||||
|
# 노트 하이라이트 모델이 있다면 사용, 없다면 빈 리스트 반환
|
||||||
|
notes = []
|
||||||
|
|
||||||
|
return notes
|
||||||
|
|
||||||
|
# 일반 노트 문서 목록 조회
|
||||||
# 동기 SQLAlchemy 스타일
|
# 동기 SQLAlchemy 스타일
|
||||||
query = db.query(NoteDocument)
|
query = db.query(NoteDocument)
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ class DocumentServerAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getCurrentUser() {
|
async getCurrentUser() {
|
||||||
return await this.get('/users/me');
|
return await this.get('/auth/me');
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshToken(refreshToken) {
|
async refreshToken(refreshToken) {
|
||||||
@@ -255,8 +255,8 @@ class DocumentServerAPI {
|
|||||||
return await this.get(`/highlights/document/${documentId}`);
|
return await this.get(`/highlights/document/${documentId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateHighlight(highlightId, data) {
|
async updateHighlight(highlightId, updateData) {
|
||||||
return await this.put(`/highlights/${highlightId}`, data);
|
return await this.put(`/highlights/${highlightId}`, updateData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteHighlight(highlightId) {
|
async deleteHighlight(highlightId) {
|
||||||
@@ -266,22 +266,24 @@ class DocumentServerAPI {
|
|||||||
// === 하이라이트 메모 (Highlight Memo) 관련 API ===
|
// === 하이라이트 메모 (Highlight Memo) 관련 API ===
|
||||||
// 용어 정의: 하이라이트에 달리는 짧은 코멘트
|
// 용어 정의: 하이라이트에 달리는 짧은 코멘트
|
||||||
async createNote(noteData) {
|
async createNote(noteData) {
|
||||||
return await this.post('/notes/', noteData);
|
return await this.post('/highlight-notes/', noteData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNotes(params = {}) {
|
async getNotes(params = {}) {
|
||||||
return await this.get('/notes/', params);
|
return await this.get('/highlight-notes/', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateNote(noteId, updateData) {
|
||||||
|
return await this.put(`/highlight-notes/${noteId}`, updateData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 문서 메모 조회 ===
|
// === 문서 메모 조회 ===
|
||||||
// 용어 정의: 특정 문서의 모든 하이라이트 메모 조회
|
// 용어 정의: 특정 문서의 모든 하이라이트 메모 조회
|
||||||
async getDocumentNotes(documentId) {
|
async getDocumentNotes(documentId) {
|
||||||
return await this.get(`/notes/document/${documentId}`);
|
return await this.get(`/highlight-notes/`, { document_id: documentId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateNote(noteId, data) {
|
|
||||||
return await this.put(`/notes/${noteId}`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteNote(noteId) {
|
async deleteNote(noteId) {
|
||||||
return await this.delete(`/notes/${noteId}`);
|
return await this.delete(`/notes/${noteId}`);
|
||||||
@@ -375,10 +377,6 @@ class DocumentServerAPI {
|
|||||||
return await this.post('/notes/', noteData);
|
return await this.post('/notes/', noteData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateNote(noteId, noteData) {
|
|
||||||
return await this.put(`/notes/${noteId}`, noteData);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteNote(noteId) {
|
async deleteNote(noteId) {
|
||||||
return await this.delete(`/notes/${noteId}`);
|
return await this.delete(`/notes/${noteId}`);
|
||||||
}
|
}
|
||||||
@@ -500,10 +498,6 @@ class DocumentServerAPI {
|
|||||||
return await this.post('/notes/', noteData);
|
return await this.post('/notes/', noteData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateNote(noteId, noteData) {
|
|
||||||
return await this.put(`/notes/${noteId}`, noteData);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteNote(noteId) {
|
async deleteNote(noteId) {
|
||||||
return await this.delete(`/notes/${noteId}`);
|
return await this.delete(`/notes/${noteId}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,14 +42,20 @@ class HighlightManager {
|
|||||||
*/
|
*/
|
||||||
async loadNotes(documentId, contentType) {
|
async loadNotes(documentId, contentType) {
|
||||||
try {
|
try {
|
||||||
|
console.log('📝 메모 로드 시작:', { documentId, contentType });
|
||||||
|
|
||||||
if (contentType === 'note') {
|
if (contentType === 'note') {
|
||||||
this.notes = await this.api.get(`/note/${documentId}/notes`).catch(() => []);
|
// 노트 문서의 하이라이트 메모
|
||||||
|
this.notes = await this.api.get(`/highlight-notes/`, { note_document_id: documentId }).catch(() => []);
|
||||||
} else {
|
} else {
|
||||||
this.notes = await this.cachedApi.get('/notes', { document_id: documentId, content_type: contentType }, { category: 'notes' }).catch(() => []);
|
// 일반 문서의 하이라이트 메모
|
||||||
|
this.notes = await this.api.get(`/highlight-notes/`, { document_id: documentId }).catch(() => []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('📝 메모 로드 완료:', this.notes.length, '개');
|
||||||
return this.notes || [];
|
return this.notes || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('메모 로드 실패:', error);
|
console.error('❌ 메모 로드 실패:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,22 +241,46 @@ class HighlightManager {
|
|||||||
span.style.cursor = 'help';
|
span.style.cursor = 'help';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 하이라이트 클릭 이벤트 추가 (겹치는 요소 감지)
|
// 하이라이트 클릭 이벤트 추가 (통합 툴팁 사용)
|
||||||
span.style.cursor = 'pointer';
|
span.style.cursor = 'pointer';
|
||||||
span.addEventListener('click', (e) => {
|
span.addEventListener('click', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// 겹치는 링크나 백링크가 있는지 확인
|
console.log('🎨 하이라이트 클릭됨:', {
|
||||||
if (window.documentViewerInstance && window.documentViewerInstance.linkManager) {
|
text: span.textContent,
|
||||||
const overlappingElements = window.documentViewerInstance.linkManager.findOverlappingElements(span);
|
highlightId: span.dataset.highlightId,
|
||||||
if (overlappingElements.length > 0) {
|
classList: Array.from(span.classList)
|
||||||
window.documentViewerInstance.linkManager.showOverlapMenu(e, span, overlappingElements, 'highlight');
|
});
|
||||||
return;
|
|
||||||
|
// 링크, 백링크, 하이라이트 모두 찾기
|
||||||
|
const overlappingElements = window.documentViewerInstance.getOverlappingElements(span);
|
||||||
|
const totalElements = overlappingElements.links.length + overlappingElements.backlinks.length + overlappingElements.highlights.length;
|
||||||
|
|
||||||
|
console.log('🎨 하이라이트 클릭 분석:', {
|
||||||
|
links: overlappingElements.links.length,
|
||||||
|
backlinks: overlappingElements.backlinks.length,
|
||||||
|
highlights: overlappingElements.highlights.length,
|
||||||
|
total: totalElements,
|
||||||
|
selectedText: overlappingElements.selectedText
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalElements > 1) {
|
||||||
|
// 통합 툴팁 표시 (링크 + 백링크 + 하이라이트)
|
||||||
|
console.log('🎯 통합 툴팁 표시 시작 (하이라이트에서)');
|
||||||
|
await window.documentViewerInstance.showUnifiedTooltip(overlappingElements, span);
|
||||||
|
} else {
|
||||||
|
// 단일 하이라이트 툴팁
|
||||||
|
console.log('🎨 단일 하이라이트 툴팁 표시');
|
||||||
|
// 클릭된 하이라이트 찾기
|
||||||
|
const clickedHighlightId = span.dataset.highlightId;
|
||||||
|
const clickedHighlight = this.highlights.find(h => h.id === clickedHighlightId);
|
||||||
|
if (clickedHighlight) {
|
||||||
|
await this.showHighlightTooltip(clickedHighlight, span);
|
||||||
|
} else {
|
||||||
|
console.error('❌ 클릭된 하이라이트를 찾을 수 없음:', clickedHighlightId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.showHighlightModal(highlights);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// DOM 교체
|
// DOM 교체
|
||||||
@@ -498,6 +528,109 @@ class HighlightManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 하이라이트 색상 변경
|
||||||
|
*/
|
||||||
|
async updateHighlightColor(highlightId, newColor) {
|
||||||
|
try {
|
||||||
|
console.log('🎨 하이라이트 색상 업데이트:', highlightId, newColor);
|
||||||
|
|
||||||
|
// API 호출 (구현 필요)
|
||||||
|
await this.api.updateHighlight(highlightId, { highlight_color: newColor });
|
||||||
|
|
||||||
|
// 로컬 데이터 업데이트
|
||||||
|
const highlight = this.highlights.find(h => h.id === highlightId);
|
||||||
|
if (highlight) {
|
||||||
|
highlight.highlight_color = newColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 하이라이트 다시 렌더링
|
||||||
|
this.renderHighlights();
|
||||||
|
this.hideTooltip();
|
||||||
|
|
||||||
|
console.log('✅ 하이라이트 색상 변경 완료');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 하이라이트 색상 변경 실패:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 하이라이트 복사
|
||||||
|
*/
|
||||||
|
async duplicateHighlight(highlightId) {
|
||||||
|
try {
|
||||||
|
console.log('📋 하이라이트 복사:', highlightId);
|
||||||
|
|
||||||
|
const originalHighlight = this.highlights.find(h => h.id === highlightId);
|
||||||
|
if (!originalHighlight) {
|
||||||
|
throw new Error('원본 하이라이트를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 새 하이라이트 데이터 생성 (약간 다른 위치에)
|
||||||
|
const duplicateData = {
|
||||||
|
document_id: originalHighlight.document_id,
|
||||||
|
start_offset: originalHighlight.start_offset,
|
||||||
|
end_offset: originalHighlight.end_offset,
|
||||||
|
selected_text: originalHighlight.selected_text,
|
||||||
|
highlight_color: originalHighlight.highlight_color,
|
||||||
|
highlight_type: originalHighlight.highlight_type
|
||||||
|
};
|
||||||
|
|
||||||
|
// API 호출
|
||||||
|
const newHighlight = await this.api.createHighlight(duplicateData);
|
||||||
|
|
||||||
|
// 로컬 데이터에 추가
|
||||||
|
this.highlights.push(newHighlight);
|
||||||
|
|
||||||
|
// 하이라이트 다시 렌더링
|
||||||
|
this.renderHighlights();
|
||||||
|
this.hideTooltip();
|
||||||
|
|
||||||
|
console.log('✅ 하이라이트 복사 완료:', newHighlight);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 하이라이트 복사 실패:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메모 업데이트
|
||||||
|
*/
|
||||||
|
async updateNote(noteId, newContent) {
|
||||||
|
try {
|
||||||
|
console.log('✏️ 메모 업데이트:', noteId, newContent);
|
||||||
|
|
||||||
|
// API 호출
|
||||||
|
const apiToUse = this.cachedApi || this.api;
|
||||||
|
await apiToUse.updateNote(noteId, { content: newContent });
|
||||||
|
|
||||||
|
// 로컬 데이터 업데이트
|
||||||
|
const note = this.notes.find(n => n.id === noteId);
|
||||||
|
if (note) {
|
||||||
|
note.content = newContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 툴팁 새로고침 (현재 표시 중인 경우)
|
||||||
|
const tooltip = document.getElementById('highlight-tooltip');
|
||||||
|
if (tooltip) {
|
||||||
|
// 간단히 툴팁을 다시 로드하는 대신 텍스트만 업데이트
|
||||||
|
const noteElement = document.querySelector(`[data-note-id="${noteId}"] .text-gray-800`);
|
||||||
|
if (noteElement) {
|
||||||
|
noteElement.textContent = newContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ 메모 업데이트 완료');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 메모 업데이트 실패:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 텍스트 오프셋 계산
|
* 텍스트 오프셋 계산
|
||||||
*/
|
*/
|
||||||
@@ -729,13 +862,70 @@ class HighlightManager {
|
|||||||
return colorNames[color] || '기타';
|
return colorNames[color] || '기타';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 날짜 포맷팅 (상세)
|
||||||
|
*/
|
||||||
|
formatDate(dateString) {
|
||||||
|
if (!dateString) return '알 수 없음';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('ko-KR', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 날짜 포맷팅 (간단)
|
||||||
|
*/
|
||||||
|
formatShortDate(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 === 2) {
|
||||||
|
return '어제';
|
||||||
|
} else if (diffDays <= 7) {
|
||||||
|
return `${diffDays - 1}일 전`;
|
||||||
|
} else {
|
||||||
|
return date.toLocaleDateString('ko-KR', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 하이라이트 툴팁 표시
|
* 하이라이트 툴팁 표시
|
||||||
*/
|
*/
|
||||||
showHighlightTooltip(clickedHighlight, element) {
|
async showHighlightTooltip(clickedHighlight, element) {
|
||||||
// 기존 말풍선 제거
|
// 기존 말풍선 제거
|
||||||
this.hideTooltip();
|
this.hideTooltip();
|
||||||
|
|
||||||
|
// 메모 데이터 다시 로드 (최신 상태 보장)
|
||||||
|
console.log('📝 하이라이트 툴팁용 메모 로드 시작...');
|
||||||
|
const documentId = window.documentViewerInstance.documentId;
|
||||||
|
const contentType = window.documentViewerInstance.contentType;
|
||||||
|
|
||||||
|
console.log('📝 메모 로드 파라미터:', { documentId, contentType });
|
||||||
|
console.log('📝 기존 메모 개수:', this.notes ? this.notes.length : 'undefined');
|
||||||
|
|
||||||
|
await this.loadNotes(documentId, contentType);
|
||||||
|
|
||||||
|
console.log('📝 메모 로드 완료:', this.notes.length, '개');
|
||||||
|
console.log('📝 로드된 메모 상세:', this.notes.map(n => ({
|
||||||
|
id: n.id,
|
||||||
|
highlight_id: n.highlight_id,
|
||||||
|
content: n.content,
|
||||||
|
created_at: n.created_at
|
||||||
|
})));
|
||||||
|
|
||||||
// 동일한 범위의 모든 하이라이트 찾기
|
// 동일한 범위의 모든 하이라이트 찾기
|
||||||
const overlappingHighlights = this.findOverlappingHighlights(clickedHighlight);
|
const overlappingHighlights = this.findOverlappingHighlights(clickedHighlight);
|
||||||
const colorGroups = this.groupHighlightsByColor(overlappingHighlights);
|
const colorGroups = this.groupHighlightsByColor(overlappingHighlights);
|
||||||
@@ -754,9 +944,19 @@ class HighlightManager {
|
|||||||
|
|
||||||
let tooltipHTML = `
|
let 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-gray-900 bg-gray-100 px-3 py-2 rounded border-l-4 border-blue-500">
|
<div class="text-lg font-semibold text-blue-800 flex items-center">
|
||||||
"${longestText}"
|
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
|
||||||
|
</svg>
|
||||||
|
하이라이트 정보
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">${overlappingHighlights.length}개 하이라이트</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="font-medium text-gray-900 bg-blue-50 px-4 py-3 rounded-lg border-l-4 border-blue-500">
|
||||||
|
<div class="text-sm text-blue-700 mb-1">선택된 텍스트</div>
|
||||||
|
<div class="text-base">"${longestText}"</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -766,39 +966,84 @@ class HighlightManager {
|
|||||||
|
|
||||||
Object.entries(colorGroups).forEach(([color, highlights]) => {
|
Object.entries(colorGroups).forEach(([color, highlights]) => {
|
||||||
const colorName = this.getColorName(color);
|
const colorName = this.getColorName(color);
|
||||||
const allNotes = highlights.flatMap(h =>
|
|
||||||
this.notes.filter(note => note.highlight_id === h.id)
|
// 각 하이라이트에 대한 메모 찾기 (디버깅 로그 추가)
|
||||||
);
|
const allNotes = highlights.flatMap(h => {
|
||||||
|
const notesForHighlight = this.notes.filter(note => note.highlight_id === h.id);
|
||||||
|
console.log(`📝 하이라이트 ${h.id}에 대한 메모:`, notesForHighlight.length, '개');
|
||||||
|
if (notesForHighlight.length > 0) {
|
||||||
|
console.log('📝 메모 내용:', notesForHighlight.map(n => n.content));
|
||||||
|
}
|
||||||
|
return notesForHighlight;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`🎨 ${colorName} 하이라이트의 총 메모:`, allNotes.length, '개');
|
||||||
|
|
||||||
|
const createdDate = highlights[0].created_at ? this.formatDate(highlights[0].created_at) : '알 수 없음';
|
||||||
|
|
||||||
tooltipHTML += `
|
tooltipHTML += `
|
||||||
<div class="border rounded-lg p-3" style="border-left: 4px solid ${color}">
|
<div class="border rounded-lg p-4 bg-gradient-to-r from-gray-50 to-gray-100" style="border-left: 4px solid ${color}">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-3">
|
||||||
<div class="w-3 h-3 rounded" style="background-color: ${color}"></div>
|
<div class="w-4 h-4 rounded-full shadow-sm" style="background-color: ${color}"></div>
|
||||||
<span class="text-sm font-medium text-gray-700">${colorName} 메모 (${allNotes.length})</span>
|
<div>
|
||||||
|
<span class="text-sm font-semibold text-gray-800">${colorName} 하이라이트</span>
|
||||||
|
<div class="text-xs text-gray-600">${createdDate} 생성</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<button onclick="window.documentViewerInstance.highlightManager.changeHighlightColor('${highlights[0].id}')"
|
||||||
|
class="text-xs bg-gray-500 text-white px-2 py-1 rounded hover:bg-gray-600 transition-colors"
|
||||||
|
title="색상 변경">
|
||||||
|
🎨
|
||||||
|
</button>
|
||||||
|
<button onclick="window.documentViewerInstance.highlightManager.showAddNoteForm('${highlights[0].id}')"
|
||||||
|
class="text-xs bg-blue-500 text-white px-2 py-1 rounded hover:bg-blue-600 transition-colors">
|
||||||
|
📝 메모 추가
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="window.documentViewerInstance.highlightManager.showAddNoteForm('${highlights[0].id}')"
|
|
||||||
class="text-xs bg-blue-500 text-white px-2 py-1 rounded hover:bg-blue-600">
|
|
||||||
+ 추가
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="notes-list-${highlights[0].id}" class="space-y-2 max-h-32 overflow-y-auto">
|
<!-- 메모 목록 -->
|
||||||
${allNotes.length > 0 ?
|
<div class="mb-3">
|
||||||
allNotes.map(note => `
|
<div class="text-sm font-medium text-gray-700 mb-2 flex items-center">
|
||||||
<div class="bg-gray-50 p-2 rounded text-sm">
|
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<div class="text-gray-800">${note.content}</div>
|
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"></path>
|
||||||
<div class="text-xs text-gray-500 mt-1 flex justify-between items-center">
|
<path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 102 0V3a2 2 0 012 0v1a1 1 0 102 0V3a2 2 0 012 2v6.586A2 2 0 0115.414 13L13 15.586A2 2 0 0111.586 16H6a2 2 0 01-2-2V5zm8 4a1 1 0 10-2 0v2a1 1 0 102 0V9z" clip-rule="evenodd"></path>
|
||||||
<span>${this.formatShortDate(note.created_at)} · Administrator</span>
|
</svg>
|
||||||
<button onclick="window.documentViewerInstance.highlightManager.deleteNote('${note.id}')"
|
메모 (${allNotes.length}개)
|
||||||
class="text-red-600 hover:text-red-800">
|
</div>
|
||||||
삭제
|
|
||||||
</button>
|
<div id="notes-list-${highlights[0].id}" class="space-y-2 max-h-40 overflow-y-auto">
|
||||||
|
${allNotes.length > 0 ?
|
||||||
|
allNotes.map(note => `
|
||||||
|
<div class="bg-white p-3 rounded-lg border shadow-sm group">
|
||||||
|
<div class="text-gray-800 text-sm leading-relaxed">${note.content}</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-2 flex justify-between items-center">
|
||||||
|
<span class="flex items-center">
|
||||||
|
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
${this.formatShortDate(note.created_at)}
|
||||||
|
</span>
|
||||||
|
<div class="opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<button onclick="window.documentViewerInstance.highlightManager.editNote('${note.id}', '${note.content.replace(/'/g, "\\'")}');"
|
||||||
|
class="text-blue-600 hover:text-blue-800 mr-2 text-xs"
|
||||||
|
title="메모 편집">
|
||||||
|
✏️
|
||||||
|
</button>
|
||||||
|
<button onclick="window.documentViewerInstance.highlightManager.deleteNote('${note.id}')"
|
||||||
|
class="text-red-600 hover:text-red-800 text-xs"
|
||||||
|
title="메모 삭제">
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
`).join('') :
|
||||||
`).join('') :
|
'<div class="text-sm text-gray-500 italic bg-white p-3 rounded-lg border">메모가 없습니다. 위의 "📝 메모 추가" 버튼을 클릭해보세요!</div>'
|
||||||
'<div class="text-sm text-gray-500 italic">메모가 없습니다</div>'
|
}
|
||||||
}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -806,13 +1051,36 @@ class HighlightManager {
|
|||||||
|
|
||||||
tooltipHTML += '</div>';
|
tooltipHTML += '</div>';
|
||||||
|
|
||||||
// 하이라이트 삭제 버튼
|
// 하이라이트 관리 버튼들
|
||||||
tooltipHTML += `
|
tooltipHTML += `
|
||||||
<div class="mt-4 pt-3 border-t">
|
<div class="mt-4 pt-4 border-t border-gray-200">
|
||||||
<button onclick="window.documentViewerInstance.highlightManager.deleteHighlight('${clickedHighlight.id}')"
|
<div class="flex items-center justify-between">
|
||||||
class="text-xs bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600">
|
<div class="text-sm font-medium text-gray-700 flex items-center">
|
||||||
하이라이트 삭제
|
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||||
</button>
|
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
하이라이트 관리
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<button onclick="window.documentViewerInstance.highlightManager.duplicateHighlight('${clickedHighlight.id}')"
|
||||||
|
class="text-xs bg-green-500 text-white px-3 py-1 rounded hover:bg-green-600 transition-colors flex items-center"
|
||||||
|
title="하이라이트 복사">
|
||||||
|
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z"></path>
|
||||||
|
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z"></path>
|
||||||
|
</svg>
|
||||||
|
복사
|
||||||
|
</button>
|
||||||
|
<button onclick="window.documentViewerInstance.highlightManager.deleteHighlightWithConfirm('${clickedHighlight.id}')"
|
||||||
|
class="text-xs bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600 transition-colors flex items-center"
|
||||||
|
title="하이라이트 삭제">
|
||||||
|
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||||
|
</svg>
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -295,16 +295,36 @@ class LinkManager {
|
|||||||
box-sizing: border-box !important;
|
box-sizing: border-box !important;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 클릭 이벤트 추가 (겹치는 요소 감지)
|
// 클릭 이벤트 추가 (통합 툴팁 사용)
|
||||||
span.addEventListener('click', (e) => {
|
span.addEventListener('click', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// 겹치는 하이라이트나 백링크가 있는지 확인
|
console.log('🔗 링크 클릭됨:', {
|
||||||
const overlappingElements = this.findOverlappingElements(span);
|
text: span.textContent,
|
||||||
if (overlappingElements.length > 0) {
|
linkId: link.id,
|
||||||
this.showOverlapMenu(e, span, overlappingElements, 'link');
|
classList: Array.from(span.classList)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 링크, 백링크, 하이라이트 모두 찾기
|
||||||
|
const overlappingElements = window.documentViewerInstance.getOverlappingElements(span);
|
||||||
|
const totalElements = overlappingElements.links.length + overlappingElements.backlinks.length + overlappingElements.highlights.length;
|
||||||
|
|
||||||
|
console.log('🎯 링크 클릭 분석:', {
|
||||||
|
links: overlappingElements.links.length,
|
||||||
|
backlinks: overlappingElements.backlinks.length,
|
||||||
|
highlights: overlappingElements.highlights.length,
|
||||||
|
total: totalElements,
|
||||||
|
selectedText: overlappingElements.selectedText
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalElements > 1) {
|
||||||
|
// 통합 툴팁 표시 (링크 + 백링크 + 하이라이트)
|
||||||
|
console.log('🎨 통합 툴팁 표시 시작 (링크에서)');
|
||||||
|
await window.documentViewerInstance.showUnifiedTooltip(overlappingElements, span);
|
||||||
} else {
|
} else {
|
||||||
|
// 단일 링크 툴팁
|
||||||
|
console.log('🔗 단일 링크 툴팁 표시');
|
||||||
this.showLinkTooltip(link, span);
|
this.showLinkTooltip(link, span);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -503,16 +523,36 @@ class LinkManager {
|
|||||||
box-sizing: border-box !important;
|
box-sizing: border-box !important;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 클릭 이벤트 추가 (겹치는 요소 감지)
|
// 클릭 이벤트 추가 (통합 툴팁 사용)
|
||||||
span.addEventListener('click', (e) => {
|
span.addEventListener('click', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// 겹치는 하이라이트나 링크가 있는지 확인
|
console.log('🔙 백링크 클릭됨:', {
|
||||||
const overlappingElements = this.findOverlappingElements(span);
|
text: span.textContent,
|
||||||
if (overlappingElements.length > 0) {
|
backlinkId: backlink.id,
|
||||||
this.showOverlapMenu(e, span, overlappingElements, 'backlink');
|
classList: Array.from(span.classList)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 링크, 백링크, 하이라이트 모두 찾기
|
||||||
|
const overlappingElements = window.documentViewerInstance.getOverlappingElements(span);
|
||||||
|
const totalElements = overlappingElements.links.length + overlappingElements.backlinks.length + overlappingElements.highlights.length;
|
||||||
|
|
||||||
|
console.log('🔙 백링크 클릭 분석:', {
|
||||||
|
links: overlappingElements.links.length,
|
||||||
|
backlinks: overlappingElements.backlinks.length,
|
||||||
|
highlights: overlappingElements.highlights.length,
|
||||||
|
total: totalElements,
|
||||||
|
selectedText: overlappingElements.selectedText
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalElements > 1) {
|
||||||
|
// 통합 툴팁 표시 (링크 + 백링크 + 하이라이트)
|
||||||
|
console.log('🎯 통합 툴팁 표시 시작 (백링크에서)');
|
||||||
|
await window.documentViewerInstance.showUnifiedTooltip(overlappingElements, span);
|
||||||
} else {
|
} else {
|
||||||
|
// 단일 백링크 툴팁
|
||||||
|
console.log('🔙 단일 백링크 툴팁 표시');
|
||||||
this.showBacklinkTooltip(backlink, span);
|
this.showBacklinkTooltip(backlink, span);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -530,7 +570,160 @@ class LinkManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 링크 툴팁 표시
|
* 겹치는 링크들 찾기
|
||||||
|
*/
|
||||||
|
getOverlappingLinks(clickedElement) {
|
||||||
|
const clickedLinkId = clickedElement.getAttribute('data-link-id');
|
||||||
|
const clickedText = clickedElement.textContent;
|
||||||
|
|
||||||
|
console.log('🔍 겹치는 링크 찾기:', {
|
||||||
|
clickedLinkId: clickedLinkId,
|
||||||
|
clickedText: clickedText,
|
||||||
|
totalLinks: this.documentLinks.length
|
||||||
|
});
|
||||||
|
|
||||||
|
// 동일한 텍스트 범위에 있는 모든 링크 찾기
|
||||||
|
const overlappingLinks = this.documentLinks.filter(link => {
|
||||||
|
// 클릭된 링크와 텍스트가 겹치는지 확인
|
||||||
|
const linkElement = document.querySelector(`[data-link-id="${link.id}"]`);
|
||||||
|
if (!linkElement) return false;
|
||||||
|
|
||||||
|
// 텍스트가 겹치는지 확인 (간단한 방법: 텍스트 내용 비교)
|
||||||
|
const isOverlapping = linkElement.textContent === clickedText;
|
||||||
|
|
||||||
|
if (isOverlapping) {
|
||||||
|
console.log('✅ 겹치는 링크 발견:', {
|
||||||
|
id: link.id,
|
||||||
|
text: linkElement.textContent,
|
||||||
|
target: link.target_document_title
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return isOverlapping;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`🔍 총 ${overlappingLinks.length}개의 겹치는 링크 발견`);
|
||||||
|
return overlappingLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 다중 링크 툴팁 표시
|
||||||
|
*/
|
||||||
|
showMultiLinkTooltip(links, element, selectedText) {
|
||||||
|
console.log('🔗 다중 링크 툴팁 표시:', links.length, '개');
|
||||||
|
|
||||||
|
// 기존 툴팁 제거
|
||||||
|
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 = '400px';
|
||||||
|
tooltip.style.maxHeight = '80vh';
|
||||||
|
tooltip.style.overflowY = 'auto';
|
||||||
|
|
||||||
|
let tooltipHTML = `
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="text-sm text-gray-600 mb-2">선택된 텍스트</div>
|
||||||
|
<div class="font-medium text-gray-900 bg-gray-100 px-3 py-2 rounded border-l-4 border-purple-500">
|
||||||
|
"${selectedText}"
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (links.length > 1) {
|
||||||
|
tooltipHTML += `
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="text-sm font-medium text-gray-700 mb-2 flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-2 text-purple-600" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
연결된 링크 (${links.length}개)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltipHTML += '<div class="space-y-3">';
|
||||||
|
|
||||||
|
links.forEach((link, index) => {
|
||||||
|
const createdDate = link.created_at ? this.formatDate(link.created_at) : '알 수 없음';
|
||||||
|
const isNote = link.target_content_type === 'note';
|
||||||
|
const iconClass = isNote ? 'text-green-600' : 'text-purple-600';
|
||||||
|
const bgClass = isNote ? 'hover:bg-green-50' : 'hover:bg-purple-50';
|
||||||
|
|
||||||
|
tooltipHTML += `
|
||||||
|
<div class="border rounded-lg p-3 ${bgClass} transition-colors duration-200 relative group">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex-1 cursor-pointer" onclick="window.documentViewerInstance.navigateToLink(${JSON.stringify(link).replace(/"/g, '"')})">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<svg class="w-4 h-4 mr-2 ${iconClass}" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
${isNote ?
|
||||||
|
'<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"></path><path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 102 0V3a2 2 0 012 0v1a1 1 0 102 0V3a2 2 0 012 2v6.586A2 2 0 0115.414 13L13 15.586A2 2 0 0111.586 16H6a2 2 0 01-2-2V5zm8 4a1 1 0 10-2 0v2a1 1 0 102 0V9z" clip-rule="evenodd"></path>' :
|
||||||
|
'<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"></path>'
|
||||||
|
}
|
||||||
|
</svg>
|
||||||
|
<span class="font-medium ${iconClass}">${link.target_document_title}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 연결된 텍스트 정보 -->
|
||||||
|
${link.target_text ? `
|
||||||
|
<div class="mb-2 p-2 bg-gray-50 rounded border-l-3 ${isNote ? 'border-green-400' : 'border-purple-400'}">
|
||||||
|
<div class="text-xs ${isNote ? 'text-green-700' : 'text-purple-700'} mb-1">연결된 텍스트</div>
|
||||||
|
<div class="text-sm text-gray-800 font-medium">"${link.target_text}"</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${link.description ? `
|
||||||
|
<div class="mb-2 p-2 bg-blue-50 rounded border-l-3 border-blue-400">
|
||||||
|
<div class="text-xs text-blue-700 mb-1">링크 설명</div>
|
||||||
|
<div class="text-sm text-blue-800">${link.description}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-500 flex items-center justify-between">
|
||||||
|
<span class="flex items-center">
|
||||||
|
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
${link.link_type === 'text_fragment' ? '텍스트 조각 링크' : '문서 링크'}
|
||||||
|
</span>
|
||||||
|
<span>${createdDate}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 삭제 버튼 -->
|
||||||
|
<button onclick="event.stopPropagation(); window.documentViewerInstance.deleteLinkWithConfirm('${link.id}', '${link.target_document_title.replace(/'/g, "\\'")}');"
|
||||||
|
class="ml-3 p-1 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors duration-200 opacity-0 group-hover:opacity-100"
|
||||||
|
title="링크 삭제">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
tooltipHTML += '</div>';
|
||||||
|
|
||||||
|
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 - 200) + 'px';
|
||||||
|
|
||||||
|
document.body.appendChild(tooltip);
|
||||||
|
|
||||||
|
// 외부 클릭 시 툴팁 숨기기
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', this.handleTooltipOutsideClick.bind(this));
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 링크 툴팁 표시 (단일 링크용)
|
||||||
*/
|
*/
|
||||||
showLinkTooltip(link, element) {
|
showLinkTooltip(link, element) {
|
||||||
this.hideTooltip();
|
this.hideTooltip();
|
||||||
@@ -645,9 +838,28 @@ class LinkManager {
|
|||||||
<div class="text-xs text-gray-500">${createdDate}</div>
|
<div class="text-xs text-gray-500">${createdDate}</div>
|
||||||
</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="mb-4 p-3 bg-blue-50 rounded-lg border-l-4 border-blue-400">
|
||||||
<div class="text-base">"${backlink.target_text || backlink.selected_text}"</div>
|
<div class="text-sm text-blue-800">
|
||||||
|
<strong>💡 백링크란?</strong><br>
|
||||||
|
다른 문서에서 현재 문서의 이 텍스트를 참조하는 연결입니다.<br>
|
||||||
|
클릭하면 참조하는 문서로 이동할 수 있습니다.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 현재 문서의 참조된 텍스트 (강화된 정보) -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<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 font-semibold">"${backlink.target_text || backlink.selected_text}"</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${backlink.target_text && backlink.target_text !== backlink.selected_text ? `
|
||||||
|
<div class="mt-2 p-3 bg-gray-50 rounded-lg border-l-4 border-gray-400">
|
||||||
|
<div class="text-xs text-gray-600 mb-1">원본 링크에서 선택한 텍스트</div>
|
||||||
|
<div class="text-sm text-gray-800">"${backlink.selected_text}"</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -656,28 +868,47 @@ class LinkManager {
|
|||||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
<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>
|
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
참조하는 문서
|
이 텍스트를 참조하는 문서
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gradient-to-r from-orange-50 to-orange-100 p-4 rounded-lg border">
|
<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="cursor-pointer" onclick="window.documentViewerInstance.navigateToBacklink(${JSON.stringify(backlink).replace(/"/g, '"')})">
|
||||||
<div class="bg-white p-3 rounded border-l-3 border-orange-400">
|
<div class="font-semibold text-gray-900 mb-3 flex items-center">
|
||||||
<div class="text-xs text-gray-600 mb-1">원본 텍스트</div>
|
<svg class="w-4 h-4 mr-2 text-orange-600" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<div class="text-sm text-gray-800">"${backlink.selected_text}"</div>
|
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"></path>
|
||||||
</div>
|
</svg>
|
||||||
${backlink.description ? `
|
${backlink.source_document_title}
|
||||||
<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 class="space-y-3">
|
||||||
|
<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 font-medium">"${backlink.selected_text}"</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${backlink.target_text ? `
|
||||||
|
<div class="bg-white p-3 rounded border-l-3 border-blue-400">
|
||||||
|
<div class="text-xs text-blue-600 mb-1">현재 문서에서 연결된 구체적인 텍스트</div>
|
||||||
|
<div class="text-sm text-blue-800 font-medium">"${backlink.target_text}"</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${backlink.description ? `
|
||||||
|
<div class="bg-white p-3 rounded border-l-3 border-green-400">
|
||||||
|
<div class="text-xs text-green-600 mb-1">링크 설명</div>
|
||||||
|
<div class="text-sm text-green-800">${backlink.description}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row gap-3 mb-4">
|
<div class="flex justify-center mb-4">
|
||||||
<button onclick="window.documentViewerInstance.linkManager.navigateToSourceDocument('${backlink.source_document_id}', ${JSON.stringify(backlink).replace(/"/g, '"')})"
|
<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">
|
class="bg-gradient-to-r from-orange-500 to-orange-600 text-white px-6 py-2 rounded-lg hover:from-orange-600 hover:to-orange-700 transition-all duration-200 flex items-center justify-center font-medium shadow-md hover:shadow-lg">
|
||||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<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>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-2M7 7l10 10M17 7v4h-4"></path>
|
||||||
</svg>
|
</svg>
|
||||||
원본 문서로 이동
|
원본 문서로 이동
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -232,22 +232,24 @@ class CachedAPI {
|
|||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 기존 API 메서드 직접 사용
|
// 하이라이트 메모 API 사용
|
||||||
try {
|
try {
|
||||||
let result;
|
let result;
|
||||||
if (contentType === 'note') {
|
if (contentType === 'note') {
|
||||||
result = await this.api.get(`/note/${documentId}/notes`).catch(() => []);
|
// 노트 문서의 하이라이트 메모
|
||||||
|
result = await this.api.get(`/highlight-notes/`, { note_document_id: documentId }).catch(() => []);
|
||||||
} else {
|
} else {
|
||||||
result = await this.api.getDocumentNotes(documentId).catch(() => []);
|
// 일반 문서의 하이라이트 메모
|
||||||
|
result = await this.api.get(`/highlight-notes/`, { document_id: documentId }).catch(() => []);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 캐시에 저장
|
// 캐시에 저장
|
||||||
this.cache.set(cacheKey, result, 'notes', 10 * 60 * 1000);
|
this.cache.set(cacheKey, result, 'notes', 10 * 60 * 1000);
|
||||||
console.log(`💾 메모 캐시 저장: ${documentId}`);
|
console.log(`💾 메모 캐시 저장: ${documentId} (${result.length}개)`);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('메모 로드 실패:', error);
|
console.error('❌ 메모 로드 실패:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,7 +361,16 @@ class CachedAPI {
|
|||||||
* 메모 생성 (캐시 무효화)
|
* 메모 생성 (캐시 무효화)
|
||||||
*/
|
*/
|
||||||
async createNote(data) {
|
async createNote(data) {
|
||||||
return await this.post('/notes/', data, {
|
return await this.post('/highlight-notes/', data, {
|
||||||
|
invalidateCategories: ['notes', 'highlights']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메모 업데이트 (캐시 무효화)
|
||||||
|
*/
|
||||||
|
async updateNote(noteId, data) {
|
||||||
|
return await this.put(`/highlight-notes/${noteId}`, data, {
|
||||||
invalidateCategories: ['notes', 'highlights']
|
invalidateCategories: ['notes', 'highlights']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1136,6 +1136,423 @@ window.documentViewer = () => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ==================== 통합 툴팁 처리 ====================
|
||||||
|
/**
|
||||||
|
* 클릭된 요소에서 링크, 백링크, 하이라이트 모두 찾기 (완전 개선 버전)
|
||||||
|
*/
|
||||||
|
getOverlappingElements(clickedElement) {
|
||||||
|
const selectedText = clickedElement.textContent.trim();
|
||||||
|
console.log('🔍 통합 요소 찾기 시작:', selectedText);
|
||||||
|
console.log('🔍 하이라이트 매니저 상태:', {
|
||||||
|
highlightManager: !!this.highlightManager,
|
||||||
|
highlightsCount: this.highlightManager?.highlights?.length || 0,
|
||||||
|
highlights: this.highlightManager?.highlights || []
|
||||||
|
});
|
||||||
|
|
||||||
|
// 결과 배열들
|
||||||
|
const overlappingLinks = [];
|
||||||
|
const overlappingBacklinks = [];
|
||||||
|
const overlappingHighlights = [];
|
||||||
|
|
||||||
|
// 1. 모든 링크 요소 찾기 (같은 텍스트)
|
||||||
|
const allLinkElements = document.querySelectorAll('.document-link');
|
||||||
|
allLinkElements.forEach(linkEl => {
|
||||||
|
if (linkEl.textContent.trim() === selectedText) {
|
||||||
|
const linkId = linkEl.dataset.linkId;
|
||||||
|
const link = this.linkManager.documentLinks.find(l => l.id === linkId);
|
||||||
|
if (link && !overlappingLinks.find(l => l.id === link.id)) {
|
||||||
|
overlappingLinks.push(link);
|
||||||
|
console.log('✅ 겹치는 링크 발견:', link.target_document_title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 모든 백링크 요소 찾기 (같은 텍스트)
|
||||||
|
const allBacklinkElements = document.querySelectorAll('.backlink-highlight');
|
||||||
|
allBacklinkElements.forEach(backlinkEl => {
|
||||||
|
if (backlinkEl.textContent.trim() === selectedText) {
|
||||||
|
const backlinkId = backlinkEl.dataset.backlinkId;
|
||||||
|
const backlink = this.linkManager.backlinks.find(b => b.id === backlinkId);
|
||||||
|
if (backlink && !overlappingBacklinks.find(b => b.id === backlink.id)) {
|
||||||
|
overlappingBacklinks.push(backlink);
|
||||||
|
console.log('✅ 겹치는 백링크 발견:', backlink.source_document_title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 모든 하이라이트 요소 찾기 (같은 텍스트)
|
||||||
|
const allHighlightElements = document.querySelectorAll('.highlight-span');
|
||||||
|
console.log('🔍 페이지의 모든 하이라이트 요소:', allHighlightElements.length, '개');
|
||||||
|
allHighlightElements.forEach(highlightEl => {
|
||||||
|
const highlightText = highlightEl.textContent.trim();
|
||||||
|
|
||||||
|
// 텍스트가 정확히 일치하거나 포함 관계인 경우
|
||||||
|
if (highlightText === selectedText ||
|
||||||
|
highlightText.includes(selectedText) ||
|
||||||
|
selectedText.includes(highlightText)) {
|
||||||
|
|
||||||
|
const highlightId = highlightEl.dataset.highlightId;
|
||||||
|
console.log('🔍 하이라이트 요소 확인:', {
|
||||||
|
element: highlightEl,
|
||||||
|
highlightId: highlightId,
|
||||||
|
text: highlightText,
|
||||||
|
selectedText: selectedText
|
||||||
|
});
|
||||||
|
|
||||||
|
const highlight = this.highlightManager.highlights.find(h => h.id === highlightId);
|
||||||
|
if (highlight && !overlappingHighlights.find(h => h.id === highlight.id)) {
|
||||||
|
overlappingHighlights.push(highlight);
|
||||||
|
console.log('✅ 겹치는 하이라이트 발견:', {
|
||||||
|
id: highlight.id,
|
||||||
|
text: highlightText,
|
||||||
|
color: highlight.highlight_color
|
||||||
|
});
|
||||||
|
} else if (!highlight) {
|
||||||
|
console.log('❌ 하이라이트 데이터를 찾을 수 없음:', highlightId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📊 최종 발견된 요소들:', {
|
||||||
|
links: overlappingLinks.length,
|
||||||
|
backlinks: overlappingBacklinks.length,
|
||||||
|
highlights: overlappingHighlights.length,
|
||||||
|
selectedText: selectedText
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
links: overlappingLinks,
|
||||||
|
backlinks: overlappingBacklinks,
|
||||||
|
highlights: overlappingHighlights,
|
||||||
|
selectedText: selectedText
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 통합 툴팁 표시 (링크 + 하이라이트 + 백링크)
|
||||||
|
*/
|
||||||
|
async showUnifiedTooltip(overlappingElements, element) {
|
||||||
|
const { links = [], highlights = [], backlinks = [], selectedText } = overlappingElements;
|
||||||
|
|
||||||
|
console.log('🎯 통합 툴팁 표시:', {
|
||||||
|
links: links.length,
|
||||||
|
highlights: highlights.length,
|
||||||
|
backlinks: backlinks.length
|
||||||
|
});
|
||||||
|
|
||||||
|
// 하이라이트가 있으면 메모 데이터 로드
|
||||||
|
if (highlights.length > 0) {
|
||||||
|
console.log('📝 통합 툴팁용 메모 로드 시작...');
|
||||||
|
const documentId = this.documentId;
|
||||||
|
const contentType = this.contentType;
|
||||||
|
await this.highlightManager.loadNotes(documentId, contentType);
|
||||||
|
console.log('📝 통합 툴팁용 메모 로드 완료:', this.highlightManager.notes.length, '개');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기존 툴팁들 숨기기
|
||||||
|
this.linkManager.hideTooltip();
|
||||||
|
this.highlightManager.hideTooltip();
|
||||||
|
|
||||||
|
// 툴팁 컨테이너 생성
|
||||||
|
const tooltip = document.createElement('div');
|
||||||
|
tooltip.id = 'unified-tooltip';
|
||||||
|
tooltip.className = 'fixed z-50 bg-white rounded-xl shadow-2xl border border-gray-200';
|
||||||
|
tooltip.style.cssText = `
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 9999;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const totalElements = links.length + highlights.length + backlinks.length;
|
||||||
|
|
||||||
|
let tooltipHTML = `
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div class="text-lg font-semibold text-gray-800 flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-purple-600" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"/>
|
||||||
|
겹치는 요소들
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">${totalElements}개 요소</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="font-medium text-gray-900 bg-purple-50 px-4 py-3 rounded-lg border-l-4 border-purple-500">
|
||||||
|
<div class="text-sm text-purple-700 mb-1">선택된 텍스트</div>
|
||||||
|
<div class="text-base">"${selectedText}"</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 하이라이트 섹션
|
||||||
|
if (highlights.length > 0) {
|
||||||
|
tooltipHTML += `
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-2 text-yellow-600" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||||
|
하이라이트 (${highlights.length}개)
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
`;
|
||||||
|
|
||||||
|
highlights.forEach(highlight => {
|
||||||
|
const colorName = this.highlightManager.getColorName(highlight.highlight_color);
|
||||||
|
const createdDate = this.formatDate(highlight.created_at);
|
||||||
|
const notes = this.highlightManager.notes.filter(note => note.highlight_id === highlight.id);
|
||||||
|
|
||||||
|
console.log(`📝 통합 툴팁 - 하이라이트 ${highlight.id}의 메모:`, notes.length, '개');
|
||||||
|
if (notes.length > 0) {
|
||||||
|
console.log('📝 메모 내용:', notes.map(n => n.content));
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltipHTML += `
|
||||||
|
<div class="border rounded-lg p-3 bg-gradient-to-r from-yellow-50 to-orange-50 cursor-pointer hover:shadow-md transition-shadow duration-200"
|
||||||
|
onclick="window.documentViewerInstance.highlightManager.showHighlightTooltip([${JSON.stringify(highlight).replace(/"/g, '"')}], this.parentElement)">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<div class="w-3 h-3 rounded-full" style="background-color: ${highlight.highlight_color}"></div>
|
||||||
|
<span class="text-sm font-medium text-gray-800">${colorName}</span>
|
||||||
|
<span class="text-xs text-gray-500">${createdDate}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600">${notes.length}개 메모</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
tooltipHTML += `
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 링크 섹션
|
||||||
|
if (links.length > 0) {
|
||||||
|
tooltipHTML += `
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-2 text-purple-600" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
링크 (${links.length}개)
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
`;
|
||||||
|
|
||||||
|
links.forEach(link => {
|
||||||
|
const isNote = link.target_content_type === 'note';
|
||||||
|
const bgClass = isNote ? 'from-green-50 to-emerald-50' : 'from-purple-50 to-indigo-50';
|
||||||
|
const iconClass = isNote ? 'text-green-600' : 'text-purple-600';
|
||||||
|
const createdDate = this.formatDate(link.created_at);
|
||||||
|
|
||||||
|
tooltipHTML += `
|
||||||
|
<div class="border rounded-lg p-3 bg-gradient-to-r ${bgClass} transition-colors duration-200 relative group">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex-1 cursor-pointer" onclick="window.documentViewerInstance.navigateToLink(${JSON.stringify(link).replace(/"/g, '"')})">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<svg class="w-4 h-4 mr-2 ${iconClass}" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
${isNote ?
|
||||||
|
'<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 102 0V3a2 2 0 012 0v1a1 1 0 102 0V3a2 2 0 012 2v6.586A2 2 0 0115.414 13L13 15.586A2 2 0 0111.586 16H6a2 2 0 01-2-2V5zm8 4a1 1 0 10-2 0v2a1 1 0 102 0V9z" clip-rule="evenodd"/>' :
|
||||||
|
'<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/>'
|
||||||
|
}
|
||||||
|
</svg>
|
||||||
|
<span class="font-medium ${iconClass}">${link.target_document_title}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${link.target_text ? `
|
||||||
|
<div class="mb-2 p-2 bg-gray-50 rounded border-l-3 ${isNote ? 'border-green-400' : 'border-purple-400'}">
|
||||||
|
<div class="text-xs ${isNote ? 'text-green-700' : 'text-purple-700'} mb-1">연결된 텍스트</div>
|
||||||
|
<div class="text-sm text-gray-800 font-medium">"${link.target_text}"</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${link.description ? `
|
||||||
|
<div class="mb-2 p-2 bg-blue-50 rounded border-l-3 border-blue-400">
|
||||||
|
<div class="text-xs text-blue-700 mb-1">링크 설명</div>
|
||||||
|
<div class="text-sm text-blue-800">${link.description}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-500 flex items-center justify-between">
|
||||||
|
<span class="flex items-center">
|
||||||
|
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd"/>
|
||||||
|
${link.link_type === 'text_fragment' ? '텍스트 조각 링크' : '문서 링크'}
|
||||||
|
</span>
|
||||||
|
<span>${createdDate}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 삭제 버튼 -->
|
||||||
|
<button onclick="event.stopPropagation(); window.documentViewerInstance.deleteLinkWithConfirm('${link.id}', '${link.target_document_title.replace(/'/g, "\\'")}');"
|
||||||
|
class="ml-3 p-1 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors duration-200 opacity-0 group-hover:opacity-100"
|
||||||
|
title="링크 삭제">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
tooltipHTML += `
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 백링크 섹션
|
||||||
|
if (backlinks.length > 0) {
|
||||||
|
tooltipHTML += `
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-2 text-orange-600" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M7.707 3.293a1 1 0 010 1.414L5.414 7H11a7 7 0 017 7v2a1 1 0 11-2 0v-2a5 5 0 00-5-5H5.414l2.293 2.293a1 1 0 11-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
백링크 (${backlinks.length}개)
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
`;
|
||||||
|
|
||||||
|
backlinks.forEach(backlink => {
|
||||||
|
const createdDate = this.formatDate(backlink.created_at);
|
||||||
|
|
||||||
|
tooltipHTML += `
|
||||||
|
<div class="border rounded-lg p-3 bg-gradient-to-r from-orange-50 to-red-50 transition-colors duration-200 relative group">
|
||||||
|
<div class="cursor-pointer" onclick="window.documentViewerInstance.navigateToBacklink(${JSON.stringify(backlink).replace(/"/g, '"')})">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<svg class="w-4 h-4 mr-2 text-orange-600" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/>
|
||||||
|
<span class="font-medium text-orange-600">${backlink.source_document_title}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2 p-2 bg-white rounded border-l-3 border-orange-400">
|
||||||
|
<div class="text-xs text-orange-600 mb-1">원본 문서에서 링크로 설정한 텍스트</div>
|
||||||
|
<div class="text-sm text-gray-800 font-medium">"${backlink.selected_text}"</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${backlink.target_text ? `
|
||||||
|
<div class="mb-2 p-2 bg-white rounded border-l-3 border-blue-400">
|
||||||
|
<div class="text-xs text-blue-600 mb-1">현재 문서에서 연결된 구체적인 텍스트</div>
|
||||||
|
<div class="text-sm text-blue-800 font-medium">"${backlink.target_text}"</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${backlink.description ? `
|
||||||
|
<div class="mb-2 p-2 bg-white rounded border-l-3 border-green-400">
|
||||||
|
<div class="text-xs text-green-600 mb-1">링크 설명</div>
|
||||||
|
<div class="text-sm text-green-800">${backlink.description}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-500 flex items-center justify-between">
|
||||||
|
<span class="flex items-center">
|
||||||
|
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M7.707 3.293a1 1 0 010 1.414L5.414 7H11a7 7 0 017 7v2a1 1 0 11-2 0v-2a5 5 0 00-5-5H5.414l2.293 2.293a1 1 0 11-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||||
|
백링크
|
||||||
|
</span>
|
||||||
|
<span>${createdDate}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
tooltipHTML += `
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltipHTML += `
|
||||||
|
<div class="flex justify-end pt-4 border-t border-gray-200">
|
||||||
|
<button onclick="window.documentViewerInstance.hideUnifiedTooltip()"
|
||||||
|
class="text-xs bg-gray-500 text-white px-3 py-1 rounded hover:bg-gray-600 transition-colors">
|
||||||
|
닫기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tooltip.innerHTML = tooltipHTML;
|
||||||
|
document.body.appendChild(tooltip);
|
||||||
|
|
||||||
|
// 위치 조정
|
||||||
|
this.positionTooltip(tooltip, element);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 통합 툴팁 숨기기
|
||||||
|
*/
|
||||||
|
hideUnifiedTooltip() {
|
||||||
|
const tooltip = document.getElementById('unified-tooltip');
|
||||||
|
if (tooltip) {
|
||||||
|
tooltip.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 툴팁 위치 조정 (화면 밖으로 나가지 않도록 개선)
|
||||||
|
*/
|
||||||
|
positionTooltip(tooltip, element) {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const tooltipRect = tooltip.getBoundingClientRect();
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const scrollX = window.scrollX;
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
|
||||||
|
console.log('🎯 툴팁 위치 계산:', {
|
||||||
|
elementRect: rect,
|
||||||
|
tooltipSize: { width: tooltipRect.width, height: tooltipRect.height },
|
||||||
|
viewport: { width: viewportWidth, height: viewportHeight }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 기본 위치: 요소 아래 중앙
|
||||||
|
let left = rect.left + scrollX + (rect.width / 2) - (tooltipRect.width / 2);
|
||||||
|
let top = rect.bottom + scrollY + 10;
|
||||||
|
|
||||||
|
// 좌우 경계 체크 및 조정
|
||||||
|
const margin = 20;
|
||||||
|
if (left < margin) {
|
||||||
|
left = margin;
|
||||||
|
console.log('🔧 좌측 경계 조정:', left);
|
||||||
|
} else if (left + tooltipRect.width > viewportWidth - margin) {
|
||||||
|
left = viewportWidth - tooltipRect.width - margin;
|
||||||
|
console.log('🔧 우측 경계 조정:', left);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상하 경계 체크 및 조정
|
||||||
|
if (top + tooltipRect.height > viewportHeight - margin) {
|
||||||
|
// 요소 위쪽에 표시
|
||||||
|
top = rect.top + scrollY - tooltipRect.height - 10;
|
||||||
|
console.log('🔧 상단으로 이동:', top);
|
||||||
|
|
||||||
|
// 위쪽에도 공간이 부족하면 뷰포트 내에 강제로 맞춤
|
||||||
|
if (top < margin) {
|
||||||
|
top = margin;
|
||||||
|
console.log('🔧 상단 경계 조정:', top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 최종 위치 설정
|
||||||
|
tooltip.style.position = 'fixed';
|
||||||
|
tooltip.style.left = `${left - scrollX}px`;
|
||||||
|
tooltip.style.top = `${top - scrollY}px`;
|
||||||
|
|
||||||
|
console.log('✅ 최종 툴팁 위치:', {
|
||||||
|
left: left - scrollX,
|
||||||
|
top: top - scrollY
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// ==================== 유틸리티 메서드 ====================
|
// ==================== 유틸리티 메서드 ====================
|
||||||
formatDate(dateString) {
|
formatDate(dateString) {
|
||||||
return new Date(dateString).toLocaleString('ko-KR');
|
return new Date(dateString).toLocaleString('ko-KR');
|
||||||
@@ -1145,20 +1562,6 @@ window.documentViewer = () => ({
|
|||||||
return new Date(dateString).toLocaleDateString('ko-KR');
|
return new Date(dateString).toLocaleDateString('ko-KR');
|
||||||
},
|
},
|
||||||
|
|
||||||
getColorName(color) {
|
|
||||||
const colorNames = {
|
|
||||||
'#FFFF00': '노란색',
|
|
||||||
'#00FF00': '초록색',
|
|
||||||
'#FF0000': '빨간색',
|
|
||||||
'#0000FF': '파란색',
|
|
||||||
'#FF00FF': '보라색',
|
|
||||||
'#00FFFF': '청록색',
|
|
||||||
'#FFA500': '주황색',
|
|
||||||
'#FFC0CB': '분홍색'
|
|
||||||
};
|
|
||||||
return colorNames[color] || '기타';
|
|
||||||
},
|
|
||||||
|
|
||||||
getSelectedBookTitle() {
|
getSelectedBookTitle() {
|
||||||
const selectedBook = this.availableBooks.find(book => book.id === this.linkForm.target_book_id);
|
const selectedBook = this.availableBooks.find(book => book.id === this.linkForm.target_book_id);
|
||||||
return selectedBook ? selectedBook.title : '서적을 선택하세요';
|
return selectedBook ? selectedBook.title : '서적을 선택하세요';
|
||||||
@@ -1260,6 +1663,230 @@ window.documentViewer = () => ({
|
|||||||
return this.linkManager.navigateToSourceDocument(backlink.source_document_id, backlink);
|
return this.linkManager.navigateToSourceDocument(backlink.source_document_id, backlink);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 링크 삭제 (확인 후)
|
||||||
|
async deleteLinkWithConfirm(linkId, targetTitle) {
|
||||||
|
console.log('🗑️ 링크 삭제 요청:', { linkId, targetTitle });
|
||||||
|
|
||||||
|
const confirmed = confirm(`"${targetTitle}"로의 링크를 삭제하시겠습니까?`);
|
||||||
|
if (!confirmed) {
|
||||||
|
console.log('❌ 링크 삭제 취소됨');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🗑️ 링크 삭제 시작:', linkId);
|
||||||
|
|
||||||
|
// API 호출
|
||||||
|
await this.api.deleteDocumentLink(linkId);
|
||||||
|
console.log('✅ 링크 삭제 성공');
|
||||||
|
|
||||||
|
// 툴팁 숨기기
|
||||||
|
this.linkManager.hideTooltip();
|
||||||
|
|
||||||
|
// 캐시 무효화
|
||||||
|
console.log('🗑️ 링크 캐시 무효화 시작...');
|
||||||
|
if (window.cachedApi && window.cachedApi.invalidateRelatedCache) {
|
||||||
|
if (this.contentType === 'note') {
|
||||||
|
window.cachedApi.invalidateRelatedCache(`/note-documents/${this.documentId}/links`, ['links']);
|
||||||
|
} else {
|
||||||
|
window.cachedApi.invalidateRelatedCache(`/documents/${this.documentId}/links`, ['links']);
|
||||||
|
}
|
||||||
|
console.log('✅ 링크 캐시 무효화 완료');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 링크 목록 새로고침
|
||||||
|
console.log('🔄 링크 목록 새로고침 시작...');
|
||||||
|
await this.linkManager.loadDocumentLinks(this.documentId, this.contentType);
|
||||||
|
this.documentLinks = this.linkManager.documentLinks || [];
|
||||||
|
console.log('📊 새로고침된 링크 개수:', this.documentLinks.length);
|
||||||
|
|
||||||
|
// 링크 렌더링
|
||||||
|
console.log('🎨 링크 렌더링 시작...');
|
||||||
|
this.linkManager.renderDocumentLinks();
|
||||||
|
console.log('✅ 링크 렌더링 완료');
|
||||||
|
|
||||||
|
// 백링크도 다시 로드 (삭제된 링크가 다른 문서의 백링크였을 수 있음)
|
||||||
|
console.log('🔄 백링크 새로고침 시작...');
|
||||||
|
if (window.cachedApi && window.cachedApi.invalidateRelatedCache) {
|
||||||
|
if (this.contentType === 'note') {
|
||||||
|
window.cachedApi.invalidateRelatedCache(`/note-documents/${this.documentId}/backlinks`, ['links']);
|
||||||
|
} else {
|
||||||
|
window.cachedApi.invalidateRelatedCache(`/documents/${this.documentId}/backlinks`, ['links']);
|
||||||
|
}
|
||||||
|
console.log('✅ 백링크 캐시도 무효화 완료');
|
||||||
|
}
|
||||||
|
await this.linkManager.loadBacklinks(this.documentId, this.contentType);
|
||||||
|
this.backlinks = this.linkManager.backlinks || [];
|
||||||
|
this.linkManager.renderBacklinks();
|
||||||
|
console.log('✅ 백링크 새로고침 완료');
|
||||||
|
|
||||||
|
// 성공 메시지
|
||||||
|
this.showSuccessMessage('링크가 삭제되었습니다.');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 링크 삭제 실패:', error);
|
||||||
|
alert('링크 삭제에 실패했습니다: ' + error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 성공 메시지 표시
|
||||||
|
showSuccessMessage(message) {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg z-50 transition-opacity duration-300';
|
||||||
|
toast.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.opacity = '0';
|
||||||
|
setTimeout(() => {
|
||||||
|
if (toast.parentNode) {
|
||||||
|
toast.parentNode.removeChild(toast);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, 2000);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 하이라이트 관련 추가 기능들
|
||||||
|
async changeHighlightColor(highlightId) {
|
||||||
|
console.log('🎨 하이라이트 색상 변경:', highlightId);
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
{ name: '노란색', value: '#FFFF00' },
|
||||||
|
{ name: '초록색', value: '#00FF00' },
|
||||||
|
{ name: '파란색', value: '#00BFFF' },
|
||||||
|
{ name: '분홍색', value: '#FFB6C1' },
|
||||||
|
{ name: '주황색', value: '#FFA500' },
|
||||||
|
{ name: '보라색', value: '#DDA0DD' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const colorOptions = colors.map(c => `${c.name} (${c.value})`).join('\n');
|
||||||
|
const selectedColor = prompt(`새로운 색상을 선택하세요:\n\n${colorOptions}\n\n색상 코드를 입력하세요 (예: #FFFF00):`);
|
||||||
|
|
||||||
|
if (selectedColor && selectedColor.match(/^#[0-9A-Fa-f]{6}$/)) {
|
||||||
|
try {
|
||||||
|
await this.highlightManager.updateHighlightColor(highlightId, selectedColor);
|
||||||
|
this.showSuccessMessage('하이라이트 색상이 변경되었습니다.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 색상 변경 실패:', error);
|
||||||
|
alert('색상 변경에 실패했습니다: ' + error.message);
|
||||||
|
}
|
||||||
|
} else if (selectedColor !== null) {
|
||||||
|
alert('올바른 색상 코드를 입력해주세요 (예: #FFFF00)');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async duplicateHighlight(highlightId) {
|
||||||
|
console.log('📋 하이라이트 복사:', highlightId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.highlightManager.duplicateHighlight(highlightId);
|
||||||
|
this.showSuccessMessage('하이라이트가 복사되었습니다.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 하이라이트 복사 실패:', error);
|
||||||
|
alert('하이라이트 복사에 실패했습니다: ' + error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteHighlightWithConfirm(highlightId) {
|
||||||
|
console.log('🗑️ 하이라이트 삭제 확인:', highlightId);
|
||||||
|
|
||||||
|
const confirmed = confirm('이 하이라이트를 삭제하시겠습니까?\n\n⚠️ 주의: 연결된 모든 메모도 함께 삭제됩니다.');
|
||||||
|
if (!confirmed) {
|
||||||
|
console.log('❌ 하이라이트 삭제 취소됨');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.highlightManager.deleteHighlight(highlightId);
|
||||||
|
this.highlightManager.hideTooltip();
|
||||||
|
this.showSuccessMessage('하이라이트가 삭제되었습니다.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 하이라이트 삭제 실패:', error);
|
||||||
|
alert('하이라이트 삭제에 실패했습니다: ' + error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async editNote(noteId, currentContent) {
|
||||||
|
console.log('✏️ 메모 편집:', noteId);
|
||||||
|
console.log('🔍 HighlightManager 상태:', this.highlightManager);
|
||||||
|
console.log('🔍 updateNote 함수 존재:', typeof this.highlightManager?.updateNote);
|
||||||
|
|
||||||
|
if (!this.highlightManager) {
|
||||||
|
console.error('❌ HighlightManager가 초기화되지 않음');
|
||||||
|
alert('하이라이트 매니저가 초기화되지 않았습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.highlightManager.updateNote !== 'function') {
|
||||||
|
console.error('❌ updateNote 함수가 존재하지 않음');
|
||||||
|
alert('메모 업데이트 함수가 존재하지 않습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContent = prompt('메모 내용을 수정하세요:', currentContent);
|
||||||
|
if (newContent !== null && newContent.trim() !== currentContent) {
|
||||||
|
try {
|
||||||
|
await this.highlightManager.updateNote(noteId, newContent.trim());
|
||||||
|
this.showSuccessMessage('메모가 수정되었습니다.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 메모 수정 실패:', error);
|
||||||
|
alert('메모 수정에 실패했습니다: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 백링크 삭제 (확인 후)
|
||||||
|
async deleteBacklinkWithConfirm(backlinkId, sourceTitle) {
|
||||||
|
console.log('🗑️ 백링크 삭제 요청:', { backlinkId, sourceTitle });
|
||||||
|
|
||||||
|
const confirmed = confirm(`"${sourceTitle}"에서 오는 백링크를 삭제하시겠습니까?\n\n⚠️ 주의: 이는 원본 문서의 링크를 삭제합니다.`);
|
||||||
|
if (!confirmed) {
|
||||||
|
console.log('❌ 백링크 삭제 취소됨');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🗑️ 백링크 삭제 시작:', backlinkId);
|
||||||
|
|
||||||
|
// 백링크 삭제는 실제로는 원본 링크를 삭제하는 것
|
||||||
|
await this.api.deleteDocumentLink(backlinkId);
|
||||||
|
console.log('✅ 백링크 삭제 성공');
|
||||||
|
|
||||||
|
// 툴팁 숨기기
|
||||||
|
this.linkManager.hideTooltip();
|
||||||
|
|
||||||
|
// 캐시 무효화 (현재 문서의 백링크 캐시)
|
||||||
|
console.log('🗑️ 백링크 캐시 무효화 시작...');
|
||||||
|
if (window.cachedApi && window.cachedApi.invalidateRelatedCache) {
|
||||||
|
if (this.contentType === 'note') {
|
||||||
|
window.cachedApi.invalidateRelatedCache(`/note-documents/${this.documentId}/backlinks`, ['links']);
|
||||||
|
} else {
|
||||||
|
window.cachedApi.invalidateRelatedCache(`/documents/${this.documentId}/backlinks`, ['links']);
|
||||||
|
}
|
||||||
|
console.log('✅ 백링크 캐시 무효화 완료');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 백링크 목록 새로고침
|
||||||
|
console.log('🔄 백링크 목록 새로고침 시작...');
|
||||||
|
await this.linkManager.loadBacklinks(this.documentId, this.contentType);
|
||||||
|
this.backlinks = this.linkManager.backlinks || [];
|
||||||
|
console.log('📊 새로고침된 백링크 개수:', this.backlinks.length);
|
||||||
|
|
||||||
|
// 백링크 렌더링
|
||||||
|
console.log('🎨 백링크 렌더링 시작...');
|
||||||
|
this.linkManager.renderBacklinks();
|
||||||
|
console.log('✅ 백링크 렌더링 완료');
|
||||||
|
|
||||||
|
// 성공 메시지
|
||||||
|
this.showSuccessMessage('백링크가 삭제되었습니다.');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 백링크 삭제 실패:', error);
|
||||||
|
alert('백링크 삭제에 실패했습니다: ' + error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 북마크 관련
|
// 북마크 관련
|
||||||
scrollToBookmark(bookmark) {
|
scrollToBookmark(bookmark) {
|
||||||
return this.bookmarkManager.scrollToBookmark(bookmark);
|
return this.bookmarkManager.scrollToBookmark(bookmark);
|
||||||
|
|||||||
@@ -773,22 +773,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 스크립트 -->
|
<!-- 스크립트 -->
|
||||||
<script src="/static/js/api.js?v=2025012614"></script>
|
<script src="/static/js/api.js?v=2025012615"></script>
|
||||||
|
|
||||||
<!-- 캐시 및 성능 최적화 시스템 -->
|
<!-- 캐시 및 성능 최적화 시스템 -->
|
||||||
<script src="/static/js/viewer/utils/cache-manager.js?v=2025012607"></script>
|
<script src="/static/js/viewer/utils/cache-manager.js?v=2025012607"></script>
|
||||||
<script src="/static/js/viewer/utils/cached-api.js?v=2025012607"></script>
|
<script src="/static/js/viewer/utils/cached-api.js?v=2025012615"></script>
|
||||||
<script src="/static/js/viewer/utils/module-loader.js?v=2025012607"></script>
|
<script src="/static/js/viewer/utils/module-loader.js?v=2025012607"></script>
|
||||||
|
|
||||||
<!-- 모든 모듈들 직접 로드 -->
|
<!-- 모든 모듈들 직접 로드 -->
|
||||||
<script src="/static/js/viewer/core/document-loader.js?v=2025012607"></script>
|
<script src="/static/js/viewer/core/document-loader.js?v=2025012607"></script>
|
||||||
<script src="/static/js/viewer/features/ui-manager.js?v=2025012607"></script>
|
<script src="/static/js/viewer/features/ui-manager.js?v=2025012607"></script>
|
||||||
<script src="/static/js/viewer/features/highlight-manager.js?v=2025012607"></script>
|
<script src="/static/js/viewer/features/highlight-manager.js?v=2025012617"></script>
|
||||||
<script src="/static/js/viewer/features/link-manager.js?v=2025012607"></script>
|
<script src="/static/js/viewer/features/link-manager.js?v=2025012607"></script>
|
||||||
<script src="/static/js/viewer/features/bookmark-manager.js?v=2025012607"></script>
|
<script src="/static/js/viewer/features/bookmark-manager.js?v=2025012607"></script>
|
||||||
|
|
||||||
<!-- ViewerCore (Alpine.js 컴포넌트) -->
|
<!-- ViewerCore (Alpine.js 컴포넌트) -->
|
||||||
<script src="/static/js/viewer/viewer-core.js?v=2025012607"></script>
|
<script src="/static/js/viewer/viewer-core.js?v=2025012617"></script>
|
||||||
|
|
||||||
<!-- Alpine.js 프레임워크 -->
|
<!-- Alpine.js 프레임워크 -->
|
||||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user