문서 목차
이 페이지에서목차

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:feature or @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#

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

검색어를 입력하면 문서를 찾습니다.

↑ ↓ 이동 · Enter 열기Esc 닫기