결재 비활성 안내 개선 및 테이블 기능 보강

This commit is contained in:
JiWoong Sul
2025-09-29 15:49:06 +09:00
parent fef7108479
commit 98724762ec
18 changed files with 1134 additions and 297 deletions

View File

@@ -1,8 +1,37 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
import 'package:shadcn_ui/shadcn_ui.dart';
/// 테이블 정렬 상태 정보를 보관하는 모델.
class SuperportTableSortState {
const SuperportTableSortState({
required this.columnIndex,
required this.ascending,
});
final int columnIndex;
final bool ascending;
}
/// 테이블 페이지네이션 정보를 보관하는 모델.
class SuperportTablePagination {
const SuperportTablePagination({
required this.currentPage,
required this.totalPages,
required this.totalItems,
required this.pageSize,
this.pageSizeOptions = const <int>[10, 20, 50],
});
final int currentPage;
final int totalPages;
final int totalItems;
final int pageSize;
final List<int> pageSizeOptions;
}
/// ShadTable.list를 감싼 공통 테이블 래퍼.
class SuperportTable extends StatelessWidget {
const SuperportTable({
@@ -14,6 +43,13 @@ class SuperportTable extends StatelessWidget {
this.maxHeight,
this.onRowTap,
this.emptyLabel = '데이터가 없습니다.',
this.sortableColumns,
this.sortState,
this.onSortChanged,
this.pagination,
this.onPageChange,
this.onPageSizeChange,
this.isLoading = false,
}) : _columns = columns,
_rows = rows,
_headerCells = null,
@@ -28,6 +64,13 @@ class SuperportTable extends StatelessWidget {
this.maxHeight,
this.onRowTap,
this.emptyLabel = '데이터가 없습니다.',
this.sortableColumns,
this.sortState,
this.onSortChanged,
this.pagination,
this.onPageChange,
this.onPageSizeChange,
this.isLoading = false,
}) : _columns = null,
_rows = null,
_headerCells = header,
@@ -42,6 +85,13 @@ class SuperportTable extends StatelessWidget {
final double? maxHeight;
final void Function(int index)? onRowTap;
final String emptyLabel;
final Set<int>? sortableColumns;
final SuperportTableSortState? sortState;
final void Function(int columnIndex, bool ascending)? onSortChanged;
final SuperportTablePagination? pagination;
final void Function(int page)? onPageChange;
final void Function(int pageSize)? onPageSizeChange;
final bool isLoading;
@override
Widget build(BuildContext context) {
@@ -60,7 +110,7 @@ class SuperportTable extends StatelessWidget {
if (header == null) {
throw StateError('header cells must not be null when using fromCells');
}
headerCells = header;
headerCells = [...header];
tableRows = rows;
} else {
final rows = _rows;
@@ -71,13 +121,33 @@ class SuperportTable extends StatelessWidget {
child: Center(child: Text(emptyLabel, style: theme.textTheme.muted)),
);
}
headerCells = _columns!
.map(
(cell) => cell is ShadTableCell
? cell
: ShadTableCell.header(child: cell),
)
.toList();
final columns = _columns!;
final sortable = sortableColumns ?? const <int>{};
headerCells = <ShadTableCell>[];
for (var i = 0; i < columns.length; i++) {
final columnWidget = columns[i];
if (columnWidget is ShadTableCell) {
headerCells.add(columnWidget);
continue;
}
final shouldAttachSorter =
onSortChanged != null && sortable.contains(i);
final headerChild = shouldAttachSorter
? _SortableHeader(
isActive: sortState?.columnIndex == i,
ascending: sortState?.ascending ?? true,
onTap: () {
final isActive = sortState?.columnIndex == i;
final nextAscending = isActive
? !(sortState?.ascending ?? true)
: true;
onSortChanged!(i, nextAscending);
},
child: columnWidget,
)
: columnWidget;
headerCells.add(ShadTableCell.header(child: headerChild));
}
tableRows = [
for (final row in rows)
row
@@ -98,7 +168,7 @@ class SuperportTable extends StatelessWidget {
: math.min(estimatedHeight, maxHeight!),
);
return SizedBox(
final tableView = SizedBox(
height: effectiveHeight,
child: ShadTable.list(
header: headerCells,
@@ -109,5 +179,140 @@ class SuperportTable extends StatelessWidget {
children: tableRows,
),
);
final pagination = this.pagination;
if (pagination == null) {
return tableView;
}
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
tableView,
const SizedBox(height: 12),
_PaginationFooter(
pagination: pagination,
onPageChange: onPageChange,
onPageSizeChange: onPageSizeChange,
isLoading: isLoading,
),
],
);
}
}
class _SortableHeader extends StatelessWidget {
const _SortableHeader({
required this.child,
required this.onTap,
required this.isActive,
required this.ascending,
});
final Widget child;
final VoidCallback onTap;
final bool isActive;
final bool ascending;
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final icon = isActive
? (ascending
? lucide.LucideIcons.arrowUp
: lucide.LucideIcons.arrowDown)
: lucide.LucideIcons.arrowUpDown;
final color = isActive
? theme.colorScheme.foreground
: theme.colorScheme.mutedForeground;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(6),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(child: child),
const SizedBox(width: 8),
Icon(icon, size: 14, color: color),
],
),
),
);
}
}
class _PaginationFooter extends StatelessWidget {
const _PaginationFooter({
required this.pagination,
required this.onPageChange,
required this.onPageSizeChange,
required this.isLoading,
});
final SuperportTablePagination pagination;
final void Function(int page)? onPageChange;
final void Function(int pageSize)? onPageSizeChange;
final bool isLoading;
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final currentPage = pagination.currentPage.clamp(1, pagination.totalPages);
final canGoPrev = currentPage > 1 && !isLoading;
final canGoNext = currentPage < pagination.totalPages && !isLoading;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: 170,
child: ShadSelect<int>(
key: ValueKey(pagination.pageSize),
initialValue: pagination.pageSize,
selectedOptionBuilder: (_, value) => Text('$value개 / 페이지'),
onChanged: isLoading
? null
: (value) {
if (value == null || value == pagination.pageSize) {
return;
}
onPageSizeChange?.call(value);
},
options: [
for (final option in pagination.pageSizeOptions)
ShadOption(value: option, child: Text('$option개 / 페이지')),
],
),
),
Row(
children: [
Text(
'${pagination.totalItems}건 · 페이지 $currentPage / ${pagination.totalPages}',
style: theme.textTheme.small,
),
const SizedBox(width: 12),
ShadButton.ghost(
size: ShadButtonSize.sm,
onPressed: canGoPrev
? () => onPageChange?.call(currentPage - 1)
: null,
child: const Icon(lucide.LucideIcons.chevronLeft, size: 16),
),
const SizedBox(width: 8),
ShadButton.ghost(
size: ShadButtonSize.sm,
onPressed: canGoNext
? () => onPageChange?.call(currentPage + 1)
: null,
child: const Icon(lucide.LucideIcons.chevronRight, size: 16),
),
],
),
],
);
}
}