Initial commit: habit tracker PWA with Google OAuth, push notifications
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>
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}페이지를 찾을 수 없어요 · 습관 트래커{% endblock %}
|
||||
{% block content %}
|
||||
<div style="min-height: 60vh; display:flex; align-items:center; justify-content:center; text-align:center;">
|
||||
<div>
|
||||
<h1>페이지를 찾을 수 없어요</h1>
|
||||
<p>주소가 잘못되었거나 삭제된 페이지예요.</p>
|
||||
<a href="/today" class="btn btn-primary">오늘 화면으로</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}오류가 발생했어요 · 습관 트래커{% endblock %}
|
||||
{% block content %}
|
||||
<div style="min-height: 60vh; display:flex; align-items:center; justify-content:center; text-align:center;">
|
||||
<div>
|
||||
<h1>문제가 발생했어요</h1>
|
||||
<p>일시적인 오류일 수 있어요. 잠시 후 다시 시도해주세요.</p>
|
||||
<a href="/today" class="btn btn-primary">오늘 화면으로</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,65 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>{% block title %}습관 트래커{% endblock %}</title>
|
||||
|
||||
<link rel="manifest" href="/static/manifest.json" />
|
||||
<meta name="theme-color" content="#d97757" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="습관 트래커" />
|
||||
<link rel="apple-touch-icon" href="/static/icons/icon-192.png" />
|
||||
<link rel="icon" href="/static/icons/icon-192.png" />
|
||||
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
<script src="/static/js/vendor/htmx.min.js" defer></script>
|
||||
<script src="/static/js/push-register.js" defer></script>
|
||||
<script src="/static/js/vendor/sortable.min.js" defer></script>
|
||||
<script src="/static/js/habit-reorder.js" defer></script>
|
||||
<script src="/static/js/vendor/alpine.min.js" defer></script>
|
||||
<script src="/static/js/app.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell{% block shell_class %}{% endblock %}">
|
||||
{% if logged_in %}
|
||||
<div id="ios-install-banner" class="ios-install-banner">
|
||||
<span>홈 화면에 추가하면 알림도 받을 수 있어요. 공유 버튼 → ‘홈 화면에 추가’를 눌러보세요.</span>
|
||||
<button type="button" onclick="dismissIosInstallBanner()" aria-label="닫기">×</button>
|
||||
</div>
|
||||
<nav class="top-nav">
|
||||
<span class="brand">습관 트래커</span>
|
||||
<div class="nav-links">
|
||||
<a href="/today" class="{% block nav_today %}{% endblock %}">오늘</a>
|
||||
<a href="/habits" class="{% block nav_habits %}{% endblock %}">습관 관리</a>
|
||||
<a href="/history" class="{% block nav_history %}{% endblock %}">기록</a>
|
||||
</div>
|
||||
{% if current_user %}
|
||||
<div class="nav-user">
|
||||
<span>{{ current_user.name or current_user.email }}</span>
|
||||
<form method="post" action="/auth/logout">
|
||||
<button type="submit" class="btn-link">로그아웃</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<nav class="bottom-tab-bar">
|
||||
<a href="/today" class="tab-item {{ self.nav_today() }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9" /><path d="M8 12l3 3 5-6" /></svg>
|
||||
<span>오늘</span>
|
||||
</a>
|
||||
<a href="/habits" class="tab-item {{ self.nav_habits() }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="6" x2="20" y2="6" /><line x1="4" y1="12" x2="20" y2="12" /><line x1="4" y1="18" x2="20" y2="18" /></svg>
|
||||
<span>습관 관리</span>
|
||||
</a>
|
||||
<a href="/history" class="tab-item {{ self.nav_history() }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="16" rx="2" /><line x1="3" y1="10" x2="21" y2="10" /><line x1="8" y1="3" x2="8" y2="7" /><line x1="16" y1="3" x2="16" y2="7" /></svg>
|
||||
<span>기록</span>
|
||||
</a>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}습관 관리 · 습관 트래커{% endblock %}
|
||||
{% block nav_habits %}active{% endblock %}
|
||||
{% block content %}
|
||||
<h1>습관 관리</h1>
|
||||
|
||||
<div class="tabs">
|
||||
<a href="/habits?tab=build" class="{{ 'active' if tab == 'build' }}">만들고 싶은 습관</a>
|
||||
<a href="/habits?tab=quit" class="{{ 'active' if tab == 'quit' }}">멈추고 싶은 습관</a>
|
||||
<a href="/habits?tab=completed" class="{{ 'active' if tab == 'completed' }}">완료된 습관</a>
|
||||
</div>
|
||||
|
||||
{% if tab in ("build", "quit") %}
|
||||
{% include "partials/habit_form.html" %}
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
{% if habits %}
|
||||
<div id="habit-list">
|
||||
{% for habit in habits %}
|
||||
{% include "partials/habit_item.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
{% if tab == "build" %}만들고 싶은 습관이 아직 없어요. 위에서 추가해보세요.
|
||||
{% elif tab == "quit" %}끊고 싶은 습관이 아직 없어요. 위에서 추가해보세요.
|
||||
{% else %}아직 완료된 습관이 없어요.{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,92 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}로그인 · 습관 트래커{% endblock %}
|
||||
{% block content %}
|
||||
<div style="min-height: 70vh; display: flex; align-items: center; justify-content: center;">
|
||||
<div class="card" style="width: 100%; max-width: 320px;">
|
||||
<h1 style="text-align:center;">습관 트래커</h1>
|
||||
<p style="text-align:center;">구글 계정으로 로그인하세요</p>
|
||||
<a href="/auth/google/login" class="btn btn-primary btn-block">Google로 로그인</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="field">
|
||||
<label>시행 요일</label>
|
||||
<div class="weekday-picker">
|
||||
<template x-for="(day, idx) in days" :key="idx">
|
||||
<button
|
||||
type="button"
|
||||
class="weekday-pill"
|
||||
:class="{ selected: (mask & (1 << idx)) !== 0 }"
|
||||
@click="if (mask !== (1 << idx)) mask = mask ^ (1 << idx)"
|
||||
x-text="day"
|
||||
></button>
|
||||
</template>
|
||||
<button
|
||||
type="button"
|
||||
class="everyday-btn"
|
||||
:class="{ selected: mask === 127 }"
|
||||
@click="mask = 127"
|
||||
>매일</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label style="display:flex; align-items:center; gap:8px;">
|
||||
<input type="checkbox" x-model="alarmOn" style="width:auto;" />
|
||||
알람 사용
|
||||
</label>
|
||||
<input x-show="alarmOn" x-cloak type="time" name="reminder_time" value="{{ reminder_value|default('') }}" style="margin-top:6px;" />
|
||||
</div>
|
||||
@@ -0,0 +1,42 @@
|
||||
<div
|
||||
class="card"
|
||||
x-data="{
|
||||
open: false,
|
||||
mask: 127,
|
||||
alarmOn: false,
|
||||
days: ['월', '화', '수', '목', '금', '토', '일'],
|
||||
}"
|
||||
>
|
||||
<button type="button" class="btn btn-secondary btn-block" @click="open = !open">
|
||||
<span x-show="!open">{{ '+ 멈추고 싶은 습관 추가' if habit_type == 'quit' else '+ 만들고 싶은 습관 추가' }}</span>
|
||||
<span x-show="open" x-cloak>닫기</span>
|
||||
</button>
|
||||
|
||||
<form
|
||||
x-show="open"
|
||||
x-cloak
|
||||
style="margin-top: var(--space-2);"
|
||||
hx-post="/habits/new"
|
||||
hx-target="#habit-form-error-{{ habit_type }}"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<input type="hidden" name="habit_type" value="{{ habit_type }}" />
|
||||
<input type="hidden" name="weekdays_mask" :value="mask" />
|
||||
|
||||
<div class="field">
|
||||
<label for="name-{{ habit_type }}">습관 이름</label>
|
||||
<input type="text" id="name-{{ habit_type }}" name="name" required placeholder="{{ '예: 물 2L 마시기' if habit_type == 'build' else '예: 야식 끊기' }}" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="condition-{{ habit_type }}">달성 조건 (선택)</label>
|
||||
<input type="text" id="condition-{{ habit_type }}" name="condition_text" placeholder="{{ '예: 하루 8잔 이상' if habit_type == 'build' else '예: 주 3회 이하로 줄이기' }}" />
|
||||
</div>
|
||||
|
||||
{% include "partials/_weekday_alarm_fields.html" %}
|
||||
|
||||
<div id="habit-form-error-{{ habit_type }}" class="form-error-text"></div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block">추가하기</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,94 @@
|
||||
<div
|
||||
data-habit-id="{{ habit.id }}"
|
||||
x-data="{
|
||||
editing: false,
|
||||
mask: {{ habit.weekdays_mask }},
|
||||
alarmOn: {{ 'true' if habit.reminder_time else 'false' }},
|
||||
days: ['월', '화', '수', '목', '금', '토', '일'],
|
||||
}"
|
||||
>
|
||||
<div class="habit-item" x-show="!editing">
|
||||
<div class="habit-item-main">
|
||||
<span class="drag-handle" aria-hidden="true">⠿</span>
|
||||
<div>
|
||||
<div class="habit-item-name">{{ habit.name }}</div>
|
||||
{% if habit.condition_text %}<div class="habit-item-condition">{{ habit.condition_text }}</div>{% endif %}
|
||||
<div class="habit-item-meta">
|
||||
{{ weekday_label(habit.weekdays_mask) }}
|
||||
{% if habit.reminder_time %}· 알람 {{ habit.reminder_time.strftime("%H:%M") }}{% endif %}
|
||||
</div>
|
||||
{% set stats = stats_map[habit.id] %}
|
||||
{% if stats.scheduled_days > 0 %}
|
||||
<div class="habit-item-stats">
|
||||
<span class="badge">완료율 {{ stats.completion_rate }}%</span>
|
||||
{% if stats.current_streak > 0 %}
|
||||
<span class="badge{{ ' badge-milestone' if is_milestone_streak(stats.current_streak) }}">
|
||||
{{ '🏆' if is_milestone_streak(stats.current_streak) else '🔥' }} 연속 {{ stats.current_streak }}일
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="habit-item-actions">
|
||||
<button type="button" class="btn btn-secondary" @click="editing = true">수정</button>
|
||||
{% if habit.status.value == "active" %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
hx-post="/habits/{{ habit.id }}/complete"
|
||||
hx-target="body"
|
||||
hx-swap="none"
|
||||
hx-confirm="'{{ habit.name }}' 습관을 완료 처리할까요?"
|
||||
>완료 처리</button>
|
||||
{% else %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
hx-post="/habits/{{ habit.id }}/reactivate"
|
||||
hx-target="body"
|
||||
hx-swap="none"
|
||||
>다시 진행</button>
|
||||
{% endif %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-danger-ghost"
|
||||
hx-delete="/habits/{{ habit.id }}/delete"
|
||||
hx-target="body"
|
||||
hx-swap="none"
|
||||
hx-confirm="'{{ habit.name }}' 습관을 삭제할까요? 기록도 함께 삭제됩니다."
|
||||
>삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form
|
||||
x-show="editing"
|
||||
x-cloak
|
||||
style="padding: 12px 4px; border-bottom: 1px solid var(--color-border);"
|
||||
hx-post="/habits/{{ habit.id }}/edit"
|
||||
hx-target="#habit-edit-error-{{ habit.id }}"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<input type="hidden" name="weekdays_mask" :value="mask" />
|
||||
|
||||
<div class="field">
|
||||
<label for="edit-name-{{ habit.id }}">습관 이름</label>
|
||||
<input type="text" id="edit-name-{{ habit.id }}" name="name" required value="{{ habit.name }}" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="edit-condition-{{ habit.id }}">달성 조건 (선택)</label>
|
||||
<input type="text" id="edit-condition-{{ habit.id }}" name="condition_text" value="{{ habit.condition_text or '' }}" />
|
||||
</div>
|
||||
|
||||
{% set reminder_value = habit.reminder_time.strftime("%H:%M") if habit.reminder_time else "" %}
|
||||
{% include "partials/_weekday_alarm_fields.html" %}
|
||||
|
||||
<div id="habit-edit-error-{{ habit.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>
|
||||
@@ -0,0 +1,43 @@
|
||||
{% if celebrate_streak %}
|
||||
<div class="celebration-banner" x-data="{ show: true }" x-init="setTimeout(() => show = false, 4000)" x-show="show" x-transition>
|
||||
🎉 <strong>{{ celebrate_habit_name }}</strong> {{ celebrate_streak }}일 연속 달성! 축하해요!
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div
|
||||
class="card"
|
||||
style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:8px;"
|
||||
x-data="{ pushState: 'checking' }"
|
||||
x-init="habitPush.getSubscriptionState().then(s => pushState = s)"
|
||||
>
|
||||
<h2 style="margin:0;">오늘</h2>
|
||||
<div style="display:flex; align-items:center; gap:8px;">
|
||||
<span class="badge">{{ checked_count }}/{{ total_count }} 완료</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
x-show="pushState === 'unsubscribed'"
|
||||
x-cloak
|
||||
@click="habitPush.subscribePush().then(ok => { if (ok) pushState = 'subscribed'; })"
|
||||
>알림 켜기</button>
|
||||
<span class="badge" x-show="pushState === 'subscribed'" x-cloak>🔔 알림 켜짐</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>형성 습관</h2>
|
||||
<div class="card">
|
||||
{% for item in build_items %}
|
||||
{% include "partials/today_item.html" %}
|
||||
{% else %}
|
||||
<div class="empty-state">오늘 예정된 형성 습관이 없어요.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h2>중단 습관</h2>
|
||||
<div class="card">
|
||||
{% for item in quit_items %}
|
||||
{% include "partials/today_item.html" %}
|
||||
{% else %}
|
||||
<div class="empty-state">오늘 예정된 중단 습관이 없어요.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
<div class="habit-item{{ ' today-checked' if item.checked }}">
|
||||
<div class="habit-item-main">
|
||||
<button
|
||||
type="button"
|
||||
class="check-indicator{{ ' checked' if item.checked }}"
|
||||
hx-post="/today/{{ item.habit_id }}/toggle"
|
||||
hx-target="#today-content"
|
||||
hx-swap="innerHTML"
|
||||
aria-label="{{ item.name }} 체크"
|
||||
>{% if item.checked %}✓{% endif %}</button>
|
||||
<div>
|
||||
<div class="habit-item-name">{{ item.name }}</div>
|
||||
{% if item.condition_text %}<div class="habit-item-condition">{{ item.condition_text }}</div>{% endif %}
|
||||
{% if item.reminder_time %}<div class="habit-item-meta">알람 {{ item.reminder_time }}</div>{% endif %}
|
||||
{% if item.scheduled_days > 0 %}
|
||||
<div class="habit-item-stats">
|
||||
<span class="badge">완료율 {{ item.completion_rate }}%</span>
|
||||
{% if item.current_streak > 0 %}
|
||||
<span class="badge{{ ' badge-milestone' if is_milestone_streak(item.current_streak) }}">
|
||||
{{ '🏆' if is_milestone_streak(item.current_streak) else '🔥' }} 연속 {{ item.current_streak }}일
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}오늘 · 습관 트래커{% endblock %}
|
||||
{% block nav_today %}active{% endblock %}
|
||||
{% block content %}
|
||||
<div id="today-content">
|
||||
{% include "partials/today_content.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user