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
+46
View File
@@ -0,0 +1,46 @@
from app.markdown_utils import render_markdown
def test_bold_and_heading_render_as_html():
html = render_markdown("# 제목\n\n**굵게** 쓴 문장입니다")
assert "<h1>제목</h1>" in html
assert "<strong>굵게</strong>" in html
def test_single_newline_becomes_line_break():
html = render_markdown("첫째 줄\n둘째 줄")
assert "<br" in html
def test_list_renders_as_html_list():
html = render_markdown("- 하나\n- 둘")
assert "<ul>" in html
assert "<li>하나</li>" in html
def test_dash_and_star_bullets_render_identically():
# 마크다운 글머리 기호는 -/*/+ 전부 동일하게 처리돼야 한다 — 에디터 쪽 기본 기호(unorderedListStyle)만
# "-"로 바뀌었을 뿐, 렌더링 결과는 어떤 기호를 써도 같아야 한다.
assert render_markdown("- 하나\n- 둘") == render_markdown("* 하나\n* 둘") == render_markdown("+ 하나\n+ 둘")
def test_script_tag_is_stripped_not_executed():
html = render_markdown('<script>alert("xss")</script>본문')
assert "<script" not in html
assert "alert" not in html or "&lt;script" not in html # 태그는 지워지고 텍스트만 남아야 함
def test_javascript_href_is_neutralized():
html = render_markdown('[click me](javascript:alert(1))')
assert "javascript:" not in html
def test_onerror_attribute_is_stripped():
html = render_markdown('<img src=x onerror="alert(1)">본문')
assert "onerror" not in html
assert "<img" not in html # img는 허용 태그 목록에 없음
def test_allowed_link_href_is_preserved():
html = render_markdown("[내 블로그](https://example.com)")
assert 'href="https://example.com"' in html