""" 노트북 (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"}