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>
This commit is contained in:
2026-08-05 12:24:18 +09:00
co-authored by Claude Sonnet 5
parent c466cc6639
commit bdf9d0bae7
18 changed files with 564 additions and 30 deletions
+26
View File
@@ -8,6 +8,7 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from app.database import get_db
from app.markdown_utils import render_markdown
from app.models.journal import JournalMood
from app.routers.pages import _current_user_or_redirect, templates
from app.schemas.journal import JournalCategoryCreate, JournalEntryCreate, JournalEntryUpdate
@@ -33,6 +34,7 @@ templates.env.globals["journal_mood_emoji"] = {m.value: emoji for m, emoji, _ in
# Jinja2 내장 |tojson 필터가 이 policy를 읽어서 json.dumps에 넘긴다 — 기본값(ensure_ascii=True)이면
# 한글이 \uXXXX로 이스케이프돼 응답 본문에서 읽기 힘들어진다.
templates.env.policies["json.dumps_kwargs"] = {"ensure_ascii": False}
templates.env.filters["markdown"] = render_markdown
def _parse_moods(raw: str) -> list[JournalMood]:
@@ -133,6 +135,17 @@ def journal_day_detail(request: Request, entry_date: date, db: Session = Depends
return _render_day_detail(request, db, current.id, entry_date)
@router.post("/journal/preview")
def preview_entry_content(request: Request, content: str = Form(""), db: Session = Depends(get_db)):
current = _current_user_or_redirect(request, db)
if isinstance(current, RedirectResponse):
return current
if not content.strip():
return HTMLResponse('<span class="empty-state">미리보기가 여기에 표시돼요</span>')
return HTMLResponse(render_markdown(content))
@router.post("/journal/new")
def create_entry_page(
request: Request,
@@ -149,6 +162,9 @@ def create_entry_page(
if isinstance(current, RedirectResponse):
return current
if journal_service.get_category(db, category_id, current.id) is None:
return HTMLResponse("카테고리를 찾을 수 없어요")
try:
parsed_date = date.fromisoformat(entry_date)
data = JournalEntryCreate(
@@ -202,6 +218,16 @@ def edit_entry_page(
original_date = entry.entry_date
if journal_service.get_category(db, category_id, current.id) is None:
return _render_day_detail(
request,
db,
current.id,
original_date,
edit_error="카테고리를 찾을 수 없어요",
editing_entry_id=entry_id,
)
try:
parsed_date = date.fromisoformat(entry_date)
data = JournalEntryUpdate(