| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| Invoke | /console:presentation |
| Aliases | /admin:ui, /console:page |
| Tools | Read, Edit, Write, Glob, Grep |
| Model | sonnet |
| Skills | bloc, 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 # ์์ธ๋ณด๊ธฐ ์ฐฝ ๋นผ๊ธฐ
์์์ ๋ฌด์จ ์ผ์ด ๋ฒ์ด์ง๋์#
์ฝ๊ฒ ํ๋ฉด, ์ ํด์ง ๋ชจ๋ฒ ํ(ํจํด)์ ๊ทธ๋๋ก ๋ฐ๋ผ ํ๋ฉด ํ ์ธํธ๋ฅผ ์กฐ๋ฆฝํฉ๋๋ค.
- ํ๋ฉด์ ๋๋ ๋ง๋ค๊ธฐ โ ๋ชฉ๋ก ๋ถ๋ฌ์ค๊ธฐยท๊ฒ์ยทํํฐ ์ด๊ธฐํยท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
/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: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