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,126 @@
|
||||
from datetime import date, datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
|
||||
from app.models.journal import JournalAttachmentType, JournalMood
|
||||
|
||||
|
||||
class JournalCategoryBase(BaseModel):
|
||||
name: str
|
||||
color: str | None = None
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def name_not_blank(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError("카테고리 이름을 입력해주세요")
|
||||
return v
|
||||
|
||||
@field_validator("color")
|
||||
@classmethod
|
||||
def blank_color_to_none(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return None
|
||||
v = v.strip()
|
||||
return v or None
|
||||
|
||||
|
||||
class JournalCategoryCreate(JournalCategoryBase):
|
||||
pass
|
||||
|
||||
|
||||
class JournalCategoryOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
name: str
|
||||
color: str | None
|
||||
sort_order: int | None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class JournalCategoryReorderRequest(BaseModel):
|
||||
category_ids: list[int]
|
||||
|
||||
|
||||
class JournalEntryBase(BaseModel):
|
||||
category_id: int
|
||||
entry_date: date
|
||||
title: str | None = None
|
||||
content: str
|
||||
moods: list[JournalMood] = []
|
||||
tags: list[str] = []
|
||||
|
||||
@field_validator("content")
|
||||
@classmethod
|
||||
def content_not_blank(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError("내용을 입력해주세요")
|
||||
return v
|
||||
|
||||
@field_validator("title")
|
||||
@classmethod
|
||||
def blank_title_to_none(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return None
|
||||
v = v.strip()
|
||||
return v or None
|
||||
|
||||
@field_validator("moods")
|
||||
@classmethod
|
||||
def dedupe_moods(cls, v: list[JournalMood]) -> list[JournalMood]:
|
||||
cleaned: list[JournalMood] = []
|
||||
for mood in v:
|
||||
if mood not in cleaned:
|
||||
cleaned.append(mood)
|
||||
return cleaned
|
||||
|
||||
@field_validator("tags")
|
||||
@classmethod
|
||||
def clean_tags(cls, v: list[str]) -> list[str]:
|
||||
cleaned: list[str] = []
|
||||
for raw in v:
|
||||
name = raw.strip()
|
||||
if name and name not in cleaned:
|
||||
cleaned.append(name)
|
||||
return cleaned
|
||||
|
||||
|
||||
class JournalEntryCreate(JournalEntryBase):
|
||||
pass
|
||||
|
||||
|
||||
class JournalEntryUpdate(JournalEntryBase):
|
||||
pass
|
||||
|
||||
|
||||
class JournalAttachmentOut(BaseModel):
|
||||
id: int
|
||||
media_type: JournalAttachmentType
|
||||
original_filename: str
|
||||
has_thumbnail: bool
|
||||
|
||||
|
||||
class JournalCalendarDay(BaseModel):
|
||||
entry_date: date
|
||||
total_count: int
|
||||
category_colors: list[str] # 그 날 엔트리가 있는 카테고리들의 색상(점 표시용, 중복 제거)
|
||||
|
||||
|
||||
class JournalDayDetailItem(BaseModel):
|
||||
id: int
|
||||
category_id: int
|
||||
category_name: str
|
||||
category_color: str | None
|
||||
title: str | None
|
||||
content: str
|
||||
moods: list[JournalMood]
|
||||
tags: list[str]
|
||||
attachments: list[JournalAttachmentOut]
|
||||
|
||||
|
||||
class JournalOnThisDayItem(JournalDayDetailItem):
|
||||
entry_date: date
|
||||
years_ago: int
|
||||
Reference in New Issue
Block a user