Adds an 8th development stage that lets users keep a free-form journal alongside habit tracking, reusing the existing Google OAuth/DB/PWA infrastructure instead of a separate project. Users organize entries into custom categories, filter by a month calendar with day-detail drill-down, attach photos/videos (served via an authenticated route, never /static), tag entries, and pick multiple emotions per entry from a curated 9-option set. Includes a global journal prompt bank for lightweight guided journaling. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
136 lines
5.7 KiB
HTML
136 lines
5.7 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) },
|
|
}"
|
|
>
|
|
<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 }}</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>
|
|
<textarea name="content" rows="4" required>{{ item.content }}</textarea>
|
|
</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>
|