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 โ€” ๊ด€๋ฆฌ์ž ํ™”๋ฉด ๋งŒ๋“ค์–ด ์ฃผ๋Š” ๋„์šฐ๋ฏธ#

ํ•œ๋งˆ๋””๋กœ#

์—‘์…€ ํ‘œ์ฒ˜๋Ÿผ ๋ฐ์ดํ„ฐ๊ฐ€ ์ค„์ค„์ด ์ •๋ฆฌ๋œ ๊ด€๋ฆฌ์ž(์–ด๋“œ๋ฏผ) ํ™”๋ฉด์„ ์ž๋™์œผ๋กœ ๋งŒ๋“ค์–ด ์ฃผ๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ์ง์›์ด ํšŒ์›ยท๋„์„œยท๋ฐฐ๋„ˆ ๊ฐ™์€ ๋ชฉ๋ก์„ ๋ณด๊ณ , ๊ฒ€์ƒ‰ํ•˜๊ณ , ๋‚ด๋ณด๋‚ด๋Š” "์‚ฌ๋ฌด์‹ค ๊ด€๋ฆฌ ํ™”๋ฉด"์„ ํ•œ ๋ฒˆ์— ์ฐ์–ด๋‚ด ์ค€๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๋ˆ„๊ฐ€ยท์–ธ์ œ ์“ฐ๋‚˜์š”#

  • ํšŒ์› ๋ชฉ๋ก, ์ฃผ๋ฌธ ๋ชฉ๋ก์ฒ˜๋Ÿผ ํ‘œ ํ˜•ํƒœ์˜ ๊ด€๋ฆฌ์ž ํ™”๋ฉด์ด ํ•„์š”ํ•œ ๊ฐœ๋ฐœ์ž๊ฐ€ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • /console:presentation ๋ช…๋ น์„ ์ง์ ‘ ์ž…๋ ฅํ–ˆ์„ ๋•Œ, ๋˜๋Š” ์ „์ฒด ๊ธฐ๋Šฅ์„ ํ•œ ๋ฒˆ์— ๋งŒ๋“œ๋Š” /feature:create ๊ณผ์ •์—์„œ ๊ด€๋ฆฌ์ž ํ™”๋ฉด ๋ถ€๋ถ„์„ ๋งŒ๋“ค ๋•Œ ์ž๋™์œผ๋กœ ๋ถˆ๋ ค ๋‚˜์˜ต๋‹ˆ๋‹ค.

๋ฌด์—‡์„ ํ•ด์ฃผ๋‚˜์š”#

๊ด€๋ฆฌ์ž ํ™”๋ฉด์— ํ•„์š”ํ•œ ํŒŒ์ผ ๋ฌถ์Œ์„ ์ž๋™์œผ๋กœ ๋งŒ๋“ค์–ด ์ค๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ๋งŒ๋“ค์–ด์ง€๋Š” ๊ฒƒ๋“ค:

  • ๋ชฉ๋ก ํ™”๋ฉด(table) โ€” ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ๋กœ ๋ณด์—ฌ์ฃผ๊ณ , ๋ณด๊ธฐยท์ˆ˜์ •ยท์‚ญ์ œ ๋ฒ„ํŠผ์ด ๋‹ฌ๋ ค ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๊ฒ€์ƒ‰ยทํ•„ํ„ฐ ํŒจ๋„(filter panel) โ€” ํ‚ค์›Œ๋“œยท์ƒํƒœยท๊ธฐ๊ฐ„์œผ๋กœ ์›ํ•˜๋Š” ํ•ญ๋ชฉ๋งŒ ์ถ”๋ ค ๋ด…๋‹ˆ๋‹ค.
  • ํŽ˜์ด์ง€ ๋„˜๊ธฐ๊ธฐ(pagination) โ€” ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์„ ๋•Œ ์—ฌ๋Ÿฌ ์ชฝ์œผ๋กœ ๋‚˜๋ˆ  ๋ด…๋‹ˆ๋‹ค.
  • CSV ๋‚ด๋ณด๋‚ด๊ธฐ(csv_formatter) โ€” ํ™”๋ฉด์˜ ๋ชฉ๋ก์„ ์—‘์…€์—์„œ ์—ด ์ˆ˜ ์žˆ๋Š” ํŒŒ์ผ๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  • ์ƒ์„ธ๋ณด๊ธฐยท์ˆ˜์ • ์ฐฝ(detail modal) โ€” ํ•ญ๋ชฉ ํ•˜๋‚˜๋ฅผ ๋ˆŒ๋Ÿฌ ์ž์„ธํžˆ ๋ณด๊ฑฐ๋‚˜ ๊ณ ์นฉ๋‹ˆ๋‹ค.

์ด ๋ชจ๋“  ํŒŒ์ผ์€ feature/console/... ํด๋” ์•„๋ž˜ presentation ์˜์—ญ์— ์ •๋ฆฌ๋˜์–ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

์–ด๋–ป๊ฒŒ ์“ฐ๋‚˜์š”#

