Files
life-helper/docs/design/204-flutter-bootstrap/06-ux-contracts.md
joungmin b8e563176b [Architect] #204 design spec — Flutter bootstrap + 18 tables
Phase 1 설계서 작성 완료. docs/design/204-flutter-bootstrap/ 13 개 파일:
- README.md (12 섹션 모두 채움, 함수 19 개 명세, AC 16 항)
- 01-project-structure.md (feature-first + layer-first 하이브리드)
- 02-drift-schema-catalog.md (Catalog 7 테이블 Dart 정의)
- 03-drift-schema-user.md (User 11 테이블 + R1~R10 강제 매트릭스)
- 04-migrations.md (schemaVersion v1 + 인덱스 17 개)
- 05-seed-data.md (assets/seed/*.json + first-run import)
- 06-ux-contracts.md (체크인 R8 ≤ 60 초 흐름)
- fn-recommend-variant / fn-compute-streak / fn-weekly-minimum-ratio
- fn-validate-frame-level / fn-active-habit-quota / fn-seed-importer

핵심 결정: dose_variants 는 별도 habit_dose_variant 테이블로 정규화 (FK
무결성 + recommendVariant SQL 단순성). ADR-0002 승격 권장.

Refs #204

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-11 16:57:31 +09:00

6.8 KiB

06 — UX Contracts: 일일 체크인 · 추천 · 주간 reflection (#204)

부모 설계서: README.md 참조: R8 (≤ 60 초), R9 (variant 무제한), R10 (minimum_ratio hint), ADR-0001.

1. 일일 체크인 화면 (R8 ≤ 60 초 보장)

1.1 화면 계약

┌────────────────────────────────┐
│ 오늘  ·  hb_… 아침 햇빛 10분    │  ← AppBar (≤ 1 줄)
├────────────────────────────────┤
│                                │
│  장소                          │  ← chip row 1 (수평 스크롤)
│  [집] [짐] [출장] [외부] +     │
│                                │
│  컨디션                        │  ← chip row 2
│  [좋음] [보통] [나쁨]          │
│                                │
│  ─────────────────────────────│
│  추천 도즈                    │  ← variant card (자동 추천)
│  📍 짐-메인                   │
│  데드 리프트 60kg 5x5         │
│  [○ 완료]  [다른 옵션 ▾]      │
│                                │
└────────────────────────────────┘

탭 수:

  1. 장소 chip 1 탭
  2. 컨디션 chip 1 탭
  3. ○ 완료 1 탭 (또는 다른 옵션 → override 1 탭 → ○ 완료 1 탭)

→ 최소 3 탭, 최대 5 탭. R8 (≤ 60 초) 자연스럽게 달성.

1.2 R8 측정

CheckInTimer 가 화면 진입 시 시작, ○ 완료 탭 시 종료.

  • elapsed > 60s 면 dev console warning.
  • elapsed > 120s 면 사용자에게 toast "체크인이 오래 걸렸어요. UI 개선 피드백" (선택, A/B).

1.3 데이터 흐름

화면 진입
  └─ CheckInTimer.start()
  └─ DAO.loadVariantsForHabit(habitId) → List<HabitDoseVariant>

장소 chip 선택
  └─ controller.setLocation(s)
  └─ recommendVariant(habit, ctx(loc, null)) → 부분 추천 (location 만)

컨디션 chip 선택
  └─ controller.setCondition(s)
  └─ recommendVariant(habit, ctx(loc, cond)) → 최종 추천 + score

[다른 옵션] 탭
  └─ 모달 시트: 모든 variants (score 내림차순) + 사용자 선택

[○ 완료]
  └─ TrackerDao.recordCheckIn(
       habitId, today, 'done', variantId, ctx(loc, cond))
  └─ computeStreak(habitId, today) → StreakState
  └─ UI: 오늘 셀 ○ + tier badge 갱신
  └─ if T1~T4 진입 → Celebration 모달
  └─ CheckInTimer.stop()

1.4 blank 처리

  • "○ 완료" 안 누르고 화면 종료 → tracker_entry insert 안 됨. blank 는 자동 — 명시적 insert 불요. (computeStreak 은 entry 부재 = blank 로 해석.)

2. 추천 매칭 알고리즘 (의사코드)

상세는 fn-recommend-variant.md.

function recommendVariant(habit, ctx) -> {variant, score, reason}
  variants = habit.dose_variants
  if variants.isEmpty: return null

  scored = []
  for v in variants:
    s = scoreVariant(v, ctx)
    scored.push({v, s})

  scored.sortByDesc(s.score, breakTies = v.sortOrder)

  best = scored[0]
  if best.score > 0:
    return best
  else:
    # fallback: is_minimum=true 첫 variant
    minimum = variants.firstWhere(v -> v.is_minimum, orElse: variants.first)
    return {minimum, score: 0, reason: 'fallback_minimum'}

function scoreVariant(v, ctx) -> int
  score = 0
  if ctx.location in v.context_tags: score += 2
  if ctx.condition in v.condition_tags: score += 2
  if v.is_minimum and ctx.condition == 'bad': score += 1
  return score

복잡도: O(N) per call, N = variants 개수. R9 무제한이지만 실용 N ≤ 약 10 → < 50 µs.

3. 사용자 override 흐름

[다른 옵션 ▾] 탭
  ↓
모달 sheet (전체 variants 표시, score 표시)
  ↓
사용자 선택 → controller.overrideVariant(variantId)
  ↓
변경 후에도 ○ 완료 가능. tracker_entry.variant_id = overridden.

override 시에도 score 0 의 fallback variant 도 선택 가능 — 사용자 autonomy (SDT).

4. habit 생성 폼 (vertical slice — 최소)

[1] protocol 선택 (목록 — category 탭)
       ↓
[2] frame.framed_text 입력 (L2 default, L3 toggle)
       ↓ validateFrameLevel
       (L0/L1 감지 시 변환 제안 모달)
       ↓
[3] anchor.when, after_what, where 입력 (Tiny Habits)
       ↓
[4] 기본 variant 자동 생성 (label='기본', dose_text=protocol.min_dose_for_start, is_minimum=true)
       ↓ checkActiveHabitQuota (R1/R2)
       ↓
[5] [habit 생성] → HabitDao.insertWithVariants

dose_variants 추가 입력은 v1 vertical slice 에선 생략 가능 (기본 1 개 자동). 추가 입력 UI 는 같은 Phase 안의 후속 sub-task 로 둘 수 있음 (Developer 가 시간 보고 분리).

5. 주간 reflection 화면

5.1 진입

수동 — Tracker view 의 "이번 주 회고" 버튼.

5.2 minimum_ratio 표시

이번 주 (6/8 ~ 6/14)
  ── kept (잘 한 것): _________
  ── missed (못 한 것): _________
  ── adjust (다음 주 변경 1 개): _________

[hint] 이번 주 done 중 'tiny' (최소 도즈) 비율: 43%
   ↑ 강제 임계값 없음. 정보 hint only (R10).

5.3 계산식

minimum_ratio = (done && is_minimum_true 카운트) / (done 카운트)
done = 0 → null (UI: "이번 주 done 없음")

상세는 fn-weekly-minimum-ratio.md.

5.4 표시 트리거

  • minimum_ratio ≥ 0.7 → UI hint: "tiny 가 자주 고정되고 있어요. 컨디션 좋은 날 메인 도즈를 시도해보세요." (이것도 강제 X, 단순 메시지)
  • minimum_ratio ≤ 0.2 → UI hint: "본 도즈를 잘 유지하고 있어요." (긍정 강화)
  • 그 외 → 표시만, 메시지 없음.

6. Celebration (T0~T4)

진입 UI
T0 (매일 완료 직후) 작은 ○ 애니메이션 + user.preferences.celebration_style 적용 (verbal/gesture/emoji/silent)
T1 (3 연속 done) 모달 1 회: "3 회 스트릭. 작은 변화의 시작." + (선언된 reward_text 있으면 표시)
T2 (7 일) 모달 1 회: 주간 축하 + reward_text
T3 (30 일 중 24 일) 모달 + reward_claim insert 유도 (사용자가 fulfilled=true 체크 가능)
T4 (42 일 = 6 주) 모달 + phase 종료 안내 (status='completed' 토글)

celebration 은 한 번만. 동일 tier 재진입은 표시 X (per-tier flag).

7. R8 보장 검증

  • AC: 체크인 평균 elapsed < 30 초 (수동 QA + 측정).
  • 변동성: 새 variant 만들 때 (최초) 는 90 초 허용 (도즈 입력 자체가 사용자 의도적 작업).

8. 접근성 / 한국어

  • 최소 폰트 16 sp.
  • 모든 chip text 한국어. 카테고리는 SoT 의 ko 제목 그대로.
  • 다크모드 대응은 Phase 2 (out of scope).