Files
life-helper/docs/design/204-flutter-bootstrap/fn-active-habit-quota.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

3.7 KiB

함수 설계서: checkActiveHabitQuota (#204)

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

1. 시그니처

Future<QuotaResult> checkActiveHabitQuota({
  required String userId,
  required HabitType type,        // build | break
  required HabitDao habitDao,
  String? excludeHabitId,          // 자기 자신 제외 (status 변경 시)
});

class QuotaResult {
  final bool ok;
  final int currentActiveCount;
  final int max;             // build=3, break=1
  final String? reason;      // ok=false 시
}

2. 책임

R1 (build ≤ 3) 와 R2 (break ≤ 1) 강제. habit insert 또는 status 'paused'→'active' 변경 직전 호출.

3. 입력

파라미터 타입 제약/검증 설명
userId String not null 'u_local_default' default
type HabitType enum build / break
habitDao DAO DB read
excludeHabitId String? nullable 이 ID 의 habit 은 카운트에서 제외 (자기 자신)

4. 출력

  • 반환: QuotaResult.
    • ok=true → insert 진행.
    • ok=false → reason 메시지로 UI 차단.
  • 부수효과: DB read only.

5. 동작 / 알고리즘

1. max = (type == build) ? 3 : 1
2. count = habitDao.countActive(userId, type, excludeHabitId)
3. if count >= max:
     return QuotaResult(ok=false, count, max,
       reason='active ${type} habit 은 최대 ${max} 개입니다. 진행 중인 habit 1 개를 paused/completed/abandoned 로 변경 후 다시 시도하세요.')
4. return QuotaResult(ok=true, count, max)

5.1 SQL

SELECT COUNT(*) FROM habits
WHERE user_id = :userId
  AND status = 'active'
  AND type = :type
  AND (:excludeHabitId IS NULL OR id != :excludeHabitId)

IDX_habits_user_status_type 인덱스로 < 5 ms.

6. 에러 & 실패 모드

조건 처리 반환
DAO 예외 상위로 throw (transactional rollback)
excludeHabitId 가 존재하지 않음 무영향 (필터 조건만 적용) ok=true (조건만 영향)
동시성 (다른 트랜잭션이 동시에 insert) 본 Phase 단일 사용자 = race 사실상 0. 안전 보장은 호출자가 transaction 으로 quota check + insert 묶어 처리.

7. 엣지케이스

  • 사용자가 build 3 개 active 인 상태에서 새 build 시도 → reject.
  • 사용자가 break 1 개 active 인 상태에서 새 break 시도 → reject.
  • 사용자가 build 3 개 active 인 상태에서 새 break 시도 → ok (별도 quota).
  • status='paused' 인 habit 은 카운트 제외.
  • excludeHabitId = 자기 자신 (status 변경 시) → 정상 동작.

8. 복잡도 / 성능

  • 시간: O(log N) (인덱스 lookup) + COUNT.
  • 호출 빈도: habit 생성/상태변경 시 1 회. 무시.

9. 의존성

  • HabitDao.countActive(userId, type, excludeHabitId).
  • Riverpod provider 로 호출 가능.

10. 테스트 케이스

  • 신규 build, active 0 개 → ok, count=0, max=3
  • 신규 build, active 3 개 → reject
  • 신규 build, active 2 개 → ok, count=2
  • 신규 break, active 0 개 → ok
  • 신규 break, active 1 개 → reject
  • excludeHabitId 적용: active 3 중 1 개를 exclude → ok, count=2
  • paused habit 은 카운트 제외 (status='paused' 3 개 있어도 active 0 → ok)
  • 빈 DB → count=0, ok

11. 추적성

  • 인수조건: #204 AC-7.
  • 관련 메모리: feedback_sustainable_minimal.md (build ≤ 3), project_habit_breaking_module.md (break ≤ 1).
  • 관련 schema: habit.schema.json, data-model.md §3 R1/R2.