🎉 Initial commit: Document Server MVP
✨ Features implemented: - FastAPI backend with JWT authentication - PostgreSQL database with async SQLAlchemy - HTML document viewer with smart highlighting - Note system connected to highlights (1:1 relationship) - Bookmark system for quick navigation - Integrated search (documents + notes) - Tag system for document organization - Docker containerization with Nginx 🔧 Technical stack: - Backend: FastAPI + PostgreSQL + Redis - Frontend: Alpine.js + Tailwind CSS - Authentication: JWT tokens - File handling: HTML + PDF support - Search: Full-text search with relevance scoring 📋 Core functionality: - Text selection → Highlight creation - Highlight → Note attachment - Note management with search/filtering - Bookmark creation at scroll positions - Document upload with metadata - User management (admin creates accounts)
This commit is contained in:
300
backend/src/api/routes/bookmarks.py
Normal file
300
backend/src/api/routes/bookmarks.py
Normal file
@@ -0,0 +1,300 @@
|
||||
"""
|
||||
책갈피 관리 API 라우터
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, delete, and_
|
||||
from sqlalchemy.orm import joinedload
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from src.core.database import get_db
|
||||
from src.models.user import User
|
||||
from src.models.document import Document
|
||||
from src.models.bookmark import Bookmark
|
||||
from src.api.dependencies import get_current_active_user
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class CreateBookmarkRequest(BaseModel):
|
||||
"""책갈피 생성 요청"""
|
||||
document_id: str
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
page_number: Optional[int] = None
|
||||
scroll_position: int = 0
|
||||
element_id: Optional[str] = None
|
||||
element_selector: Optional[str] = None
|
||||
|
||||
|
||||
class UpdateBookmarkRequest(BaseModel):
|
||||
"""책갈피 업데이트 요청"""
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
page_number: Optional[int] = None
|
||||
scroll_position: Optional[int] = None
|
||||
element_id: Optional[str] = None
|
||||
element_selector: Optional[str] = None
|
||||
|
||||
|
||||
class BookmarkResponse(BaseModel):
|
||||
"""책갈피 응답"""
|
||||
id: str
|
||||
document_id: str
|
||||
title: str
|
||||
description: Optional[str]
|
||||
page_number: Optional[int]
|
||||
scroll_position: int
|
||||
element_id: Optional[str]
|
||||
element_selector: Optional[str]
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime]
|
||||
document_title: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/", response_model=BookmarkResponse)
|
||||
async def create_bookmark(
|
||||
bookmark_data: CreateBookmarkRequest,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""책갈피 생성"""
|
||||
# 문서 존재 및 권한 확인
|
||||
result = await db.execute(select(Document).where(Document.id == bookmark_data.document_id))
|
||||
document = result.scalar_one_or_none()
|
||||
|
||||
if not document:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Document not found"
|
||||
)
|
||||
|
||||
# 문서 접근 권한 확인
|
||||
if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not enough permissions to access this document"
|
||||
)
|
||||
|
||||
# 책갈피 생성
|
||||
bookmark = Bookmark(
|
||||
user_id=current_user.id,
|
||||
document_id=bookmark_data.document_id,
|
||||
title=bookmark_data.title,
|
||||
description=bookmark_data.description,
|
||||
page_number=bookmark_data.page_number,
|
||||
scroll_position=bookmark_data.scroll_position,
|
||||
element_id=bookmark_data.element_id,
|
||||
element_selector=bookmark_data.element_selector
|
||||
)
|
||||
|
||||
db.add(bookmark)
|
||||
await db.commit()
|
||||
await db.refresh(bookmark)
|
||||
|
||||
# 응답 데이터 생성
|
||||
response_data = BookmarkResponse.from_orm(bookmark)
|
||||
response_data.document_title = document.title
|
||||
|
||||
return response_data
|
||||
|
||||
|
||||
@router.get("/", response_model=List[BookmarkResponse])
|
||||
async def list_user_bookmarks(
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
document_id: Optional[str] = None,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""사용자의 모든 책갈피 조회"""
|
||||
query = (
|
||||
select(Bookmark)
|
||||
.options(joinedload(Bookmark.document))
|
||||
.where(Bookmark.user_id == current_user.id)
|
||||
)
|
||||
|
||||
if document_id:
|
||||
query = query.where(Bookmark.document_id == document_id)
|
||||
|
||||
query = query.order_by(Bookmark.created_at.desc()).offset(skip).limit(limit)
|
||||
|
||||
result = await db.execute(query)
|
||||
bookmarks = result.scalars().all()
|
||||
|
||||
# 응답 데이터 변환
|
||||
response_data = []
|
||||
for bookmark in bookmarks:
|
||||
bookmark_data = BookmarkResponse.from_orm(bookmark)
|
||||
bookmark_data.document_title = bookmark.document.title
|
||||
response_data.append(bookmark_data)
|
||||
|
||||
return response_data
|
||||
|
||||
|
||||
@router.get("/document/{document_id}", response_model=List[BookmarkResponse])
|
||||
async def get_document_bookmarks(
|
||||
document_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""특정 문서의 책갈피 목록 조회"""
|
||||
# 문서 존재 및 권한 확인
|
||||
result = await db.execute(select(Document).where(Document.id == document_id))
|
||||
document = result.scalar_one_or_none()
|
||||
|
||||
if not document:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Document not found"
|
||||
)
|
||||
|
||||
# 문서 접근 권한 확인
|
||||
if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not enough permissions to access this document"
|
||||
)
|
||||
|
||||
# 사용자의 책갈피만 조회
|
||||
result = await db.execute(
|
||||
select(Bookmark)
|
||||
.options(joinedload(Bookmark.document))
|
||||
.where(
|
||||
and_(
|
||||
Bookmark.document_id == document_id,
|
||||
Bookmark.user_id == current_user.id
|
||||
)
|
||||
)
|
||||
.order_by(Bookmark.page_number, Bookmark.scroll_position)
|
||||
)
|
||||
bookmarks = result.scalars().all()
|
||||
|
||||
# 응답 데이터 변환
|
||||
response_data = []
|
||||
for bookmark in bookmarks:
|
||||
bookmark_data = BookmarkResponse.from_orm(bookmark)
|
||||
bookmark_data.document_title = bookmark.document.title
|
||||
response_data.append(bookmark_data)
|
||||
|
||||
return response_data
|
||||
|
||||
|
||||
@router.get("/{bookmark_id}", response_model=BookmarkResponse)
|
||||
async def get_bookmark(
|
||||
bookmark_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""책갈피 상세 조회"""
|
||||
result = await db.execute(
|
||||
select(Bookmark)
|
||||
.options(joinedload(Bookmark.document))
|
||||
.where(Bookmark.id == bookmark_id)
|
||||
)
|
||||
bookmark = result.scalar_one_or_none()
|
||||
|
||||
if not bookmark:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Bookmark not found"
|
||||
)
|
||||
|
||||
# 소유자 확인
|
||||
if bookmark.user_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not enough permissions"
|
||||
)
|
||||
|
||||
response_data = BookmarkResponse.from_orm(bookmark)
|
||||
response_data.document_title = bookmark.document.title
|
||||
|
||||
return response_data
|
||||
|
||||
|
||||
@router.put("/{bookmark_id}", response_model=BookmarkResponse)
|
||||
async def update_bookmark(
|
||||
bookmark_id: str,
|
||||
bookmark_data: UpdateBookmarkRequest,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""책갈피 업데이트"""
|
||||
result = await db.execute(
|
||||
select(Bookmark)
|
||||
.options(joinedload(Bookmark.document))
|
||||
.where(Bookmark.id == bookmark_id)
|
||||
)
|
||||
bookmark = result.scalar_one_or_none()
|
||||
|
||||
if not bookmark:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Bookmark not found"
|
||||
)
|
||||
|
||||
# 소유자 확인
|
||||
if bookmark.user_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not enough permissions"
|
||||
)
|
||||
|
||||
# 업데이트
|
||||
if bookmark_data.title is not None:
|
||||
bookmark.title = bookmark_data.title
|
||||
if bookmark_data.description is not None:
|
||||
bookmark.description = bookmark_data.description
|
||||
if bookmark_data.page_number is not None:
|
||||
bookmark.page_number = bookmark_data.page_number
|
||||
if bookmark_data.scroll_position is not None:
|
||||
bookmark.scroll_position = bookmark_data.scroll_position
|
||||
if bookmark_data.element_id is not None:
|
||||
bookmark.element_id = bookmark_data.element_id
|
||||
if bookmark_data.element_selector is not None:
|
||||
bookmark.element_selector = bookmark_data.element_selector
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(bookmark)
|
||||
|
||||
response_data = BookmarkResponse.from_orm(bookmark)
|
||||
response_data.document_title = bookmark.document.title
|
||||
|
||||
return response_data
|
||||
|
||||
|
||||
@router.delete("/{bookmark_id}")
|
||||
async def delete_bookmark(
|
||||
bookmark_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""책갈피 삭제"""
|
||||
result = await db.execute(select(Bookmark).where(Bookmark.id == bookmark_id))
|
||||
bookmark = result.scalar_one_or_none()
|
||||
|
||||
if not bookmark:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Bookmark not found"
|
||||
)
|
||||
|
||||
# 소유자 확인
|
||||
if bookmark.user_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not enough permissions"
|
||||
)
|
||||
|
||||
# 책갈피 삭제
|
||||
await db.execute(delete(Bookmark).where(Bookmark.id == bookmark_id))
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Bookmark deleted successfully"}
|
||||
Reference in New Issue
Block a user