- 전체 371개 파일 중 82개 미사용 파일 식별 - Phase 1: 33개 파일 삭제 예정 (100% 안전) - Phase 2: 30개 파일 삭제 검토 예정 - Phase 3: 19개 파일 수동 검토 예정 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
540 lines
16 KiB
Dart
540 lines
16 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
|
import 'package:superport/models/user_model.dart';
|
|
import 'package:superport/screens/common/theme_shadcn.dart';
|
|
import 'package:superport/screens/common/components/shadcn_components.dart';
|
|
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
|
import 'package:superport/screens/common/widgets/standard_action_bar.dart';
|
|
import 'package:superport/screens/common/widgets/pagination.dart';
|
|
import 'package:superport/screens/user/controllers/user_list_controller.dart';
|
|
import 'package:superport/core/constants/app_constants.dart';
|
|
import 'package:superport/utils/constants.dart';
|
|
|
|
/// shadcn/ui 스타일로 재설계된 사용자 관리 화면
|
|
class UserList extends StatefulWidget {
|
|
const UserList({super.key});
|
|
|
|
@override
|
|
State<UserList> createState() => _UserListState();
|
|
}
|
|
|
|
class _UserListState extends State<UserList> {
|
|
late UserListController _controller;
|
|
final TextEditingController _searchController = TextEditingController();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_controller = UserListController();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_controller.initialize(pageSize: AppConstants.userPageSize);
|
|
});
|
|
|
|
_searchController.addListener(() {
|
|
_onSearchChanged(_searchController.text);
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
_searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Timer? _debounce;
|
|
void _onSearchChanged(String query) {
|
|
if (_debounce?.isActive ?? false) _debounce?.cancel();
|
|
_debounce = Timer(const Duration(milliseconds: 500), () {
|
|
_controller.setSearchQuery(query);
|
|
});
|
|
}
|
|
|
|
/// 사용자 권한 표시 배지
|
|
Widget _buildUserRoleBadge(UserRole role) {
|
|
final roleName = role.displayName;
|
|
ShadcnBadgeVariant variant;
|
|
|
|
switch (role) {
|
|
case UserRole.admin:
|
|
variant = ShadcnBadgeVariant.destructive;
|
|
break;
|
|
case UserRole.manager:
|
|
variant = ShadcnBadgeVariant.primary;
|
|
break;
|
|
case UserRole.staff:
|
|
variant = ShadcnBadgeVariant.secondary;
|
|
break;
|
|
}
|
|
|
|
return ShadcnBadge(
|
|
text: roleName,
|
|
variant: variant,
|
|
size: ShadcnBadgeSize.small,
|
|
);
|
|
}
|
|
|
|
/// 사용자 추가 폼으로 이동
|
|
void _navigateToAdd() async {
|
|
final result = await Navigator.pushNamed(context, Routes.userAdd);
|
|
if (result == true && mounted) {
|
|
_controller.loadUsers(refresh: true);
|
|
}
|
|
}
|
|
|
|
/// 사용자 수정 폼으로 이동
|
|
void _navigateToEdit(int userId) async {
|
|
final result = await Navigator.pushNamed(
|
|
context,
|
|
Routes.userEdit,
|
|
arguments: userId,
|
|
);
|
|
if (result == true && mounted) {
|
|
_controller.loadUsers(refresh: true);
|
|
}
|
|
}
|
|
|
|
/// 사용자 삭제 다이얼로그
|
|
void _showDeleteDialog(int userId, String userName) {
|
|
showShadDialog(
|
|
context: context,
|
|
builder: (context) => ShadDialog(
|
|
title: const Text('사용자 삭제'),
|
|
description: Text('"$userName" 사용자를 정말로 삭제하시겠습니까?'),
|
|
actions: [
|
|
ShadButton.outline(
|
|
child: const Text('취소'),
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
),
|
|
ShadButton.destructive(
|
|
child: const Text('삭제'),
|
|
onPressed: () async {
|
|
Navigator.of(context).pop();
|
|
|
|
await _controller.deleteUser(userId);
|
|
ShadToaster.of(context).show(
|
|
ShadToast(
|
|
title: const Text('삭제 완료'),
|
|
description: const Text('사용자가 삭제되었습니다'),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 상태 변경 확인 다이얼로그
|
|
void _showStatusChangeDialog(User user) {
|
|
final newStatus = !user.isActive;
|
|
final statusText = newStatus ? '활성화' : '비활성화';
|
|
|
|
showShadDialog(
|
|
context: context,
|
|
builder: (context) => ShadDialog(
|
|
title: const Text('사용자 상태 변경'),
|
|
description: Text('"${user.name}" 사용자를 $statusText 하시겠습니까?'),
|
|
actions: [
|
|
ShadButton.outline(
|
|
child: const Text('취소'),
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
),
|
|
ShadButton(
|
|
child: Text(statusText),
|
|
onPressed: () async {
|
|
Navigator.of(context).pop();
|
|
await _controller.changeUserStatus(user, !user.isActive);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: ShadcnTheme.background,
|
|
appBar: AppBar(
|
|
title: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'사용자 관리',
|
|
style: ShadcnTheme.headingH4,
|
|
),
|
|
Text(
|
|
'시스템 사용자를 관리합니다',
|
|
style: ShadcnTheme.bodySmall,
|
|
),
|
|
],
|
|
),
|
|
backgroundColor: ShadcnTheme.background,
|
|
elevation: 0,
|
|
),
|
|
body: ListenableBuilder(
|
|
listenable: _controller,
|
|
builder: (context, child) {
|
|
return BaseListScreen(
|
|
headerSection: _buildStatisticsCards(),
|
|
searchBar: _buildSearchBar(),
|
|
actionBar: _buildActionBar(),
|
|
dataTable: _buildDataTable(),
|
|
pagination: _buildPagination(),
|
|
isLoading: _controller.isLoading && _controller.users.isEmpty,
|
|
error: _controller.error,
|
|
onRefresh: () => _controller.loadUsers(refresh: true),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatisticsCards() {
|
|
if (_controller.isLoading) return const SizedBox();
|
|
|
|
return Row(
|
|
children: [
|
|
_buildStatCard(
|
|
'전체 사용자',
|
|
_controller.total.toString(),
|
|
Icons.people,
|
|
ShadcnTheme.primary,
|
|
),
|
|
const SizedBox(width: ShadcnTheme.spacing4),
|
|
_buildStatCard(
|
|
'활성 사용자',
|
|
_controller.users.where((u) => u.isActive).length.toString(),
|
|
Icons.check_circle,
|
|
ShadcnTheme.success,
|
|
),
|
|
const SizedBox(width: ShadcnTheme.spacing4),
|
|
_buildStatCard(
|
|
'비활성 사용자',
|
|
_controller.users.where((u) => !u.isActive).length.toString(),
|
|
Icons.person_off,
|
|
ShadcnTheme.mutedForeground,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildSearchBar() {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: ShadInputFormField(
|
|
controller: _searchController,
|
|
placeholder: const Text('이름, 이메일로 검색...'),
|
|
),
|
|
),
|
|
const SizedBox(width: ShadcnTheme.spacing4),
|
|
ShadButton.outline(
|
|
onPressed: () => _controller.clearFilters(),
|
|
child: const Text('초기화'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildActionBar() {
|
|
return StandardActionBar(
|
|
totalCount: _controller.totalCount,
|
|
leftActions: const [
|
|
Text('사용자 목록', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
|
],
|
|
rightActions: [
|
|
ShadButton(
|
|
onPressed: _navigateToAdd,
|
|
child: const Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.person_add, size: 16),
|
|
SizedBox(width: ShadcnTheme.spacing1),
|
|
Text('사용자 추가'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildDataTable() {
|
|
final users = _controller.users;
|
|
|
|
return Container(
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.black),
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// 고정 헤더
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
|
border: Border(bottom: BorderSide(color: Colors.black)),
|
|
),
|
|
child: Row(children: _buildHeaderCells()),
|
|
),
|
|
// 스크롤 바디
|
|
Expanded(
|
|
child: users.isEmpty
|
|
? Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.people_outlined,
|
|
size: 64,
|
|
color: ShadcnTheme.mutedForeground,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'등록된 사용자가 없습니다',
|
|
style: ShadcnTheme.bodyMedium.copyWith(
|
|
color: ShadcnTheme.mutedForeground,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: ListView.builder(
|
|
itemCount: users.length,
|
|
itemBuilder: (context, index) => _buildTableRow(users[index], index),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
List<Widget> _buildHeaderCells() {
|
|
return [
|
|
_buildHeaderCell('번호', flex: 0, useExpanded: false, minWidth: 50),
|
|
_buildHeaderCell('이름', flex: 2, useExpanded: true, minWidth: 80),
|
|
_buildHeaderCell('이메일', flex: 3, useExpanded: true, minWidth: 120),
|
|
_buildHeaderCell('회사', flex: 2, useExpanded: true, minWidth: 80),
|
|
_buildHeaderCell('권한', flex: 0, useExpanded: false, minWidth: 80),
|
|
_buildHeaderCell('상태', flex: 0, useExpanded: false, minWidth: 80),
|
|
_buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 120),
|
|
];
|
|
}
|
|
|
|
Widget _buildHeaderCell(
|
|
String text, {
|
|
required int flex,
|
|
required bool useExpanded,
|
|
required double minWidth,
|
|
}) {
|
|
final child = Container(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
text,
|
|
style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500),
|
|
),
|
|
);
|
|
|
|
if (useExpanded) {
|
|
return Expanded(flex: flex, child: child);
|
|
} else {
|
|
return SizedBox(width: minWidth, child: child);
|
|
}
|
|
}
|
|
|
|
Widget _buildDataCell(
|
|
Widget child, {
|
|
required int flex,
|
|
required bool useExpanded,
|
|
required double minWidth,
|
|
}) {
|
|
final container = Container(
|
|
alignment: Alignment.centerLeft,
|
|
child: child,
|
|
);
|
|
|
|
if (useExpanded) {
|
|
return Expanded(flex: flex, child: container);
|
|
} else {
|
|
return SizedBox(width: minWidth, child: container);
|
|
}
|
|
}
|
|
|
|
Widget _buildTableRow(User user, int index) {
|
|
final rowNumber = (_controller.currentPage - 1) * _controller.pageSize + index + 1;
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: index.isEven
|
|
? ShadcnTheme.muted.withValues(alpha: 0.1)
|
|
: null,
|
|
border: const Border(
|
|
bottom: BorderSide(color: Colors.black),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
_buildDataCell(
|
|
Text(
|
|
rowNumber.toString(),
|
|
style: ShadcnTheme.bodySmall,
|
|
),
|
|
flex: 0,
|
|
useExpanded: false,
|
|
minWidth: 50,
|
|
),
|
|
_buildDataCell(
|
|
Text(
|
|
user.name,
|
|
style: ShadcnTheme.bodyMedium.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
flex: 2,
|
|
useExpanded: true,
|
|
minWidth: 80,
|
|
),
|
|
_buildDataCell(
|
|
Text(
|
|
user.email ?? '',
|
|
style: ShadcnTheme.bodyMedium,
|
|
),
|
|
flex: 3,
|
|
useExpanded: true,
|
|
minWidth: 120,
|
|
),
|
|
_buildDataCell(
|
|
Text(
|
|
'-', // Company name not available in current model
|
|
style: ShadcnTheme.bodySmall,
|
|
),
|
|
flex: 2,
|
|
useExpanded: true,
|
|
minWidth: 80,
|
|
),
|
|
_buildDataCell(
|
|
_buildUserRoleBadge(user.role),
|
|
flex: 0,
|
|
useExpanded: false,
|
|
minWidth: 80,
|
|
),
|
|
_buildDataCell(
|
|
_buildStatusChip(user.isActive),
|
|
flex: 0,
|
|
useExpanded: false,
|
|
minWidth: 80,
|
|
),
|
|
_buildDataCell(
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ShadButton.ghost(
|
|
onPressed: user.id != null ? () => _navigateToEdit(user.id!) : null,
|
|
child: const Icon(Icons.edit, size: 16),
|
|
),
|
|
const SizedBox(width: ShadcnTheme.spacing1),
|
|
ShadButton.ghost(
|
|
onPressed: () => _showStatusChangeDialog(user),
|
|
child: Icon(
|
|
user.isActive ? Icons.person_off : Icons.person,
|
|
size: 16,
|
|
),
|
|
),
|
|
const SizedBox(width: ShadcnTheme.spacing1),
|
|
ShadButton.ghost(
|
|
onPressed: user.id != null ? () => _showDeleteDialog(user.id!, user.name) : null,
|
|
child: const Icon(Icons.delete, size: 16),
|
|
),
|
|
],
|
|
),
|
|
flex: 0,
|
|
useExpanded: false,
|
|
minWidth: 120,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPagination() {
|
|
if (_controller.totalPages <= 1) return const SizedBox();
|
|
|
|
return Pagination(
|
|
currentPage: _controller.currentPage,
|
|
totalCount: _controller.total,
|
|
pageSize: _controller.pageSize,
|
|
onPageChanged: (page) => _controller.goToPage(page),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatCard(
|
|
String title,
|
|
String value,
|
|
IconData icon,
|
|
Color color,
|
|
) {
|
|
return Expanded(
|
|
child: ShadCard(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(ShadcnTheme.spacing4),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(ShadcnTheme.spacing3),
|
|
decoration: BoxDecoration(
|
|
color: color.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
color: color,
|
|
size: 20,
|
|
),
|
|
),
|
|
const SizedBox(width: ShadcnTheme.spacing3),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: ShadcnTheme.bodySmall,
|
|
),
|
|
Text(
|
|
value,
|
|
style: ShadcnTheme.headingH6.copyWith(
|
|
color: color,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatusChip(bool isActive) {
|
|
if (isActive) {
|
|
return ShadBadge.secondary(
|
|
child: const Text('활성'),
|
|
);
|
|
} else {
|
|
return ShadBadge.destructive(
|
|
child: const Text('비활성'),
|
|
);
|
|
}
|
|
}
|
|
|
|
// StandardDataRow 임시 정의
|
|
}
|
|
|