LogoSkills

console-presentation-agent

Admin console Presentation Layer generation specialist. Used for table, search, and filter UI implementation

항ëĒŠë‚´ėšŠ
Invoke/console:presentation
Aliases/admin:ui, /console:page
ToolsRead, Edit, Write, Glob, Grep
Modelsonnet
Skillsbloc, flutter-ui

Console Presentation Agent#

Specialized agent for generating admin console Presentation Layer


Role#

Generates Presentation Layer for the admin console.

  • Table-based list UI (CoUI Table)
  • Search/filter panel configuration
  • Pagination pattern
  • CSV export utility
  • Modal dialogs (detail view, edit)

Activation Conditions#

  • /console:presentation Activated when command is invoked
  • Called when generating Console modules from /feature:create orchestration

Parameters#

ParameterRequiredDescription
feature_name✅Feature module name (snake_case)
entity_name✅Entity name (PascalCase)
include_search❌Include search panel (default: true)
include_export❌CSV export (default: true)
include_detail_modal❌Detail view modal (default: true)

Generated Files#

feature/console/{console_feature_name}/lib/src/presentation/
├── blocs/
│   └── {feature_name}/
│       ├── {feature_name}_bloc.dart
│       ├── {feature_name}_event.dart
│       └── {feature_name}_state.dart
├── pages/
│   ├── {feature_name}_page.dart           # ëŠ”ė¸ íŽ˜ė´ė§€
│   ├── {feature_name}_detail_page.dart    # ėƒė„¸ íŽ˜ė´ė§€ (ė˜ĩė…˜)
│   └── components/
│       ├── {feature_name}_table.dart      # í…Œė´ë¸” ėģ´íŦ넌트
│       ├── {feature_name}_filter_panel.dart # 필터 패널
│       └── {feature_name}_detail_modal.dart # ėƒė„¸ëŗ´ę¸° ëǍë‹Ŧ
└── utils/
    ├── utils.dart
    ├── csv_formatter.dart                 # CSV ë‚´ëŗ´ë‚´ę¸°
    └── {feature_name}_search_params.dart  # ę˛€ėƒ‰ 파ëŧ미터

Import Order (Required)#

// 1. Flutter/Dart standard
import 'package:flutter/material.dart';

// 2. State management
import 'package:flutter_bloc/flutter_bloc.dart';

// 3. UI Kit (CoUI)
import 'package:resources/resources.dart';

// 4. Common dependencies
import 'package:dependencies/dependencies.dart';

// 5. Feature internal
import '../../blocs/blocs.dart';
import '../../utils/utils.dart';

Core Patterns#

1. Console BLoC Event/State (sealed class pattern)#

part of '{feature_name}_bloc.dart';

@immutable
sealed class {Feature}Event extends Equatable {
  const {Feature}Event();

  const factory {Feature}Event.load({
    int? page,
    int? limit,
    {Feature}SearchParams? searchParams,
  }) = _Load;

  const factory {Feature}Event.search({
    required {Feature}SearchParams params,
  }) = _Search;

  const factory {Feature}Event.resetFilter() = _ResetFilter;

  const factory {Feature}Event.exportCsv() = _ExportCsv;

  const factory {Feature}Event.delete({required int id}) = _Delete;
}

final class _Load extends {Feature}Event {
  const _Load({this.page, this.limit, this.searchParams});
  final int? page;
  final int? limit;
  final {Feature}SearchParams? searchParams;

  @override
  List<Object?> get props => [page, limit, searchParams];
}

// State ė •ė˜
@immutable
sealed class {Feature}State {
  const {Feature}State();
}

@immutable
final class {Feature}Initial extends {Feature}State {
  const {Feature}Initial();
}

@immutable
final class {Feature}Loading extends {Feature}State {
  const {Feature}Loading();
}

@immutable
final class {Feature}Loaded extends {Feature}State {
  const {Feature}Loaded({
    required this.items,
    required this.totalCount,
    required this.currentPage,
    required this.searchParams,
  });

  final List<{Entity}> items;
  final int totalCount;
  final int currentPage;
  final {Feature}SearchParams searchParams;
}

@immutable
final class {Feature}Error extends {Feature}State {
  const {Feature}Error(this.failure);
  final Failure failure;
}

2. Console Page Structure#

class Console{Feature}Page extends StatelessWidget {
  const Console{Feature}Page({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => {Feature}Bloc()
        ..add(const {Feature}Event.load()),
      child: const _Console{Feature}View(),
    );
  }
}

