"""study_topics / study_topic_documents 테이블 ORM — 학습 워크스페이스 1차 컨테이너 목적: 필기 세션(StudySession) 과 자료(documents) 를 한 학습 주제(예: 가스기사) 아래로 묶는 컨테이너. 향후 단어장/오디오/문제세트 같은 학습 자산이 같은 컨테이너 아래로 들어올 수 있도록 설계. 설계 원칙: - documents.category(자료실 UI 축) 와 직교한 별도 분류 축. 자료실 facet/카테고리 미터치. - StudySession.certification/subject/topic 컬럼은 보존, 본 컨테이너 와 직교 세부 메타. - study_type 은 느슨한 분류. DB/Pydantic 강한 enum 미사용. 권장값: certification / language / school / work / general (UI 드롭다운에서만 안내). - soft delete (deleted_at). 동일 user_id+name 의 active 행만 partial unique index 로 중복 방지 — 삭제된 주제명 재생성 가능. - 자산 다대다 매핑: 본 PR 은 documents 만 (study_topic_documents). 향후 자산 타입별 조인 테이블 추가 (study_topic_audio_assets 등). polymorphic 단일 테이블 금지. """ from datetime import datetime from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, String, Text from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship from core.database import Base class StudyTopic(Base): __tablename__ = "study_topics" id: Mapped[int] = mapped_column(BigInteger, primary_key=True) user_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("users.id", ondelete="CASCADE"), nullable=False ) name: Mapped[str] = mapped_column(String(120), nullable=False) description: Mapped[str | None] = mapped_column(Text) color: Mapped[str | None] = mapped_column(String(20)) # 느슨한 분류 (certification/language/school/work/general 권장) study_type: Mapped[str | None] = mapped_column(String(40)) sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) # PR-6: 시험 메타 (회차당 문항 수 + 과목 리스트) exam_round_size: Mapped[int | None] = mapped_column(Integer) exam_subjects: Mapped[list] = mapped_column(JSONB, nullable=False, default=list) 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 ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) # 연관 — 세션 (1:N), 자료 매핑 (N:M), 문제 (1:N PR-2) sessions: Mapped[list["StudySession"]] = relationship( # type: ignore[name-defined] # noqa: F821 "StudySession", back_populates="study_topic", lazy="noload" ) document_links: Mapped[list["StudyTopicDocument"]] = relationship( back_populates="topic", cascade="all, delete-orphan", order_by="StudyTopicDocument.sort_order", lazy="noload", ) questions: Mapped[list["StudyQuestion"]] = relationship( # type: ignore[name-defined] # noqa: F821 "StudyQuestion", back_populates="topic", lazy="noload" ) class StudyTopicDocument(Base): __tablename__ = "study_topic_documents" study_topic_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("study_topics.id", ondelete="CASCADE"), primary_key=True ) document_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("documents.id", ondelete="CASCADE"), primary_key=True ) user_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("users.id", ondelete="CASCADE"), 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 ) topic: Mapped["StudyTopic"] = relationship(back_populates="document_links")