FastAPI + SQLAlchemy/Alembic + MariaDB backend with Jinja2/htmx/Alpine server-rendered frontend. Multi-user via Google OAuth, daily habit tracking, monthly/weekly history views, Web Push reminders via APScheduler, and PWA support (manifest, service worker, offline caching). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
93 lines
3.3 KiB
HTML
93 lines
3.3 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}기록 · 습관 트래커{% endblock %}
|
||
{% block nav_history %}active{% endblock %}
|
||
{% block shell_class %} wide{% endblock %}
|
||
{% block content %}
|
||
<h1>기록</h1>
|
||
|
||
<div class="tabs">
|
||
<a href="/history?view=month" class="{{ 'active' if view == 'month' }}">월별</a>
|
||
<a href="/history?view=week" class="{{ 'active' if view == 'week' }}">주별</a>
|
||
</div>
|
||
|
||
{% if view == "month" %}
|
||
<div class="card">
|
||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom: var(--space-2);">
|
||
<a href="/history?view=month&year={{ prev_year }}&month={{ prev_month }}" class="btn btn-secondary">‹</a>
|
||
<h2 style="margin:0;">{{ year }}년 {{ month }}월</h2>
|
||
<a href="/history?view=month&year={{ next_year }}&month={{ next_month }}" class="btn btn-secondary">›</a>
|
||
</div>
|
||
<div style="text-align:center; margin-bottom: var(--space-2);">
|
||
<span class="badge">이번 달 완료율 {{ completion_rate }}%</span>
|
||
</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 summary = summary_map.get(d) %}
|
||
<div
|
||
class="calendar-cell{{ '' if d.month == month else ' muted' }}"
|
||
style="background: rgba(var(--color-accent-rgb), {{ heatmap_opacity(summary.checked_count, summary.scheduled_count) if summary else 0 }});"
|
||
>
|
||
<span class="calendar-date">{{ d.day }}</span>
|
||
{% if summary and summary.scheduled_count %}
|
||
<span class="calendar-ratio">{{ summary.checked_count }}/{{ summary.scheduled_count }}</span>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
{% else %}
|
||
<div class="card" style="overflow-x:auto;">
|
||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom: var(--space-2);">
|
||
<a href="/history?view=week&start={{ prev_week }}" class="btn btn-secondary">‹</a>
|
||
<h2 style="margin:0;">{{ week_start.strftime("%Y.%m.%d") }} - {{ week_end.strftime("%m.%d") }}</h2>
|
||
<a href="/history?view=week&start={{ next_week }}" class="btn btn-secondary">›</a>
|
||
</div>
|
||
|
||
{% if rows %}
|
||
<table class="week-matrix">
|
||
<thead>
|
||
<tr>
|
||
<th>습관</th>
|
||
{% for wd in ["일", "월", "화", "수", "목", "금", "토"] %}
|
||
<th>{{ wd }}</th>
|
||
{% endfor %}
|
||
<th>완료율</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for row in rows %}
|
||
<tr>
|
||
<td>
|
||
{{ row.name }}
|
||
<span class="badge">{{ "형성" if row.habit_type == "build" else "중단" }}</span>
|
||
</td>
|
||
{% for iso_date, checked in row.checks.items() %}
|
||
<td class="week-matrix-cell">
|
||
{% if checked is none %}<span class="wm-dash">–</span>
|
||
{% elif checked %}<span class="wm-check">✓</span>
|
||
{% else %}<span class="wm-empty"></span>
|
||
{% endif %}
|
||
</td>
|
||
{% endfor %}
|
||
<td class="week-matrix-cell">{{ row.completion_rate }}%</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<div class="empty-state">진행 중인 습관이 없어요.</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
{% endblock %}
|