class _Console{Feature}View extends StatelessWidget {
  const _Console{Feature}View();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(context.i10n.{feature}Management),
        actions: [
          // CSV ë‚´ëŗ´ë‚´ę¸° 버íŠŧ
          IconButton(
            onPressed: () => context.read<{Feature}Bloc>()
              .add(const {Feature}Event.exportCsv()),
            icon: const Icon(Icons.file_download),
          ),
        ],
      ),
      body: Column(
        children: [
          // ę˛€ėƒ‰/필터 패널
          const {Feature}FilterPanel(),
          // ë°ė´í„° í…Œė´ë¸”
          Expanded(
            child: BlocBuilder<{Feature}Bloc, {Feature}State>(
              builder: (context, state) {
                return switch (state) {
                  {Feature}Initial() || {Feature}Loading() =>
                    const Center(child: CircularProgressIndicator()),
                  {Feature}Loaded(:final items, :final totalCount, :final currentPage) =>
                    {Feature}Table(
                      items: items,
                      totalCount: totalCount,
                      currentPage: currentPage,
                      onPageChanged: (page) => context.read<{Feature}Bloc>()
                        .add({Feature}Event.load(page: page)),
                    ),
                  {Feature}Error(:final failure) =>
                    Center(child: Text(failure.message)),
                };
              },
            ),
          ),
        ],
      ),
    );
  }
}

3. Table Component#

class {Feature}Table extends StatelessWidget {
  const {Feature}Table({
    required this.items,
    required this.totalCount,
    required this.currentPage,
    required this.onPageChanged,
    super.key,
  });

  final List<{Entity}> items;
  final int totalCount;
  final int currentPage;
  final ValueChanged<int> onPageChanged;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // CoUI data table
        Expanded(
          child: SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            child: DataTable(
              columns: const [
                DataColumn(label: Text('ID')),
                DataColumn(label: Text('ė´ëĻ„')),
                DataColumn(label: Text('ėƒė„ąėŧ')),
                DataColumn(label: Text('ėž‘ė—…')),
              ],
              rows: items.map((item) => _buildRow(context, item)).toList(),
            ),
          ),
        ),
        // Pagination
        ConsolePagination(
          totalCount: totalCount,
          currentPage: currentPage,
          itemsPerPage: 20,
          onPageChanged: onPageChanged,
        ),
      ],
    );
  }

  DataRow _buildRow(BuildContext context, {Entity} item) {
    return DataRow(
      cells: [
        DataCell(Text(item.id.toString())),
        DataCell(Text(item.name)),
        DataCell(Text(DateFormat('yyyy-MM-dd').format(item.createdAt))),
        DataCell(
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              IconButton(
                icon: const Icon(Icons.visibility),
                onPressed: () => _showDetailModal(context, item),
              ),
              IconButton(
                icon: const Icon(Icons.edit),
                onPressed: () => _navigateToEdit(context, item),
              ),
              IconButton(
                icon: const Icon(Icons.delete),
                onPressed: () => _confirmDelete(context, item),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

4. Search Parameters#

class {Feature}SearchParams extends Equatable {
  const {Feature}SearchParams({
    this.keyword,
    this.status,
    this.startDate,
    this.endDate,
  });

  final String? keyword;
  final {Feature}Status? status;
  final DateTime? startDate;
  final DateTime? endDate;

  /// 뚈 파ëŧ미터 ė—Ŧëļ€
  bool get isEmpty =>
      keyword == null &&
      status == null &&
      startDate == null &&
      endDate == null;

  /// copyWith 패턴
  {Feature}SearchParams copyWith({
    String? keyword,
    {Feature}Status? status,
    DateTime? startDate,
    DateTime? endDate,
  }) {
    return {Feature}SearchParams(
      keyword: keyword ?? this.keyword,
      status: status ?? this.status,
      startDate: startDate ?? this.startDate,
      endDate: endDate ?? this.endDate,
    );
  }

  /// ė´ˆę¸°í™”
  static const {Feature}SearchParams empty = {Feature}SearchParams();

  @override
  List<Object?> get props => [keyword, status, startDate, endDate];
}

5. CSV Export#

class {Feature}CsvFormatter {
  const {Feature}CsvFormatter._();

  static String format(List<{Entity}> items) {
    final buffer = StringBuffer();

    // 헤더
    buffer.writeln('ID,ė´ëĻ„,ėƒíƒœ,ėƒė„ąėŧ');

    // ë°ė´í„°
    for (final item in items) {
      buffer.writeln([
        item.id,
        '"${item.name.replaceAll('"', '""')}"', // ė´ėŠ¤ėŧ€ė´í”„
        item.status.name,
        DateFormat('yyyy-MM-dd HH:mm').format(item.createdAt),
      ].join(','));
    }

    return buffer.toString();
  }
}

Reference Files#

feature/console/console_member_list/lib/src/presentation/
feature/console/console_book_list/lib/src/presentation/
feature/console/console_banner_list/lib/src/presentation/
package/resources/lib/src/widgets/table/

Checklist#

  • Apply sealed class Event/State pattern
  • Check isClosed after await
  • Implement search/filter panel
  • Separate table component
  • Implement pagination
  • Implement CSV export
  • Implement detail view modal
  • Loading/error state UI