- Fix SyntaxError in viewer.js line 2868 (.bind(this) issue in setTimeout) - Resolve Alpine.js 'Can't find variable' errors (documentViewer, goBack, etc.) - Fix backlink rendering and persistence during temporary highlights - Add backlink protection and restoration mechanism in highlightAndScrollToText - Implement Note Management System with hierarchical notebooks - Add note highlights and memos functionality - Update cache version to force browser refresh (v=2025012641) - Add comprehensive logging for debugging backlink issues
271 lines
8.7 KiB
Python
271 lines
8.7 KiB
Python
"""
|
|
노트북 (Notebook) 관리 API
|
|
|
|
용어 정의:
|
|
- 노트북 (Notebook): 노트 문서들을 그룹화하는 폴더
|
|
- 노트 (Note Document): 독립적인 HTML 기반 문서 작성
|
|
- 메모 (Memo): 하이라이트에 달리는 짧은 코멘트 (별도 API)
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import func, desc, asc, select
|
|
from typing import List, Optional
|
|
|
|
from ...core.database import get_sync_db
|
|
from ...models.notebook import (
|
|
Notebook,
|
|
NotebookCreate,
|
|
NotebookUpdate,
|
|
NotebookResponse,
|
|
NotebookListItem,
|
|
NotebookStats
|
|
)
|
|
from ...models.note_document import NoteDocument
|
|
from ...models.user import User
|
|
from ..dependencies import get_current_user
|
|
|
|
router = APIRouter()
|
|
|
|
@router.get("/", response_model=List[NotebookListItem])
|
|
def get_notebooks(
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=100),
|
|
search: Optional[str] = Query(None),
|
|
active_only: bool = Query(True),
|
|
sort_by: str = Query("updated_at", regex="^(title|created_at|updated_at|sort_order)$"),
|
|
order: str = Query("desc", regex="^(asc|desc)$"),
|
|
db: Session = Depends(get_sync_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""노트북 목록 조회"""
|
|
query = db.query(Notebook)
|
|
|
|
# 필터링
|
|
if search:
|
|
search_term = f"%{search}%"
|
|
query = query.filter(
|
|
(Notebook.title.ilike(search_term)) |
|
|
(Notebook.description.ilike(search_term))
|
|
)
|
|
|
|
if active_only:
|
|
query = query.filter(Notebook.is_active == True)
|
|
|
|
# 정렬
|
|
if sort_by == 'title':
|
|
query = query.order_by(asc(Notebook.title) if order == 'asc' else desc(Notebook.title))
|
|
elif sort_by == 'created_at':
|
|
query = query.order_by(asc(Notebook.created_at) if order == 'asc' else desc(Notebook.created_at))
|
|
elif sort_by == 'sort_order':
|
|
query = query.order_by(asc(Notebook.sort_order) if order == 'asc' else desc(Notebook.sort_order))
|
|
else:
|
|
query = query.order_by(desc(Notebook.updated_at))
|
|
|
|
# 페이지네이션
|
|
notebooks = query.offset(skip).limit(limit).all()
|
|
|
|
# 노트 개수 계산
|
|
result = []
|
|
for notebook in notebooks:
|
|
note_count = db.query(func.count(NoteDocument.id)).filter(
|
|
NoteDocument.notebook_id == notebook.id
|
|
).scalar()
|
|
|
|
notebook_item = NotebookListItem.from_orm(notebook, note_count)
|
|
result.append(notebook_item)
|
|
|
|
return result
|
|
|
|
@router.get("/stats", response_model=NotebookStats)
|
|
def get_notebook_stats(
|
|
db: Session = Depends(get_sync_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""노트북 통계 정보"""
|
|
total_notebooks = db.query(func.count(Notebook.id)).scalar()
|
|
active_notebooks = db.query(func.count(Notebook.id)).filter(
|
|
Notebook.is_active == True
|
|
).scalar()
|
|
|
|
total_notes = db.query(func.count(NoteDocument.id)).scalar()
|
|
notes_without_notebook = db.query(func.count(NoteDocument.id)).filter(
|
|
NoteDocument.notebook_id.is_(None)
|
|
).scalar()
|
|
|
|
return NotebookStats(
|
|
total_notebooks=total_notebooks,
|
|
active_notebooks=active_notebooks,
|
|
total_notes=total_notes,
|
|
notes_without_notebook=notes_without_notebook
|
|
)
|
|
|
|
@router.get("/{notebook_id}", response_model=NotebookResponse)
|
|
def get_notebook(
|
|
notebook_id: str,
|
|
db: Session = Depends(get_sync_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""특정 노트북 조회"""
|
|
notebook = db.query(Notebook).filter(Notebook.id == notebook_id).first()
|
|
if not notebook:
|
|
raise HTTPException(status_code=404, detail="Notebook not found")
|
|
|
|
# 노트 개수 계산
|
|
note_count = db.query(func.count(NoteDocument.id)).filter(
|
|
NoteDocument.notebook_id == notebook.id
|
|
).scalar()
|
|
|
|
return NotebookResponse.from_orm(notebook, note_count)
|
|
|
|
@router.post("/", response_model=NotebookResponse)
|
|
def create_notebook(
|
|
notebook_data: NotebookCreate,
|
|
db: Session = Depends(get_sync_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""새 노트북 생성"""
|
|
notebook = Notebook(
|
|
title=notebook_data.title,
|
|
description=notebook_data.description,
|
|
color=notebook_data.color,
|
|
icon=notebook_data.icon,
|
|
is_active=notebook_data.is_active,
|
|
sort_order=notebook_data.sort_order,
|
|
created_by=current_user.email
|
|
)
|
|
|
|
db.add(notebook)
|
|
db.commit()
|
|
db.refresh(notebook)
|
|
|
|
return NotebookResponse.from_orm(notebook, 0)
|
|
|
|
@router.put("/{notebook_id}", response_model=NotebookResponse)
|
|
def update_notebook(
|
|
notebook_id: str,
|
|
notebook_data: NotebookUpdate,
|
|
db: Session = Depends(get_sync_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""노트북 업데이트"""
|
|
notebook = db.query(Notebook).filter(Notebook.id == notebook_id).first()
|
|
if not notebook:
|
|
raise HTTPException(status_code=404, detail="Notebook not found")
|
|
|
|
# 업데이트할 필드만 적용
|
|
update_data = notebook_data.dict(exclude_unset=True)
|
|
|
|
for field, value in update_data.items():
|
|
setattr(notebook, field, value)
|
|
|
|
db.commit()
|
|
db.refresh(notebook)
|
|
|
|
# 노트 개수 계산
|
|
note_count = db.query(func.count(NoteDocument.id)).filter(
|
|
NoteDocument.notebook_id == notebook.id
|
|
).scalar()
|
|
|
|
return NotebookResponse.from_orm(notebook, note_count)
|
|
|
|
@router.delete("/{notebook_id}")
|
|
def delete_notebook(
|
|
notebook_id: str,
|
|
force: bool = Query(False, description="강제 삭제 (노트가 있어도 삭제)"),
|
|
db: Session = Depends(get_sync_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""노트북 삭제"""
|
|
notebook = db.query(Notebook).filter(Notebook.id == notebook_id).first()
|
|
if not notebook:
|
|
raise HTTPException(status_code=404, detail="Notebook not found")
|
|
|
|
# 노트북에 포함된 노트 확인
|
|
note_count = db.query(func.count(NoteDocument.id)).filter(
|
|
NoteDocument.notebook_id == notebook.id
|
|
).scalar()
|
|
|
|
if note_count > 0 and not force:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Cannot delete notebook with {note_count} notes. Use force=true to delete anyway."
|
|
)
|
|
|
|
if force and note_count > 0:
|
|
# 노트들의 notebook_id를 NULL로 설정 (기본 노트북으로 이동)
|
|
db.query(NoteDocument).filter(
|
|
NoteDocument.notebook_id == notebook.id
|
|
).update({NoteDocument.notebook_id: None})
|
|
|
|
db.delete(notebook)
|
|
db.commit()
|
|
|
|
return {"message": "Notebook deleted successfully"}
|
|
|
|
@router.get("/{notebook_id}/notes")
|
|
def get_notebook_notes(
|
|
notebook_id: str,
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=100),
|
|
db: Session = Depends(get_sync_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""노트북에 포함된 노트들 조회"""
|
|
# 노트북 존재 확인
|
|
notebook = db.query(Notebook).filter(Notebook.id == notebook_id).first()
|
|
if not notebook:
|
|
raise HTTPException(status_code=404, detail="Notebook not found")
|
|
|
|
# 노트들 조회
|
|
notes = db.query(NoteDocument).filter(
|
|
NoteDocument.notebook_id == notebook_id
|
|
).order_by(desc(NoteDocument.updated_at)).offset(skip).limit(limit).all()
|
|
|
|
return notes
|
|
|
|
@router.post("/{notebook_id}/notes/{note_id}")
|
|
def add_note_to_notebook(
|
|
notebook_id: str,
|
|
note_id: str,
|
|
db: Session = Depends(get_sync_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""노트를 노트북에 추가"""
|
|
# 노트북과 노트 존재 확인
|
|
notebook = db.query(Notebook).filter(Notebook.id == notebook_id).first()
|
|
if not notebook:
|
|
raise HTTPException(status_code=404, detail="Notebook not found")
|
|
|
|
note = db.query(NoteDocument).filter(NoteDocument.id == note_id).first()
|
|
if not note:
|
|
raise HTTPException(status_code=404, detail="Note not found")
|
|
|
|
# 노트를 노트북에 할당
|
|
note.notebook_id = notebook_id
|
|
db.commit()
|
|
|
|
return {"message": "Note added to notebook successfully"}
|
|
|
|
@router.delete("/{notebook_id}/notes/{note_id}")
|
|
def remove_note_from_notebook(
|
|
notebook_id: str,
|
|
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,
|
|
NoteDocument.notebook_id == notebook_id
|
|
).first()
|
|
|
|
if not note:
|
|
raise HTTPException(status_code=404, detail="Note not found in this notebook")
|
|
|
|
# 노트북에서 제거 (기본 노트북으로 이동)
|
|
note.notebook_id = None
|
|
db.commit()
|
|
|
|
return {"message": "Note removed from notebook successfully"}
|