Files
life-helper/docs/design/204-flutter-bootstrap/01-project-structure.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

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 방지)