diff --git a/app/routers/pages.py b/app/routers/pages.py index bab696d..b8d45f3 100644 --- a/app/routers/pages.py +++ b/app/routers/pages.py @@ -367,6 +367,20 @@ def _week_context(db: Session, user_id: int, week_start: date) -> dict: } +@router.get("/history/day/{log_date}") +def history_day_detail(request: Request, log_date: date, db: Session = Depends(get_db)): + current = _current_user_or_redirect(request, db) + if isinstance(current, RedirectResponse): + return current + + items = log_service.get_day_detail(db, current.id, log_date) + return templates.TemplateResponse( + request, + "partials/day_detail.html", + {"log_date": log_date, "items": items}, + ) + + @router.get("/history") def history_page( request: Request, diff --git a/app/schemas/habit_log.py b/app/schemas/habit_log.py index 0c3501e..19a323b 100644 --- a/app/schemas/habit_log.py +++ b/app/schemas/habit_log.py @@ -41,6 +41,14 @@ class WeeklyMatrixRow(BaseModel): completion_rate: float # 이번 주, 오늘까지 지난 예정일 중 체크한 비율 (%) +class DayDetailItem(BaseModel): + habit_id: int + name: str + habit_type: str + condition_text: str | None + status: str # "checked" | "failed" | "missed"(과거, 미결정) | "pending"(오늘/미래, 미결정) + + class HabitStats(BaseModel): completion_rate: float # 습관 생성일부터 오늘까지, 예정된 날 중 체크한 비율 (%) current_streak: int # 오늘(또는 어제)부터 거슬러 올라가며 끊기지 않고 체크한 예정일 수 diff --git a/app/services/log_service.py b/app/services/log_service.py index 7ef449f..9737b1f 100644 --- a/app/services/log_service.py +++ b/app/services/log_service.py @@ -6,7 +6,7 @@ from sqlalchemy.orm import Session from app.models.habit import Habit, HabitStatus, HabitType from app.models.habit_log import HabitLog, HabitLogStatus -from app.schemas.habit_log import HabitStats, MonthlySummaryDay, TodayItem, WeeklyMatrixRow +from app.schemas.habit_log import DayDetailItem, HabitStats, MonthlySummaryDay, TodayItem, WeeklyMatrixRow from app.schemas.level import LevelInfo from app.services import habit_service, level_service, push_service @@ -182,6 +182,51 @@ def get_monthly_summary(db: Session, user_id: int, year: int, month: int) -> lis return summaries +def get_day_detail(db: Session, user_id: int, target_date: date) -> list[DayDetailItem]: + """월별 캘린더에서 특정 날짜를 선택했을 때, 그 날 예정되어 있던 습관별 체크/실패/미결정 상태를 반환한다. + + get_monthly_summary와 같은 규칙(현재 active 습관 기준, 습관 생성일 이전 제외, 과거 요일 변경 + 이력 미추적)을 그대로 따른다. + """ + active_habits = habit_service.list_habits(db, user_id, status=HabitStatus.ACTIVE) + scheduled = [ + h for h in active_habits if h.created_at.date() <= target_date and h.is_scheduled_on(target_date.weekday()) + ] + + habit_ids = [h.id for h in scheduled] + logs_by_habit: dict[int, HabitLogStatus] = {} + if habit_ids: + rows = db.execute( + select(HabitLog.habit_id, HabitLog.status).where( + HabitLog.habit_id.in_(habit_ids), HabitLog.log_date == target_date + ) + ).all() + logs_by_habit = {r[0]: r[1] for r in rows} + + today = date.today() + items = [] + for h in scheduled: + log_status = logs_by_habit.get(h.id) + if log_status == HabitLogStatus.DONE: + status = "checked" + elif log_status == HabitLogStatus.FAILED: + status = "failed" + elif target_date < today: + status = "missed" + else: + status = "pending" + items.append( + DayDetailItem( + habit_id=h.id, + name=h.name, + habit_type=h.habit_type.value, + condition_text=h.condition_text, + status=status, + ) + ) + return items + + def summarize_completion_rate(summaries: list[MonthlySummaryDay], up_to: date) -> float: """월별 요약에서 up_to(보통 오늘)까지 지난 날짜만 모아 전체 완료율(%)을 계산한다. diff --git a/app/static/css/style.css b/app/static/css/style.css index f207825..4cf30f7 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -729,6 +729,42 @@ label { color: var(--color-text-muted); } +.calendar-cell.clickable { + cursor: pointer; +} + +.day-detail-card { + margin-top: var(--space-2); +} + +.day-detail-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.day-detail-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid var(--color-border); +} + +.day-detail-item:last-child { + border-bottom: none; +} + +.day-detail-status-checked { + color: var(--color-success); + border-color: var(--color-success); +} + +.day-detail-status-missed { + color: var(--color-danger); + border-color: var(--color-danger); +} + /* 주별 매트릭스 */ .week-matrix { width: 100%; diff --git a/app/templates/history.html b/app/templates/history.html index 21bfc59..48e19a5 100644 --- a/app/templates/history.html +++ b/app/templates/history.html @@ -32,8 +32,11 @@ {% for d in week %} {% set summary = summary_map.get(d) %}