Files
habit-tracker/app/schemas/journal.py
T
shinalokandClaude Sonnet 5 bdf9d0bae7 journal: real markdown editor (EasyMDE) with live preview toggle, fix default template
- Replace the plain textarea with EasyMDE (vendored locally, no CDN) for
  markdown authoring: syntax highlighting, smart list continuation, and a
  custom text-based toolbar (built-in EasyMDE toolbar icons require Font
  Awesome from a CDN, which this app doesn't use). unorderedListStyle is set
  to "-" to match the app's own template convention.
- Add a preview/edit toggle button that swaps the editor for the exact same
  server-rendered markdown (via /journal/preview) shown after saving, instead
  of always showing both.
- Fix create/edit entry routes to verify the submitted category_id actually
  belongs to the current user before inserting -- every other write path in
  this app already checked ownership; this one didn't (found while manually
  testing the new editor with a typo'd category id that happened to belong to
  someone else's category, which surfaced as an IntegrityError 500 instead of
  a clean 404-equivalent).
- Fix the default "일상" category template: bare "-" bullet lines don't parse
  as list items in the markdown renderer (they need a trailing space), and
  the content_template validator was silently stripping that trailing space
  off on every save. Backfill migration updates any category still holding
  the old, broken template text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 12:24:18 +09:00

140 lines
3.6 KiB
Python

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
content_template: 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
@field_validator("content_template")
@classmethod
def blank_template_to_none(cls, v: str | None) -> str | None:
# color/name과 달리 여기선 .strip()으로 값 자체를 바꾸지 않는다 — 템플릿 맨 끝의
# "- "(대시+공백)처럼 의미 있는 trailing whitespace가 있을 수 있고, 그걸 지우면
# markdown이 그 줄을 목록으로 인식하지 못하게 된다(빈 값인지 판단만 strip으로 하고,
# 실제로 저장하는 값은 원본을 그대로 쓴다).
if v is None or not v.strip():
return None
return v
class JournalCategoryCreate(JournalCategoryBase):
pass
class JournalCategoryOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
color: str | None
content_template: 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