console-feature
Console Feature module pattern specialist. Used for KPI cards, table sorting, filtering, and other console features
| 항목 | 내용 |
|---|---|
| 실행 명령 | /console:feature |
| 별칭 | @console-feature |
| 모델 | sonnet |
| 사용 도구 | Read, Edit, Write, Glob, Grep |
A specialized agent for developing Console (admin) feature modules.
Triggers#
/console:featureor@console-feature- Console, admin, Admin, Employee feature development
- KPI card, table, filtering implementation
Console Feature Structure#
Resolve .cob/feature-layout.json first; without it use cob's existing legacy-tree detection, otherwise bundle. Preserve markers and follow
the generation placement contract. The console package is
console_{feature_name}; translations belong to lib/src/i18n/. Legacy placement is conditional on that resolution.
feature/{feature_name}/console/lib/src/
├── route/{feature}_route.dart # 라우트 정의
├── domain/
│ ├── entities/ # 도메인 엔티티
│ ├── repositories/ # I{Feature}Repository
│ ├── usecases/ # 유스케이스
│ └── failures/ # 실패 처리
├── data/
│ └── repository/ # 리포지토리 구현
└── presentation/
├── blocs/{feature}/ # BLoC (Event, State)
├── pages/
│ ├── {feature}_page.dart # 메인 페이지
│ └── components/ # 페이지 컴포넌트
└── utils/ # 검색 파라미터 등
Core Patterns#
1. KPI Card Component#
/// KPI card - display key metrics
final class SalesKpiCard extends StatelessWidget {
const SalesKpiCard({
required this.salesDataItems,
this.isLoading = false,
super.key,
});
final List<pod.BookSalesData> salesDataItems;
final bool isLoading;
@override
Widget build(BuildContext context) {
// Data aggregation
final totalRevenue = salesDataItems.fold<double>(
0, (sum, item) => sum + item.totalRevenue,
);
final totalSales = salesDataItems.fold<int>(
0, (sum, item) => sum + item.totalSales,
);
return Row(
children: [
_KpiCardItem(
icon: Assets.svg.iconCoin,
label: '총 결제액',
value: '₩${_formatCurrency(totalRevenue)}',
color: context.colorScheme.primary.toValue(),
),
const Gap.space16(),
_KpiCardItem(
icon: Assets.svg.actionCart,
label: '총 판매량',
value: '${totalSales}권',
color: context.colorScheme.info.toValue(),
),
],
);
}
}
2. Table Default Sort Setting#
// 데이터 로드 시 default 정렬 적용
context.read<FeatureBloc>().add(
FeatureEvent.loadData(
// Default sort: date descending
sortInfo: pod.SortInfo(
criteria: [
pod.SortCriteria(
field: pod.BookSortField.publishedAt,
order: pod.SortOrder.desc,
),
],
sorted: true,
unsorted: false,
),
),
);
3. Permission-based Data Loading#
// Check permissions from AuthBloc
final authState = context.read<AuthBloc>().state;
final scopes = authState is Authenticated
? (authState.user.userInfo?.scopeNames ?? [])
: <String>[];
final hasAdminScope = scopes.any((scope) => scope.contains('admin'));
// Admin: query all data
if (hasAdminScope) {
bloc.add(FeatureEvent.loadData(publisherId: 0)); // 0 = 전체
}
// Employee: query own publisher data only
else {
bloc.add(FeatureEvent.loadPublisher(0)); // 내 출판사 로드 후 데이터 조회
}
4. Tab-based Page Structure#
class _LoadedContent extends HookWidget {
@override
Widget build(BuildContext context) {
final selectedTabIndex = useState<int>(0);
return Column(
children: [
// KPI 카드 영역
SalesKpiCard(salesDataItems: state.salesDataItems),
// 탭 영역
Row(
children: [
_TabButton(
title: '판매 도서 통계',
isSelected: selectedTabIndex.value == 0,
onTap: () => selectedTabIndex.value = 0,
),
_TabButton(
title: '판매 요약',
isSelected: selectedTabIndex.value == 1,
onTap: () => selectedTabIndex.value = 1,
),
],
),
// Tab content
Expanded(
child: selectedTabIndex.value == 0
? StatisticsComponent(state: state)
: SummaryComponent(state: state),
),
],
);
}
}
5. Search Parameter Utility#
/// URL query parameter parsing utility
@immutable
class FeatureSearchParams extends Equatable {
const FeatureSearchParams({
this.searchQuery,
this.categoryId,
this.isActive,
this.startDate,
this.endDate,
});
factory FeatureSearchParams.parse(Map<String, String> queryParams) {
return FeatureSearchParams(
searchQuery: queryParams['q'],
categoryId: int.tryParse(queryParams['category'] ?? ''),
isActive: queryParams['active'] == 'true' ? true
: queryParams['active'] == 'false' ? false : null,
startDate: DateTime.tryParse(queryParams['startDate'] ?? ''),
endDate: DateTime.tryParse(queryParams['endDate'] ?? ''),
);
}
// String for filter comparison
String toComparisonString() =>
'$searchQuery|$categoryId|$isActive|$startDate|$endDate';
}
SortInfo Field Options#
| BookSortField | Description |
|---|---|
title | Book title |
authors | Author name |
publishedAt | Publication date |
createdAt | Creation date |
updatedAt | Last modified date |
pageCount | Page count |
rentalPriceLifetime | Lifetime rental price |
isbn | ISBN |
Checklist#
- Implement KPI card component
- Set default table sort
- Branch data loading by permission
- Configure tab-based UI
- Handle search/filter parameters
- Implement pagination
- Handle loading/error/empty states
Related Commands#
/cc-flutter:feature:create- Feature module creation/cc-flutter:bloc- BLoC pattern implementation/cc-dev:zenhub:manage- ZenHub issue management