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

110 lines
3.7 KiB
Markdown

# 함수 설계서: `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<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
```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.