| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| Invoke | /feature:presentation |
| Aliases | /presentation, /presentation:create |
| Category | petmedi-workflow |
| Complexity | standard |
| MCP Servers | serena, context7, magic |
/feature:presentation#
Context Framework Note: This behavioral instruction activates when Claude Code users type
/feature:presentationpatterns.
Triggers#
- When a new Feature Presentation Layer is needed
- When BLoC, Page, Widget, and Route generation are needed
/feature:createorchestration Step 5
Context Trigger Pattern#
/feature:presentation {feature_name} {entity_name} [--options]
Parameters#
| Parameter | Required | Description | Example |
|---|---|---|---|
feature_name |
โ | Feature module name (snake_case) | community, chat |
entity_name |
โ | Entity name (PascalCase) | Post, Message |
--location |
โ | Location |
application
,
common
,
console
(default:
application
)
|
--pages |
โ | Page list to generate | "list, detail, create" |
Behavioral Flow#
1. Existing Pattern Analysis#
Analyze existing Presentation Layer patterns using Serena MCP:
- feature/application/community/lib/src/presentation/bloc/post_list/post_list_bloc.dart
- feature/application/community/lib/src/presentation/bloc/post_list/post_list_event.dart
- feature/application/community/lib/src/presentation/bloc/post_list/post_list_state.dart
2. BLoC Event Generation (sealed class + private implementation)#
part of '{feature}_list_bloc.dart';
/// {Feature} ๋ชฉ๋ก ์ด๋ฒคํธ
@immutable
sealed class {Feature}ListEvent {
const {Feature}ListEvent();
// Factory constructors (Public API)
const factory {Feature}ListEvent.loadRequested() = _LoadRequested;
const factory {Feature}ListEvent.refreshRequested() = _RefreshRequested;
const factory {Feature}ListEvent.categoryChanged({
{Entity}Category? category,
}) = _CategoryChanged;
}
// Private implementation classes
@immutable
final class _LoadRequested extends {Feature}ListEvent {
const _LoadRequested();
}
@immutable
final class _RefreshRequested extends {Feature}ListEvent {
const _RefreshRequested();
}
@immutable
final class _CategoryChanged extends {Feature}ListEvent {
const _CategoryChanged({this.category});
final {Entity}Category? category;
}
3. BLoC State Generation#
part of '{feature}_list_bloc.dart';
/// {Feature} ๋ชฉ๋ก ์ํ
@immutable
sealed class {Feature}ListState {
const {Feature}ListState({
required this.{entity}s,
required this.currentSort,
this.currentCategory,
});
final List<{Entity}> {entity}s;
final {Entity}Category? currentCategory;
final {Entity}SortType currentSort;
}
@immutable
final class {Feature}ListInitial extends {Feature}ListState {
const {Feature}ListInitial()
: super({entity}s: const [], currentSort: {Entity}SortType.latest);
}
@immutable
final class {Feature}ListLoading extends {Feature}ListState { ... }
@immutable
final class {Feature}ListLoaded extends {Feature}ListState {
// ... hasMore, total ์ถ๊ฐ
// copyWith ๋ฉ์๋ ํฌํจ
}
@immutable
final class {Feature}ListError extends {Feature}ListState {
// ... message ์ถ๊ฐ
}
4. BLoC Class Generation (Optional Constructor Injection)#
import 'package:dependencies/dependencies.dart';
part '{feature}_list_event.dart';
part '{feature}_list_state.dart';
/// {Feature} ๋ชฉ๋ก BLoC
class {Feature}ListBloc extends Bloc<{Feature}ListEvent, {Feature}ListState> {
{Feature}ListBloc({
Get{Entity}sUsecase? get{Entity}sUsecase,
}) : _get{Entity}sUsecase = get{Entity}sUsecase ?? const Get{Entity}sUsecase(),
super(const {Feature}ListInitial()) {
on<_LoadRequested>(_onLoadRequested);
on<_RefreshRequested>(_onRefreshRequested);
}
final Get{Entity}sUsecase _get{Entity}sUsecase;
Future<void> _onLoadRequested(
_LoadRequested event,
Emitter<{Feature}ListState> emit,
) async {
emit({Feature}ListLoading(...));
// โ
UseCase call
final result = await _get{Entity}sUsecase(
Get{Entity}sParams(
limit: _pageSize,
offset: _currentOffset,
category: state.currentCategory,
),
);
result.fold(
(failure) {
if (!isClosed) { // โ
BLoC closed check
emit({Feature}ListError(message: failure.message ?? '์ค๋ฅ'));
}
},
({entity}ListResult) {
if (!isClosed) {
emit({Feature}ListLoaded(...));
}
},
);
}
}
5. BLoC Test Generation#
void main() {
late MockGet{Entity}sUsecase mockUsecase;
setUpAll(registerFallbackValues);
setUp(() {
mockUsecase = MockGet{Entity}sUsecase();
});
blocTest<{Feature}ListBloc, {Feature}ListState>(
'๋ก๋ ์ฑ๊ณต ์ {Feature}ListLoaded ์ํ',
build: () => {Feature}ListBloc(
get{Entity}sUsecase: mockUsecase, // โ
Direct mock injection
),
setUp: () {
when(() => mockUsecase(any()))
.thenAnswer((_) async => Right(testResult));
},
act: (bloc) => bloc.add(const {Feature}ListEvent.loadRequested()),
expect: () => [
isA<{Feature}ListLoading>(),
isA<{Feature}ListLoaded>(),
],
);
}
6. Page Generation (BlocProvider wrapping)#
class {Feature}Page extends StatelessWidget {
const {Feature}Page({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
{Feature}ListBloc()..add(const {Feature}ListEvent.loadRequested()),
child: const {Feature}View(),
);
}
}
class {Feature}View extends StatelessWidget {
const {Feature}View({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('{Feature}')),
body: BlocBuilder<{Feature}ListBloc, {Feature}ListState>(
builder: (context, state) {
return switch (state) {
{Feature}ListInitial() => const SizedBox.shrink(),
{Feature}ListLoading() => const Center(child: CircularProgressIndicator()),
{Feature}ListLoaded(:final {entity}s) => ListView.builder(...),
{Feature}ListError(:final message) => Center(child: Text('์ค๋ฅ: $message')),
};
},
),
);
}
}
7. Widget Generation (super.key last)#
class {Entity}Card extends StatelessWidget {
const {Entity}Card({
required this.{entity},
this.onTap,
super.key, // โ
Always last
});
final {Entity} {entity};
final VoidCallback? onTap;
@override
Widget build(BuildContext context) { ... }
}
8. Widget Test Generation#
testWidgets('{์ํฐํฐ} ๋ชฉ๋ก ํ์', (tester) async {
when(() => mockRepository.get{Entity}s(...))
.thenAnswer((_) async => Right(testResult));
await tester.pumpWidget(const MaterialApp(home: {Feature}Page()));
await tester.pumpAndSettle();
expect(find.byType({Entity}Card), findsNWidgets(2));
});
9. Route Generation#
@TypedGoRoute<{Feature}Route>(path: '/{feature}')
class {Feature}Route extends GoRouteData with ${Feature}Route {
const {Feature}Route();
static RouteBase get base => ${feature}Route;
@override
MaterialPage<void> buildPage(BuildContext context, GoRouterState state) {
return const MaterialPage<void>(child: {Feature}Page());
}
}
abstract class {Feature}RouteName {
static const String path = '/{feature}';
}
10. Generate Widgetbook UseCase#
@widgetbook.UseCase(name: 'Default', type: {Entity}Card)
Widget build{Entity}CardUseCase(BuildContext context) {
return {Entity}Card(
{entity}: const {Entity}(id: 1, title: 'ํ
์คํธ', ...),
);
}
11. BDD Test Integration (Optional)#
/bdd:generate generated separately via command:
# Generate BDD tests
/bdd:generate {feature_name} --location {location}
Generation๋๋ BDD File:
feature/{location}/{feature_name}/test/src/bdd/
โโโ {feature}_list.feature
โโโ {feature}_detail.feature
โโโ {feature}_form.feature
โโโ step/
โ โโโ {feature}_steps.dart
โโโ hooks/
โโโ hooks.dart
BDD Scenario Examples:
Feature: {feature} List # {feature} ๋ชฉ๋ก
As a user, I want to view the {feature} list. # ์ฌ์ฉ์๋ก์ {feature} ๋ชฉ๋ก์ ๋ณด๊ณ ์ถ์ต๋๋ค
@smoke
Scenario: List loads successfully # ๋ชฉ๋ก ๋ก๋ฉ ์ฑ๊ณต
Given the app is running # ์ฑ์ด ์คํ ์ค์
๋๋ค
When I navigate to the {feature} page # {feature} ํ์ด์ง๋ก ์ด๋ํฉ๋๋ค
Then the {feature} list is displayed # {feature} ๋ชฉ๋ก์ด ๋ณด์
๋๋ค
Output Files#
feature/{location}/{feature_name}/lib/src/presentation/
โโโ bloc/{feature}_list/
โ โโโ {feature}_list_bloc.dart
โ โโโ {feature}_list_event.dart
โ โโโ {feature}_list_state.dart
โโโ page/
โ โโโ {feature}_page.dart
โโโ widget/
โ โโโ {entity}_card.dart
โโโ route/
โโโ {feature}_route.dart
feature/{location}/{feature_name}/test/
โโโ presentation/
โ โโโ bloc/{feature}_list_bloc_test.dart
โ โโโ page/{feature}_page_test.dart
โโโ src/bdd/ # BDD ํ
์คํธ (์ ํ)
โโโ {feature}_list.feature
โโโ {feature}_detail.feature
โโโ {feature}_form.feature
โโโ step/{feature}_steps.dart
โโโ hooks/hooks.dart
app/petmedi_widgetbook/lib/src/{feature_name}/
โโโ {entity}_card_use_case.dart
MCP Integration#
- Serena: Existing Presentation Layer Pattern Analysis
- Context7: BLoC, GoRouter Document reference
- Magic (21st.dev): UI ์ปดํฌ๋ํธ Generation, ์ ๊ทผ์ฑ ๊ฒ์ฌ
Reference Agents#
Detailed implementation rules in ~/.claude/commands/agents/presentation-layer-agent.md reference
Core Rules Summary#
โ BLoC Pattern#
// Event: sealed class + factory + private implementation
sealed class {Feature}ListEvent {
const factory {Feature}ListEvent.loadRequested() = _LoadRequested;
}
final class _LoadRequested extends {Feature}ListEvent {
const _LoadRequested();
}
// BLoC: Optional Constructor Injection
class {Feature}ListBloc extends Bloc<...> {
{Feature}ListBloc({
Get{Entity}sUsecase? get{Entity}sUsecase,
}) : _get{Entity}sUsecase = get{Entity}sUsecase ?? const Get{Entity}sUsecase(),
super(...);
final Get{Entity}sUsecase _get{Entity}sUsecase;
Future<void> _onLoadRequested(...) async {
final result = await _get{Entity}sUsecase(params);
if (!isClosed) { emit(...); } // โ
Closed check
}
}
โ Prohibited Patterns#
// โ getIt usage prohibited
// create: (_) => getIt<{Feature}Bloc>(),
// โ Key? key ํจํด ๊ธ์ง
// const {Entity}Card({Key? key}) : super(key: key);
Widget super.key Position#
const {Entity}Card({
required this.{entity}, // required ๋จผ์
this.onTap, // optional ๋ค์
super.key, // โ
super.key ๋ง์ง๋ง
});