---
description: Flutter 모바일 앱 개발 규칙
---

> 이 파일은 `.claude/commands/flutter.md`에 위치한다.
> 사용법: `/flutter [요청 내용]`

---

## 언어 & 기본 원칙

- **Dart Sound Null Safety** 완벽 준수 (`?`, `!`, `late` 올바르게 사용)
- `dynamic` 타입 사용 **지양** → 명시적 타입 선언
- 모든 주석은 **한국어**로 작성

---

## 프로젝트 구조

```
lib/
├── main.dart
├── app.dart                    ← MaterialApp / 라우터 설정
├── core/
│   ├── constants/              ← 상수 (색상, 크기, 텍스트 등)
│   ├── theme/                  ← 앱 테마 정의
│   ├── network/                ← Dio 인스턴스, 인터셉터
│   ├── storage/                ← flutter_secure_storage 래퍼
│   └── utils/                  ← 순수 유틸 함수
├── features/
│   └── user/
│       ├── data/
│       │   ├── models/         ← JSON 직렬화 모델
│       │   └── repositories/   ← API 호출 구현체
│       ├── domain/
│       │   └── entities/       ← 순수 비즈니스 엔티티
│       └── presentation/
│           ├── pages/          ← 화면 단위 위젯
│           ├── widgets/        ← 재사용 위젯
│           └── providers/      ← 상태 관리
└── shared/
    └── widgets/                ← 앱 공통 위젯
```

---

## 위젯 작성 원칙

```dart
// ✅ 좋은 예 — 작은 위젯으로 분리
class UserProfilePage extends StatelessWidget {
  const UserProfilePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: const _AppBar(),
      body: const _ProfileBody(),
    );
  }
}

class _ProfileBody extends StatelessWidget {
  const _ProfileBody();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const _AvatarSection(),
        const _InfoSection(),
      ],
    );
  }
}
```

- 위젯 트리 depth가 깊어지면 **커스텀 위젯으로 분리**
- `build()` 메서드 내 비즈니스 로직 작성 **금지**
- `const` 생성자 적극 활용 (불필요한 리빌드 방지)

---

## 반응형 UI

```dart
// ❌ 하드코딩 금지
Container(width: 375, height: 200)

// ✅ 반응형 처리
Container(
  width: MediaQuery.of(context).size.width * 0.9,
  child: ...,
)

// ✅ 유연한 레이아웃
Expanded(flex: 2, child: LeftPanel()),
Expanded(flex: 1, child: RightPanel()),
```

- `Expanded`, `Flexible`, `MediaQuery`, `LayoutBuilder` 적극 활용
- 하드코딩 픽셀값 사용 **금지**
- 폰트 크기는 `Theme.of(context).textTheme` 활용

---

## 상태 관리

```dart
// Riverpod 예시 (권장)
@riverpod
Future<User> fetchUser(FetchUserRef ref, int id) async {
  final repo = ref.watch(userRepositoryProvider);
  return repo.getUser(id);
}

// 위젯에서 사용
class UserWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(fetchUserProvider(userId));
    return userAsync.when(
      data: (user) => Text(user.name),
      loading: () => const CircularProgressIndicator(),
      error: (e, _) => const ErrorWidget(),
    );
  }
}
```

- **Riverpod** 권장 (Provider는 레거시로 간주)
- `StatefulWidget`은 애니메이션, 폼 등 로컬 UI 상태에만 제한적 사용

---

## API 통신 (Dio + 인터셉터)

```dart
// core/network/dio_client.dart
class DioClient {
  late final Dio _dio;

  DioClient() {
    _dio = Dio(BaseOptions(baseUrl: Env.apiUrl));
    _dio.interceptors.add(AuthInterceptor());   // 토큰 자동 첨부
    _dio.interceptors.add(ErrorInterceptor());  // 에러 코드 처리
  }
}

// 에러 인터셉터 — CLAUDE.md 에러 맵 기준 한국어 메시지 처리
class ErrorInterceptor extends Interceptor {
  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    // 에러 코드 파싱 후 SnackBar/Dialog 표시
  }
}
```

---

## 보안 (토큰 저장)

```dart
// flutter_secure_storage 사용
const storage = FlutterSecureStorage();

// 저장
await storage.write(key: 'access_token', value: token);

// 읽기
final token = await storage.read(key: 'access_token');

// 삭제 (로그아웃)
await storage.deleteAll();
```

- JWT, 민감 정보는 반드시 `flutter_secure_storage` 사용
- `SharedPreferences`에 토큰 저장 **금지**

---

## 성능 & 최신 트렌드 추가 규칙

- **모델 직렬화**: `json_serializable` + `freezed` 사용 권장 (불변 모델)
- **이미지**: `cached_network_image`로 네트워크 이미지 캐싱
- **목록**: 긴 목록은 `ListView.builder` 사용 (전체 렌더링 금지)
- **에러 핸들링**: `Either<Failure, Success>` 패턴 고려 (`fpdart` 패키지)
- **라우팅**: `go_router` 사용 권장 (딥링크, 중첩 라우팅 지원)
- **환경 분리**: `--dart-define` 또는 `flutter_dotenv`로 dev/prod 분리
