import enum from datetime import date, datetime from sqlalchemy import Column, Date, Enum, ForeignKey, Integer, String, Table, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.sql import func from app.database import Base from app.models.habit import _by_value class JournalMood(str, enum.Enum): PAIN = "pain" # 아픔 ACHIEVEMENT = "achievement" # 성취 ANGER = "anger" # 분노 EXCITED = "excited" # 신남 CALM = "calm" # 평온 HAPPY = "happy" # 행복 WORRY = "worry" # 걱정 TIRED = "tired" # 피곤 SAD = "sad" # 슬픔 class JournalAttachmentType(str, enum.Enum): IMAGE = "image" VIDEO = "video" journal_entry_tag = Table( "journal_entry_tag", Base.metadata, Column("entry_id", ForeignKey("journal_entry.id", ondelete="CASCADE"), primary_key=True), Column("tag_id", ForeignKey("journal_tag.id", ondelete="CASCADE"), primary_key=True), ) class JournalCategory(Base): __tablename__ = "journal_category" __table_args__ = (UniqueConstraint("user_id", "name", name="uq_journal_category_user_name"),) id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"), nullable=False) name: Mapped[str] = mapped_column(String(50), nullable=False) color: Mapped[str | None] = mapped_column(String(20), nullable=True) sort_order: Mapped[int | None] = mapped_column(Integer, nullable=True) # 이 카테고리로 새 기록을 쓸 때 내용칸에 미리 채워주는 틀(예: Story/Feelings/Decisions/... 회고 양식). # 사용자가 직접 타이핑을 시작하면 더 이상 덮어쓰지 않는다(프론트에서 처리). content_template: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(server_default=func.now(), nullable=False) entries: Mapped[list["JournalEntry"]] = relationship( back_populates="category", cascade="all, delete-orphan", passive_deletes=True ) class JournalEntry(Base): __tablename__ = "journal_entry" id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"), nullable=False) category_id: Mapped[int] = mapped_column( ForeignKey("journal_category.id", ondelete="CASCADE"), nullable=False ) entry_date: Mapped[date] = mapped_column(Date, nullable=False) title: Mapped[str | None] = mapped_column(String(200), nullable=True) content: Mapped[str] = mapped_column(Text, nullable=False) created_at: Mapped[datetime] = mapped_column(server_default=func.now(), nullable=False) updated_at: Mapped[datetime] = mapped_column( server_default=func.now(), onupdate=func.now(), nullable=False ) category: Mapped["JournalCategory"] = relationship(back_populates="entries") tags: Mapped[list["JournalTag"]] = relationship( secondary=journal_entry_tag, back_populates="entries" ) moods: Mapped[list["JournalEntryMood"]] = relationship( back_populates="entry", cascade="all, delete-orphan", passive_deletes=True ) attachments: Mapped[list["JournalAttachment"]] = relationship( back_populates="entry", cascade="all, delete-orphan", passive_deletes=True ) class JournalEntryMood(Base): """엔트리 하나에 여러 감정을 태그처럼 붙일 수 있게 하는 연결 테이블. mood 자체가 고정 enum이라 JournalTag처럼 별도 엔티티(이름 등)를 둘 필요가 없어 journal_entry_tag와 달리 값 자체를 PK로 쓴다.""" __tablename__ = "journal_entry_mood" entry_id: Mapped[int] = mapped_column(ForeignKey("journal_entry.id", ondelete="CASCADE"), primary_key=True) mood: Mapped[JournalMood] = mapped_column( Enum(JournalMood, native_enum=False, length=20, values_callable=_by_value), primary_key=True ) entry: Mapped["JournalEntry"] = relationship(back_populates="moods") class JournalTag(Base): __tablename__ = "journal_tag" __table_args__ = (UniqueConstraint("user_id", "name", name="uq_journal_tag_user_name"),) id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"), nullable=False) name: Mapped[str] = mapped_column(String(50), nullable=False) entries: Mapped[list["JournalEntry"]] = relationship( secondary=journal_entry_tag, back_populates="tags" ) class JournalAttachment(Base): __tablename__ = "journal_attachment" id: Mapped[int] = mapped_column(Integer, primary_key=True) entry_id: Mapped[int] = mapped_column(ForeignKey("journal_entry.id", ondelete="CASCADE"), nullable=False) media_type: Mapped[JournalAttachmentType] = mapped_column( Enum(JournalAttachmentType, native_enum=False, length=10, values_callable=_by_value), nullable=False ) file_path: Mapped[str] = mapped_column(String(500), nullable=False) thumbnail_path: Mapped[str | None] = mapped_column(String(500), nullable=True) original_filename: Mapped[str] = mapped_column(String(255), nullable=False) file_size: Mapped[int] = mapped_column(Integer, nullable=False) created_at: Mapped[datetime] = mapped_column(server_default=func.now(), nullable=False) entry: Mapped["JournalEntry"] = relationship(back_populates="attachments") class JournalPrompt(Base): """카테고리 무관 전역 질문 뱅크. 특정 카테고리 전용 프롬프트는 v1 범위 밖(향후 확장 여지).""" __tablename__ = "journal_prompt" id: Mapped[int] = mapped_column(Integer, primary_key=True) question_text: Mapped[str] = mapped_column(String(300), nullable=False)