journal: multi-select emotions and per-category writing templates

- Mood switches from a single enum column to a many-to-many
  JournalEntryMood table so an entry can carry several feelings at
  once; the vocabulary is trimmed to 9 named emotions (dropped the
  overlapping satisfaction scale) with unified noun-style labels.
- Categories can define a content_template that pre-fills the "new
  entry" textarea when selected (only if the user hasn't started
  typing), seeded with a Story/Feelings/Decisions/Insights/Actions
  reflection template on the default "일상" category.
- Fixes a real attribute-injection bug found while building the
  template feature: Jinja's built-in |tojson filter doesn't escape
  double quotes, which breaks a double-quoted x-data="..." attribute
  when the JSON payload contains one; added |forceescape and a
  regression test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 19:12:59 +09:00
co-authored by Claude Sonnet 5
parent 54c971875c
commit c466cc6639
8 changed files with 172 additions and 9 deletions
+21 -2
View File
@@ -59,6 +59,10 @@
<label for="edit-category-color-{{ category.id }}">색상</label>
<input type="color" id="edit-category-color-{{ category.id }}" name="color" value="{{ category.color or '#d97757' }}" />
</div>
<div class="field">
<label for="edit-category-template-{{ category.id }}">기본 작성 틀 (선택)</label>
<textarea id="edit-category-template-{{ category.id }}" name="content_template" rows="4" placeholder="이 카테고리로 새 기록을 쓸 때 내용칸에 미리 채워줄 틀을 입력하세요">{{ category.content_template or '' }}</textarea>
</div>
<div id="journal-category-edit-error-{{ category.id }}" class="form-error-text"></div>
<div style="display:flex; gap:8px;">
<button type="submit" class="btn btn-primary" style="flex:1;">저장</button>
@@ -85,6 +89,10 @@
<label for="new-category-color">색상 (선택)</label>
<input type="color" id="new-category-color" name="color" value="#d97757" />
</div>
<div class="field">
<label for="new-category-template">기본 작성 틀 (선택)</label>
<textarea id="new-category-template" name="content_template" rows="4" placeholder="이 카테고리로 새 기록을 쓸 때 내용칸에 미리 채워줄 틀을 입력하세요"></textarea>
</div>
<div id="journal-category-error" class="form-error-text"></div>
<button type="submit" class="btn btn-primary btn-block">추가하기</button>
</form>
@@ -113,6 +121,17 @@
open: false,
moods: [],
toggleMood(v) { this.moods.includes(v) ? this.moods = this.moods.filter(m => m !== v) : this.moods.push(v) },
categoryTemplates: {{ category_templates|tojson|forceescape }},
content: '',
lastAutoFilled: '',
onCategoryChange(categoryId) {
const tmpl = this.categoryTemplates[categoryId] || '';
if (this.content === this.lastAutoFilled) {
this.content = tmpl;
this.lastAutoFilled = tmpl;
}
},
init() { this.onCategoryChange(this.$refs.categorySelect.value); },
}"
>
<button type="button" class="btn btn-primary btn-block" @click="open = !open">
@@ -135,7 +154,7 @@
>
<div class="field">
<label for="new-entry-category">카테고리</label>
<select id="new-entry-category" name="category_id">
<select id="new-entry-category" name="category_id" x-ref="categorySelect" @change="onCategoryChange($event.target.value)">
{% for category in categories %}
<option value="{{ category.id }}" {{ 'selected' if category_id == category.id }}>{{ category.name }}</option>
{% endfor %}
@@ -154,7 +173,7 @@
<div class="field">
<label for="new-entry-content">내용</label>
<textarea id="new-entry-content" name="content" rows="5" required placeholder="오늘 하루는 어땠나요?"></textarea>
<textarea id="new-entry-content" name="content" rows="8" required placeholder="오늘 하루는 어땠나요?" x-model="content"></textarea>
</div>
<div class="field">