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/widgets/pagination.dart'; import 'package:superport/screens/user/controllers/user_list_controller.dart'; import 'package:superport/utils/constants.dart'; /// shadcn/ui 스타일로 재설계된 사용자 관리 화면 class UserList extends StatefulWidget { const UserList({super.key}); @override State createState() => _UserListState(); } class _UserListState extends State { // MockDataService 제거 - 실제 API 사용 late UserListController _controller; final TextEditingController _searchController = TextEditingController(); @override void initState() { super.initState(); // 초기 데이터 로드 _controller = UserListController(); WidgetsBinding.instance.addPostFrameCallback((_) { _controller.initialize(pageSize: 10); // 통일된 초기화 방식 }); // 검색 디바운싱 _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: 300), () { _controller.setSearchQuery(query); // Controller가 페이지 리셋 처리 }); } /// 상태별 색상 반환 Color _getStatusColor(bool isActive) { return isActive ? Colors.green : Colors.red; } /// 사용자 권한 표시 배지 (새 UserRole 시스템) 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, newStatus); }, ), ], ), ); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: _controller, builder: (context, child) { if (_controller.isLoading && _controller.users.isEmpty) { return const Center( child: ShadProgress(), ); } if (_controller.error != null && _controller.users.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 64, color: Colors.red[300]), const SizedBox(height: 16), Text( '데이터를 불러올 수 없습니다', style: ShadcnTheme.headingH4, ), const SizedBox(height: 8), Text( _controller.error!, style: ShadcnTheme.bodyMuted, textAlign: TextAlign.center, ), const SizedBox(height: 16), ShadcnButton( text: '다시 시도', onPressed: () => _controller.loadUsers(refresh: true), variant: ShadcnButtonVariant.primary, ), ], ), ); } // Controller가 이미 페이징된 데이터를 제공 final List pagedUsers = _controller.users; // 이미 페이징됨 final int totalUsers = _controller.total; // 실제 전체 개수 return SingleChildScrollView( padding: const EdgeInsets.all(ShadcnTheme.spacing6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 검색 및 필터 섹션 Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), side: BorderSide(color: Colors.black), ), child: Padding( padding: const EdgeInsets.all(ShadcnTheme.spacing4), child: Column( children: [ // 검색 바 ShadInputFormField( controller: _searchController, placeholder: const Text('이름, 이메일, 사용자명으로 검색...'), ), const SizedBox(height: ShadcnTheme.spacing3), // 필터 버튼들 Row( children: [ // 상태 필터 ShadcnButton( text: _controller.filterIsActive == null ? '모든 상태' : _controller.filterIsActive! ? '활성 사용자' : '비활성 사용자', onPressed: () { _controller.setFilters( isActive: _controller.filterIsActive == null ? true : _controller.filterIsActive! ? false : null, ); }, variant: ShadcnButtonVariant.secondary, icon: const Icon(Icons.filter_list), ), const SizedBox(width: ShadcnTheme.spacing2), // 권한 필터 (새 UserRole 시스템) PopupMenuButton( child: ShadcnButton( text: _controller.filterRole == null ? '모든 권한' : _controller.filterRole!.displayName, onPressed: null, variant: ShadcnButtonVariant.secondary, icon: const Icon(Icons.person), ), onSelected: (roleString) { _controller.setFilters(roleString: roleString); }, itemBuilder: (context) => [ const PopupMenuItem( value: null, child: Text('모든 권한'), ), const PopupMenuItem( value: 'admin', child: Text('관리자'), ), const PopupMenuItem( value: 'manager', child: Text('매니저'), ), const PopupMenuItem( value: 'staff', child: Text('직원'), ), ], ), const Spacer(), // 관리자용 비활성 포함 체크박스 Row( children: [ ShadCheckbox( value: _controller.includeInactive, onChanged: (_) => setState(() { _controller.toggleIncludeInactive(); }), ), const SizedBox(width: 8), const Text('비활성 포함'), ], ), const SizedBox(width: ShadcnTheme.spacing2), // 필터 초기화 if (_controller.searchQuery.isNotEmpty || _controller.filterIsActive != null || _controller.filterRole != null) ShadcnButton( text: '필터 초기화', onPressed: () { _searchController.clear(); _controller.clearFilters(); }, variant: ShadcnButtonVariant.ghost, icon: const Icon(Icons.clear_all), ), ], ), ], ), ), ), const SizedBox(height: ShadcnTheme.spacing4), // 헤더 액션 바 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '총 ${_controller.users.length}명 사용자', style: ShadcnTheme.bodyMuted, ), Row( children: [ ShadcnButton( text: '새로고침', onPressed: () => _controller.loadUsers(refresh: true), variant: ShadcnButtonVariant.secondary, icon: const Icon(Icons.refresh), ), const SizedBox(width: ShadcnTheme.spacing2), ShadcnButton( text: '사용자 추가', onPressed: _navigateToAdd, variant: ShadcnButtonVariant.primary, textColor: Colors.white, icon: const Icon(Icons.add), ), ], ), ], ), const SizedBox(height: ShadcnTheme.spacing4), // 테이블 컨테이너 Container( width: double.infinity, decoration: BoxDecoration( border: Border.all(color: Colors.black), borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 테이블 헤더 Container( padding: const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing4, vertical: ShadcnTheme.spacing3, ), decoration: BoxDecoration( color: ShadcnTheme.muted.withValues(alpha: 0.3), border: Border( bottom: BorderSide(color: Colors.black), ), ), child: Row( children: [ const SizedBox(width: 50, child: Text('번호', style: TextStyle(fontWeight: FontWeight.bold))), const Expanded(flex: 2, child: Text('사용자명', style: TextStyle(fontWeight: FontWeight.bold))), const Expanded(flex: 2, child: Text('이메일', style: TextStyle(fontWeight: FontWeight.bold))), const Expanded(flex: 2, child: Text('전화번호', style: TextStyle(fontWeight: FontWeight.bold))), const Expanded(flex: 2, child: Text('생성일', style: TextStyle(fontWeight: FontWeight.bold))), const SizedBox(width: 100, child: Text('권한', style: TextStyle(fontWeight: FontWeight.bold))), const SizedBox(width: 80, child: Text('상태', style: TextStyle(fontWeight: FontWeight.bold))), const SizedBox(width: 120, child: Text('관리', style: TextStyle(fontWeight: FontWeight.bold))), ], ), ), // 테이블 데이터 if (_controller.users.isEmpty) Container( padding: const EdgeInsets.all(ShadcnTheme.spacing8), child: Center( child: Text( _controller.searchQuery.isNotEmpty || _controller.filterIsActive != null || _controller.filterRole != null ? '검색 결과가 없습니다.' : '등록된 사용자가 없습니다.', style: ShadcnTheme.bodyMuted, ), ), ) else ...pagedUsers.asMap().entries.map((entry) { final int index = ((_controller.currentPage - 1) * _controller.pageSize) + entry.key; final User user = entry.value; return Container( padding: const EdgeInsets.all(ShadcnTheme.spacing4), decoration: BoxDecoration( border: Border( bottom: BorderSide(color: Colors.black), ), color: index % 2 == 0 ? null : ShadcnTheme.muted.withValues(alpha: 0.1), ), child: Row( children: [ // 번호 SizedBox( width: 50, child: Text( '${index + 1}', style: ShadcnTheme.bodySmall, ), ), // 사용자명 Expanded( flex: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( user.name, style: ShadcnTheme.bodyMedium, ), Text( '@${user.username}', style: ShadcnTheme.bodySmall.copyWith( color: ShadcnTheme.muted, ), ), ], ), ), // 이메일 Expanded( flex: 2, child: Text( user.email, style: ShadcnTheme.bodySmall, ), ), // 전화번호 Expanded( flex: 2, child: Text( user.phone ?? '미등록', style: ShadcnTheme.bodySmall, ), ), // 생성일 Expanded( flex: 2, child: Text( user.createdAt != null ? '${user.createdAt!.year}-${user.createdAt!.month.toString().padLeft(2, '0')}-${user.createdAt!.day.toString().padLeft(2, '0')}' : '미설정', style: ShadcnTheme.bodySmall, ), ), // 권한 SizedBox( width: 100, child: _buildUserRoleBadge(user.role), ), // 상태 SizedBox( width: 80, child: Row( children: [ Icon( Icons.circle, size: 8, color: _getStatusColor(user.isActive), ), const SizedBox(width: 4), Text( user.isActive ? '활성' : '비활성', style: ShadcnTheme.bodySmall.copyWith( color: _getStatusColor(user.isActive), ), ), ], ), ), // 관리 SizedBox( width: 120, child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: IconButton( constraints: const BoxConstraints( minWidth: 30, minHeight: 30, ), padding: const EdgeInsets.all(4), icon: Icon( Icons.power_settings_new, size: 16, color: user.isActive ? Colors.orange : Colors.green, ), onPressed: user.id != null ? () => _showStatusChangeDialog(user) : null, tooltip: user.isActive ? '비활성화' : '활성화', ), ), Flexible( child: IconButton( constraints: const BoxConstraints( minWidth: 30, minHeight: 30, ), padding: const EdgeInsets.all(4), icon: Icon( Icons.edit, size: 16, color: ShadcnTheme.primary, ), onPressed: user.id != null ? () => _navigateToEdit(user.id!) : null, tooltip: '수정', ), ), Flexible( child: IconButton( constraints: const BoxConstraints( minWidth: 30, minHeight: 30, ), padding: const EdgeInsets.all(4), icon: Icon( Icons.delete, size: 16, color: ShadcnTheme.destructive, ), onPressed: user.id != null ? () => _showDeleteDialog(user.id!, user.name) : null, tooltip: '삭제', ), ), ], ), ), ], ), ); }), ], ), ), // 페이지네이션 컴포넌트 (Controller 상태 사용) if (_controller.total > _controller.pageSize) Pagination( totalCount: _controller.total, currentPage: _controller.currentPage, pageSize: _controller.pageSize, onPageChanged: (page) { // 특정 페이지로 이동 (데이터 교체) _controller.goToPage(page); }, ), ], ), ); }, ); } }