# ๊ธฐ๋ณธ: ๊ธฐ๋Šฅ ์ด๋ฆ„(์†Œ๋ฌธ์ž_์–ธ๋”๋ฐ”)๊ณผ ์—”ํ‹ฐํ‹ฐ ์ด๋ฆ„(๋Œ€๋ฌธ์ž ์‹œ์ž‘)๋งŒ ์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค
/console:presentation feature_name=member entity_name=Member

# ๋ณ„์นญ(๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๋ถ€๋ฅด๋Š” ๋‹ค๋ฅธ ์ด๋ฆ„)
/admin:ui ...
/console:page ...

ํ•„์š” ์—†๋Š” ๋ถ€๋ถ„์€ ๊บผ์„œ ๋” ๋‹จ์ˆœํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋‘ ๊ธฐ๋ณธ๊ฐ’์€ "์ผœ์ง"์ด๋ผ, ๋„๊ณ  ์‹ถ์„ ๋•Œ๋งŒ ์ ์œผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

include_search=false        # ๊ฒ€์ƒ‰ ํŒจ๋„ ๋นผ๊ธฐ
include_export=false        # CSV ๋‚ด๋ณด๋‚ด๊ธฐ ๋นผ๊ธฐ
include_detail_modal=false  # ์ƒ์„ธ๋ณด๊ธฐ ์ฐฝ ๋นผ๊ธฐ

์•ˆ์—์„œ ๋ฌด์Šจ ์ผ์ด ๋ฒŒ์–ด์ง€๋‚˜์š”#

์‰ฝ๊ฒŒ ํ’€๋ฉด, ์ •ํ•ด์ง„ ๋ชจ๋ฒ” ํ‹€(ํŒจํ„ด)์„ ๊ทธ๋Œ€๋กœ ๋”ฐ๋ผ ํ™”๋ฉด ํ•œ ์„ธํŠธ๋ฅผ ์กฐ๋ฆฝํ•ฉ๋‹ˆ๋‹ค.

  1. ํ™”๋ฉด์˜ ๋‘๋‡Œ ๋งŒ๋“ค๊ธฐ โ€” ๋ชฉ๋ก ๋ถˆ๋Ÿฌ์˜ค๊ธฐยท๊ฒ€์ƒ‰ยทํ•„ํ„ฐ ์ดˆ๊ธฐํ™”ยทCSV ๋‚ด๋ณด๋‚ด๊ธฐยท์‚ญ์ œ ๊ฐ™์€ ๋™์ž‘๊ณผ ํ™”๋ฉด ์ƒํƒœ(๋กœ๋”ฉ ์ค‘/์™„๋ฃŒ/์˜ค๋ฅ˜)๋ฅผ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  2. ๋ฉ”์ธ ํ™”๋ฉด ์งœ๊ธฐ โ€” ์œ„์ชฝ์— ๊ฒ€์ƒ‰ยทํ•„ํ„ฐ ํŒจ๋„, ์•„๋ž˜์ชฝ์— ๋ฐ์ดํ„ฐ ํ‘œ๋ฅผ ๋ฐฐ์น˜ํ•˜๊ณ , ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์—๋Š” ๋กœ๋”ฉ ํ‘œ์‹œ, ์‹คํŒจํ•˜๋ฉด ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.
  3. ํ‘œ ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค๊ธฐ โ€” IDยท์ด๋ฆ„ยท์ƒ์„ฑ์ผ ๊ฐ™์€ ์นธ๊ณผ ๋ณด๊ธฐยท์ˆ˜์ •ยท์‚ญ์ œ ๋ฒ„ํŠผ์„ ๋„ฃ๊ณ , ์•„๋ž˜์— ํŽ˜์ด์ง€ ๋„˜๊น€์„ ๋‹ต๋‹ˆ๋‹ค.
  4. ๊ฒ€์ƒ‰ ์กฐ๊ฑด๊ณผ ๋‚ด๋ณด๋‚ด๊ธฐ ๋„๊ตฌ ๋งŒ๋“ค๊ธฐ โ€” ๊ฒ€์ƒ‰ ํ•ญ๋ชฉ์„ ๋‹ด๋Š” ๊ทธ๋ฆ‡๊ณผ, ๋ชฉ๋ก์„ CSV ํŒŒ์ผ๋กœ ๋ฐ”๊ฟ” ์ฃผ๋Š” ๋„๊ตฌ๋ฅผ ํ•จ๊ป˜ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  5. ๋งˆ๋ฌด๋ฆฌ ์ ๊ฒ€ โ€” ๊ฒ€์ƒ‰ยทํ‘œยทํŽ˜์ด์ง€ยทCSVยท์ƒ์„ธ๋ณด๊ธฐยท์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๊ฐ€ ๋น ์ง์—†์ด ๋“ค์–ด๊ฐ”๋Š”์ง€ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋กœ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

โš™๏ธ ์ƒ์„ธ ์˜ต์…˜ยท์‹คํ–‰ ๋ช…์„ธ (๊ฐœ๋ฐœ์ž / AI ์—์ด์ „ํŠธ์šฉ)

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