Files
2025-11-10 14:14:08 +09:00

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: '삭제',
),
),
);
}
}