Files
Hyungi Ahn 49d8f68986 feat(library): 자료실 회독 카운트 추적 (PR-A backend)
자료실 자료를 사용자가 명시적으로 "1회독 완료" 클릭 시 +1 누적.
detail 진입 자동 카운트 . append-only 로그.

데이터:
- migrations 174~176: document_reads 테이블 + 인덱스 2개 (단일 statement 분할)

ORM:
- app/models/document_read.py: DocumentRead (user_id, document_id, read_at)

API (app/api/document_reads.py, /api/documents prefix):
- POST   /api/documents/{id}/read       — 회독 +1
- GET    /api/documents/{id}/read-stats — {read_count, last_read_at}
- DELETE /api/documents/{id}/read/last  — 현재 사용자의 그 문서 마지막 1건만
  · ownership: WHERE user_id=current_user.id AND document_id=:doc_id
  · documents 에 user_id 부재 (single-user). multi-user 전환 시 ownership
    check 추가 필요 — 코드 주석 명시.

응답 확장:
- DocumentResponse: read_count(default 0), last_read_at(default None)
- /api/documents/library: 페이지 N건 한정 LEFT JOIN 으로 read 통계 매핑 (N+1 회피)
- /api/library/tree CategoryTreeNode: unread_count 추가
  · 기존 path_docs 가 ancestor 누적 구조라 그대로 활용 — 하위 경로 합산 자동

규칙 (사용자 명시 — 변경 금지):
  · 같은 날 여러 번 클릭 → 각각 별개 회독
  · 실수 클릭 취소 = DELETE /read/last
  · documents 에 read_count 컬럼 추가 , 로그 기반 COUNT(*) 만

plan: ~/.claude/plans/scalable-chasing-stonebraker.md
브랜치: feature/library-reads (손글씨 트랙과 분리)
2026-04-27 12:08:36 +09:00

34 lines
1.2 KiB
Python

"""document_reads 테이블 ORM — 자료실 회독 추적.
NOTE: documents 테이블에 user_id 컬럼이 없음 (single-user 가정).
회독 ownership 은 document_reads.user_id 만으로 추적.
multi-user 전환 시 documents.user_id 추가 후 별도 ownership check 필요.
설계:
- append-only log. 회독 횟수 = COUNT(*), 마지막 시각 = MAX(read_at).
- 사용자 명시 행동 (버튼 클릭) 으로만 row insert. 자동 +1 금지.
- 같은 user/document 여러 row 허용 (회독 카운트 누적).
"""
from datetime import datetime
from sqlalchemy import BigInteger, DateTime, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from core.database import Base
class DocumentRead(Base):
__tablename__ = "document_reads"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
user_id: Mapped[int] = mapped_column(
BigInteger, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
document_id: Mapped[int] = mapped_column(
BigInteger, ForeignKey("documents.id", ondelete="CASCADE"), nullable=False
)
read_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now, nullable=False
)