import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; /// 표준 데이터 테이블 컬럼 정의 class StandardDataColumn { final String label; final double? width; final int? flex; final bool isNumeric; final TextAlign textAlign; final bool sortable; final VoidCallback? onSort; StandardDataColumn({ required this.label, this.width, this.flex, this.isNumeric = false, TextAlign? textAlign, this.sortable = false, this.onSort, }) : textAlign = textAlign ?? (isNumeric ? TextAlign.right : TextAlign.left); } /// shadcn/ui 기반 표준 데이터 테이블 위젯 /// /// 헤더 고정 + 바디 스크롤 패턴 지원 /// 모든 리스트 화면에서 일관된 테이블 스타일 제공 class StandardDataTable extends StatelessWidget { final List columns; final List rows; final bool showCheckbox; final bool? isAllSelected; final ValueChanged? onSelectAll; final bool enableHorizontalScroll; final ScrollController? horizontalScrollController; final ScrollController? verticalScrollController; final Widget? emptyWidget; final bool applyZebraStripes; // 짝수 행 배경색 적용 여부 final double headerHeight; final double? maxHeight; final bool fixedHeader; // 헤더 고정 여부 final String emptyMessage; final IconData emptyIcon; const StandardDataTable({ super.key, required this.columns, required this.rows, this.showCheckbox = false, this.isAllSelected, this.onSelectAll, this.enableHorizontalScroll = false, this.horizontalScrollController, this.verticalScrollController, this.emptyWidget, this.applyZebraStripes = true, this.headerHeight = 56.0, this.maxHeight, this.fixedHeader = true, this.emptyMessage = '데이터가 없습니다', this.emptyIcon = Icons.inbox_outlined, }); @override Widget build(BuildContext context) { if (rows.isEmpty) { return _buildEmptyState(); } // 헤더 고정 패턴 if (fixedHeader) { return _buildFixedHeaderTable(); } // 일반 테이블 return _buildRegularTable(); } /// 헤더 고정 테이블 (추천) Widget _buildFixedHeaderTable() { final content = ShadCard( child: Column( children: [ // 고정 헤더 _buildHeader(), // 스크롤 가능한 바디 Expanded( child: _buildScrollableBody(), ), ], ), ); if (maxHeight != null) { return SizedBox(height: maxHeight, child: content); } return content; } /// 일반 테이블 (하위 호환성) Widget _buildRegularTable() { final content = ShadCard( child: Column( children: [ _buildHeader(), ...rows, ], ), ); if (enableHorizontalScroll) { return SingleChildScrollView( scrollDirection: Axis.horizontal, controller: horizontalScrollController, child: content, ); } return content; } /// 스크롤 가능한 바디 영역 Widget _buildScrollableBody() { Widget scrollableContent = ListView.builder( controller: verticalScrollController, itemCount: rows.length, itemBuilder: (context, index) { final row = rows[index]; // 짝수 행 배경색 적용 if (applyZebraStripes && index.isEven) { return Container( color: ShadcnTheme.muted.withValues(alpha: 0.3), child: row, ); } return row; }, ); if (enableHorizontalScroll) { scrollableContent = SingleChildScrollView( scrollDirection: Axis.horizontal, controller: horizontalScrollController, child: Column(children: rows), ); } return scrollableContent; } Widget _buildHeader() { return Container( height: headerHeight, padding: const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing4, vertical: ShadcnTheme.spacing3, ), decoration: BoxDecoration( color: ShadcnTheme.muted.withValues(alpha: 0.5), border: const Border( bottom: BorderSide(color: ShadcnTheme.border, width: 1), ), ), child: Row( children: [ // 체크박스 컬럼 if (showCheckbox) SizedBox( width: 40, child: Checkbox( value: isAllSelected, onChanged: onSelectAll, tristate: false, ), ), // 데이터 컬럼들 ...columns.map((column) { Widget child = _buildHeaderCell(column); if (column.width != null) { return SizedBox( width: column.width, child: child, ); } else if (column.flex != null) { return Expanded( flex: column.flex!, child: child, ); } else { return Expanded(child: child); } }), ], ), ); } /// 헤더 셀 구성 Widget _buildHeaderCell(StandardDataColumn column) { Widget content = Text( column.label, style: ShadcnTheme.labelMedium.copyWith( fontWeight: FontWeight.w600, color: ShadcnTheme.foreground, ), textAlign: column.textAlign, ); // 정렬 기능이 있는 경우 if (column.sortable && column.onSort != null) { content = InkWell( onTap: column.onSort, borderRadius: BorderRadius.circular(ShadcnTheme.radiusSm), child: Padding( padding: const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing2, vertical: ShadcnTheme.spacing1, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible(child: content), const SizedBox(width: ShadcnTheme.spacing1), Icon( Icons.unfold_more, size: 16, color: ShadcnTheme.foregroundMuted, ), ], ), ), ); } return content; } Widget _buildEmptyState() { if (emptyWidget != null) { return emptyWidget!; } return ShadCard( child: Container( width: double.infinity, padding: const EdgeInsets.all(ShadcnTheme.spacing8), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( emptyIcon, size: 48, color: ShadcnTheme.mutedForeground, ), const SizedBox(height: ShadcnTheme.spacing4), Text( emptyMessage, style: ShadcnTheme.bodyMuted, ), ], ), ), ), ); } } /// 표준 데이터 행 위젯 class StandardDataRow extends StatelessWidget { final int index; final List cells; final bool showCheckbox; final bool? isSelected; final ValueChanged? onSelect; final bool applyZebraStripes; final List columns; const StandardDataRow({ super.key, required this.index, required this.cells, this.showCheckbox = false, this.isSelected, this.onSelect, this.applyZebraStripes = true, required this.columns, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing4, vertical: 4, ), decoration: BoxDecoration( color: applyZebraStripes && index % 2 == 1 ? ShadcnTheme.muted.withValues(alpha: 0.1) : null, border: Border( bottom: BorderSide(color: Colors.black), ), ), child: Row( children: [ // 체크박스 if (showCheckbox) SizedBox( width: 40, child: Checkbox( value: isSelected, onChanged: onSelect, tristate: false, ), ), // 데이터 셀들 ...cells.asMap().entries.map((entry) { final cellIndex = entry.key; final cell = entry.value; if (cellIndex >= columns.length) return const SizedBox.shrink(); final column = columns[cellIndex]; if (column.width != null) { return SizedBox( width: column.width, child: cell, ); } else if (column.flex != null) { return Expanded( flex: column.flex!, child: cell, ); } else { return Expanded(child: cell); } }), ], ), ); } } /// 표준 관리 버튼 세트 class StandardActionButtons extends StatelessWidget { final VoidCallback? onView; final VoidCallback? onEdit; final VoidCallback? onDelete; final List? customButtons; final double buttonSize; const StandardActionButtons({ super.key, this.onView, this.onEdit, this.onDelete, this.customButtons, this.buttonSize = 32, }); @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ if (onView != null) _buildIconButton( Icons.visibility_outlined, onView!, '보기', ShadcnTheme.primary, ), if (onEdit != null) _buildIconButton( Icons.edit_outlined, onEdit!, '수정', ShadcnTheme.primary, ), if (onDelete != null) _buildIconButton( Icons.delete_outline, onDelete!, '삭제', ShadcnTheme.destructive, ), if (customButtons != null) ...customButtons!, ], ); } Widget _buildIconButton( IconData icon, VoidCallback onPressed, String tooltip, Color color, ) { return IconButton( constraints: BoxConstraints( minWidth: buttonSize, minHeight: buttonSize, maxWidth: buttonSize, maxHeight: buttonSize, ), padding: const EdgeInsets.all(4), icon: Icon(icon, size: 16, color: color), onPressed: onPressed, tooltip: tooltip, ); } }