refactor: UI 일관성 개선 및 테이블 구조 통일

장비관리 화면을 기준으로 전체 화면 UI 일관성 개선:

- 모든 화면 검색바/버튼/드롭다운 높이 40px 통일
- 테이블 헤더 패딩 vertical 10px, 행 패딩 vertical 4px 통일
- 배지 스타일 통일 (border 제거, opacity 0.9 적용)
- 페이지네이션 10개 이하에서도 항상 표시
- 테이블 헤더 폰트 스타일 통일 (fontSize: 13, fontWeight: w500)

각 화면별 수정사항:
1. 장비관리: 컬럼 너비 최적화, 검색 컴포넌트 높이 명시
2. 입고지 관리: 페이지네이션 조건 개선
3. 회사관리: UnifiedSearchBar 통합, 배지 스타일 개선
4. 유지보수: ListView.builder → map() 변경, 테이블 구조 재설계

키포인트 색상을 teal로 통일하여 브랜드 일관성 확보
This commit is contained in:
JiWoong Sul
2025-08-08 18:03:07 +09:00
parent 844c7bd92f
commit b8f10dd588
17 changed files with 2065 additions and 1035 deletions

View File

@@ -0,0 +1,315 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/common/theme_shadcn.dart';
/// 표준 데이터 테이블 컬럼 정의
class DataColumn {
final String label;
final double? width;
final int? flex;
final bool isNumeric;
final TextAlign textAlign;
DataColumn({
required this.label,
this.width,
this.flex,
this.isNumeric = false,
TextAlign? textAlign,
}) : textAlign = textAlign ?? (isNumeric ? TextAlign.right : TextAlign.left);
}
/// 표준 데이터 테이블 위젯
///
/// 모든 리스트 화면에서 일관된 테이블 스타일 제공
class StandardDataTable extends StatelessWidget {
final List<DataColumn> columns;
final List<Widget> rows;
final bool showCheckbox;
final bool? isAllSelected;
final ValueChanged<bool?>? onSelectAll;
final bool enableHorizontalScroll;
final ScrollController? horizontalScrollController;
final Widget? emptyWidget;
final bool applyZebraStripes; // 짝수 행 배경색 적용 여부
const StandardDataTable({
Key? key,
required this.columns,
required this.rows,
this.showCheckbox = false,
this.isAllSelected,
this.onSelectAll,
this.enableHorizontalScroll = false,
this.horizontalScrollController,
this.emptyWidget,
this.applyZebraStripes = true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (rows.isEmpty) {
return _buildEmptyState();
}
final table = Container(
width: double.infinity,
decoration: BoxDecoration(
color: ShadcnTheme.card,
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
border: Border.all(color: Colors.black),
boxShadow: ShadcnTheme.cardShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 테이블 헤더
_buildHeader(),
// 테이블 데이터 행들
...rows,
],
),
);
if (enableHorizontalScroll) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: horizontalScrollController,
child: table,
);
}
return table;
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: ShadcnTheme.spacing4,
vertical: 10,
),
decoration: BoxDecoration(
color: ShadcnTheme.muted.withValues(alpha: 0.3),
border: Border(
bottom: BorderSide(color: Colors.black),
),
),
child: Row(
children: [
// 체크박스 컬럼
if (showCheckbox)
SizedBox(
width: 40,
child: Checkbox(
value: isAllSelected,
onChanged: onSelectAll,
tristate: false,
),
),
// 데이터 컬럼들
...columns.map((column) {
Widget child = Text(
column.label,
style: ShadcnTheme.bodyMedium.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: column.textAlign,
);
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 _buildEmptyState() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(ShadcnTheme.spacing8),
decoration: BoxDecoration(
color: ShadcnTheme.card,
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
border: Border.all(color: Colors.black),
boxShadow: ShadcnTheme.cardShadow,
),
child: emptyWidget ??
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inbox_outlined,
size: 48,
color: ShadcnTheme.muted,
),
const SizedBox(height: ShadcnTheme.spacing4),
Text(
'데이터가 없습니다',
style: ShadcnTheme.bodyMuted,
),
],
),
),
);
}
}
/// 표준 데이터 행 위젯
class StandardDataRow extends StatelessWidget {
final int index;
final List<Widget> cells;
final bool showCheckbox;
final bool? isSelected;
final ValueChanged<bool?>? onSelect;
final bool applyZebraStripes;
final List<DataColumn> columns;
const StandardDataRow({
Key? key,
required this.index,
required this.cells,
this.showCheckbox = false,
this.isSelected,
this.onSelect,
this.applyZebraStripes = true,
required this.columns,
}) : super(key: key);
@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<Widget>? customButtons;
final double buttonSize;
const StandardActionButtons({
Key? key,
this.onView,
this.onEdit,
this.onDelete,
this.customButtons,
this.buttonSize = 32,
}) : super(key: key);
@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,
);
}
}