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

8.7 KiB

01 — Flutter 프로젝트 구조 (#204)

부모 설계서: 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 의존성 (요약)

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 의존성 와이어링 (스케치)

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 핵심

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