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>
6.8 KiB
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 │
│ [○ 완료] [다른 옵션 ▾] │
│ │
└────────────────────────────────┘
탭 수:
- 장소 chip 1 탭
- 컨디션 chip 1 탭
- ○ 완료 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. 추천 매칭 알고리즘 (의사코드)
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).