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>
226 lines
8.7 KiB
Markdown
226 lines
8.7 KiB
Markdown
# 01 — Flutter 프로젝트 구조 (#204)
|
|
|
|
> 부모 설계서: [README.md](./README.md)
|
|
|
|
## 1. 결정: feature-first + layer-first 하이브리드
|
|
|
|
| 레이어 | 위치 | 원칙 |
|
|
|--------|------|------|
|
|
| 화면 단위 (UI + 화면 상태) | `lib/features/<feature>/` | **feature-first** — 한 화면을 만들 때 보는 코드는 한 폴더 안 |
|
|
| 도메인 로직 (순수 함수) | `lib/domain/<area>/` | **layer-first** — Drift 미참조, 테스트 가능 |
|
|
| 데이터 I/O (Drift + assets) | `lib/data/` | **layer-first** — DAO 패턴 |
|
|
| 공통 유틸 | `lib/core/` | id 생성, datetime, Result type 등 |
|
|
|
|
**이유**: feature-first 만 가면 R1~R10 같은 cross-cutting 규칙이 여러 feature 폴더에 흩어진다. layer-first 만 가면 작은 vertical slice 가 4 폴더로 흩어진다. 둘 다 채택하되 경계를 분명히 한다 (domain ↔ data ↔ features).
|
|
|
|
## 2. 최종 디렉토리 트리
|
|
|
|
```
|
|
app/
|
|
├── pubspec.yaml
|
|
├── analysis_options.yaml
|
|
├── build.yaml # build_runner 옵션
|
|
├── README.md # Developer 용 간단 가이드 (Phase 1 종료 후 작성)
|
|
├── lib/
|
|
│ ├── main.dart # 앱 진입점 (Riverpod ProviderScope + AppDatabase wire)
|
|
│ ├── app.dart # MaterialApp + routes
|
|
│ ├── core/
|
|
│ │ ├── id.dart # generateUlid, isValidUlid
|
|
│ │ ├── result.dart # sealed class Result<Ok, Err>
|
|
│ │ ├── time.dart # nowKst, dateOnly, weekStart
|
|
│ │ └── constants.dart # USER_LOCAL_DEFAULT 등
|
|
│ ├── data/
|
|
│ │ ├── db/
|
|
│ │ │ ├── app_database.dart # @DriftDatabase 진입 + onCreate/onUpgrade
|
|
│ │ │ ├── tables/
|
|
│ │ │ │ ├── catalog/
|
|
│ │ │ │ │ ├── protocols.dart
|
|
│ │ │ │ │ ├── break_protocols.dart
|
|
│ │ │ │ │ ├── common_frames.dart
|
|
│ │ │ │ │ ├── methodologies.dart
|
|
│ │ │ │ │ ├── frame_patterns.dart
|
|
│ │ │ │ │ ├── reward_menu_items.dart
|
|
│ │ │ │ │ └── references.dart
|
|
│ │ │ │ ├── user/
|
|
│ │ │ │ │ ├── users.dart
|
|
│ │ │ │ │ ├── phases.dart
|
|
│ │ │ │ │ ├── habits.dart
|
|
│ │ │ │ │ ├── habit_dose_variants.dart # 정규화 산출
|
|
│ │ │ │ │ ├── if_then_rules.dart
|
|
│ │ │ │ │ ├── tracker_entries.dart
|
|
│ │ │ │ │ ├── lapse_logs.dart
|
|
│ │ │ │ │ ├── urge_logs.dart
|
|
│ │ │ │ │ ├── reward_declarations.dart
|
|
│ │ │ │ │ ├── reward_claims.dart
|
|
│ │ │ │ │ └── reflections.dart
|
|
│ │ │ │ └── meta_kv.dart # 시드 완료 플래그 등 보조 KV
|
|
│ │ │ ├── converters/ # TypeConverter (enum, list<string>, datetime)
|
|
│ │ │ ├── daos/
|
|
│ │ │ │ ├── habit_dao.dart
|
|
│ │ │ │ ├── tracker_dao.dart
|
|
│ │ │ │ ├── phase_dao.dart
|
|
│ │ │ │ ├── reward_dao.dart
|
|
│ │ │ │ ├── reflection_dao.dart
|
|
│ │ │ │ └── catalog_dao.dart
|
|
│ │ │ └── migrations/
|
|
│ │ │ └── v1_initial.dart # schemaVersion=1 진입점
|
|
│ │ └── seed/
|
|
│ │ ├── seed_importer.dart # importIfNeeded
|
|
│ │ ├── adapters/
|
|
│ │ │ ├── protocol_seed_adapter.dart
|
|
│ │ │ ├── break_protocol_seed_adapter.dart
|
|
│ │ │ └── ... (시드별 어댑터)
|
|
│ │ └── seed_models.dart # freezed + json_serializable
|
|
│ ├── domain/
|
|
│ │ ├── models/ # plain Dart 모델 (Drift 미참조)
|
|
│ │ │ ├── habit.dart
|
|
│ │ │ ├── tracker_entry.dart
|
|
│ │ │ ├── frame.dart
|
|
│ │ │ └── ...
|
|
│ │ ├── rules/
|
|
│ │ │ ├── active_habit_quota.dart # R1/R2
|
|
│ │ │ ├── reward_window.dart # R4
|
|
│ │ │ ├── tracker_value.dart # R5
|
|
│ │ │ ├── phase_anchor_change.dart # R6
|
|
│ │ │ └── xor_protocol.dart
|
|
│ │ ├── frame/
|
|
│ │ │ ├── validate_frame_level.dart # R3
|
|
│ │ │ └── detect_avoidance_keywords.dart # R7
|
|
│ │ ├── recommend/
|
|
│ │ │ ├── recommend_variant.dart # fn-recommend-variant
|
|
│ │ │ └── scoring.dart
|
|
│ │ ├── streak/
|
|
│ │ │ ├── compute_streak.dart # fn-compute-streak
|
|
│ │ │ └── weekly_minimum_ratio.dart # fn-weekly-minimum-ratio
|
|
│ │ └── checkin/
|
|
│ │ └── checkin_timer.dart # R8 측정
|
|
│ └── features/
|
|
│ ├── habit_create/
|
|
│ │ ├── habit_create_screen.dart
|
|
│ │ ├── habit_create_controller.dart # Riverpod StateNotifier
|
|
│ │ └── widgets/
|
|
│ ├── daily_checkin/
|
|
│ │ ├── daily_checkin_sheet.dart
|
|
│ │ ├── daily_checkin_controller.dart
|
|
│ │ └── widgets/
|
|
│ └── tracker_view/
|
|
│ ├── tracker_view_screen.dart
|
|
│ └── widgets/
|
|
├── assets/
|
|
│ └── seed/
|
|
│ ├── protocols.json
|
|
│ ├── break_protocols.json
|
|
│ ├── common_frames.json
|
|
│ ├── methodologies.json
|
|
│ ├── frame_patterns.json
|
|
│ ├── reward_menu_items.json
|
|
│ └── references.json
|
|
└── test/
|
|
├── data/
|
|
│ ├── db/
|
|
│ └── seed/
|
|
├── domain/
|
|
│ ├── rules/
|
|
│ ├── frame/
|
|
│ ├── recommend/
|
|
│ └── streak/
|
|
├── features/
|
|
└── fixtures/
|
|
└── seed/ # 작은 테스트용 seed
|
|
```
|
|
|
|
## 3. pubspec.yaml 의존성 (요약)
|
|
|
|
```yaml
|
|
name: life_helper
|
|
description: Huberman + Atomic Habits + Tiny Habits + If-Then. Local-first.
|
|
publish_to: none
|
|
version: 0.1.0+1
|
|
|
|
environment:
|
|
sdk: ">=3.4.0 <4.0.0"
|
|
flutter: ">=3.22.0"
|
|
|
|
dependencies:
|
|
flutter:
|
|
sdk: flutter
|
|
flutter_riverpod: ^2.5.1
|
|
drift: ^2.18.0
|
|
sqlite3_flutter_libs: ^0.5.0
|
|
path_provider: ^2.1.0
|
|
path: ^1.9.0
|
|
freezed_annotation: ^2.4.0
|
|
json_annotation: ^4.9.0
|
|
ulid: ^2.0.0 # 또는 uuid + 자체 base32
|
|
|
|
dev_dependencies:
|
|
flutter_test:
|
|
sdk: flutter
|
|
drift_dev: ^2.18.0
|
|
build_runner: ^2.4.0
|
|
freezed: ^2.5.0
|
|
json_serializable: ^6.8.0
|
|
flutter_lints: ^4.0.0
|
|
|
|
flutter:
|
|
assets:
|
|
- assets/seed/
|
|
```
|
|
|
|
## 4. build_runner / Drift 코드 생성 흐름
|
|
|
|
```
|
|
.dart (source) ──► build_runner ──► .g.dart (generated)
|
|
│
|
|
├─ data/db/tables/**/*.dart → app_database.g.dart (Drift)
|
|
├─ data/seed/seed_models.dart → seed_models.g.dart / .freezed.dart
|
|
└─ domain/models/*.dart → *.freezed.dart (선택)
|
|
|
|
명령:
|
|
$ dart run build_runner build --delete-conflicting-outputs
|
|
(watch 모드: dart run build_runner watch)
|
|
```
|
|
|
|
- `.g.dart` / `.freezed.dart` 는 commit. 이유: 새 환경에서 codegen 실패해도 일단 빌드 가능 + diff 추적 (스키마 변화를 PR diff 로 검토).
|
|
- `build.yaml` 에 Drift 옵션: `null_aware_type_converters: true`, `use_sql_column_name_as_json_key: false`.
|
|
|
|
## 5. main.dart 의존성 와이어링 (스케치)
|
|
|
|
```dart
|
|
Future<void> main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
final dbDir = await getApplicationDocumentsDirectory();
|
|
final db = AppDatabase(NativeDatabase.createInBackground(
|
|
File('${dbDir.path}/life_helper.sqlite')));
|
|
await SeedImporter(db).importIfNeeded();
|
|
runApp(ProviderScope(
|
|
overrides: [appDatabaseProvider.overrideWithValue(db)],
|
|
child: const LifeHelperApp(),
|
|
));
|
|
}
|
|
```
|
|
|
|
## 6. analysis_options.yaml 핵심
|
|
|
|
```yaml
|
|
include: package:flutter_lints/flutter.yaml
|
|
analyzer:
|
|
language:
|
|
strict-casts: true
|
|
strict-raw-types: true
|
|
errors:
|
|
invalid_annotation_target: ignore # freezed 호환
|
|
linter:
|
|
rules:
|
|
- prefer_const_constructors
|
|
- require_trailing_commas
|
|
- avoid_print
|
|
```
|
|
|
|
## 7. CI 훅 (Phase 1 종료 시점 작업으로 권장, 본 Phase 는 명세만)
|
|
|
|
- `flutter analyze`
|
|
- `flutter test`
|
|
- `dart run build_runner build --delete-conflicting-outputs` (smoke)
|
|
- Drift schema dump → `tool/schema/v1.json` 과 diff (regression 방지)
|