Files
life-helper/docs/design/204-flutter-bootstrap/fn-weekly-minimum-ratio.md
joungmin 29befe4d97 [Architect] Refs #204 — apply OQ decisions: diet_pattern (19th), ADR-0002 normalize dose_variants
- OQ-1: dose_variants 정규화 결정을 ADR-0002 로 승격 (ADR-0001 = 왜, ADR-0002 = 어떻게).
- OQ-3: nutrition diet 패턴 5개를 별도 diet_pattern 카탈로그(19번째 SoT)로 분리.
  · 02-catalog §8 신규, 인덱스 IDX_diet_patterns_evidence / IDX_diet_patterns_kfit.
  · 05-seed: diet_patterns.json (5행) 추가, 로딩 순서 끝에 배치.
  · 04-migrations: v1 테이블 합계 = Catalog 8 + User 11 + 부속 1 + meta_kv = 21.
- README §2/§3/§6/§11 갱신: 18→19 SoT, AC-2 에 diet_pattern=5 검증 추가.
- README §12 OQ → Resolved Open Questions 표 (OQ-1~OQ-8 결정 결과).
- habit_dose_variant → habit_dose_variants 표기 통일.
- fn-weekly-minimum-ratio, 03-drift-schema-user 의 ADR-0002 cross-link.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-11 17:13:04 +09:00

4.8 KiB

함수 설계서: weeklyMinimumRatio (#204)

부모 설계서: README.md · 상태: Draft 작성: [AI] Architect · 구현: lib/domain/streak/weekly_minimum_ratio.dart::weeklyMinimumRatio (TBD) 테스트: test/domain/streak/weekly_min_ratio_test.dart (TBD)

1. 시그니처

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_entryvalue='done' 인 항목들에서 variant_idhabit_dose_variants.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 == 0null (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

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 (R10 — hint only).
  • 06-ux-contracts.md §5 의 표시 트리거가 본 함수 결과를 소비.