24bd363beb
자료실 자료 detail 에 "필기" 버튼 → 본문 아래에 HandwriteCanvas 띄움.
자료당 사용자별 1개 캔버스 (UNIQUE user×document). upsert 방식.
Backend:
- migrations 177~178: document_notes (user_id, document_id, strokes_json,
canvas 크기) + UNIQUE(user_id, document_id) + 인덱스
- app/models/document_note.py: DocumentNote ORM
- app/api/document_notes.py:
· GET /api/documents/{id}/note — 단건 조회 (없으면 strokes_json=null)
· PUT /api/documents/{id}/note — upsert (PostgreSQL ON CONFLICT)
· DELETE /api/documents/{id}/note
· ownership: WHERE user_id=current_user.id (single-user 가정)
- app/main.py: document_notes_router 등록 (/api/documents prefix)
Frontend:
- routes/documents/[id]/+page.svelte:
· 자료실 자료 (category='library') 의 affordance row 에 "필기" 토글 추가
· 클릭 시 GET /note 로 strokes 로드 → HandwriteCanvas 본문 카드 아래 마운트
· 캔버스 onChange → PUT /note 자동 저장 (HandwriteCanvas 내부 3초 idle 디바운스 활용)
· 60vh / min-h-[400px] 분할. 모바일에선 본문 아래 스크롤로 자연스럽게.
- HandwriteCanvas 재사용 — sessionId prop 에 documentId 전달.
localStorage 키도 그대로 사용 (자료별로 namespacing).
45 lines
1.8 KiB
Python
45 lines
1.8 KiB
Python
"""document_notes 테이블 ORM — 자료별 손글씨 노트 (자료 1:1).
|
||
|
||
설계:
|
||
- user×document UNIQUE — 자료당 사용자별 한 캔버스.
|
||
- upsert 방식. PUT /api/documents/{id}/note 로 strokes_json 전체 갱신.
|
||
- 회독 (document_reads, append-only log) 와 별개.
|
||
|
||
NOTE: documents 에 user_id 부재 (single-user). document_notes.user_id 로
|
||
ownership. multi-user 전환 시 documents.user_id 추가 후 별도 check 필요.
|
||
"""
|
||
|
||
from datetime import datetime
|
||
from typing import Any
|
||
|
||
from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, UniqueConstraint
|
||
from sqlalchemy.dialects.postgresql import JSONB
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from core.database import Base
|
||
|
||
|
||
class DocumentNote(Base):
|
||
__tablename__ = "document_notes"
|
||
__table_args__ = (
|
||
UniqueConstraint("user_id", "document_id", name="document_notes_user_id_document_id_key"),
|
||
)
|
||
|
||
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
|
||
)
|
||
strokes_json: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||
canvas_width: Mapped[int | None] = mapped_column(Integer)
|
||
canvas_height: Mapped[int | None] = mapped_column(Integer)
|
||
schema_version: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), default=datetime.now, nullable=False
|
||
)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), default=datetime.now, onupdate=datetime.now, nullable=False
|
||
)
|