Files
habit-tracker/app/templates/journal.html
T
shinalokandClaude Sonnet 5 54c971875c add journaling feature: categories, tags, attachments, calendar, and multi-select emotions
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>
2026-08-04 18:40:07 +09:00

231 lines
8.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 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 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) },
}"
>
<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">
{% 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>
<textarea id="new-entry-content" name="content" rows="5" required placeholder="오늘 하루는 어땠나요?"></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 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 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-dots">
{% for color in day_summary.category_colors %}
<span class="journal-day-dot" style="background: {{ color }};"></span>
{% endfor %}
</span>
{% endif %}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<div id="day-detail"></div>
{% endif %}
{% endblock %}