# 함수 설계서: `checkActiveHabitQuota` (#204) > 부모 설계서: [README.md](./README.md) · 상태: Draft > 작성: [AI] Architect · 구현: `lib/domain/rules/active_habit_quota.dart::checkActiveHabitQuota` (TBD) > 테스트: `test/domain/rules/active_habit_quota_test.dart` (TBD) ## 1. 시그니처 ```dart Future 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 ```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.