# 01 — Flutter 프로젝트 구조 (#204) > 부모 설계서: [README.md](./README.md) ## 1. 결정: feature-first + layer-first 하이브리드 | 레이어 | 위치 | 원칙 | |--------|------|------| | 화면 단위 (UI + 화면 상태) | `lib/features//` | **feature-first** — 한 화면을 만들 때 보는 코드는 한 폴더 안 | | 도메인 로직 (순수 함수) | `lib/domain//` | **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 │ │ ├── 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, 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 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 방지)