add habit goal-period difficulty and habit formation research notes

Ground the 21-day habit myth in actual research (Lally et al. 2010) and let
users pick a target period (21/66/254 days or unlimited) matching that
study's easy/median/hard automaticity timelines when creating a habit,
with progress shown on the habits list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 10:12:17 +09:00
co-authored by Claude Sonnet 5
parent 55918d56d1
commit d212451fe0
15 changed files with 355 additions and 9 deletions
+54 -2
View File
@@ -1,5 +1,17 @@
from app.models.habit import ALL_WEEKDAYS_MASK
from app.template_utils import heatmap_opacity, weekday_label
from datetime import datetime, timedelta
from app.models.habit import ALL_WEEKDAYS_MASK, Habit, HabitDifficulty, HabitType
from app.template_utils import difficulty_label, goal_progress, heatmap_opacity, weekday_label
def _habit_created_days_ago(days_ago: int, difficulty: HabitDifficulty) -> Habit:
return Habit(
name="테스트",
habit_type=HabitType.BUILD,
difficulty=difficulty,
weekdays_mask=ALL_WEEKDAYS_MASK,
created_at=datetime.now() - timedelta(days=days_ago),
)
def test_weekday_label_all_days_shows_daily():
@@ -29,3 +41,43 @@ def test_heatmap_opacity_full_ratio_caps_at_point_nine():
def test_heatmap_opacity_partial_ratio():
assert heatmap_opacity(checked_count=1, scheduled_count=2) == round(0.12 + 0.5 * 0.78, 2)
def test_difficulty_label_maps_to_target_days():
assert difficulty_label(HabitDifficulty.EASY) == "하 (21일)"
assert difficulty_label(HabitDifficulty.MEDIUM) == "중 (66일)"
assert difficulty_label(HabitDifficulty.HARD) == "상 (254일)"
assert difficulty_label(HabitDifficulty.UNLIMITED) == "기간 무제한"
def test_goal_progress_none_for_unlimited():
habit = _habit_created_days_ago(10, HabitDifficulty.UNLIMITED)
assert goal_progress(habit) is None
def test_goal_progress_day_one_on_creation_day():
habit = _habit_created_days_ago(0, HabitDifficulty.EASY)
progress = goal_progress(habit)
assert progress == {"elapsed": 1, "target": 21, "remaining": 20, "reached": False}
def test_goal_progress_mid_period():
habit = _habit_created_days_ago(10, HabitDifficulty.MEDIUM)
progress = goal_progress(habit)
assert progress == {"elapsed": 11, "target": 66, "remaining": 55, "reached": False}
def test_goal_progress_reached_caps_elapsed_at_target():
habit = _habit_created_days_ago(100, HabitDifficulty.EASY)
progress = goal_progress(habit)
assert progress == {"elapsed": 21, "target": 21, "remaining": 0, "reached": True}
def test_target_days_and_goal_target_date_on_model():
habit = _habit_created_days_ago(0, HabitDifficulty.EASY)
assert habit.target_days == 21
assert habit.goal_target_date == habit.created_at.date() + timedelta(days=20)
unlimited = _habit_created_days_ago(0, HabitDifficulty.UNLIMITED)
assert unlimited.target_days is None
assert unlimited.goal_target_date is None