import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:intl/intl.dart'; import '../../services/auth_service.dart'; import '../../services/club_service.dart'; import '../../services/member_service.dart'; import '../../models/member_model.dart'; class MembersScreen extends StatefulWidget { const MembersScreen({super.key}); @override State createState() => _MembersScreenState(); } class _MembersScreenState extends State { bool _isInit = false; String _searchQuery = ''; final TextEditingController _searchController = TextEditingController(); @override void dispose() { _searchController.dispose(); super.dispose(); } @override void didChangeDependencies() { super.didChangeDependencies(); if (!_isInit) { _loadMembers(); _isInit = true; } } Future _loadMembers() async { try { print('_loadMembers 호출됨'); final authService = Provider.of(context, listen: false); final clubService = Provider.of(context, listen: false); final memberService = Provider.of(context, listen: false); print('현재 클럽: ${clubService.currentClub?.name}'); print('토큰: ${authService.token}'); if (clubService.currentClub != null && authService.token != null) { print('멤버 서비스 초기화 시작'); memberService.initialize(authService.token!); print('멤버 서비스 초기화 완료'); print('회원 목록 가져오기 시작'); await memberService.fetchClubMembers(); print('회원 목록 가져오기 완료: ${memberService.members.length}개'); } else { print('클럽 또는 토큰이 없어서 회원 목록을 가져올 수 없습니다.'); } } catch (e) { print('회원 목록 가져오기 오류: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('회원 목록을 불러오는데 실패했습니다: $e')), ); } } } List _getFilteredMembers(List members) { if (_searchQuery.isEmpty) { return members; } final query = _searchQuery.toLowerCase(); return members.where((member) { return member.name.toLowerCase().contains(query) || member.email.toLowerCase().contains(query); }).toList(); } @override Widget build(BuildContext context) { return Scaffold( body: Column( children: [ // 검색 바 Padding( padding: const EdgeInsets.all(16.0), child: TextField( controller: _searchController, decoration: InputDecoration( hintText: '회원 검색', prefixIcon: const Icon(Icons.search), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), contentPadding: const EdgeInsets.symmetric(vertical: 0), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); setState(() { _searchQuery = ''; }); }, ) : null, ), onChanged: (value) { setState(() { _searchQuery = value; }); }, ), ), // 회원 목록 Expanded( child: Consumer( builder: (context, memberService, _) { if (memberService.isLoading) { return const Center(child: CircularProgressIndicator()); } final filteredMembers = _getFilteredMembers(memberService.members); if (filteredMembers.isEmpty) { return Center( child: Text( _searchQuery.isEmpty ? '등록된 회원이 없습니다' : '검색 결과가 없습니다', ), ); } return RefreshIndicator( onRefresh: _loadMembers, child: ListView.builder( padding: const EdgeInsets.only(bottom: 16), itemCount: filteredMembers.length, itemBuilder: (context, index) { final member = filteredMembers[index]; return _buildMemberCard(member); }, ), ); }, ), ), ], ), floatingActionButton: FloatingActionButton( onPressed: () { // 회원 추가 화면으로 이동 (추후 구현) _showAddMemberDialog(); }, backgroundColor: Colors.blue, child: const Icon(Icons.add, color: Colors.white), ), ); } Widget _buildMemberCard(Member member) { final roleText = _getRoleDisplayName(member.role ?? 'member'); return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: InkWell( onTap: () { // 회원 상세 정보 다이얼로그 표시 _showMemberDetailDialog(member); }, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16.0), child: Row( children: [ // 프로필 이미지 또는 이니셜 Container( width: 60, height: 60, decoration: BoxDecoration( color: Colors.blue.shade100, shape: BoxShape.circle, ), child: Center( child: member.profileImage != null ? ClipOval( child: Image.network( member.profileImage!, width: 60, height: 60, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Text( member.name.isNotEmpty ? member.name[0].toUpperCase() : '?', style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.blue, ), ); }, ), ) : Text( member.name.isNotEmpty ? member.name[0].toUpperCase() : '?', style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.blue, ), ), ), ), const SizedBox(width: 16), // 회원 정보 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( member.name, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( member.email, style: TextStyle( fontSize: 14, color: Colors.grey.shade700, ), ), const SizedBox(height: 4), Text( roleText, style: TextStyle( fontSize: 14, color: Colors.blue.shade700, fontWeight: FontWeight.w500, ), ), ], ), ), // 액션 버튼 PopupMenuButton( icon: const Icon(Icons.more_vert), onSelected: (value) { if (value == 'edit') { _showEditMemberDialog(member); } else if (value == 'delete') { _showDeleteConfirmationDialog(member); } }, itemBuilder: (context) => [ const PopupMenuItem( value: 'edit', child: Row( children: [ Icon(Icons.edit, size: 20), SizedBox(width: 8), Text('수정'), ], ), ), const PopupMenuItem( value: 'delete', child: Row( children: [ Icon(Icons.delete, size: 20, color: Colors.red), SizedBox(width: 8), Text('삭제', style: TextStyle(color: Colors.red)), ], ), ), ], ), ], ), ), ), ); } // 회원 추가 다이얼로그 void _showAddMemberDialog() { final nameController = TextEditingController(); final emailController = TextEditingController(); final phoneController = TextEditingController(); showDialog( context: context, builder: (context) => AlertDialog( title: const Text('회원 추가'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: nameController, decoration: const InputDecoration( labelText: '이름', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: emailController, keyboardType: TextInputType.emailAddress, decoration: const InputDecoration( labelText: '이메일', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: phoneController, keyboardType: TextInputType.phone, decoration: const InputDecoration( labelText: '전화번호 (선택사항)', border: OutlineInputBorder(), ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('취소'), ), ElevatedButton( onPressed: () async { if (nameController.text.isEmpty || emailController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('이름과 이메일은 필수 입력 항목입니다')), ); return; } try { final memberService = Provider.of(context, listen: false); final clubService = Provider.of(context, listen: false); if (clubService.currentClub != null) { await memberService.addMember({ 'name': nameController.text.trim(), 'email': emailController.text.trim(), 'phone': phoneController.text.trim(), 'clubId': clubService.currentClub!.id, }); if (mounted) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('회원이 추가되었습니다')), ); } } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('회원 추가에 실패했습니다: $e')), ); } }, child: const Text('추가'), ), ], ), ); } // 회원 수정 다이얼로그 void _showEditMemberDialog(Member member) { final nameController = TextEditingController(text: member.name); final phoneController = TextEditingController(text: member.phone ?? ''); showDialog( context: context, builder: (context) => AlertDialog( title: const Text('회원 정보 수정'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: nameController, decoration: const InputDecoration( labelText: '이름', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( enabled: false, // 이메일은 수정 불가 controller: TextEditingController(text: member.email), decoration: const InputDecoration( labelText: '이메일', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: phoneController, keyboardType: TextInputType.phone, decoration: const InputDecoration( labelText: '전화번호', border: OutlineInputBorder(), ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('취소'), ), ElevatedButton( onPressed: () async { if (nameController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('이름은 필수 입력 항목입니다')), ); return; } try { final memberService = Provider.of(context, listen: false); await memberService.updateMember(member.id, { 'name': nameController.text.trim(), 'phone': phoneController.text.trim(), }); if (mounted) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('회원 정보가 업데이트되었습니다')), ); } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('회원 정보 업데이트에 실패했습니다: $e')), ); } }, child: const Text('저장'), ), ], ), ); } // 회원 상세 정보 다이얼로그 void _showMemberDetailDialog(Member member) { bool isEditing = false; // 폼 컨트롤러 초기화 final nameController = TextEditingController(text: member.name); final phoneController = TextEditingController(text: member.phone ?? ''); final emailController = TextEditingController(text: member.email); final handicapController = TextEditingController(text: member.handicap?.toString() ?? '0'); // 선택형 필드를 위한 값 초기화 String selectedGender = member.gender ?? 'male'; String selectedMemberType = member.memberType ?? 'regular'; String selectedStatus = member.status ?? 'active'; showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setState) { // 날짜 포맷팅 String formatDate(DateTime? date) { if (date == null) return '정보 없음'; return DateFormat('yyyy년 MM월 dd일').format(date); } // 역할 표시 이름 변환 String getRoleDisplayName(String? role) { if (role == null) return '일반 회원'; switch (role) { case 'owner': return '클럽 소유자'; case 'manager': return '클럽 매니저'; case 'member': return '클럽 회원'; default: return role; } } // 회원 유형 표시 이름 변환 String getMemberTypeDisplayName(String? memberType) { if (memberType == null) return '일반 회원'; switch (memberType) { case 'regular': return '정회원'; case 'associate': return '준회원'; case 'guest': return '게스트'; case 'manager': return '매니저'; case 'owner': return '소유자'; default: return memberType; } } // 성별 표시 이름 변환 String getGenderDisplayName(String? gender) { if (gender == null) return '정보 없음'; switch (gender) { case 'male': return '남성'; case 'female': return '여성'; default: return gender; } } // 상태 표시 이름 변환 String getStatusDisplayName(String? status) { if (status == null) return '활성'; switch (status) { case 'active': return '활성'; case 'inactive': return '비활성'; case 'suspended': return '정지'; case 'deleted': return '삭제됨'; default: return status; } } // 정보 항목 표시 위젯 Widget buildInfoRow(String label, String value) { return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 100, child: Text( label, style: const TextStyle( fontWeight: FontWeight.w500, color: Colors.grey, ), ), ), Expanded( child: Text( value, style: const TextStyle( fontWeight: FontWeight.w500, ), ), ), ], ), ); } // 입력 필드 위젯 Widget buildEditField(String label, TextEditingController controller, {bool enabled = true}) { return Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontWeight: FontWeight.w500, fontSize: 14, color: Colors.grey, ), ), const SizedBox(height: 8), TextField( controller: controller, enabled: enabled, decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), ), ), ], ), ); } // Vue.js 스타일의 폼 필드 메서드 Widget buildFormField(String label, Widget field) { return Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.grey, ), ), const SizedBox(height: 8), field, ], ), ); } return AlertDialog( title: Text(isEditing ? '회원 정보 수정' : '회원 정보'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // 프로필 이미지와 현재 상태 if (!isEditing) ...[ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ // 프로필 이미지 Container( width: 70, height: 70, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.grey.shade200, image: member.profileImage != null ? DecorationImage( image: NetworkImage(member.profileImage!), fit: BoxFit.cover, ) : null, ), child: member.profileImage == null ? Center(child: Text( member.name.isNotEmpty ? member.name.substring(0, 1) : '?', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.blue.shade700), )) : null, ), const SizedBox(width: 16), // 현재 회원 상태 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( member.name, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: member.isActive ? Colors.green.shade100 : Colors.red.shade100, borderRadius: BorderRadius.circular(12), ), child: Text( getStatusDisplayName(member.status), style: TextStyle( fontSize: 12, color: member.isActive ? Colors.green.shade800 : Colors.red.shade800, fontWeight: FontWeight.w500, ), ), ), ], ), const SizedBox(height: 4), Text( getMemberTypeDisplayName(member.memberType), style: TextStyle( fontSize: 14, color: Colors.blue.shade700, fontWeight: FontWeight.w500, ), ), if (member.phone != null && member.phone!.isNotEmpty) ...[ const SizedBox(height: 4), Text( member.phone!, style: TextStyle( fontSize: 14, color: Colors.grey.shade700, ), ), ], if (member.email != null && member.email.isNotEmpty) ...[ const SizedBox(height: 2), Text( member.email, style: TextStyle( fontSize: 14, color: Colors.grey.shade700, ), ), ], ], ), ), ], ), const SizedBox(height: 16), const Divider(), ], // 폼 필드 Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 이름 필드 buildFormField('이름', isEditing ? TextField( controller: nameController, decoration: const InputDecoration( border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), ) : Text(member.name, style: const TextStyle(fontSize: 16)) ), // 성별 필드 buildFormField('성별', isEditing ? DropdownButtonFormField( value: selectedGender, decoration: const InputDecoration( border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), items: [ DropdownMenuItem(value: 'male', child: Text('남성')), DropdownMenuItem(value: 'female', child: Text('여성')), ], onChanged: isEditing ? (value) { if (value != null) { selectedGender = value; } } : null, ) : Text(getGenderDisplayName(member.gender), style: const TextStyle(fontSize: 16)) ), // 회원 유형 필드 buildFormField('회원 유형', isEditing ? DropdownButtonFormField( value: selectedMemberType, decoration: const InputDecoration( border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), items: [ DropdownMenuItem(value: 'regular', child: Text('정회원')), DropdownMenuItem(value: 'associate', child: Text('준회원')), DropdownMenuItem(value: 'guest', child: Text('게스트')), DropdownMenuItem(value: 'manager', child: Text('매니저')), DropdownMenuItem(value: 'owner', child: Text('소유자')), ], onChanged: isEditing ? (value) { if (value != null) { selectedMemberType = value; } } : null, ) : Text(getMemberTypeDisplayName(member.memberType), style: const TextStyle(fontSize: 16)) ), // 전화번호 필드 buildFormField('전화번호', isEditing ? TextField( controller: phoneController, keyboardType: TextInputType.phone, decoration: const InputDecoration( border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), ) : Text(member.phone ?? '정보 없음', style: const TextStyle(fontSize: 16)) ), // 이메일 필드 buildFormField('이메일', isEditing ? TextField( controller: emailController, keyboardType: TextInputType.emailAddress, decoration: const InputDecoration( border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), ) : Text(member.email, style: const TextStyle(fontSize: 16)) ), // 핸디캡 필드 buildFormField('핸디캡', isEditing ? TextField( controller: handicapController, keyboardType: TextInputType.number, decoration: const InputDecoration( border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), ) : Text(member.handicap?.toString() ?? '0', style: const TextStyle(fontSize: 16)) ), // 상태 필드 buildFormField('상태', isEditing ? DropdownButtonFormField( value: selectedStatus, decoration: const InputDecoration( border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), items: [ DropdownMenuItem(value: 'active', child: Text('활성')), DropdownMenuItem(value: 'inactive', child: Text('비활성')), DropdownMenuItem(value: 'suspended', child: Text('정지')), ], onChanged: isEditing ? (value) { if (value != null) { selectedStatus = value; } } : null, ) : Text(getStatusDisplayName(member.status), style: const TextStyle(fontSize: 16)) ), // 가입일 필드 (수정불가) if (!isEditing) buildFormField('가입일', Text(formatDate(member.joinedAt), style: const TextStyle(fontSize: 16))), // 시스템 정보 (수정불가) if (!isEditing) ...[ const SizedBox(height: 16), const Divider(), const SizedBox(height: 8), const Text('시스템 정보', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 8), buildFormField('회원 ID', Text(member.id, style: const TextStyle(fontSize: 14, fontFamily: 'monospace'))), if (member.userId.isNotEmpty) buildFormField('유저 ID', Text(member.userId, style: const TextStyle(fontSize: 14, fontFamily: 'monospace'))), buildFormField('클럽 ID', Text(member.clubId, style: const TextStyle(fontSize: 14, fontFamily: 'monospace'))), ], ], ), ), ], ), actions: [ // 취소 버튼 TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('취소'), ), // 수정/저장 버튼 TextButton( onPressed: () { if (isEditing) { // 수정 모드에서 저장 버튼 클릭 시 if (nameController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('이름은 필수 입력 항목입니다.')), ); return; } // 회원 정보 업데이트 final updatedMember = Member( id: member.id, clubId: member.clubId, userId: member.userId, name: nameController.text.trim(), email: emailController.text.trim(), phone: phoneController.text.trim(), address: member.address, // 주소는 유지 profileImage: member.profileImage, role: member.role, memberType: selectedMemberType, gender: selectedGender, status: selectedStatus, handicap: int.tryParse(handicapController.text) ?? member.handicap, isActive: selectedStatus == 'active', joinedAt: member.joinedAt, createdAt: member.createdAt, updatedAt: DateTime.now().toIso8601String(), ); // 회원 정보 저장 context.read().updateMember(updatedMember).then((_) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('회원 정보가 업데이트되었습니다.')), ); Navigator.of(context).pop(); }).catchError((error) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('오류: ${error.toString()}')), ); }); } else { // 보기 모드에서 수정 버튼 클릭 시 setState(() { isEditing = true; }); } }, child: Text(isEditing ? '저장' : '수정'), ), ], ); }, ), ); } // 회원 삭제 확인 다이얼로그 void _showDeleteConfirmationDialog(Member member) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('회원 삭제'), content: Text('${member.name} 회원을 정말 삭제하시겠습니까?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('취소'), ), TextButton( onPressed: () async { try { final memberService = Provider.of(context, listen: false); await memberService.deleteMember(member.id); if (mounted) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('회원이 삭제되었습니다')), ); } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('회원 삭제에 실패했습니다: $e')), ); } }, style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('삭제'), ), ], ), ); } // 역할 표시 이름 변환 String _getRoleDisplayName(String role) { switch (role) { case 'owner': return '클럽 소유자'; case 'manager': return '클럽 매니저'; case 'member': return '클럽 회원'; default: return role; } } }