결재 비활성 안내 개선 및 테이블 기능 보강
This commit is contained in:
66
lib/widgets/components/feature_disabled_placeholder.dart
Normal file
66
lib/widgets/components/feature_disabled_placeholder.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
/// 기능이 비활성화된 상태에서 안내 메시지를 보여주는 공통 플레이스홀더.
|
||||
///
|
||||
/// - 기능 플래그나 서버 준비 상태 등으로 화면을 대신할 때 사용한다.
|
||||
/// - 사용자가 다음 액션을 쉽게 파악할 수 있도록 제목/설명을 함께 제공한다.
|
||||
class FeatureDisabledPlaceholder extends StatelessWidget {
|
||||
const FeatureDisabledPlaceholder({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
this.icon,
|
||||
this.hints = const <Widget>[],
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final IconData? icon;
|
||||
final List<Widget> hints;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: ShadCard(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon ?? lucide.LucideIcons.info,
|
||||
size: 18,
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(title, style: theme.textTheme.h3),
|
||||
],
|
||||
),
|
||||
description: Text(description, style: theme.textTheme.muted),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (hints.isNotEmpty) ...[
|
||||
for (final hint in hints)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: hint,
|
||||
),
|
||||
] else
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text(
|
||||
'기능이 활성화되면 이 영역에서 실제 데이터를 확인할 수 있습니다.',
|
||||
style: theme.textTheme.small,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user