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:
Hyungi Ahn
2025-09-02 16:22:03 +09:00
parent f711998ce9
commit d01cdeb2f5
12 changed files with 1188 additions and 82 deletions

View File

@@ -419,6 +419,7 @@ class BacklinkResponse(BaseModel):
source_document_id: str
source_document_title: str
source_document_book_id: Optional[str]
source_content_type: Optional[str] = "document" # "document" or "note"
target_document_id: str
target_document_title: str
selected_text: str # 소스 문서에서 선택한 텍스트
@@ -467,8 +468,12 @@ async def get_document_backlinks(
# 이 문서를 대상으로 하는 모든 링크 조회 (백링크)
from ...models import Book
from ...models.note_link import NoteLink
from ...models.note_document import NoteDocument
from ...models.notebook import Notebook
query = select(DocumentLink, Document, Book).join(
# 1. 일반 문서에서 오는 백링크 (DocumentLink)
doc_query = select(DocumentLink, Document, Book).join(
Document, DocumentLink.source_document_id == Document.id
).outerjoin(Book, Document.book_id == Book.id).where(
and_(
@@ -478,12 +483,13 @@ async def get_document_backlinks(
)
).order_by(DocumentLink.created_at.desc())
result = await db.execute(query)
doc_result = await db.execute(doc_query)
backlinks = []
print(f"🔍 백링크 쿼리 실행 완료")
print(f"🔍 문서 백링크 쿼리 실행 완료")
for link, source_doc, book in result.fetchall():
# 일반 문서 백링크 처리
for link, source_doc, book in doc_result.fetchall():
print(f"📋 백링크 발견: {source_doc.title} -> {document.title}")
print(f" - 소스 텍스트 (selected_text): {link.selected_text}")
print(f" - 타겟 텍스트 (target_text): {link.target_text}")
@@ -495,6 +501,7 @@ async def get_document_backlinks(
source_document_id=str(link.source_document_id),
source_document_title=source_doc.title,
source_document_book_id=str(book.id) if book else None,
source_content_type="document", # 일반 문서
target_document_id=str(link.target_document_id),
target_document_title=document.title,
selected_text=link.selected_text, # 소스 문서에서 선택한 텍스트 (참고용)
@@ -509,7 +516,57 @@ async def get_document_backlinks(
created_at=link.created_at.isoformat()
))
print(f"✅ 총 {len(backlinks)}개의 백링크 반환")
# 2. 노트에서 오는 백링크 (NoteLink) - 동기 쿼리 사용
try:
from ...core.database import get_sync_db
sync_db = next(get_sync_db())
# 노트에서 이 문서를 대상으로 하는 링크들 조회
note_links = sync_db.query(NoteLink).join(
NoteDocument, NoteLink.source_note_id == NoteDocument.id
).outerjoin(Notebook, NoteDocument.notebook_id == Notebook.id).filter(
NoteLink.target_document_id == document_id
).all()
print(f"🔍 노트 백링크 쿼리 실행 완료: {len(note_links)}개 발견")
# 노트 백링크 처리
for link in note_links:
source_note = sync_db.query(NoteDocument).filter(NoteDocument.id == link.source_note_id).first()
notebook = sync_db.query(Notebook).filter(Notebook.id == source_note.notebook_id).first() if source_note else None
if source_note:
print(f"📋 노트 백링크 발견: {source_note.title} -> {document.title}")
print(f" - 소스 텍스트 (selected_text): {link.selected_text}")
print(f" - 타겟 텍스트 (target_text): {link.target_text}")
print(f" - 타겟 오프셋: {link.target_start_offset}-{link.target_end_offset}")
print(f" - 링크 타입: {link.link_type}")
backlinks.append(BacklinkResponse(
id=str(link.id),
source_document_id=str(link.source_note_id), # 노트 ID를 문서 ID로 사용
source_document_title=f"📝 {source_note.title}", # 노트임을 표시
source_document_book_id=str(notebook.id) if notebook else None,
source_content_type="note", # 노트 문서
target_document_id=str(link.target_document_id) if link.target_document_id else document_id,
target_document_title=document.title,
selected_text=link.selected_text,
start_offset=link.start_offset,
end_offset=link.end_offset,
link_text=link.link_text,
description=link.description,
link_type=link.link_type,
target_text=link.target_text,
target_start_offset=link.target_start_offset,
target_end_offset=link.target_end_offset,
created_at=link.created_at.isoformat() if link.created_at else None
))
sync_db.close()
except Exception as e:
print(f"❌ 노트 백링크 조회 실패: {e}")
print(f"✅ 총 {len(backlinks)}개의 백링크 반환 (문서 + 노트)")
return backlinks