Files
Hyungi Ahn 8b15e6e019 feat(study): 문제 첨부 이미지 (PR-8)
문제별 N개 이미지 첨부. 회로도/그래프 등이 필요한 시험 문제 지원.
입력·편집·복습 모두에서 표시.

데이터 모델 (migration 198):
- study_question_images: id, user_id FK CASCADE, study_question_id FK CASCADE,
  file_path, file_size, mime_type, sort_order, created_at
- partial idx (study_question_id, sort_order, id)

저장: NAS /documents/study_question_images/{topic_id}/{qid}/{img_id}.{ext}
file_watcher 가 보는 PKM 경로와 분리 — 자동 인덱싱 안 됨.

API:
- POST /api/study-questions/{qid}/images (multipart, MIME PNG/JPEG/WEBP/GIF,
  10MB/파일 제한, sort_order 자동 max+1)
- GET /api/study-questions/{qid}/images/{img_id}/raw (FileResponse, Bearer 인증)
- DELETE /api/study-questions/{qid}/images/{img_id} (DB row + 파일 시스템 정리)
- StudyQuestionResponse / ReviewQuestionItem 응답에 images 배열 포함
- StudyQuestionSummary 응답에 has_images bool 추가

프론트:
- 신규 lib/components/ImgAuth.svelte — Bearer 인증 endpoint 의 이미지를 fetch +
  blob URL 로 변환해 <img> 표시. unmount 시 URL.revokeObjectURL.
- /questions/new: 입력 폼에 이미지 dropzone (client-side 보유) → POST
  /questions 받은 qid 로 자동 multipart 업로드. "저장 후 계속 입력" 시 reset.
- /questions/[qid]/edit: 별도 카드 — 기존 이미지 grid + 추가/삭제. 즉시 업로드.
- /review: 문제 본문 아래 이미지 grid (max-h-72 object-contain).
- 모든 표시는 ImgAuth 컴포넌트 — accessToken 만료 케이스 대비.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 13:41:50 +09:00

32 lines
1.2 KiB
Python

"""study_question_images ORM (PR-8) — 문제별 첨부 이미지.
저장: NAS /documents/study_question_images/{topic_id}/{qid}/{img_id}.{ext}
표시: GET /api/study-questions/{qid}/images/{img_id}/raw (인증 필요)
"""
from datetime import datetime
from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from core.database import Base
class StudyQuestionImage(Base):
__tablename__ = "study_question_images"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
user_id: Mapped[int] = mapped_column(
BigInteger, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
study_question_id: Mapped[int] = mapped_column(
BigInteger, ForeignKey("study_questions.id", ondelete="CASCADE"), nullable=False
)
file_path: Mapped[str] = mapped_column(Text, nullable=False)
file_size: Mapped[int] = mapped_column(BigInteger, nullable=False)
mime_type: Mapped[str] = mapped_column(String(80), nullable=False)
sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now, nullable=False
)