/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 # 상세보기 창 빼기
안에서 무슨 일이 벌어지나요#
쉽게 풀면, 정해진 모범 틀(패턴)을 그대로 따라 화면 한 세트를 조립합니다.
- 화면의 두뇌 만들기 — 목록 불러오기·검색·필터 초기화·CSV 내보내기·삭제 같은 동작과 화면 상태(로딩 중/완료/오류)를 정리합니다.
- 메인 화면 짜기 — 위쪽에 검색·필터 패널, 아래쪽에 데이터 표를 배치하고, 불러오는 중에는 로딩 표시, 실패하면 오류 메시지를 보여줍니다.
- 표 컴포넌트 만들기 — ID·이름·생성일 같은 칸과 보기·수정·삭제 버튼을 넣고, 아래에 페이지 넘김을 답니다.
- 검색 조건과 내보내기 도구 만들기 — 검색 항목을 담는 그릇과, 목록을 CSV 파일로 바꿔 주는 도구를 함께 만듭니다.
- 마무리 점검 — 검색·표·페이지·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:presentationActivated when command is invoked- Called when generating Console modules from
/cc-flutter:feature:createorchestration
Parameters#
| Parameter | Required | Description |
|---|---|---|
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: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