1030 lines
44 KiB
Dart
1030 lines
44 KiB
Dart
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';
|
|
import '../../utils/enum_mappings.dart';
|
|
import '../../widgets/dialog_actions.dart';
|
|
import '../../theme/badges.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 {
|
|
// 비동기 이후 컨텍스트 직접 참조를 피하기 위해 메신저를 미리 캡처
|
|
final messenger = ScaffoldMessenger.of(context);
|
|
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!);
|
|
memberService.setClubId(clubService.currentClub!.id); // 클럽 ID 설정 추가
|
|
print('멤버 서비스 초기화 완료');
|
|
|
|
print('회원 목록 가져오기 시작');
|
|
await memberService.fetchClubMembers();
|
|
print('회원 목록 가져오기 완료: ${memberService.members.length}개');
|
|
} else {
|
|
print('클럽 또는 토큰이 없어서 회원 목록을 가져올 수 없습니다.');
|
|
}
|
|
} catch (e) {
|
|
print('회원 목록 가져오기 오류: $e');
|
|
messenger.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(
|
|
appBar: AppBar(
|
|
title: const Text('회원'),
|
|
actions: [
|
|
IconButton(
|
|
tooltip: '회원 추가',
|
|
icon: const Icon(Icons.person_add),
|
|
onPressed: _showAddMemberDialog,
|
|
),
|
|
],
|
|
),
|
|
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);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
// FAB 제거: AppBar actions로 이동
|
|
);
|
|
}
|
|
|
|
Widget _buildMemberCard(Member member) {
|
|
final memberTypeText = _getMemberTypeDisplayName(member.memberType);
|
|
|
|
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: [
|
|
// 회원 정보
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(
|
|
member.name,
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(width: 8),
|
|
// 성별 아이콘
|
|
Icon(
|
|
member.gender == 'female' ? Icons.female : Icons.male,
|
|
size: 18,
|
|
color: member.gender == 'female' ? Colors.pink : Colors.blue,
|
|
),
|
|
// 핸디캡
|
|
if (member.handicap != null && member.handicap != 0)
|
|
Container(
|
|
margin: const EdgeInsets.only(left: 8),
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: Colors.orange.shade100,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
(member.handicap! > 0) ? "+${member.handicap}" : member.handicap.toString(),
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.orange.shade800,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
// 평균
|
|
if (member.average != null)
|
|
Container(
|
|
margin: const EdgeInsets.only(left: 8),
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: Colors.indigo.shade100,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
'AVG ${member.average!.toStringAsFixed(1)}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.indigo.shade800,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (member.email.isNotEmpty || member.phone != null)
|
|
const SizedBox(height: 4),
|
|
if (member.phone != null)
|
|
Text(
|
|
member.phone!,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 2),
|
|
if (member.email.isNotEmpty)
|
|
Text(
|
|
member.email,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 4),
|
|
// 2열: 모든 뱃지
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 4,
|
|
crossAxisAlignment: WrapCrossAlignment.center,
|
|
children: [
|
|
// 회원 유형 배지
|
|
BadgeStyles.memberType(memberTypeText),
|
|
// 상태 배지
|
|
BadgeStyles.memberStatus(getStatusLabel(member.status)),
|
|
],
|
|
),
|
|
// 3열: 가입일
|
|
if (member.joinDate != null) ...[
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'가입: ${formatDate(member.joinDate!)}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
// 액션 버튼
|
|
PopupMenuButton<String>(
|
|
icon: const Icon(Icons.more_vert),
|
|
onSelected: (value) {
|
|
if (value == 'edit') {
|
|
_showMemberDetailDialog(member);
|
|
} else if (value == 'delete') {
|
|
_showDeleteConfirmationDialog(member);
|
|
}
|
|
},
|
|
itemBuilder: (context) => [
|
|
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();
|
|
final handicapController = TextEditingController(text: '0');
|
|
final joinDateController = TextEditingController(text: formatDate(DateTime.now()));
|
|
|
|
// 선택형 필드를 위한 값 초기화
|
|
String selectedGender = 'male';
|
|
String selectedMemberType = 'regular';
|
|
String selectedStatus = 'active';
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (dialogContext) => StatefulBuilder(
|
|
builder: (dialogContext, setState) {
|
|
return 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(),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
controller: handicapController,
|
|
keyboardType: TextInputType.number,
|
|
decoration: const InputDecoration(
|
|
labelText: '핸디캡',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
controller: joinDateController,
|
|
readOnly: true,
|
|
decoration: const InputDecoration(
|
|
labelText: '가입일',
|
|
border: OutlineInputBorder(),
|
|
suffixIcon: Icon(Icons.calendar_today),
|
|
),
|
|
onTap: () async {
|
|
final DateTime? picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: DateTime.now(),
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime(2101),
|
|
);
|
|
if (picked != null) {
|
|
setState(() {
|
|
joinDateController.text = formatDate(picked);
|
|
});
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
DropdownButtonFormField<String>(
|
|
key: const Key('members_add_gender_dropdown'),
|
|
initialValue: selectedGender,
|
|
decoration: const InputDecoration(
|
|
labelText: '성별',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
isExpanded: true,
|
|
items: [
|
|
DropdownMenuItem(value: 'male', child: Text('남성', maxLines: 1, overflow: TextOverflow.ellipsis)),
|
|
DropdownMenuItem(value: 'female', child: Text('여성', maxLines: 1, overflow: TextOverflow.ellipsis)),
|
|
],
|
|
onChanged: (value) {
|
|
if (value != null) {
|
|
setState(() {
|
|
selectedGender = value;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
DropdownButtonFormField<String>(
|
|
key: const Key('members_add_member_type_dropdown'),
|
|
initialValue: selectedMemberType,
|
|
decoration: const InputDecoration(
|
|
labelText: '회원 유형',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
isExpanded: true,
|
|
items: [
|
|
DropdownMenuItem(value: 'regular', child: Text('정회원', maxLines: 1, overflow: TextOverflow.ellipsis)),
|
|
DropdownMenuItem(value: 'associate', child: Text('준회원', maxLines: 1, overflow: TextOverflow.ellipsis)),
|
|
DropdownMenuItem(value: 'guest', child: Text('게스트', maxLines: 1, overflow: TextOverflow.ellipsis)),
|
|
],
|
|
onChanged: (value) {
|
|
if (value != null) {
|
|
setState(() {
|
|
selectedMemberType = value;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
DropdownButtonFormField<String>(
|
|
key: const Key('members_add_status_dropdown'),
|
|
initialValue: selectedStatus,
|
|
decoration: const InputDecoration(
|
|
labelText: '상태',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
isExpanded: true,
|
|
items: [
|
|
DropdownMenuItem(value: 'active', child: Text('활성', maxLines: 1, overflow: TextOverflow.ellipsis)),
|
|
DropdownMenuItem(value: 'inactive', child: Text('비활성', maxLines: 1, overflow: TextOverflow.ellipsis)),
|
|
],
|
|
onChanged: (value) {
|
|
if (value != null) {
|
|
setState(() {
|
|
selectedStatus = value;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: DialogActions.confirm(
|
|
onCancel: () => Navigator.of(dialogContext).pop(),
|
|
onConfirm: () async {
|
|
// await 이전에 컨텍스트 파생 객체 캡처
|
|
final navigator = Navigator.of(dialogContext);
|
|
final messenger = ScaffoldMessenger.of(dialogContext);
|
|
if (nameController.text.isEmpty) {
|
|
messenger.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) {
|
|
// 회원 추가 데이터 준비
|
|
final memberData = <String, dynamic>{
|
|
'name': nameController.text.trim(),
|
|
'clubId': clubService.currentClub!.id,
|
|
'gender': selectedGender,
|
|
'memberType': selectedMemberType,
|
|
'status': selectedStatus,
|
|
'handicap': int.tryParse(handicapController.text) ?? 0,
|
|
'isActive': selectedStatus == 'active',
|
|
};
|
|
|
|
// 가입일 추가
|
|
try {
|
|
final joinDate = parseDate(joinDateController.text);
|
|
if (joinDate != null) {
|
|
memberData['joinDate'] = joinDate.toIso8601String();
|
|
}
|
|
} catch (e) {
|
|
// 가입일 파싱 오류 무시
|
|
}
|
|
|
|
// 이메일이 비어있으면 null로 설정, 그렇지 않으면 트림한 값 사용
|
|
memberData['email'] = emailController.text.trim().isEmpty ? null : emailController.text.trim();
|
|
// 전화번호가 비어있으면 null로 설정, 그렇지 않으면 트림한 값 사용
|
|
memberData['phone'] = phoneController.text.trim().isEmpty ? null : phoneController.text.trim();
|
|
|
|
await memberService.addMember(memberData);
|
|
// 결과 알림 및 다이얼로그 닫기
|
|
navigator.pop();
|
|
messenger.showSnackBar(
|
|
const SnackBar(content: Text('회원이 추가되었습니다')),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
messenger.showSnackBar(
|
|
SnackBar(content: Text('회원 추가에 실패했습니다: $e')),
|
|
);
|
|
}
|
|
},
|
|
confirmText: '추가',
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// 날짜 포맷팅 함수
|
|
String formatDate(DateTime? date) {
|
|
if (date == null) return '정보 없음';
|
|
return DateFormat('yyyy-MM-dd').format(date);
|
|
}
|
|
|
|
// 날짜 파싱 함수
|
|
DateTime? parseDate(String? dateStr) {
|
|
if (dateStr == null) return null;
|
|
if (dateStr.isEmpty) return null;
|
|
try {
|
|
// 다양한 형식 지원
|
|
if (dateStr.contains('T')) {
|
|
// ISO 형식 (예: 2023-01-01T00:00:00.000Z)
|
|
return DateTime.parse(dateStr);
|
|
} else if (dateStr.contains('-')) {
|
|
// yyyy-MM-dd 형식
|
|
return DateFormat('yyyy-MM-dd').parse(dateStr);
|
|
}
|
|
return null;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// 폼 필드 생성 헬퍼 메서드
|
|
// 폼 필드 빌더 헬퍼 함수
|
|
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: 4),
|
|
field,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 성별 표시 이름 변환
|
|
String _getGenderDisplayName(String? gender) {
|
|
return getGenderLabel(gender);
|
|
}
|
|
|
|
// 회원 유형 표시 이름 변환
|
|
String _getMemberTypeDisplayName(String? memberType) {
|
|
return getMemberTypeLabel(memberType);
|
|
}
|
|
|
|
// 회원 상태에 따른 배경색 가져오기
|
|
Color getStatusColor(String status) {
|
|
switch (status) {
|
|
case 'active': return Colors.green.shade100;
|
|
case 'inactive': return Colors.grey.shade200;
|
|
case 'suspended': return Colors.red.shade100;
|
|
case 'deleted': return Colors.grey.shade300;
|
|
default: return Colors.grey.shade200;
|
|
}
|
|
}
|
|
|
|
// 회원 상태에 따른 텍스트 색상 가져오기
|
|
Color getStatusTextColor(String status) {
|
|
switch (status) {
|
|
case 'active': return Colors.green.shade800;
|
|
case 'inactive': return Colors.grey.shade700;
|
|
case 'suspended': return Colors.red.shade800;
|
|
case 'deleted': return Colors.grey.shade800;
|
|
default: return Colors.grey.shade700;
|
|
}
|
|
}
|
|
|
|
// 회원 상세 정보 다이얼로그
|
|
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');
|
|
final joinDateController = TextEditingController(text: formatDate(member.joinDate));
|
|
|
|
// 선택형 필드를 위한 값 초기화
|
|
String selectedGender = member.gender ?? 'male';
|
|
String selectedMemberType = member.memberType ?? 'regular';
|
|
String selectedStatus = member.status ?? (member.isActive ? 'active' : 'inactive');
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (dialogContext) => StatefulBuilder(
|
|
builder: (dialogContext, setState) {
|
|
return AlertDialog(
|
|
title: Text(isEditing ? '회원 정보 수정' : '회원 정보'),
|
|
content: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 회원 정보 헤더 (보기 모드에서만 표시)
|
|
if (!isEditing) ...[
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
// 현재 회원 상태
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(
|
|
member.name,
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
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.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: [
|
|
// 이름 필드 - 수정 모드에서만 표시 (상단에 이미 표시됨)
|
|
if (isEditing)
|
|
buildFormField('이름', TextField(
|
|
controller: nameController,
|
|
decoration: const InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
),
|
|
)),
|
|
|
|
// 성별 필드 - 상단에 없으므로 항상 표시
|
|
buildFormField('성별', isEditing ?
|
|
DropdownButtonFormField<String>(
|
|
key: const Key('members_edit_gender_dropdown'),
|
|
initialValue: selectedGender,
|
|
decoration: const InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
),
|
|
isExpanded: true,
|
|
items: [
|
|
DropdownMenuItem<String>(
|
|
value: 'male',
|
|
child: Text('남성', maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
),
|
|
DropdownMenuItem<String>(
|
|
value: 'female',
|
|
child: Text('여성', maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
),
|
|
],
|
|
onChanged: (value) {
|
|
if (value != null) {
|
|
selectedGender = value;
|
|
}
|
|
},
|
|
) :
|
|
Text(_getGenderDisplayName(member.gender), style: const TextStyle(fontSize: 16))
|
|
),
|
|
|
|
// 회원 유형 필드 - 수정 모드에서만 표시 (상단에 이미 표시됨)
|
|
if (isEditing)
|
|
buildFormField('회원 유형', DropdownButtonFormField<String>(
|
|
key: const Key('members_edit_member_type_dropdown'),
|
|
initialValue: selectedMemberType,
|
|
decoration: const InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
),
|
|
isExpanded: true,
|
|
items: [
|
|
DropdownMenuItem<String>(
|
|
value: 'regular',
|
|
child: Text('정회원', maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
),
|
|
DropdownMenuItem<String>(
|
|
value: 'associate',
|
|
child: Text('준회원', maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
),
|
|
DropdownMenuItem<String>(
|
|
value: 'guest',
|
|
child: Text('게스트', maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
),
|
|
DropdownMenuItem<String>(
|
|
value: 'manager',
|
|
child: Text('운영진', maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
),
|
|
DropdownMenuItem<String>(
|
|
value: 'owner',
|
|
child: Text('모임장', maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
),
|
|
],
|
|
onChanged: (value) {
|
|
if (value != null) {
|
|
selectedMemberType = value;
|
|
}
|
|
},
|
|
)),
|
|
|
|
// 전화번호 필드 - 수정 모드에서만 표시 (상단에 이미 표시됨)
|
|
if (isEditing)
|
|
buildFormField('전화번호', TextField(
|
|
controller: phoneController,
|
|
keyboardType: TextInputType.phone,
|
|
decoration: const InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
),
|
|
)),
|
|
|
|
// 이메일 필드 - 수정 모드에서만 표시 (상단에 이미 표시됨)
|
|
if (isEditing)
|
|
buildFormField('이메일', TextField(
|
|
controller: emailController,
|
|
keyboardType: TextInputType.emailAddress,
|
|
decoration: const InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
),
|
|
)),
|
|
|
|
// 핸디캡 필드 - 상단에 없으므로 항상 표시
|
|
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))
|
|
),
|
|
|
|
// 상태 필드 - 수정 모드에서만 표시 (상단에 이미 표시됨)
|
|
if (isEditing)
|
|
buildFormField('상태', DropdownButtonFormField<String>(
|
|
key: const Key('members_edit_status_dropdown'),
|
|
initialValue: selectedStatus,
|
|
decoration: const InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
),
|
|
isExpanded: true,
|
|
items: [
|
|
DropdownMenuItem<String>(
|
|
value: 'active',
|
|
child: Text('활성', maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
),
|
|
DropdownMenuItem<String>(
|
|
value: 'inactive',
|
|
child: Text('비활성', maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
),
|
|
DropdownMenuItem<String>(
|
|
value: 'suspended',
|
|
child: Text('정지', maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
),
|
|
],
|
|
onChanged: (value) {
|
|
if (value != null) {
|
|
selectedStatus = value;
|
|
}
|
|
},
|
|
)),
|
|
|
|
// 가입일 필드
|
|
buildFormField('가입일', isEditing ?
|
|
InkWell(
|
|
onTap: () async {
|
|
final DateTime? picked = await showDatePicker(
|
|
context: dialogContext,
|
|
initialDate: member.joinDate ?? DateTime.now(),
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime.now(),
|
|
// locale은 앱 전체 설정을 사용합니다
|
|
);
|
|
if (picked != null) {
|
|
setState(() {
|
|
// 새로운 날짜로 업데이트
|
|
joinDateController.text = formatDate(picked);
|
|
|
|
// copyWith 메서드를 사용하여 효율적으로 업데이트
|
|
member = member.copyWith(
|
|
joinDate: picked, // 새로 선택한 날짜
|
|
);
|
|
});
|
|
}
|
|
},
|
|
child: InputDecorator(
|
|
decoration: const InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
suffixIcon: Icon(Icons.calendar_today),
|
|
),
|
|
child: Text(formatDate(member.joinDate), style: const TextStyle(fontSize: 16)),
|
|
),
|
|
) :
|
|
Text(formatDate(member.joinDate), 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'))),
|
|
if (member.clubId.isNotEmpty)
|
|
buildFormField('클럽 ID', Text(member.clubId, style: const TextStyle(fontSize: 14, fontFamily: 'monospace'))),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
// 취소 버튼
|
|
TextButton(
|
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
|
child: const Text('취소'),
|
|
),
|
|
// 수정/저장 버튼
|
|
TextButton(
|
|
onPressed: () {
|
|
// 비동기 작업 전 컨텍스트 파생 객체 캡처
|
|
final navigator = Navigator.of(dialogContext);
|
|
final messenger = ScaffoldMessenger.of(dialogContext);
|
|
if (isEditing) {
|
|
// 수정 모드에서 저장 버튼 클릭 시
|
|
if (nameController.text.trim().isEmpty) {
|
|
messenger.showSnackBar(
|
|
const SnackBar(content: Text('이름은 필수 입력 항목입니다.')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// copyWith 메서드를 사용하여 회원 정보 업데이트
|
|
final updatedMember = member.copyWith(
|
|
name: nameController.text.trim(),
|
|
email: emailController.text.trim().isEmpty ? null : emailController.text.trim(),
|
|
phone: phoneController.text.trim().isEmpty ? null : phoneController.text.trim(),
|
|
memberType: selectedMemberType,
|
|
gender: selectedGender,
|
|
status: selectedStatus,
|
|
handicap: int.tryParse(handicapController.text) ?? member.handicap,
|
|
isActive: selectedStatus == 'active',
|
|
updatedAt: DateTime.now(),
|
|
// joinDate는 이미 날짜 선택기에서 업데이트되었으므로 여기서 다시 설정할 필요 없음
|
|
);
|
|
|
|
// 회원 정보 저장
|
|
// toJson 결과에서 clubId 제거하여 MemberService에서 설정한 clubId만 사용
|
|
final memberData = updatedMember.toJson();
|
|
memberData.remove('clubId'); // clubId 필드 제거
|
|
|
|
// 전화번호와 이메일 필드가 비어있을 때 null로 명시적 설정
|
|
if (phoneController.text.trim().isEmpty) {
|
|
memberData['phone'] = null;
|
|
}
|
|
|
|
if (emailController.text.trim().isEmpty) {
|
|
memberData['email'] = null;
|
|
}
|
|
|
|
// 가입일 필드 명시적으로 추가 (ISO 형식으로 변환)
|
|
if (member.joinDate != null) {
|
|
memberData['joinDate'] = member.joinDate!.toIso8601String();
|
|
}
|
|
|
|
context.read<MemberService>().updateMember(
|
|
updatedMember.id,
|
|
memberData,
|
|
).then((_) {
|
|
messenger.showSnackBar(
|
|
const SnackBar(content: Text('회원 정보가 업데이트되었습니다.')),
|
|
);
|
|
navigator.pop();
|
|
}).catchError((error) {
|
|
messenger.showSnackBar(
|
|
SnackBar(content: Text('오류: ${error.toString()}')),
|
|
);
|
|
});
|
|
} else {
|
|
// 보기 모드에서 수정 버튼 클릭 시
|
|
setState(() {
|
|
isEditing = true;
|
|
});
|
|
}
|
|
},
|
|
child: Text(isEditing ? '저장' : '수정'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// 회원 삭제 확인 다이얼로그
|
|
void _showDeleteConfirmationDialog(Member member) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (dialogContext) => AlertDialog(
|
|
title: const Text('회원 삭제'),
|
|
content: Text('${member.name} 회원을 정말 삭제하시겠습니까?'),
|
|
actions: DialogActions.confirm(
|
|
onCancel: () => Navigator.of(dialogContext).pop(),
|
|
onConfirm: () async {
|
|
// await 이전에 컨텍스트 파생 객체 캡처
|
|
final navigator = Navigator.of(dialogContext);
|
|
final messenger = ScaffoldMessenger.of(dialogContext);
|
|
try {
|
|
final memberService = Provider.of<MemberService>(context, listen: false);
|
|
await memberService.deleteMember(member.id);
|
|
navigator.pop();
|
|
messenger.showSnackBar(
|
|
const SnackBar(content: Text('회원이 삭제되었습니다')),
|
|
);
|
|
} catch (e) {
|
|
messenger.showSnackBar(
|
|
SnackBar(content: Text('회원 삭제에 실패했습니다: $e')),
|
|
);
|
|
}
|
|
},
|
|
destructive: true,
|
|
confirmText: '삭제',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|