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>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user