add habit leveling/XP system, explicit fail marking for missed habits, and scrollable habit tabs

- Habits and the account now accumulate XP and levels from check streaks, with
  streak/level-up celebration banners and push notifications.
- The "yesterday missed" banner on /today now lets a habit be explicitly marked
  failed (not just checked done), backed by a new HabitLog.status column so
  completion-rate/streak calculations never count a failed day as done.
- The habit management tabs scroll horizontally on narrow screens instead of
  wrapping to two lines, with a pure-CSS edge shadow indicating more content.
This commit is contained in:
2026-07-20 18:18:56 +09:00
parent 4a7ef5cc29
commit 800f42eab1
18 changed files with 665 additions and 42 deletions
+5
View File
@@ -35,6 +35,11 @@
{{ '🏆' if is_milestone_streak(stats.current_streak) else '🔥' }} 연속 {{ stats.current_streak }}일
</span>
{% endif %}
<span class="badge level-badge">{{ level_tier_emoji(stats.level_info.level) }} Lv.{{ stats.level_info.level }}</span>
</div>
<div class="xp-bar-wrap">
<div class="xp-bar"><div class="xp-bar-fill" style="width: {{ stats.level_info.progress_pct }}%;"></div></div>
<span class="xp-bar-label">{{ stats.level_info.xp_into_level }}/{{ stats.level_info.xp_for_next_level }} xp</span>
</div>
{% endif %}
</div>
+13
View File
@@ -3,6 +3,19 @@
🎉 <strong>{{ celebrate_habit_name }}</strong> {{ celebrate_streak }}일 연속 달성! 축하해요!
</div>
{% endif %}
{% if celebrate_level_up %}
<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_level_up }}로 올랐어요!
</div>
{% endif %}
<div class="card account-level-card">
<span class="badge level-badge">{{ level_tier_emoji(account_level.level) }} 계정 Lv.{{ account_level.level }}</span>
<div class="xp-bar-wrap">
<div class="xp-bar"><div class="xp-bar-fill" style="width: {{ account_level.progress_pct }}%;"></div></div>
<span class="xp-bar-label">{{ account_level.xp_into_level }}/{{ account_level.xp_for_next_level }} xp</span>
</div>
</div>
<div
class="card"
+5
View File
@@ -20,6 +20,11 @@
{{ '🏆' if is_milestone_streak(item.current_streak) else '🔥' }} 연속 {{ item.current_streak }}일
</span>
{% endif %}
<span class="badge level-badge">{{ level_tier_emoji(item.level_info.level) }} Lv.{{ item.level_info.level }}</span>
</div>
<div class="xp-bar-wrap">
<div class="xp-bar"><div class="xp-bar-fill" style="width: {{ item.level_info.progress_pct }}%;"></div></div>
<span class="xp-bar-label">{{ item.level_info.xp_into_level }}/{{ item.level_info.xp_for_next_level }} xp</span>
</div>
{% endif %}
</div>
@@ -13,4 +13,14 @@
{% if item.condition_text %}<div class="habit-item-condition">{{ item.condition_text }}</div>{% endif %}
</div>
</div>
<div class="habit-item-actions">
<button
type="button"
class="btn btn-danger-ghost"
hx-post="/today/{{ item.habit_id }}/fail-yesterday"
hx-target="#today-content"
hx-swap="innerHTML"
aria-label="{{ item.name }} 어제 실패로 표시"
>실패로 표시</button>
</div>
</div>