Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/flutter-ddd-builder/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "flutter-ddd-builder",
"version": "0.0.4",
"version": "0.1.0",
"description": "Domain Book 기반 Flutter DDD 아키텍처 코드 자동 생성 플러그인. 비즈니스 로직과 UI를 병렬 팀 작업으로 구축하고 실시간 품질 검증을 제공합니다.",
"author": {
"name": "Andy",
Expand Down
51 changes: 51 additions & 0 deletions plugins/flutter-ddd-builder/agents/logic-implementer.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,57 @@ class {Domain}Service extends _${Domain}Service {
}
```

### 4b. Pagination Support for List APIs

For list-type API endpoints, use `PaginatedResponse<T>` model:

```dart
import 'package:app/global/types/paginated_response.dart';

// List API with pagination
Future<PaginatedResponse<PostModel>> getPosts({int page = 1, int limit = 20}) async {
final dio = ref.read(dioProvider);
final response = await dio.get('/api/posts', queryParameters: {
'page': page,
'limit': limit,
});
return PaginatedResponse<PostModel>.fromJson(
response.data as Map<String, dynamic>,
(json) => PostModel.fromJson(json as Map<String, dynamic>),
);
}
```

Error handling with `AppException`:
```dart
state = await AsyncValue.guard(() async {
// AppException is thrown automatically by ErrorInterceptor
return _fetchPosts();
});
```

### 4c. Auth Provider State Pattern (auth 도메인 전용)

`authProvider`는 인증 "상태"를 표현하므로 절대 `AsyncError`로 두면 안 됨:

```dart
Future<void> login({required String email, required String password}) async {
state = const AsyncData(AuthStateModel.loading());
try {
// ... API 호출, 토큰 저장 ...
state = AsyncData(AuthStateModel.authenticated(
accessToken: token, user: user,
));
} catch (e, st) {
// ✅ 실패 → unauthenticated 복귀 (절대 AsyncError 아님)
state = const AsyncData(AuthStateModel.unauthenticated());
Error.throwWithStackTrace(e, st); // 호출자(emailLoginProvider)에 에러 전파
}
}
```

**이유**: `authProvider`가 `AsyncError`이면 router redirect에서 `authState.value == null` → 보호 라우트 가드 비활성화 위험.

### 5. Handle Dependencies
If you need models from other domains:

Expand Down
24 changes: 24 additions & 0 deletions plugins/flutter-ddd-builder/agents/logic-orchestrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,23 @@ fi
- Running build_runner here generates the initial setup
- Teammates can then work without conflicts

### 4b. Preflight Checks

Verify boilerplate utilities exist before spawning teammates:

```bash
# Check required utility files
for file in \
lib/global/types/paginated_response.dart \
lib/global/utils/validators.dart \
lib/apps/ui/common/async_value_widget.dart \
lib/apps/infra/exception/exception_handler.dart; do
if [ ! -f "$file" ]; then
echo "WARNING: Missing $file - boilerplate may be incomplete"
fi
done
```

### 5. Spawn Logic Implementers

**For each domain, spawn teammate:**
Expand Down Expand Up @@ -463,6 +480,13 @@ parse_build_errors(build_output):
retry_build()
```

**Lint verification (zero warnings)**:
```bash
flutter analyze --no-fatal-infos
# Must show: No issues found!
# If warnings exist, fix before proceeding
```

### 9. Integration

**Merge all worktrees:**
Expand Down
83 changes: 83 additions & 0 deletions plugins/flutter-ddd-builder/agents/ui-implementer.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,89 @@ Usage:
await authService.login(email, password);
```

### 6b. Standard Widget Patterns

**AsyncValueWidget (REQUIRED for all async data)**:
```dart
import 'package:app/apps/ui/common/async_value_widget.dart';

// Use instead of .when() inline
AsyncValueWidget(
value: postListState,
emptyCheck: (posts) => posts.isEmpty,
emptyMessage: '게시글이 없습니다',
data: (posts) => ListView.builder(...),
);
```

**Validators (REQUIRED for all forms)**:
```dart
import 'package:app/global/utils/validators.dart';

TextFormField(
validator: Validators.compose([
Validators.required,
Validators.minLength(2, fieldName: '제목'),
]),
);
```

**withLoaderOverlay (REQUIRED for mutation buttons)**:
```dart
import 'package:app/global/utils/with_loader_overlay.dart';

onPressed: () async {
await withLoaderOverlay(context, () async {
await ref.read(provider.notifier).create(model);
});
}
```

**Theme Tokens**:
```dart
// Always use Theme.of(context) for colors and text
final cs = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;

// Use AppSpacing/AppRadius when tokens are available
import 'package:app/core/theme/tokens/generated/spacing.gen.dart';
import 'package:app/core/theme/tokens/generated/radius.gen.dart';
Padding(padding: EdgeInsets.all(AppSpacing.spacing4));
```

### 6c. Auth State Listening Pattern (Login Pages)

로그인 페이지에서는 두 개의 `ref.listen`을 분리 사용:

```dart
// 1. 에러 표시 — emailLoginProvider 감시
ref.listen(emailLoginProvider, (prev, next) {
if (next.hasError && !next.isLoading) {
final error = next.error;
final message = error is AppException
? ExceptionHandler.getUserMessage(error)
: '로그인에 실패했습니다. 다시 시도해주세요.';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
});

// 2. 성공 네비게이션 — authProvider 감시
ref.listen(authProvider, (prev, next) {
final isAuthenticated =
next.value?.mapOrNull(authenticated: (_) => true) ?? false;
if (isAuthenticated && context.mounted) {
RouterClient.home.go(context);
}
});
```

**핵심 원칙**:
- `authProvider`는 인증 "상태" → 성공 네비게이션 담당
- `emailLoginProvider`는 폼 "결과" → 에러 메시지 담당
- 에러를 `'${next.error}'`로 표시 금지 → `ExceptionHandler.getUserMessage()` 사용

### 7. Navigation
```dart
// Import RouterClient
Expand Down
14 changes: 14 additions & 0 deletions plugins/flutter-ddd-builder/agents/ui-orchestrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ for screen in ${screens[@]}; do
done
```

### 3b. Preflight Utility Check

```bash
# Verify UI utilities exist
for file in \
lib/apps/ui/common/async_value_widget.dart \
lib/global/utils/validators.dart \
lib/global/utils/with_loader_overlay.dart; do
if [ ! -f "$file" ]; then
echo "WARNING: Missing $file - UI utilities incomplete"
fi
done
```

### 4. Spawn Implementers
```
For each screen:
Expand Down
11 changes: 11 additions & 0 deletions plugins/flutter-ddd-builder/agents/ui-planner.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,17 @@ Follow project conventions:
- Theme.of(context).textTheme for text
- No hardcoded values

**Theme (Dual Mode)**:
- If project uses token mode: `AppColors.primary500`, `AppSpacing.spacing4`
- If project uses seed mode: `cs.primary`, hardcoded spacing (16.0)
- Widget Mapping should note: `cs.primary` works in both modes
- ASCII wireframes should not hardcode colors, only reference token names or cs

**Pagination Lists**:
- Mark infinite scroll lists with `[∞ Scroll]` indicator
- Show `PostListProvider.loadMore()` service mapping
- Include loading indicator at bottom of list

**Page Location**:
- Pages are at `lib/apps/domain/{domain}/pages/{page}/{page}_page.dart`
- Not at `lib/apps/ui/pages/`
Expand Down
4 changes: 4 additions & 0 deletions plugins/flutter-ddd-builder/commands/logic.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ Your tasks:

Follow flutter-ddd-patterns skill for implementation guidelines.

Note: For list-type APIs, use PaginatedResponse<T> model from lib/global/types/paginated_response.dart
Example:
final paginated = PaginatedResponse<PostModel>.fromJson(response.data, (json) => PostModel.fromJson(json));

Files you should create:
- lib/apps/domain/{domain}/models/*_model.dart
- lib/apps/domain/{domain}/services/{domain}_service.dart
Expand Down
3 changes: 3 additions & 0 deletions plugins/flutter-ddd-builder/commands/start.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ ls ai-context/PRD.md # Required for UI generation
| `ai-context/PRD.md` | UI generation | Required unless --skip-ui |
| `pubspec.yaml` | All | Must exist |
| Git repository | Worktree management | Must be initialized |
| `lib/global/types/paginated_response.dart` | Logic | Should exist (boilerplate) |
| `lib/global/utils/validators.dart` | UI | Should exist (boilerplate) |
| `lib/apps/ui/common/async_value_widget.dart` | UI | Should exist (boilerplate) |

**If Domain Book is missing:**
```
Expand Down
11 changes: 11 additions & 0 deletions plugins/flutter-ddd-builder/commands/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,17 @@ Your tasks:
Layout reference from screen plan:
{ASCII art for this screen from screen-layouts.md}

Standard Widget Patterns (REQUIRED):
- AsyncValueWidget for all async data display (not .when() inline)
- Validators for all form fields
- withLoaderOverlay for mutation buttons
- Theme.of(context).colorScheme for colors (both theme modes)

Imports:
- package:app/apps/ui/common/async_value_widget.dart
- package:app/global/utils/validators.dart
- package:app/global/utils/with_loader_overlay.dart

Remember:
- PostToolUse hook automatically runs flutter analyze
- Use methods for callbacks, not inline functions
Expand Down
2 changes: 1 addition & 1 deletion plugins/flutter-ddd-builder/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
{
"type": "prompt",
"prompt": "Check ONLY the Dart file just modified for these anti-patterns. If found, fix immediately:\n- `ref.watch(` outside `build()` → `ref.read(`\n- `Dio().fetch(` without interceptors → add ResponseInterceptor + ErrorInterceptor\n- Model file/class with `Dto` suffix → rename to `Model`\n- `@freezed` class with `factory` missing `const` → `const factory`\n- `@riverpod` function with `FooRef ref` → `Ref ref`\n- `await ref.read(notifier).action(); ref.read(provider).when(` → use ref.listen in build()"
"prompt": "Check ONLY the Dart file just modified for these anti-patterns. If found, fix immediately:\n- `ref.watch(` outside `build()` → `ref.read(`\n- `Dio().fetch(` without interceptors → add ResponseInterceptor + ErrorInterceptor\n- Model file/class with `Dto` suffix → rename to `Model`\n- `@freezed` class with `factory` missing `const` → `const factory`\n- `@riverpod` function with `FooRef ref` → `Ref ref`\n- `await ref.read(notifier).action(); ref.read(provider).when(` → use ref.listen in build()\n- `.when(data:` inline in page body → use `AsyncValueWidget` widget\n- `EdgeInsets.all(16)` or similar hardcoded values → consider `AppSpacing.spacing4` tokens\n- Inline form validation logic → use `Validators.compose([...])` from validators.dart"
}
]
}
Expand Down
42 changes: 41 additions & 1 deletion plugins/flutter-ddd-builder/skills/flutter-ddd-patterns/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,50 @@ lib/apps/
- [ ] Route class: `static const path`, `static const name`, `go()`, `push()`
- [ ] RouterClient: `abstract final class` with const instances

### Auth State Management

- [ ] `authProvider` 로그인 실패 시 `AsyncData(unauthenticated())` 복귀 (절대 `AsyncError` 아님)
- [ ] 에러 전파: `Error.throwWithStackTrace(e, st)` (`rethrow` 대신 — 상태 설정 후 전파)
- [ ] `emailLoginProvider`가 에러 메시지 담당 (역할 분리)
- [ ] Login UI: `ref.listen(authProvider)` → 성공 네비게이션, `ref.listen(emailLoginProvider)` → 에러 표시
- [ ] 에러 메시지: `AppException` → `ExceptionHandler.getUserMessage()`, 그 외 → generic 메시지
- [ ] Router redirect: `authValue == null` → 미인증 취급 (defense-in-depth)

### Auth Interceptor

- [ ] Retry Dio에 `ResponseInterceptor` + `ErrorInterceptor` 포함
- [ ] `AuthInterceptor`는 retry Dio에서 제외 (무한루프 방지)
- [ ] 로그인 시 `refreshToken` 저장 확인

### Pagination
- [ ] `PaginatedResponse<T>` model in `lib/global/types/paginated_response.dart`
- [ ] `PaginationMeta` with offset/pageSize/totalItemCount/isFirst/isLast
- [ ] Provider uses `_page` counter + `_hasMore` flag
- [ ] `loadMore()` appends to existing list
- [ ] UI uses `NotificationListener<ScrollNotification>` or `ScrollController`

### AsyncValueWidget
- [ ] Use `AsyncValueWidget` instead of `.when(data:, error:, loading:)` inline
- [ ] Located at `lib/apps/ui/common/async_value_widget.dart`
- [ ] Supports `emptyCheck` and `emptyMessage` for empty states
- [ ] Custom `loading` and `error` builders optional

### Form Validation
- [ ] Use `Validators` from `lib/global/utils/validators.dart`
- [ ] `Validators.compose([...])` for multiple rules
- [ ] Available: `required`, `email`, `minLength`, `maxLength`, `phone`, `password`

### Loading Overlay
- [ ] `withLoaderOverlay(context, () async { ... })` for button actions
- [ ] Requires `LoaderOverlay` widget in tree (wrap Scaffold or MaterialApp)
- [ ] Auto-hides on success or error

### Theme (Dual Mode)
- [ ] `AppTheme.fromTokens()` — Figma token-based (AppColors → ColorScheme)
- [ ] `AppTheme.fromSeed(seedColor: Colors.blue)` — Material 3 auto-generated
- [ ] Both modes: `Theme.of(context).colorScheme.primary` works identically
- [ ] `AppSpacing`, `AppRadius` usable in both modes

## Naming Conventions

| Type | File | Class |
Expand Down Expand Up @@ -94,10 +132,12 @@ dart run build_runner build --delete-conflicting-outputs # Freezed + Riverpod
- **[Freezed 3.x Guide](references/freezed-3x-guide.md)** — Union types, nested models, enums, custom JSON, testing
- **[Riverpod 3.x Guide](references/riverpod-3x-guide.md)** — AsyncNotifier, provider types, family, ref.watch vs ref.read
- **[API Client Patterns](references/api-client-patterns.md)** — CRUD, pagination, file upload, error handling
- **[Anti-Patterns](references/anti-patterns.md)** — 7가지 금지 패턴 + Auth Interceptor 올바른 패턴
- **[Anti-Patterns](references/anti-patterns.md)** — 10가지 금지 패턴 + Auth Interceptor 올바른 패턴

## Working Examples

- **[user_model_example.dart](examples/user_model_example.dart)** — Freezed model with custom methods
- **[auth_service_example.dart](examples/auth_service_example.dart)** — Riverpod AsyncNotifier service
- **[post_create_page_example.dart](examples/post_create_page_example.dart)** — ref.listen UI 상태 처리
- **[paginated_list_example.dart](examples/paginated_list_example.dart)** — Pagination Provider + infinite scroll UI
- **[form_with_validation_example.dart](examples/form_with_validation_example.dart)** — Validators + withLoaderOverlay
Loading