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>
196 lines
6.8 KiB
Markdown
196 lines
6.8 KiB
Markdown
# 06 — UX Contracts: 일일 체크인 · 추천 · 주간 reflection (#204)
|
|
|
|
> 부모 설계서: [README.md](./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](./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](./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).
|