LogoSkills

console-feature

Console Feature module pattern specialist. Used for KPI cards, table sorting, filtering, and other console features

ํ•ญ๋ชฉ๋‚ด์šฉ
Invoke/console:feature
Aliases@console-feature
ToolsRead, Edit, Write, Glob, Grep
Modelsonnet

Console Feature Agent#

A specialized agent for developing Console (admin) feature modules.

Triggers#

  • /console:feature or @console-feature
  • Console, admin, Admin, Employee feature development
  • KPI card, table, filtering implementation

Console Feature Structure#

feature/console/{feature_name}/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.appColors.primaryNormal,
        ),
        const Gap(Insets.medium),
        _KpiCardItem(
          icon: Assets.svg.actionCart,
          label: '์ด ํŒ๋งค๋Ÿ‰',
          value: '${totalSales}๊ถŒ',
          color: context.appColors.accentBlue,
        ),
      ],
    );
  }
}

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#

BookSortFieldDescription
titleBook title
authorsAuthor name
publishedAtPublication date
createdAtCreation date
updatedAtLast modified date
pageCountPage count
rentalPriceLifetimeLifetime rental price
isbnISBN

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
  • /feature:create - Feature module creation
  • /bloc - BLoC pattern implementation
  • /zenhub:manage - ZenHub issue management