first commit
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
---
|
||||
description: Flutter 모바일 앱 개발 규칙
|
||||
---
|
||||
|
||||
> 이 파일은 `.claude/commands/flutter.md`에 위치한다.
|
||||
> 사용법: `/flutter [요청 내용]`
|
||||
|
||||
---
|
||||
|
||||
## 언어 & 기본 원칙
|
||||
|
||||
- **Dart Sound Null Safety** 완벽 준수 (`?`, `!`, `late` 올바르게 사용)
|
||||
- `dynamic` 타입 사용 **지양** → 명시적 타입 선언
|
||||
- 모든 주석은 **한국어**로 작성
|
||||
|
||||
---
|
||||
|
||||
## 프로젝트 구조
|
||||
|
||||
```
|
||||
lib/
|
||||
├── main.dart
|
||||
├── app.dart ← MaterialApp / 라우터 설정
|
||||
├── core/
|
||||
│ ├── constants/ ← 상수 (색상, 크기, 텍스트 등)
|
||||
│ ├── theme/ ← 앱 테마 정의
|
||||
│ ├── api/ ← Dio 인스턴스(dio_client), 에러 사전(error_lexicon), 인터셉터
|
||||
│ ├── 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/api/dio_client.dart
|
||||
class DioClient {
|
||||
late final Dio _dio;
|
||||
|
||||
DioClient() {
|
||||
// flutter_dotenv 로 .env 로드, 미설정 시 로컬 기본값
|
||||
final baseUrl = dotenv.maybeGet('API_URL') ?? 'http://localhost:3000/api';
|
||||
_dio = Dio(BaseOptions(baseUrl: baseUrl));
|
||||
_dio.interceptors.add(AuthInterceptor()); // 토큰 자동 첨부
|
||||
_dio.interceptors.add(ErrorInterceptor()); // 에러 코드 처리 (error_lexicon 참조)
|
||||
}
|
||||
}
|
||||
|
||||
// 에러 인터셉터 — CLAUDE.md 에러 맵 기준 한국어 메시지 처리
|
||||
class ErrorInterceptor extends Interceptor {
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
// 에러 코드 파싱 후 SnackBar/Dialog 표시
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 보안 (토큰 저장)
|
||||
|
||||
```dart
|
||||
// flutter_secure_storage 사용
|
||||
const storage = FlutterSecureStorage();
|
||||
|
||||
// 저장 (키 이름은 dio_client 인터셉터와 통일: jwt_token)
|
||||
await storage.write(key: 'jwt_token', value: token);
|
||||
|
||||
// 읽기
|
||||
final token = await storage.read(key: 'jwt_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 분리
|
||||
Reference in New Issue
Block a user