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
+22 -2
View File
@@ -14,6 +14,11 @@
editing: {{ 'true' if editing_entry_id == item.id else 'false' }},
moods: [{% for m in item.moods %}'{{ m.value }}'{{ ',' if not loop.last }}{% endfor %}],
toggleMood(v) { this.moods.includes(v) ? this.moods = this.moods.filter(m => m !== v) : this.moods.push(v) },
previewMode: false,
togglePreview() {
this.previewMode = !this.previewMode;
if (this.previewMode) { this.$refs.contentField.dispatchEvent(new Event('input')); }
},
}"
>
<div x-show="!editing">
@@ -24,7 +29,7 @@
{% endif %}
</div>
{% if item.title %}<div class="journal-entry-title">{{ item.title }}</div>{% endif %}
<div class="journal-entry-content">{{ item.content }}</div>
<div class="journal-entry-content">{{ item.content|markdown }}</div>
{% if item.tags %}
<div class="journal-entry-tags">
{% for tag in item.tags %}<span class="tag-chip">#{{ tag }}</span>{% endfor %}
@@ -94,7 +99,22 @@
</div>
<div class="field">
<label>내용</label>
<textarea name="content" rows="4" required>{{ item.content }}</textarea>
{% include "partials/journal_editor_toolbar.html" %}
<div x-show="!previewMode">
<textarea
name="content"
rows="6"
required
class="markdown-editor"
x-ref="contentField"
hx-post="/journal/preview"
hx-trigger="input changed delay:400ms, load"
hx-target="#journal-edit-preview-{{ item.id }}"
hx-swap="innerHTML"
hx-params="content"
>{{ item.content }}</textarea>
</div>
<div id="journal-edit-preview-{{ item.id }}" class="journal-preview journal-entry-content" x-show="previewMode" x-cloak></div>
</div>
<div class="field">
<label>기분 (여러 개 선택 가능)</label>