- 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>
156 lines
6.5 KiB
HTML
156 lines
6.5 KiB
HTML
{% set weekday_names = ["월", "화", "수", "목", "금", "토", "일"] %}
|
|
<div class="card day-detail-card">
|
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom: var(--space-2);">
|
|
<h3 style="margin:0;">{{ entry_date.strftime("%Y.%m.%d") }} ({{ weekday_names[entry_date.weekday()] }})</h3>
|
|
<button type="button" class="btn btn-secondary" onclick="document.getElementById('day-detail').innerHTML=''">닫기</button>
|
|
</div>
|
|
|
|
{% if items %}
|
|
<div class="day-detail-list">
|
|
{% for item in items %}
|
|
<div
|
|
class="journal-entry-item"
|
|
x-data="{
|
|
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">
|
|
<div style="display:flex; align-items:center; justify-content:space-between;">
|
|
<span class="badge" style="border-color: {{ item.category_color or 'var(--color-accent)' }};">{{ item.category_name }}</span>
|
|
{% if item.moods %}
|
|
<span class="mood-icon">{% for m in item.moods %}{{ journal_mood_emoji[m.value] }}{% endfor %}</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if item.title %}<div class="journal-entry-title">{{ item.title }}</div>{% endif %}
|
|
<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 %}
|
|
</div>
|
|
{% endif %}
|
|
{% if item.attachments %}
|
|
<div class="attachment-grid">
|
|
{% for attachment in item.attachments %}
|
|
<div class="attachment-thumb">
|
|
{% if attachment.media_type.value == "image" %}
|
|
<a href="/api/journal/media/{{ attachment.id }}" target="_blank">
|
|
<img src="/api/journal/media/{{ attachment.id }}?thumbnail=true" alt="{{ attachment.original_filename }}" />
|
|
</a>
|
|
{% else %}
|
|
<a href="/api/journal/media/{{ attachment.id }}" target="_blank" class="attachment-video-link">▶ {{ attachment.original_filename }}</a>
|
|
{% endif %}
|
|
<button
|
|
type="button"
|
|
class="attachment-delete-btn"
|
|
hx-post="/journal/{{ item.id }}/attachments/{{ attachment.id }}/delete"
|
|
hx-target="#day-detail"
|
|
hx-swap="innerHTML"
|
|
hx-confirm="이 첨부파일을 삭제할까요?"
|
|
aria-label="첨부파일 삭제"
|
|
>×</button>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
<div class="habit-item-actions" style="margin-top: 8px;">
|
|
<button type="button" class="btn btn-secondary" @click="editing = true">수정</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-danger-ghost"
|
|
hx-post="/journal/{{ item.id }}/delete"
|
|
hx-target="#day-detail"
|
|
hx-swap="innerHTML"
|
|
hx-confirm="이 기록을 삭제할까요?"
|
|
>삭제</button>
|
|
</div>
|
|
</div>
|
|
|
|
<form
|
|
x-show="editing"
|
|
x-cloak
|
|
style="padding: 12px 4px; border-bottom: 1px solid var(--color-border);"
|
|
hx-post="/journal/{{ item.id }}/edit"
|
|
hx-target="#day-detail"
|
|
hx-swap="innerHTML"
|
|
hx-encoding="multipart/form-data"
|
|
>
|
|
<div class="field">
|
|
<label>카테고리</label>
|
|
<select name="category_id">
|
|
{% for category in categories %}
|
|
<option value="{{ category.id }}" {{ 'selected' if category.id == item.category_id }}>{{ category.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label>날짜</label>
|
|
<input type="date" name="entry_date" value="{{ entry_date.isoformat() }}" required />
|
|
</div>
|
|
<div class="field">
|
|
<label>제목</label>
|
|
<input type="text" name="title" value="{{ item.title or '' }}" />
|
|
</div>
|
|
<div class="field">
|
|
<label>내용</label>
|
|
{% 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>
|
|
<input type="hidden" name="moods" :value="moods.join(',')" />
|
|
<div class="mood-picker">
|
|
{% for value, emoji, label in journal_mood_options %}
|
|
<button
|
|
type="button"
|
|
class="mood-pill"
|
|
:class="{ selected: moods.includes('{{ value.value }}') }"
|
|
@click="toggleMood('{{ value.value }}')"
|
|
>{{ emoji }} {{ label }}</button>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<div class="field">
|
|
<label>태그 (쉼표로 구분)</label>
|
|
<input type="text" name="tags" value="{{ item.tags|join(', ') }}" />
|
|
</div>
|
|
<div class="field">
|
|
<label>사진/영상 추가</label>
|
|
<input type="file" name="files" accept="image/*,video/*" multiple />
|
|
</div>
|
|
{% if edit_error and editing_entry_id == item.id %}
|
|
<div class="form-error-text">{{ edit_error }}</div>
|
|
{% endif %}
|
|
<div style="display:flex; gap:8px;">
|
|
<button type="submit" class="btn btn-primary" style="flex:1;">저장</button>
|
|
<button type="button" class="btn btn-secondary" @click="editing = false">취소</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">이 날 작성한 기록이 없어요.</div>
|
|
{% endif %}
|
|
</div>
|