feat: 노트북-서적 간 양방향 링크/백링크 시스템 완성
✨ 주요 기능 - 노트 ↔ 서적 문서 간 양방향 링크 생성 및 이동 - 링크 대상 타입 선택 UI (서적 문서/노트북 노트) - 통합 백링크 시스템 (일반 문서에서 노트 백링크도 표시) - 링크 목록 UI 개선 (상세 정보 표시, 타입 구분) 🔧 백엔드 개선 - NoteLink 모델 및 API 추가 (/note-documents/{id}/links, /note-documents/{id}/backlinks) - 일반 문서 백링크 API에서 노트 링크도 함께 조회 - target_content_type, source_content_type 필드 추가 - 노트 문서 콘텐츠 API 추가 (/note-documents/{id}/content) 🎨 프론트엔드 개선 - text-selector.html에서 노트 문서 지원 - 링크 이동 시 contentType에 따른 올바른 URL 생성 - URL 파라미터 파싱 수정 (contentType 지원) - 링크 타입 자동 추론 로직 - 링크 목록 UI 대폭 개선 (출발점/도착점 텍스트, 타입 배지 등) 🐛 버그 수정 - 서적 목록 로드 실패 문제 해결 - 노트에서 링크 생성 시 대상 문서 열기 문제 해결 - 더미 문서로 이동하는 문제 해결 - 캐시 관련 문제 해결
This commit is contained in:
@@ -22,14 +22,28 @@ class LinkManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 링크 데이터 로드
|
||||
* 문서/노트 링크 데이터 로드
|
||||
*/
|
||||
async loadDocumentLinks(documentId) {
|
||||
async loadDocumentLinks(documentId, contentType = 'document') {
|
||||
try {
|
||||
console.log('📡 링크 API 호출:', `/documents/${documentId}/links`);
|
||||
console.log('📡 사용 중인 documentId:', documentId);
|
||||
console.log('🔍 loadDocumentLinks 호출됨 - documentId:', documentId, 'contentType:', contentType);
|
||||
|
||||
let apiEndpoint;
|
||||
if (contentType === 'note') {
|
||||
// 노트 문서의 경우 노트 전용 링크 API 사용
|
||||
apiEndpoint = `/note-documents/${documentId}/links`;
|
||||
console.log('✅ 노트 API 엔드포인트 선택:', apiEndpoint);
|
||||
} else {
|
||||
// 일반 문서의 경우 기존 API 사용
|
||||
apiEndpoint = `/documents/${documentId}/links`;
|
||||
console.log('✅ 문서 API 엔드포인트 선택:', apiEndpoint);
|
||||
}
|
||||
|
||||
console.log('📡 링크 API 호출:', apiEndpoint);
|
||||
console.log('📡 사용 중인 documentId:', documentId, 'contentType:', contentType);
|
||||
console.log('📡 cachedApi 객체:', this.cachedApi);
|
||||
const response = await this.cachedApi.get(`/documents/${documentId}/links`, {}, { category: 'links' });
|
||||
|
||||
const response = await this.cachedApi.get(apiEndpoint, {}, { category: 'links' });
|
||||
console.log('📡 원본 API 응답:', response);
|
||||
console.log('📡 응답 타입:', typeof response);
|
||||
console.log('📡 응답이 배열인가?', Array.isArray(response));
|
||||
@@ -53,7 +67,21 @@ class LinkManager {
|
||||
this.documentLinks = [];
|
||||
}
|
||||
|
||||
console.log('📡 최종 링크 데이터:', this.documentLinks);
|
||||
// target_content_type이 없는 링크들에 대해 추론 로직 적용
|
||||
this.documentLinks = this.documentLinks.map(link => {
|
||||
if (!link.target_content_type) {
|
||||
if (link.target_note_id) {
|
||||
link.target_content_type = 'note';
|
||||
console.log('🔍 링크 타입 추론: note -', link.id);
|
||||
} else if (link.target_document_id) {
|
||||
link.target_content_type = 'document';
|
||||
console.log('🔍 링크 타입 추론: document -', link.id);
|
||||
}
|
||||
}
|
||||
return link;
|
||||
});
|
||||
|
||||
console.log('📡 최종 링크 데이터 (타입 추론 완료):', this.documentLinks);
|
||||
console.log('📡 최종 링크 개수:', this.documentLinks.length);
|
||||
return this.documentLinks;
|
||||
} catch (error) {
|
||||
@@ -66,10 +94,25 @@ class LinkManager {
|
||||
/**
|
||||
* 백링크 데이터 로드
|
||||
*/
|
||||
async loadBacklinks(documentId) {
|
||||
async loadBacklinks(documentId, contentType = 'document') {
|
||||
try {
|
||||
console.log('📡 백링크 API 호출:', `/documents/${documentId}/backlinks`);
|
||||
const response = await this.cachedApi.get(`/documents/${documentId}/backlinks`, {}, { category: 'links' });
|
||||
console.log('🔍 loadBacklinks 호출됨 - documentId:', documentId, 'contentType:', contentType);
|
||||
|
||||
let apiEndpoint;
|
||||
if (contentType === 'note') {
|
||||
// 노트 문서의 경우 노트 전용 백링크 API 사용
|
||||
apiEndpoint = `/note-documents/${documentId}/backlinks`;
|
||||
console.log('✅ 노트 백링크 API 엔드포인트 선택:', apiEndpoint);
|
||||
} else {
|
||||
// 일반 문서의 경우 기존 API 사용
|
||||
apiEndpoint = `/documents/${documentId}/backlinks`;
|
||||
console.log('✅ 문서 백링크 API 엔드포인트 선택:', apiEndpoint);
|
||||
}
|
||||
|
||||
console.log('📡 백링크 API 호출:', apiEndpoint);
|
||||
console.log('📡 사용 중인 documentId:', documentId, 'contentType:', contentType);
|
||||
|
||||
const response = await this.cachedApi.get(apiEndpoint, {}, { category: 'links' });
|
||||
console.log('📡 원본 백링크 응답:', response);
|
||||
console.log('📡 백링크 응답 타입:', typeof response);
|
||||
console.log('📡 백링크 응답이 배열인가?', Array.isArray(response));
|
||||
@@ -753,15 +796,40 @@ class LinkManager {
|
||||
* 링크된 문서로 이동
|
||||
*/
|
||||
navigateToLinkedDocument(targetDocumentId, linkInfo) {
|
||||
let targetUrl = `/viewer.html?id=${targetDocumentId}`;
|
||||
console.log('🔗 navigateToLinkedDocument 호출됨');
|
||||
console.log('📋 전달받은 파라미터:', {
|
||||
targetDocumentId: targetDocumentId,
|
||||
linkInfo: linkInfo
|
||||
});
|
||||
|
||||
if (!targetDocumentId) {
|
||||
console.error('❌ targetDocumentId가 없습니다!');
|
||||
alert('대상 문서 ID가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// contentType에 따라 적절한 URL 생성
|
||||
let targetUrl;
|
||||
|
||||
if (linkInfo.target_content_type === 'note') {
|
||||
// 노트 문서로 이동
|
||||
targetUrl = `/viewer.html?id=${targetDocumentId}&contentType=note`;
|
||||
console.log('📝 노트 문서로 이동:', targetDocumentId);
|
||||
} else {
|
||||
// 일반 문서로 이동
|
||||
targetUrl = `/viewer.html?id=${targetDocumentId}`;
|
||||
console.log('📄 일반 문서로 이동:', targetDocumentId);
|
||||
}
|
||||
|
||||
// 특정 텍스트 위치가 있는 경우 URL에 추가
|
||||
if (linkInfo.target_text && linkInfo.target_start_offset !== undefined) {
|
||||
targetUrl += `&highlight_text=${encodeURIComponent(linkInfo.target_text)}`;
|
||||
targetUrl += `&start_offset=${linkInfo.target_start_offset}`;
|
||||
targetUrl += `&end_offset=${linkInfo.target_end_offset}`;
|
||||
console.log('🎯 텍스트 하이라이트 추가:', linkInfo.target_text);
|
||||
}
|
||||
|
||||
console.log('🚀 최종 이동할 URL:', targetUrl);
|
||||
window.location.href = targetUrl;
|
||||
}
|
||||
|
||||
@@ -769,15 +837,40 @@ class LinkManager {
|
||||
* 원본 문서로 이동 (백링크)
|
||||
*/
|
||||
navigateToSourceDocument(sourceDocumentId, backlinkInfo) {
|
||||
let targetUrl = `/viewer.html?id=${sourceDocumentId}`;
|
||||
console.log('🔙 navigateToSourceDocument 호출됨');
|
||||
console.log('📋 전달받은 파라미터:', {
|
||||
sourceDocumentId: sourceDocumentId,
|
||||
backlinkInfo: backlinkInfo
|
||||
});
|
||||
|
||||
if (!sourceDocumentId) {
|
||||
console.error('❌ sourceDocumentId가 없습니다!');
|
||||
alert('소스 문서 ID가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// source_content_type에 따라 적절한 URL 생성
|
||||
let targetUrl;
|
||||
|
||||
if (backlinkInfo.source_content_type === 'note') {
|
||||
// 노트 문서로 이동
|
||||
targetUrl = `/viewer.html?id=${sourceDocumentId}&contentType=note`;
|
||||
console.log('📝 노트 문서로 이동 (백링크):', sourceDocumentId);
|
||||
} else {
|
||||
// 일반 문서로 이동
|
||||
targetUrl = `/viewer.html?id=${sourceDocumentId}`;
|
||||
console.log('📄 일반 문서로 이동 (백링크):', sourceDocumentId);
|
||||
}
|
||||
|
||||
// 원본 텍스트 위치가 있는 경우 URL에 추가
|
||||
if (backlinkInfo.selected_text && backlinkInfo.start_offset !== undefined) {
|
||||
targetUrl += `&highlight_text=${encodeURIComponent(backlinkInfo.selected_text)}`;
|
||||
targetUrl += `&start_offset=${backlinkInfo.start_offset}`;
|
||||
targetUrl += `&end_offset=${backlinkInfo.end_offset}`;
|
||||
console.log('🎯 텍스트 하이라이트 추가 (백링크):', backlinkInfo.selected_text);
|
||||
}
|
||||
|
||||
console.log('🚀 최종 이동할 URL (백링크):', targetUrl);
|
||||
window.location.href = targetUrl;
|
||||
}
|
||||
|
||||
@@ -1050,7 +1143,34 @@ class LinkManager {
|
||||
|
||||
const link = this.documentLinks.find(l => l.id === elementId);
|
||||
if (link) {
|
||||
this.navigateToLinkedDocument(link.target_document_id, link);
|
||||
console.log('🔗 메뉴에서 링크 클릭:', link);
|
||||
|
||||
// target_content_type이 없으면 ID로 추론
|
||||
let targetContentType = link.target_content_type;
|
||||
if (!targetContentType) {
|
||||
if (link.target_note_id) {
|
||||
targetContentType = 'note';
|
||||
} else if (link.target_document_id) {
|
||||
targetContentType = 'document';
|
||||
}
|
||||
console.log('🔍 메뉴에서 target_content_type 추론됨:', targetContentType);
|
||||
}
|
||||
|
||||
const targetId = link.target_document_id || link.target_note_id;
|
||||
if (!targetId) {
|
||||
console.error('❌ 메뉴에서 대상 ID가 없습니다!', link);
|
||||
alert('링크 대상을 찾을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 링크 객체에 추론된 타입 추가
|
||||
const linkWithType = {
|
||||
...link,
|
||||
target_content_type: targetContentType
|
||||
};
|
||||
|
||||
console.log('🚀 메뉴에서 최종 링크 데이터:', linkWithType);
|
||||
this.navigateToLinkedDocument(targetId, linkWithType);
|
||||
} else {
|
||||
console.warn('링크 데이터를 찾을 수 없음:', elementId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user