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:
@@ -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
|
||||
|
||||
|
||||
|
||||
270
backend/src/api/routes/note_documents.py
Normal file
270
backend/src/api/routes/note_documents.py
Normal file
@@ -0,0 +1,270 @@
|
||||
"""
|
||||
노트 문서 관련 API 엔드포인트
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, desc, asc
|
||||
from typing import List, Optional
|
||||
import html
|
||||
|
||||
from ...core.database import get_sync_db
|
||||
from ..dependencies import get_current_user
|
||||
from ...models.user import User
|
||||
from ...models.note_document import (
|
||||
NoteDocument,
|
||||
NoteDocumentCreate,
|
||||
NoteDocumentUpdate,
|
||||
NoteDocumentResponse,
|
||||
NoteDocumentListItem,
|
||||
NoteStats
|
||||
)
|
||||
from ...models.notebook import Notebook
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def calculate_reading_time(content: str) -> int:
|
||||
"""HTML 내용에서 예상 읽기 시간 계산 (분)"""
|
||||
if not content:
|
||||
return 0
|
||||
|
||||
# HTML 태그 제거
|
||||
text_content = html.unescape(content)
|
||||
# 간단한 HTML 태그 제거 (정확하지 않지만 대략적인 계산용)
|
||||
import re
|
||||
text_content = re.sub(r'<[^>]+>', '', text_content)
|
||||
|
||||
# 단어 수 계산 (한국어 + 영어)
|
||||
words = len(text_content.split())
|
||||
korean_chars = len([c for c in text_content if '\uac00' <= c <= '\ud7af'])
|
||||
|
||||
# 대략적인 읽기 속도: 영어 200단어/분, 한국어 300자/분
|
||||
english_time = words / 200
|
||||
korean_time = korean_chars / 300
|
||||
|
||||
return max(1, int(english_time + korean_time))
|
||||
|
||||
|
||||
@router.get("/", response_model=List[NoteDocumentListItem])
|
||||
def get_note_documents(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
search: Optional[str] = Query(None),
|
||||
note_type: Optional[str] = Query(None),
|
||||
published_only: bool = Query(False),
|
||||
notebook_id: Optional[str] = Query(None),
|
||||
sort_by: str = Query("updated_at", regex="^(title|created_at|updated_at|word_count)$"),
|
||||
order: str = Query("desc", regex="^(asc|desc)$"),
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""노트 문서 목록 조회"""
|
||||
query = db.query(NoteDocument)
|
||||
|
||||
# 필터링
|
||||
if search:
|
||||
search_term = f"%{search}%"
|
||||
query = query.filter(
|
||||
(NoteDocument.title.ilike(search_term)) |
|
||||
(NoteDocument.content.ilike(search_term))
|
||||
)
|
||||
|
||||
if note_type:
|
||||
query = query.filter(NoteDocument.note_type == note_type)
|
||||
|
||||
if published_only:
|
||||
query = query.filter(NoteDocument.is_published == True)
|
||||
|
||||
if notebook_id:
|
||||
query = query.filter(NoteDocument.notebook_id == notebook_id)
|
||||
|
||||
# 정렬
|
||||
if sort_by == 'title':
|
||||
query = query.order_by(asc(NoteDocument.title) if order == 'asc' else desc(NoteDocument.title))
|
||||
elif sort_by == 'created_at':
|
||||
query = query.order_by(asc(NoteDocument.created_at) if order == 'asc' else desc(NoteDocument.created_at))
|
||||
elif sort_by == 'word_count':
|
||||
query = query.order_by(asc(NoteDocument.word_count) if order == 'asc' else desc(NoteDocument.word_count))
|
||||
else:
|
||||
query = query.order_by(desc(NoteDocument.updated_at))
|
||||
|
||||
# 페이지네이션
|
||||
notes = query.offset(skip).limit(limit).all()
|
||||
|
||||
# 자식 노트 개수 계산
|
||||
result = []
|
||||
for note in notes:
|
||||
child_count = db.query(func.count(NoteDocument.id)).filter(
|
||||
NoteDocument.parent_note_id == note.id
|
||||
).scalar()
|
||||
|
||||
note_item = NoteDocumentListItem.from_orm(note, child_count)
|
||||
result.append(note_item)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/stats", response_model=NoteStats)
|
||||
def get_note_stats(
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""노트 통계 정보"""
|
||||
total_notes = db.query(func.count(NoteDocument.id)).scalar()
|
||||
published_notes = db.query(func.count(NoteDocument.id)).filter(
|
||||
NoteDocument.is_published == True
|
||||
).scalar()
|
||||
draft_notes = total_notes - published_notes
|
||||
|
||||
# 노트 타입별 통계
|
||||
type_stats = db.query(
|
||||
NoteDocument.note_type,
|
||||
func.count(NoteDocument.id)
|
||||
).group_by(NoteDocument.note_type).all()
|
||||
note_types = {note_type: count for note_type, count in type_stats}
|
||||
|
||||
# 총 단어 수와 읽기 시간
|
||||
total_words = db.query(func.sum(NoteDocument.word_count)).scalar() or 0
|
||||
total_reading_time = db.query(func.sum(NoteDocument.reading_time)).scalar() or 0
|
||||
|
||||
# 최근 노트들
|
||||
recent_notes_query = db.query(NoteDocument).order_by(
|
||||
desc(NoteDocument.updated_at)
|
||||
).limit(5).all()
|
||||
|
||||
recent_notes = [NoteDocumentListItem.from_orm(note) for note in recent_notes_query]
|
||||
|
||||
return NoteStats(
|
||||
total_notes=total_notes,
|
||||
published_notes=published_notes,
|
||||
draft_notes=draft_notes,
|
||||
note_types=note_types,
|
||||
total_words=total_words,
|
||||
total_reading_time=total_reading_time,
|
||||
recent_notes=recent_notes
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{note_id}", response_model=NoteDocumentResponse)
|
||||
def get_note_document(
|
||||
note_id: str,
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""특정 노트 문서 조회"""
|
||||
note = db.query(NoteDocument).filter(NoteDocument.id == note_id).first()
|
||||
if not note:
|
||||
raise HTTPException(status_code=404, detail="Note not found")
|
||||
|
||||
return NoteDocumentResponse.from_orm(note)
|
||||
|
||||
|
||||
@router.post("/", response_model=NoteDocumentResponse)
|
||||
def create_note_document(
|
||||
note_data: NoteDocumentCreate,
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""새 노트 문서 생성"""
|
||||
# 단어 수 및 읽기 시간 계산
|
||||
word_count = len(note_data.content or '') if note_data.content else 0
|
||||
reading_time = calculate_reading_time(note_data.content or '')
|
||||
|
||||
note = NoteDocument(
|
||||
title=note_data.title,
|
||||
content=note_data.content,
|
||||
note_type=note_data.note_type,
|
||||
tags=note_data.tags,
|
||||
is_published=note_data.is_published,
|
||||
parent_note_id=note_data.parent_note_id,
|
||||
sort_order=note_data.sort_order,
|
||||
created_by=current_user.email,
|
||||
word_count=word_count,
|
||||
reading_time=reading_time
|
||||
)
|
||||
|
||||
db.add(note)
|
||||
db.commit()
|
||||
db.refresh(note)
|
||||
|
||||
return NoteDocumentResponse.from_orm(note)
|
||||
|
||||
|
||||
@router.put("/{note_id}", response_model=NoteDocumentResponse)
|
||||
def update_note_document(
|
||||
note_id: str,
|
||||
note_data: NoteDocumentUpdate,
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""노트 문서 업데이트"""
|
||||
note = db.query(NoteDocument).filter(NoteDocument.id == note_id).first()
|
||||
if not note:
|
||||
raise HTTPException(status_code=404, detail="Note not found")
|
||||
|
||||
# 권한 확인
|
||||
if note.created_by != current_user.email and not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Permission denied")
|
||||
|
||||
# 업데이트할 필드만 적용
|
||||
update_data = note_data.dict(exclude_unset=True)
|
||||
|
||||
# 내용이 변경되면 단어 수와 읽기 시간 재계산
|
||||
if 'content' in update_data:
|
||||
update_data['word_count'] = len(update_data['content'] or '')
|
||||
update_data['reading_time'] = calculate_reading_time(update_data['content'] or '')
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(note, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(note)
|
||||
|
||||
return NoteDocumentResponse.from_orm(note)
|
||||
|
||||
|
||||
@router.delete("/{note_id}")
|
||||
def delete_note_document(
|
||||
note_id: str,
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""노트 문서 삭제"""
|
||||
note = db.query(NoteDocument).filter(NoteDocument.id == note_id).first()
|
||||
if not note:
|
||||
raise HTTPException(status_code=404, detail="Note not found")
|
||||
|
||||
# 권한 확인
|
||||
if note.created_by != current_user.email and not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Permission denied")
|
||||
|
||||
# 자식 노트들이 있는지 확인
|
||||
child_count = db.query(func.count(NoteDocument.id)).filter(
|
||||
NoteDocument.parent_note_id == note.id
|
||||
).scalar()
|
||||
|
||||
if child_count > 0:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Cannot delete note with {child_count} child notes"
|
||||
)
|
||||
|
||||
db.delete(note)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Note deleted successfully"}
|
||||
|
||||
|
||||
@router.get("/{note_id}/content")
|
||||
def get_note_document_content(
|
||||
note_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_sync_db)
|
||||
):
|
||||
"""노트 문서의 HTML 콘텐츠만 반환"""
|
||||
note = db.query(NoteDocument).filter(NoteDocument.id == note_id).first()
|
||||
|
||||
if not note:
|
||||
raise HTTPException(status_code=404, detail="Note document not found")
|
||||
|
||||
return note.content or ""
|
||||
277
backend/src/api/routes/note_links.py
Normal file
277
backend/src/api/routes/note_links.py
Normal file
@@ -0,0 +1,277 @@
|
||||
"""
|
||||
노트 문서 링크 관련 API 엔드포인트
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
import uuid
|
||||
|
||||
from ...core.database import get_sync_db
|
||||
from ..dependencies import get_current_user
|
||||
from ...models.user import User
|
||||
from ...models.note_document import NoteDocument
|
||||
from ...models.document import Document
|
||||
from ...models.note_link import NoteLink
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# Pydantic 모델들
|
||||
class NoteLinkCreate(BaseModel):
|
||||
target_note_id: Optional[str] = None
|
||||
target_document_id: Optional[str] = None
|
||||
selected_text: str
|
||||
start_offset: int
|
||||
end_offset: int
|
||||
link_text: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
target_text: Optional[str] = None
|
||||
target_start_offset: Optional[int] = None
|
||||
target_end_offset: Optional[int] = None
|
||||
link_type: Optional[str] = "note"
|
||||
|
||||
|
||||
class NoteLinkUpdate(BaseModel):
|
||||
target_note_id: Optional[str] = None
|
||||
target_document_id: Optional[str] = None
|
||||
link_text: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
target_text: Optional[str] = None
|
||||
target_start_offset: Optional[int] = None
|
||||
target_end_offset: Optional[int] = None
|
||||
link_type: Optional[str] = None
|
||||
|
||||
|
||||
class NoteLinkResponse(BaseModel):
|
||||
id: str
|
||||
source_note_id: Optional[str] = None
|
||||
source_document_id: Optional[str] = None
|
||||
target_note_id: Optional[str] = None
|
||||
target_document_id: Optional[str] = None
|
||||
target_content_type: Optional[str] = None # "document" or "note"
|
||||
selected_text: str
|
||||
start_offset: int
|
||||
end_offset: int
|
||||
link_text: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
target_text: Optional[str] = None
|
||||
target_start_offset: Optional[int] = None
|
||||
target_end_offset: Optional[int] = None
|
||||
link_type: str
|
||||
created_at: str
|
||||
updated_at: Optional[str] = None
|
||||
|
||||
# 추가 정보
|
||||
target_note_title: Optional[str] = None
|
||||
target_document_title: Optional[str] = None
|
||||
source_note_title: Optional[str] = None
|
||||
source_document_title: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("/note-documents/{note_id}/links", response_model=List[NoteLinkResponse])
|
||||
def get_note_links(
|
||||
note_id: str,
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""노트에서 나가는 링크 목록 조회"""
|
||||
# 노트 존재 확인
|
||||
note = db.query(NoteDocument).filter(NoteDocument.id == note_id).first()
|
||||
if not note:
|
||||
raise HTTPException(status_code=404, detail="Note not found")
|
||||
|
||||
# 노트에서 나가는 링크들 조회
|
||||
links = db.query(NoteLink).filter(
|
||||
NoteLink.source_note_id == note_id
|
||||
).all()
|
||||
|
||||
result = []
|
||||
for link in links:
|
||||
link_data = {
|
||||
"id": str(link.id),
|
||||
"source_note_id": str(link.source_note_id) if link.source_note_id else None,
|
||||
"source_document_id": str(link.source_document_id) if link.source_document_id else None,
|
||||
"target_note_id": str(link.target_note_id) if link.target_note_id else None,
|
||||
"target_document_id": str(link.target_document_id) if link.target_document_id else None,
|
||||
"selected_text": link.selected_text,
|
||||
"start_offset": link.start_offset,
|
||||
"end_offset": link.end_offset,
|
||||
"link_text": link.link_text,
|
||||
"description": link.description,
|
||||
"target_text": link.target_text,
|
||||
"target_start_offset": link.target_start_offset,
|
||||
"target_end_offset": link.target_end_offset,
|
||||
"link_type": link.link_type,
|
||||
"created_at": link.created_at.isoformat() if link.created_at else None,
|
||||
"updated_at": link.updated_at.isoformat() if link.updated_at else None,
|
||||
}
|
||||
|
||||
# 대상 제목 및 타입 추가
|
||||
if link.target_note_id:
|
||||
target_note = db.query(NoteDocument).filter(NoteDocument.id == link.target_note_id).first()
|
||||
if target_note:
|
||||
link_data["target_note_title"] = target_note.title
|
||||
link_data["target_content_type"] = "note"
|
||||
elif link.target_document_id:
|
||||
target_doc = db.query(Document).filter(Document.id == link.target_document_id).first()
|
||||
if target_doc:
|
||||
link_data["target_document_title"] = target_doc.title
|
||||
link_data["target_content_type"] = "document"
|
||||
|
||||
result.append(NoteLinkResponse(**link_data))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/note-documents/{note_id}/backlinks", response_model=List[NoteLinkResponse])
|
||||
def get_note_backlinks(
|
||||
note_id: str,
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""노트로 들어오는 백링크 목록 조회"""
|
||||
# 노트 존재 확인
|
||||
note = db.query(NoteDocument).filter(NoteDocument.id == note_id).first()
|
||||
if not note:
|
||||
raise HTTPException(status_code=404, detail="Note not found")
|
||||
|
||||
# 노트로 들어오는 백링크들 조회
|
||||
backlinks = db.query(NoteLink).filter(
|
||||
NoteLink.target_note_id == note_id
|
||||
).all()
|
||||
|
||||
result = []
|
||||
for link in backlinks:
|
||||
link_data = {
|
||||
"id": str(link.id),
|
||||
"source_note_id": str(link.source_note_id) if link.source_note_id else None,
|
||||
"source_document_id": str(link.source_document_id) if link.source_document_id else None,
|
||||
"target_note_id": str(link.target_note_id) if link.target_note_id else None,
|
||||
"target_document_id": str(link.target_document_id) if link.target_document_id else None,
|
||||
"selected_text": link.selected_text,
|
||||
"start_offset": link.start_offset,
|
||||
"end_offset": link.end_offset,
|
||||
"link_text": link.link_text,
|
||||
"description": link.description,
|
||||
"target_text": link.target_text,
|
||||
"target_start_offset": link.target_start_offset,
|
||||
"target_end_offset": link.target_end_offset,
|
||||
"link_type": link.link_type,
|
||||
"created_at": link.created_at.isoformat() if link.created_at else None,
|
||||
"updated_at": link.updated_at.isoformat() if link.updated_at else None,
|
||||
}
|
||||
|
||||
# 출발지 제목 추가
|
||||
if link.source_note_id:
|
||||
source_note = db.query(NoteDocument).filter(NoteDocument.id == link.source_note_id).first()
|
||||
if source_note:
|
||||
link_data["source_note_title"] = source_note.title
|
||||
elif link.source_document_id:
|
||||
source_doc = db.query(Document).filter(Document.id == link.source_document_id).first()
|
||||
if source_doc:
|
||||
link_data["source_document_title"] = source_doc.title
|
||||
|
||||
result.append(NoteLinkResponse(**link_data))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/note-documents/{note_id}/links", response_model=NoteLinkResponse)
|
||||
def create_note_link(
|
||||
note_id: str,
|
||||
link_data: NoteLinkCreate,
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""노트에서 다른 노트/문서로의 링크 생성"""
|
||||
# 출발지 노트 존재 확인
|
||||
source_note = db.query(NoteDocument).filter(NoteDocument.id == note_id).first()
|
||||
if not source_note:
|
||||
raise HTTPException(status_code=404, detail="Source note not found")
|
||||
|
||||
# 대상 확인 (노트 또는 문서 중 하나는 반드시 있어야 함)
|
||||
if not link_data.target_note_id and not link_data.target_document_id:
|
||||
raise HTTPException(status_code=400, detail="Either target_note_id or target_document_id is required")
|
||||
|
||||
if link_data.target_note_id and link_data.target_document_id:
|
||||
raise HTTPException(status_code=400, detail="Cannot specify both target_note_id and target_document_id")
|
||||
|
||||
# 대상 존재 확인
|
||||
if link_data.target_note_id:
|
||||
target_note = db.query(NoteDocument).filter(NoteDocument.id == link_data.target_note_id).first()
|
||||
if not target_note:
|
||||
raise HTTPException(status_code=404, detail="Target note not found")
|
||||
|
||||
if link_data.target_document_id:
|
||||
target_doc = db.query(Document).filter(Document.id == link_data.target_document_id).first()
|
||||
if not target_doc:
|
||||
raise HTTPException(status_code=404, detail="Target document not found")
|
||||
|
||||
# 링크 생성
|
||||
note_link = NoteLink(
|
||||
source_note_id=note_id,
|
||||
target_note_id=link_data.target_note_id,
|
||||
target_document_id=link_data.target_document_id,
|
||||
selected_text=link_data.selected_text,
|
||||
start_offset=link_data.start_offset,
|
||||
end_offset=link_data.end_offset,
|
||||
link_text=link_data.link_text,
|
||||
description=link_data.description,
|
||||
target_text=link_data.target_text,
|
||||
target_start_offset=link_data.target_start_offset,
|
||||
target_end_offset=link_data.target_end_offset,
|
||||
link_type=link_data.link_type or "note",
|
||||
created_by=current_user.id
|
||||
)
|
||||
|
||||
db.add(note_link)
|
||||
db.commit()
|
||||
db.refresh(note_link)
|
||||
|
||||
# 응답 데이터 구성
|
||||
response_data = {
|
||||
"id": str(note_link.id),
|
||||
"source_note_id": str(note_link.source_note_id) if note_link.source_note_id else None,
|
||||
"source_document_id": str(note_link.source_document_id) if note_link.source_document_id else None,
|
||||
"target_note_id": str(note_link.target_note_id) if note_link.target_note_id else None,
|
||||
"target_document_id": str(note_link.target_document_id) if note_link.target_document_id else None,
|
||||
"selected_text": note_link.selected_text,
|
||||
"start_offset": note_link.start_offset,
|
||||
"end_offset": note_link.end_offset,
|
||||
"link_text": note_link.link_text,
|
||||
"description": note_link.description,
|
||||
"target_text": note_link.target_text,
|
||||
"target_start_offset": note_link.target_start_offset,
|
||||
"target_end_offset": note_link.target_end_offset,
|
||||
"link_type": note_link.link_type,
|
||||
"created_at": note_link.created_at.isoformat() if note_link.created_at else None,
|
||||
"updated_at": note_link.updated_at.isoformat() if note_link.updated_at else None,
|
||||
}
|
||||
|
||||
return NoteLinkResponse(**response_data)
|
||||
|
||||
|
||||
@router.delete("/note-links/{link_id}")
|
||||
def delete_note_link(
|
||||
link_id: str,
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""노트 링크 삭제"""
|
||||
link = db.query(NoteLink).filter(NoteLink.id == link_id).first()
|
||||
if not link:
|
||||
raise HTTPException(status_code=404, detail="Link not found")
|
||||
|
||||
# 권한 확인 (링크 생성자 또는 관리자만 삭제 가능)
|
||||
if link.created_by != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Permission denied")
|
||||
|
||||
db.delete(link)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Link deleted successfully"}
|
||||
Reference in New Issue
Block a user