[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>
This commit is contained in:
129
docs/design/204-flutter-bootstrap/fn-weekly-minimum-ratio.md
Normal file
129
docs/design/204-flutter-bootstrap/fn-weekly-minimum-ratio.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# 함수 설계서: `weeklyMinimumRatio` (#204)
|
||||
|
||||
> 부모 설계서: [README.md](./README.md) · 상태: Draft
|
||||
> 작성: [AI] Architect · 구현: `lib/domain/streak/weekly_minimum_ratio.dart::weeklyMinimumRatio` (TBD)
|
||||
> 테스트: `test/domain/streak/weekly_min_ratio_test.dart` (TBD)
|
||||
|
||||
## 1. 시그니처
|
||||
|
||||
```dart
|
||||
Future<double?> weeklyMinimumRatio({
|
||||
required String userId,
|
||||
required DateTime weekStart, // 월요일 00:00 KST (week 시작 정의)
|
||||
String? habitId, // null = user 전체 / 지정 = 해당 habit 만
|
||||
required TrackerDao trackerDao,
|
||||
required HabitDoseVariantDao variantDao,
|
||||
});
|
||||
```
|
||||
|
||||
## 2. 책임
|
||||
|
||||
해당 주(7 일)의 `tracker_entry` 중 `value='done'` 인 항목들에서 `variant_id` → `habit_dose_variant.is_minimum=true` 비율을 계산한다 (R10).
|
||||
|
||||
## 3. 입력
|
||||
|
||||
| 파라미터 | 타입 | 제약/검증 | 설명 |
|
||||
|----------|------|-----------|------|
|
||||
| `userId` | `String` | not null | 단일 사용자 → 'u_local_default' |
|
||||
| `weekStart` | `DateTime` | 월요일 00:00 (KST) | 7 일 윈도우 시작 |
|
||||
| `habitId` | `String?` | nullable | null = user 전체 habits 합산, 값 = 해당 habit 만 |
|
||||
| `trackerDao` | DAO | | DB 조회용 |
|
||||
| `variantDao` | DAO | | variant.is_minimum 조회용 |
|
||||
|
||||
## 4. 출력
|
||||
|
||||
- **반환**:
|
||||
- `done count == 0` → `null` (UI: "이번 주 done 없음")
|
||||
- 그 외 → `0.0 ≤ ratio ≤ 1.0`
|
||||
- **부수효과**: DB read only. 쓰기 없음.
|
||||
|
||||
> 본 함수는 I/O 가 있지만 도메인 의도가 명확해 domain layer 에 둔다. DAO 인터페이스로 mock 가능.
|
||||
|
||||
## 5. 동작 / 알고리즘
|
||||
|
||||
```
|
||||
1. weekEnd = weekStart + 7 days (exclusive)
|
||||
2. # 윈도우 안 done entries 조회
|
||||
entries = trackerDao.findDoneInRange(
|
||||
userId, weekStart, weekEnd, habitId? )
|
||||
3. doneCount = entries.length
|
||||
4. if doneCount == 0: return null
|
||||
5. # variant_id 별 is_minimum 조회 (batch)
|
||||
variantIds = entries
|
||||
.where(e => e.variantId != null)
|
||||
.map(e => e.variantId).toSet()
|
||||
variants = variantDao.findByIds(variantIds)
|
||||
minimumSet = variants.where(v => v.isMinimum).map(v => v.variantId).toSet()
|
||||
6. minimumCount = entries.where(e =>
|
||||
e.variantId != null && minimumSet.contains(e.variantId)).length
|
||||
# variant_id 가 null 인 entry (variant 없는 habit) 는 is_minimum=false 로 취급
|
||||
7. return minimumCount / doneCount
|
||||
```
|
||||
|
||||
### 5.1 user 전체 합산 모드
|
||||
|
||||
- `habitId == null` → 모든 habits 합산. 카운트 단위 = entry 1 건 (habit 별 가중치 없음).
|
||||
- 한 user 가 build 3 + break 1 = 최대 4 habits → 주당 done entry 최대 28 건.
|
||||
|
||||
### 5.2 SQL
|
||||
|
||||
```sql
|
||||
SELECT te.variant_id, te.habit_id
|
||||
FROM tracker_entries te
|
||||
JOIN habits h ON h.id = te.habit_id
|
||||
WHERE h.user_id = :userId
|
||||
AND te.value = 'done'
|
||||
AND te.date >= :weekStart
|
||||
AND te.date < :weekEnd
|
||||
AND (:habitId IS NULL OR te.habit_id = :habitId)
|
||||
```
|
||||
|
||||
`IDX_tracker_date` 인덱스로 가속. 7 일 윈도우 → < 50 ms.
|
||||
|
||||
## 6. 에러 & 실패 모드
|
||||
|
||||
| 조건 | 처리 | 반환 |
|
||||
|------|------|------|
|
||||
| done = 0 | null | (정의대로) |
|
||||
| `weekStart` 이 월요일이 아님 | 호출자 책임. 함수는 받은 그대로 7 일 윈도우 적용 | (계산은 함) |
|
||||
| variant 가 모두 삭제됨 (orphan variant_id) | `findByIds` 결과 비어 → minimumCount=0 → ratio = 0 | 0.0 |
|
||||
| variant_id 가 NULL 인 entries 만 | minimumCount=0 → ratio=0 | 0.0 |
|
||||
| 같은 entries 가 두 번 join | UNIQUE INDEX 가 보장 → 도달 불가 | — |
|
||||
|
||||
## 7. 엣지케이스
|
||||
|
||||
- 모든 done 이 is_minimum=true → 1.0.
|
||||
- 모든 done 이 is_minimum=false → 0.0.
|
||||
- variants 없는 habit 의 done → variant_id NULL → ratio 분모에 들어가지만 분자엔 0.
|
||||
- `weekStart` 가 미래 → entries 0 → null.
|
||||
- timezone 차이로 같은 날짜가 두 주에 걸치는 케이스: weekStart 가 KST 월요일 0 시 → date 컬럼 (YYYY-MM-DD) 기준 비교라 timezone 무관.
|
||||
|
||||
## 8. 복잡도 / 성능
|
||||
|
||||
- 시간: O(D + V), D = 주간 entries 수 (≤ 28), V = unique variants (≤ 28). 무시 가능.
|
||||
- 공간: O(D + V).
|
||||
- 호출 빈도: 주간 reflection 화면 진입 시 1 회. 무시 가능.
|
||||
|
||||
## 9. 의존성
|
||||
|
||||
- `TrackerDao.findDoneInRange` (`data/db/daos/tracker_dao.dart`).
|
||||
- `HabitDoseVariantDao.findByIds`.
|
||||
- 둘 다 interface 로 추상화 → 테스트는 in-memory fake.
|
||||
|
||||
## 10. 테스트 케이스
|
||||
|
||||
- [ ] 정상 1: 주간 done 6, minimum 3 → 0.5
|
||||
- [ ] 정상 2: 주간 done 7, minimum 7 → 1.0
|
||||
- [ ] 정상 3: 주간 done 7, minimum 0 → 0.0
|
||||
- [ ] done 0 → null
|
||||
- [ ] variant_id NULL 만 → 0.0
|
||||
- [ ] habitId 지정 모드 — 해당 habit 만 카운트
|
||||
- [ ] week 경계: weekStart=2026-06-08 (월) 00:00, entries date=2026-06-08 ~ 2026-06-14 inclusive
|
||||
- [ ] orphan variant_id (variant 삭제 후) → 0
|
||||
- [ ] 미래 주 → null
|
||||
|
||||
## 11. 추적성
|
||||
|
||||
- 인수조건: #204 AC-13.
|
||||
- 관련 ADR: [ADR-0001](../../adr/0001-dose-variants.md) (R10 — hint only).
|
||||
- 06-ux-contracts.md §5 의 표시 트리거가 본 함수 결과를 소비.
|
||||
Reference in New Issue
Block a user