Files
habit-tracker/app/template_utils.py
T
shinalok 800f42eab1 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.
2026-07-20 18:18:56 +09:00

64 lines
1.9 KiB
Python

from datetime import date
from app.models.habit import ALL_WEEKDAYS_MASK, HABIT_DIFFICULTY_TARGET_DAYS, Habit, HabitDifficulty
from app.services.log_service import MILESTONE_STREAKS
_DAY_LABELS = ["월", "화", "수", "목", "금", "토", "일"]
_DIFFICULTY_LABELS: dict[HabitDifficulty, str] = {
HabitDifficulty.EASY: "하 (21일)",
HabitDifficulty.MEDIUM: "중 (66일)",
HabitDifficulty.HARD: "상 (254일)",
HabitDifficulty.UNLIMITED: "기간 무제한",
}
def is_milestone_streak(streak: int) -> bool:
return streak in MILESTONE_STREAKS
def level_tier_emoji(level: int) -> str:
if level >= 20:
return "💎"
if level >= 10:
return "⭐"
if level >= 5:
return "🔥"
return "🌱"
def weekday_label(mask: int) -> str:
if mask == ALL_WEEKDAYS_MASK:
return "매일"
days = [label for i, label in enumerate(_DAY_LABELS) if mask & (1 << i)]
return ", ".join(days) if days else "선택된 요일 없음"
def difficulty_label(difficulty: HabitDifficulty) -> str:
return _DIFFICULTY_LABELS[difficulty]
def goal_progress(habit: Habit) -> dict | None:
"""습관의 목표 기간 진행 상황. 기간 무제한(UNLIMITED)이면 None."""
target_days = HABIT_DIFFICULTY_TARGET_DAYS[habit.difficulty]
if target_days is None:
return None
elapsed = (date.today() - habit.created_at.date()).days + 1 # 시작일을 1일차로 계산
elapsed = max(elapsed, 1)
return {
"elapsed": min(elapsed, target_days),
"target": target_days,
"remaining": max(target_days - elapsed, 0),
"reached": elapsed >= target_days,
}
def heatmap_opacity(checked_count: int, scheduled_count: int) -> float:
"""월별 캘린더 히트맵 셀의 배경 투명도(0~0.9)를 계산한다."""
if not scheduled_count:
return 0.0
ratio = checked_count / scheduled_count
if ratio <= 0:
return 0.0
return round(0.12 + ratio * 0.78, 2)