Files
shinalokandClaude Sonnet 5 54c971875c add journaling feature: categories, tags, attachments, calendar, and multi-select emotions
Adds an 8th development stage that lets users keep a free-form journal alongside
habit tracking, reusing the existing Google OAuth/DB/PWA infrastructure instead
of a separate project. Users organize entries into custom categories, filter by
a month calendar with day-detail drill-down, attach photos/videos (served via an
authenticated route, never /static), tag entries, and pick multiple emotions per
entry from a curated 9-option set. Includes a global journal prompt bank for
lightweight guided journaling.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 18:40:07 +09:00

144 lines
6.2 KiB
Python

"""add journal tables (category, entry, tag, attachment, prompt)
Revision ID: 0010_add_journal_tables
Revises: 0009_add_habit_log_status
Create Date: 2026-08-04
습관 체크에 곁들이는 회고 + 독립적인 자유 일기를 함께 지원하는 저널링 기능을 위한 테이블.
카테고리(예: "일상", "투자")는 사용자별로 자유롭게 만들 수 있고, 하루에 같은 카테고리로
여러 엔트리를 쓸 수 있어 (habit_id, log_date) 같은 유니크 제약은 두지 않는다. journal_prompt는
카테고리 무관 전역 질문 뱅크로, 최소한의 한국어 질문 시드 데이터를 함께 넣는다(AI 기반 프롬프트는
후속 업데이트로 미룸).
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "0010_add_journal_tables"
down_revision: Union[str, None] = "0009_add_habit_log_status"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"journal_category",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("user_id", sa.Integer(), sa.ForeignKey("user.id", ondelete="CASCADE"), nullable=False),
sa.Column("name", sa.String(length=50), nullable=False),
sa.Column("color", sa.String(length=20), nullable=True),
sa.Column("sort_order", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.UniqueConstraint("user_id", "name", name="uq_journal_category_user_name"),
mysql_engine="InnoDB",
mysql_charset="utf8mb4",
)
op.create_table(
"journal_entry",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("user_id", sa.Integer(), sa.ForeignKey("user.id", ondelete="CASCADE"), nullable=False),
sa.Column(
"category_id",
sa.Integer(),
sa.ForeignKey("journal_category.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("entry_date", sa.Date(), nullable=False),
sa.Column("title", sa.String(length=200), nullable=True),
sa.Column("content", sa.Text(), nullable=False),
sa.Column("mood", sa.String(length=20), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column(
"updated_at",
sa.DateTime(),
nullable=False,
server_default=sa.func.now(),
server_onupdate=sa.func.now(),
),
sa.CheckConstraint(
"mood in ('great','good','neutral','bad','awful')", name="ck_journal_entry_mood"
),
mysql_engine="InnoDB",
mysql_charset="utf8mb4",
)
op.create_table(
"journal_tag",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("user_id", sa.Integer(), sa.ForeignKey("user.id", ondelete="CASCADE"), nullable=False),
sa.Column("name", sa.String(length=50), nullable=False),
sa.UniqueConstraint("user_id", "name", name="uq_journal_tag_user_name"),
mysql_engine="InnoDB",
mysql_charset="utf8mb4",
)
op.create_table(
"journal_entry_tag",
sa.Column(
"entry_id", sa.Integer(), sa.ForeignKey("journal_entry.id", ondelete="CASCADE"), primary_key=True
),
sa.Column(
"tag_id", sa.Integer(), sa.ForeignKey("journal_tag.id", ondelete="CASCADE"), primary_key=True
),
mysql_engine="InnoDB",
mysql_charset="utf8mb4",
)
op.create_table(
"journal_attachment",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column(
"entry_id", sa.Integer(), sa.ForeignKey("journal_entry.id", ondelete="CASCADE"), nullable=False
),
sa.Column("media_type", sa.String(length=10), nullable=False),
sa.Column("file_path", sa.String(length=500), nullable=False),
sa.Column("thumbnail_path", sa.String(length=500), nullable=True),
sa.Column("original_filename", sa.String(length=255), nullable=False),
sa.Column("file_size", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.CheckConstraint("media_type in ('image','video')", name="ck_journal_attachment_media_type"),
mysql_engine="InnoDB",
mysql_charset="utf8mb4",
)
op.create_table(
"journal_prompt",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("question_text", sa.String(length=300), nullable=False),
mysql_engine="InnoDB",
mysql_charset="utf8mb4",
)
journal_prompt = sa.table(
"journal_prompt",
sa.column("question_text", sa.String),
)
op.bulk_insert(
journal_prompt,
[
{"question_text": "오늘 가장 기억에 남는 순간은 언제였나요?"},
{"question_text": "오늘 감사했던 일은 무엇인가요?"},
{"question_text": "오늘 나를 힘들게 한 건 무엇이었고, 어떻게 다뤘나요?"},
{"question_text": "오늘 배운 것이 있다면 무엇인가요?"},
{"question_text": "내일의 나에게 해주고 싶은 말은?"},
{"question_text": "오늘 스스로를 칭찬하고 싶은 부분은?"},
{"question_text": "오늘 놓쳤지만 다음엔 다르게 해보고 싶은 것은?"},
{"question_text": "오늘 누군가에게 도움을 받았거나 준 적이 있나요?"},
{"question_text": "지금 가장 신경 쓰이는 고민은 무엇인가요?"},
{"question_text": "오늘 하루를 한 문장으로 요약한다면?"},
{"question_text": "요즘 내가 놓치고 있는 것은 없을까요?"},
{"question_text": "이번 주 목표에 얼마나 가까워졌나요?"},
],
)
def downgrade() -> None:
op.drop_table("journal_prompt")
op.drop_table("journal_attachment")
op.drop_table("journal_entry_tag")
op.drop_table("journal_tag")
op.drop_table("journal_entry")
op.drop_table("journal_category")