- calendar-cell-journal: bump min-height 44px -> 76px, scale up date/dot/title font sizes - journal calendar titles were centered; left-align them - rework JournalCalendarDay: replace separate category_colors/titles lists (mismatched lengths, no way to pair a dot with its title) with a single entries list of (color, title) pairs so the dot and its title render on the same non-wrapping row per entry Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
280 lines
11 KiB
HTML
280 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(() => {
|
||
if (this.$refs.contentField._easymde) { this.$refs.contentField._easymde.value(tmpl); }
|
||
this.$refs.contentField.dispatchEvent(new Event('input'));
|
||
});
|
||
}
|
||
},
|
||
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 %}
|