- Mood switches from a single enum column to a many-to-many JournalEntryMood table so an entry can carry several feelings at once; the vocabulary is trimmed to 9 named emotions (dropped the overlapping satisfaction scale) with unified noun-style labels. - Categories can define a content_template that pre-fills the "new entry" textarea when selected (only if the user hasn't started typing), seeded with a Story/Feelings/Decisions/Insights/Actions reflection template on the default "일상" category. - Fixes a real attribute-injection bug found while building the template feature: Jinja's built-in |tojson filter doesn't escape double quotes, which breaks a double-quoted x-data="..." attribute when the JSON payload contains one; added |forceescape and a regression test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
135 lines
5.7 KiB
Python
135 lines
5.7 KiB
Python
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)
|