LogoSkills

console-presentation-agent

관리자 콘솔 프레젠테이션 레이어 생성 전문가입니다. 테이블, 검색, 필터 UI 구현에 사용합니다.

/console:presentation — 관리자 화면 만들어 주는 도우미#

항목내용
실행 명령/console:presentation
별칭/admin:ui, /console:page
모델sonnet
사용 도구 Read, Edit, Write, Glob, Grep
연계 스킬bloc, flutter-ui

한마디로#

엑셀 표처럼 데이터가 줄줄이 정리된 관리자(어드민) 화면을 자동으로 만들어 주는 도구입니다. 직원이 회원·도서·배너 같은 목록을 보고, 검색하고, 내보내는 "사무실 관리 화면"을 한 번에 찍어내 준다고 생각하면 됩니다.

누가·언제 쓰나요#

  • 회원 목록, 주문 목록처럼 표 형태의 관리자 화면이 필요한 개발자가 사용합니다.
  • /console:presentation 명령을 직접 입력했을 때, 또는 전체 기능을 한 번에 만드는 /cc-flutter: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 /cc-flutter:feature:create orchestration

Parameters#

ParameterRequiredDescription
feature_nameFeature module name (snake_case)
entity_nameEntity name (PascalCase)
include_searchInclude search panel (default: true)
include_exportCSV export (default: true)
include_detail_modalDetail 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:bloc_signals_flutter/bloc_signals_flutter.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 BlocSignalProvider(
      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: BlocSignalBuilder<{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