이벤트 가져오기 위젯 테스트 추가 및 테스트 훅 도입
This commit is contained in:
@@ -7,6 +7,8 @@ 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});
|
||||
@@ -36,6 +38,8 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
}
|
||||
|
||||
Future<void> _loadMembers() async {
|
||||
// 비동기 이후 컨텍스트 직접 참조를 피하기 위해 메신저를 미리 캡처
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
try {
|
||||
print('_loadMembers 호출됨');
|
||||
final authService = Provider.of<AuthService>(context, listen: false);
|
||||
@@ -59,11 +63,9 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
}
|
||||
} catch (e) {
|
||||
print('회원 목록 가져오기 오류: $e');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('회원 목록을 불러오는데 실패했습니다: $e')),
|
||||
);
|
||||
}
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('회원 목록을 불러오는데 실패했습니다: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +195,8 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 성별 아이콘
|
||||
@@ -230,8 +234,10 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(height: 2),
|
||||
if (member.email.isNotEmpty)
|
||||
Text(
|
||||
member.email,
|
||||
@@ -239,19 +245,17 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
memberTypeText, // 회원 유형으로 변경
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.blue.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 회원 유형 배지 (공통 스타일 적용)
|
||||
BadgeStyles.memberType(memberTypeText),
|
||||
// 가입일 표시
|
||||
if (member.joinDate != null)
|
||||
Text(
|
||||
@@ -261,23 +265,8 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 상태 표시
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: getStatusColor(member.status ?? 'inactive'),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
getStatusLabel(member.status),
|
||||
style: TextStyle(
|
||||
color: getStatusTextColor(member.status ?? 'inactive'),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 상태 표시 (공통 배지 적용)
|
||||
BadgeStyles.memberStatus(getStatusLabel(member.status)),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -328,8 +317,8 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (dialogContext, setState) {
|
||||
return AlertDialog(
|
||||
title: const Text('회원 추가'),
|
||||
content: SingleChildScrollView(
|
||||
@@ -395,14 +384,16 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
key: const Key('members_add_gender_dropdown'),
|
||||
value: selectedGender,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '성별',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'male', child: Text('남성')),
|
||||
DropdownMenuItem(value: 'female', child: Text('여성')),
|
||||
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) {
|
||||
@@ -414,15 +405,17 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
key: const Key('members_add_member_type_dropdown'),
|
||||
value: selectedMemberType,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '회원 유형',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'regular', child: Text('정회원')),
|
||||
DropdownMenuItem(value: 'associate', child: Text('준회원')),
|
||||
DropdownMenuItem(value: 'guest', child: Text('게스트')),
|
||||
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) {
|
||||
@@ -434,14 +427,16 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
key: const Key('members_add_status_dropdown'),
|
||||
value: selectedStatus,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '상태',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'active', child: Text('활성')),
|
||||
DropdownMenuItem(value: 'inactive', child: Text('비활성')),
|
||||
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) {
|
||||
@@ -454,78 +449,65 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
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);
|
||||
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로 설정, 그렇지 않으면 트림한 값 사용
|
||||
if (emailController.text.trim().isEmpty) {
|
||||
memberData['email'] = null;
|
||||
} else {
|
||||
memberData['email'] = emailController.text.trim();
|
||||
}
|
||||
|
||||
// 전화번호가 비어있으면 null로 설정, 그렇지 않으면 트림한 값 사용
|
||||
if (phoneController.text.trim().isEmpty) {
|
||||
memberData['phone'] = null;
|
||||
} else {
|
||||
memberData['phone'] = phoneController.text.trim();
|
||||
}
|
||||
|
||||
await memberService.addMember(memberData);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('회원이 추가되었습니다')),
|
||||
);
|
||||
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) {
|
||||
// 가입일 파싱 오류 무시
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('회원 추가에 실패했습니다: $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('회원이 추가되었습니다')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('추가'),
|
||||
),
|
||||
],
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('회원 추가에 실패했습니다: $e')),
|
||||
);
|
||||
}
|
||||
},
|
||||
confirmText: '추가',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -619,7 +601,7 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
// 폼 컨트롤러 초기화
|
||||
final nameController = TextEditingController(text: member.name);
|
||||
final phoneController = TextEditingController(text: member.phone ?? '');
|
||||
final emailController = TextEditingController(text: member.email ?? '');
|
||||
final emailController = TextEditingController(text: member.email);
|
||||
final handicapController = TextEditingController(text: member.handicap?.toString() ?? '0');
|
||||
final joinDateController = TextEditingController(text: formatDate(member.joinDate));
|
||||
|
||||
@@ -630,8 +612,8 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (dialogContext, setState) {
|
||||
return AlertDialog(
|
||||
title: Text(isEditing ? '회원 정보 수정' : '회원 정보'),
|
||||
content: SingleChildScrollView(
|
||||
@@ -718,19 +700,21 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
// 성별 필드 - 상단에 없으므로 항상 표시
|
||||
buildFormField('성별', isEditing ?
|
||||
DropdownButtonFormField<String>(
|
||||
key: const Key('members_edit_gender_dropdown'),
|
||||
value: selectedGender,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
isExpanded: true,
|
||||
items: [
|
||||
DropdownMenuItem<String>(
|
||||
value: 'male',
|
||||
child: Text('남성'),
|
||||
child: Text('남성', maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'female',
|
||||
child: Text('여성'),
|
||||
child: Text('여성', maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
@@ -745,31 +729,33 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
// 회원 유형 필드 - 수정 모드에서만 표시 (상단에 이미 표시됨)
|
||||
if (isEditing)
|
||||
buildFormField('회원 유형', DropdownButtonFormField<String>(
|
||||
key: const Key('members_edit_member_type_dropdown'),
|
||||
value: selectedMemberType,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
isExpanded: true,
|
||||
items: [
|
||||
DropdownMenuItem<String>(
|
||||
value: 'regular',
|
||||
child: Text('정회원'),
|
||||
child: Text('정회원', maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'associate',
|
||||
child: Text('준회원'),
|
||||
child: Text('준회원', maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'guest',
|
||||
child: Text('게스트'),
|
||||
child: Text('게스트', maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'manager',
|
||||
child: Text('운영진'),
|
||||
child: Text('운영진', maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'owner',
|
||||
child: Text('모임장'),
|
||||
child: Text('모임장', maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
@@ -817,23 +803,25 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
// 상태 필드 - 수정 모드에서만 표시 (상단에 이미 표시됨)
|
||||
if (isEditing)
|
||||
buildFormField('상태', DropdownButtonFormField<String>(
|
||||
key: const Key('members_edit_status_dropdown'),
|
||||
value: selectedStatus,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
isExpanded: true,
|
||||
items: [
|
||||
DropdownMenuItem<String>(
|
||||
value: 'active',
|
||||
child: Text('활성'),
|
||||
child: Text('활성', maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'inactive',
|
||||
child: Text('비활성'),
|
||||
child: Text('비활성', maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'suspended',
|
||||
child: Text('정지'),
|
||||
child: Text('정지', maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
@@ -848,7 +836,7 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
context: dialogContext,
|
||||
initialDate: member.joinDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime.now(),
|
||||
@@ -900,16 +888,19 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
actions: [
|
||||
// 취소 버튼
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
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) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(content: Text('이름은 필수 입력 항목입니다.')),
|
||||
);
|
||||
return;
|
||||
@@ -952,12 +943,12 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
updatedMember.id,
|
||||
memberData,
|
||||
).then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(content: Text('회원 정보가 업데이트되었습니다.')),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
navigator.pop();
|
||||
}).catchError((error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('오류: ${error.toString()}')),
|
||||
);
|
||||
});
|
||||
@@ -981,37 +972,31 @@ class _MembersScreenState extends State<MembersScreen> {
|
||||
void _showDeleteConfirmationDialog(Member member) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
builder: (dialogContext) => 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('삭제'),
|
||||
),
|
||||
],
|
||||
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: '삭제',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user