Files
bowlingManager/mobile/lib/screens/club/members_screen.dart.bak
T
2025-08-01 16:45:26 +09:00

1007 lines
39 KiB
Plaintext

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<MembersScreen> createState() => _MembersScreenState();
}
class _MembersScreenState extends State<MembersScreen> {
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<void> _loadMembers() async {
try {
print('_loadMembers 호출됨');
final authService = Provider.of<AuthService>(context, listen: false);
final clubService = Provider.of<ClubService>(context, listen: false);
final memberService = Provider.of<MemberService>(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<Member> _getFilteredMembers(List<Member> 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<MemberService>(
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<String>(
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<MemberService>(context, listen: false);
final clubService = Provider.of<ClubService>(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<MemberService>(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<String>(
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<String>(
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<String>(
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<MemberService>().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<MemberService>(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;
}
}
}