Dropping EasyMDE entirely (previous commit) fixed the Korean IME jamo split but also removed the live markdown syntax highlighting everywhere, even on platforms that never had the bug. Only iOS WebKit breaks CJK IME composition under CodeMirror, so journal-editor.js now detects iOS (including the desktop-UA-masquerading iPad, via MacIntel + multi-touch) and only skips EasyMDE there, falling back to a native <textarea>. Everywhere else keeps the full EasyMDE/CodeMirror editing experience as before. window.JournalEditor now dispatches to the right engine per textarea (checking t._easymde) so the toolbar buttons and the category-template autofill in journal.html work unchanged regardless of which engine backs a given textarea. Restores the easymde vendor files, base.html includes, and service worker precache entries removed in the previous commit (cache bumped to v8). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
277 lines
11 KiB
HTML
277 lines
11 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}일기 · 해빗랩{% endblock %}
|
||
{% block nav_journal %}active{% endblock %}
|
||
{% block shell_class %} wide{% endblock %}
|
||
{% block content %}
|
||
<h1>일기</h1>
|
||
|
||
<div style="display:flex; align-items:center; gap: var(--space-1);">
|
||
<div class="tabs" style="flex:1; min-width:0;">
|
||
<a href="/journal?year={{ year }}&month={{ month }}" class="{{ 'active' if tab != 'manage' and not category_id }}">전체</a>
|
||
{% for category in categories %}
|
||
<a
|
||
href="/journal?year={{ year }}&month={{ month }}&category_id={{ category.id }}"
|
||
class="{{ 'active' if tab != 'manage' and category_id == category.id }}"
|
||
>{{ category.name }}</a>
|
||
{% endfor %}
|
||
</div>
|
||
<a href="/journal?tab=manage" class="btn btn-secondary{{ ' active' if tab == 'manage' }}" style="flex-shrink:0; margin-bottom: var(--space-2); text-decoration:none;">⚙ 관리</a>
|
||
</div>
|
||
|
||
{% if tab == "manage" %}
|
||
{% if categories %}
|
||
<div class="card">
|
||
<div class="day-detail-list" id="journal-category-list">
|
||
{% for category in categories %}
|
||
<div class="journal-entry-item" data-category-id="{{ category.id }}" x-data="{ editing: false }">
|
||
<div class="day-detail-item" x-show="!editing">
|
||
<span style="display:flex; align-items:center; gap: 8px;">
|
||
<span class="drag-handle" aria-hidden="true">⠿</span>
|
||
<span class="badge" style="border-color: {{ category.color or 'var(--color-accent)' }};">{{ category.name }}</span>
|
||
<span class="badge">기록 {{ entry_counts.get(category.id, 0) }}개</span>
|
||
</span>
|
||
<span style="display:flex; gap:8px;">
|
||
<button type="button" class="btn btn-secondary" @click="editing = true">수정</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-danger-ghost"
|
||
hx-post="/journal/categories/{{ category.id }}/delete"
|
||
hx-target="body"
|
||
hx-swap="none"
|
||
hx-confirm="'{{ category.name }}' 카테고리를 삭제할까요? 이 카테고리의 모든 기록({{ entry_counts.get(category.id, 0) }}개)과 첨부파일도 함께 삭제됩니다."
|
||
>삭제</button>
|
||
</span>
|
||
</div>
|
||
|
||
<form
|
||
x-show="editing"
|
||
x-cloak
|
||
style="padding: 8px 0;"
|
||
hx-post="/journal/categories/{{ category.id }}/edit"
|
||
hx-target="#journal-category-edit-error-{{ category.id }}"
|
||
hx-swap="innerHTML"
|
||
>
|
||
<div class="field">
|
||
<label for="edit-category-name-{{ category.id }}">카테고리 이름</label>
|
||
<input type="text" id="edit-category-name-{{ category.id }}" name="name" required value="{{ category.name }}" />
|
||
</div>
|
||
<div class="field">
|
||
<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>
|
||
<button type="button" class="btn btn-secondary" @click="editing = false">취소</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="card" x-data="{ open: false }">
|
||
<button type="button" class="btn btn-secondary btn-block" @click="open = !open">
|
||
<span x-show="!open">+ 카테고리 추가</span>
|
||
<span x-show="open" x-cloak>닫기</span>
|
||
</button>
|
||
<form x-show="open" x-cloak style="margin-top: var(--space-2);" hx-post="/journal/categories/new" hx-target="#journal-category-error" hx-swap="innerHTML">
|
||
<div class="field">
|
||
<label for="new-category-name">카테고리 이름</label>
|
||
<input type="text" id="new-category-name" name="name" required placeholder="예: 투자" />
|
||
</div>
|
||
<div class="field">
|
||
<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>
|
||
</div>
|
||
|
||
{% else %}
|
||
|
||
{% if on_this_day %}
|
||
<div class="card on-this-day-card">
|
||
<h3 style="margin-top:0;">📅 {{ on_this_day|length }}개의 지난 오늘</h3>
|
||
{% for item in on_this_day %}
|
||
<div class="day-detail-item">
|
||
<span>
|
||
<span class="badge" style="border-color: {{ item.category_color or 'var(--color-accent)' }};">{{ item.category_name }}</span>
|
||
{{ item.entry_date.year }}년 · {{ item.years_ago }}년 전
|
||
</span>
|
||
<span>{{ item.title or (item.content[:30] ~ ('…' if item.content|length > 30 else '')) }}</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div
|
||
class="card"
|
||
x-data="{
|
||
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: '',
|
||
previewMode: false,
|
||
togglePreview() {
|
||
this.previewMode = !this.previewMode;
|
||
if (this.previewMode) { this.$refs.contentField.dispatchEvent(new Event('input')); }
|
||
},
|
||
onCategoryChange(categoryId) {
|
||
const tmpl = this.categoryTemplates[categoryId] || '';
|
||
if (this.content === this.lastAutoFilled) {
|
||
this.content = tmpl;
|
||
this.lastAutoFilled = tmpl;
|
||
this.$nextTick(() => { JournalEditor.setValue(this.$refs.contentField, tmpl); });
|
||
}
|
||
},
|
||
init() { this.onCategoryChange(this.$refs.categorySelect.value); },
|
||
}"
|
||
>
|
||
<button type="button" class="btn btn-primary btn-block" @click="open = !open">
|
||
<span x-show="!open">+ 새 기록 쓰기</span>
|
||
<span x-show="open" x-cloak>닫기</span>
|
||
</button>
|
||
|
||
{% if prompt %}
|
||
<div class="journal-prompt-card">💭 {{ prompt.question_text }}</div>
|
||
{% endif %}
|
||
|
||
<form
|
||
x-show="open"
|
||
x-cloak
|
||
style="margin-top: var(--space-2);"
|
||
hx-post="/journal/new"
|
||
hx-target="#journal-form-error"
|
||
hx-swap="innerHTML"
|
||
hx-encoding="multipart/form-data"
|
||
>
|
||
<div class="field">
|
||
<label for="new-entry-category">카테고리</label>
|
||
<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 %}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label for="new-entry-date">날짜</label>
|
||
<input type="date" id="new-entry-date" name="entry_date" value="{{ today_iso }}" required />
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label for="new-entry-title">제목 (선택)</label>
|
||
<input type="text" id="new-entry-title" name="title" placeholder="제목을 입력하세요" />
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label for="new-entry-content">내용</label>
|
||
{% include "partials/journal_editor_toolbar.html" %}
|
||
<div x-show="!previewMode">
|
||
<textarea
|
||
id="new-entry-content"
|
||
name="content"
|
||
rows="8"
|
||
required
|
||
placeholder="오늘 하루는 어땠나요?"
|
||
class="markdown-editor"
|
||
x-model="content"
|
||
x-ref="contentField"
|
||
hx-post="/journal/preview"
|
||
hx-trigger="input changed delay:400ms, load"
|
||
hx-target="#new-entry-preview"
|
||
hx-swap="innerHTML"
|
||
hx-params="content"
|
||
></textarea>
|
||
</div>
|
||
<div id="new-entry-preview" 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 for="new-entry-tags">태그 (선택, 쉼표로 구분)</label>
|
||
<input type="text" id="new-entry-tags" name="tags" placeholder="예: 투자, 회고" />
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label for="new-entry-files">사진/영상 첨부 (선택)</label>
|
||
<input type="file" id="new-entry-files" name="files" accept="image/*,video/*" multiple />
|
||
</div>
|
||
|
||
<div id="journal-form-error" class="form-error-text"></div>
|
||
|
||
<button type="submit" class="btn btn-primary btn-block">기록 저장</button>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom: var(--space-2);">
|
||
<a href="/journal?year={{ prev_year }}&month={{ prev_month }}{{ '&category_id=' ~ category_id if category_id }}" class="btn btn-secondary">‹</a>
|
||
<h2 style="margin:0;">{{ year }}년 {{ month }}월</h2>
|
||
<a href="/journal?year={{ next_year }}&month={{ next_month }}{{ '&category_id=' ~ category_id if category_id }}" class="btn btn-secondary">›</a>
|
||
</div>
|
||
|
||
<div class="calendar-grid calendar-grid-header">
|
||
{% for wd in ["일", "월", "화", "수", "목", "금", "토"] %}
|
||
<div class="calendar-weekday">{{ wd }}</div>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
{% for week in weeks %}
|
||
<div class="calendar-grid">
|
||
{% for d in week %}
|
||
{% set day_summary = summary_map.get(d) %}
|
||
<div
|
||
class="calendar-cell calendar-cell-journal clickable{{ '' if d.month == month else ' muted' }}"
|
||
hx-get="/journal/day/{{ d.isoformat() }}"
|
||
hx-target="#day-detail"
|
||
hx-swap="innerHTML"
|
||
>
|
||
<span class="calendar-date">{{ d.day }}</span>
|
||
{% if day_summary %}
|
||
<span class="journal-day-entries">
|
||
{% for entry in day_summary.entries %}
|
||
<span class="journal-day-entry">
|
||
<span class="journal-day-dot" style="background: {{ entry.color }};"></span>
|
||
<span class="journal-day-title">{{ entry.title }}</span>
|
||
</span>
|
||
{% endfor %}
|
||
</span>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<div id="day-detail"></div>
|
||
{% endif %}
|
||
{% endblock %}
|