Line data Source code
1 : import 'package:flutter/material.dart';
2 : import 'package:provider/provider.dart';
3 : import 'package:intl/intl.dart';
4 :
5 : import '../../services/auth_service.dart';
6 : import '../../services/club_service.dart';
7 : import '../../services/member_service.dart';
8 : import '../../models/member_model.dart';
9 : import '../../utils/enum_mappings.dart';
10 :
11 : class MembersScreen extends StatefulWidget {
12 24 : const MembersScreen({super.key});
13 :
14 0 : @override
15 0 : State<MembersScreen> createState() => _MembersScreenState();
16 : }
17 :
18 : class _MembersScreenState extends State<MembersScreen> {
19 : bool _isInit = false;
20 : String _searchQuery = '';
21 : final TextEditingController _searchController = TextEditingController();
22 :
23 0 : @override
24 : void dispose() {
25 0 : _searchController.dispose();
26 0 : super.dispose();
27 : }
28 :
29 0 : @override
30 : void didChangeDependencies() {
31 0 : super.didChangeDependencies();
32 0 : if (!_isInit) {
33 0 : _loadMembers();
34 0 : _isInit = true;
35 : }
36 : }
37 :
38 0 : Future<void> _loadMembers() async {
39 : try {
40 0 : print('_loadMembers 호출됨');
41 0 : final authService = Provider.of<AuthService>(context, listen: false);
42 0 : final clubService = Provider.of<ClubService>(context, listen: false);
43 0 : final memberService = Provider.of<MemberService>(context, listen: false);
44 :
45 0 : print('현재 클럽: ${clubService.currentClub?.name}');
46 0 : print('토큰: ${authService.token}');
47 :
48 0 : if (clubService.currentClub != null && authService.token != null) {
49 0 : print('멤버 서비스 초기화 시작');
50 0 : memberService.initialize(authService.token!);
51 0 : memberService.setClubId(clubService.currentClub!.id); // 클럽 ID 설정 추가
52 0 : print('멤버 서비스 초기화 완료');
53 :
54 0 : print('회원 목록 가져오기 시작');
55 0 : await memberService.fetchClubMembers();
56 0 : print('회원 목록 가져오기 완료: ${memberService.members.length}개');
57 : } else {
58 0 : print('클럽 또는 토큰이 없어서 회원 목록을 가져올 수 없습니다.');
59 : }
60 : } catch (e) {
61 0 : print('회원 목록 가져오기 오류: $e');
62 0 : if (mounted) {
63 0 : ScaffoldMessenger.of(context).showSnackBar(
64 0 : SnackBar(content: Text('회원 목록을 불러오는데 실패했습니다: $e')),
65 : );
66 : }
67 : }
68 : }
69 :
70 0 : List<Member> _getFilteredMembers(List<Member> members) {
71 0 : if (_searchQuery.isEmpty) {
72 : return members;
73 : }
74 :
75 0 : final query = _searchQuery.toLowerCase();
76 0 : return members.where((member) {
77 0 : return member.name.toLowerCase().contains(query) ||
78 0 : member.email.toLowerCase().contains(query);
79 0 : }).toList();
80 : }
81 :
82 0 : @override
83 : Widget build(BuildContext context) {
84 0 : return Scaffold(
85 0 : body: Column(
86 0 : children: [
87 : // 검색 바
88 0 : Padding(
89 : padding: const EdgeInsets.all(16.0),
90 0 : child: TextField(
91 0 : controller: _searchController,
92 0 : decoration: InputDecoration(
93 : hintText: '회원 검색',
94 : prefixIcon: const Icon(Icons.search),
95 0 : border: OutlineInputBorder(
96 0 : borderRadius: BorderRadius.circular(12),
97 : ),
98 : contentPadding: const EdgeInsets.symmetric(vertical: 0),
99 0 : suffixIcon: _searchQuery.isNotEmpty
100 0 : ? IconButton(
101 : icon: const Icon(Icons.clear),
102 0 : onPressed: () {
103 0 : _searchController.clear();
104 0 : setState(() {
105 0 : _searchQuery = '';
106 : });
107 : },
108 : )
109 : : null,
110 : ),
111 0 : onChanged: (value) {
112 0 : setState(() {
113 0 : _searchQuery = value;
114 : });
115 : },
116 : ),
117 : ),
118 :
119 : // 회원 목록
120 0 : Expanded(
121 0 : child: Consumer<MemberService>(
122 0 : builder: (context, memberService, _) {
123 0 : if (memberService.isLoading) {
124 : return const Center(child: CircularProgressIndicator());
125 : }
126 :
127 0 : final filteredMembers = _getFilteredMembers(memberService.members);
128 :
129 0 : if (filteredMembers.isEmpty) {
130 0 : return Center(
131 0 : child: Text(
132 0 : _searchQuery.isEmpty
133 : ? '등록된 회원이 없습니다'
134 : : '검색 결과가 없습니다',
135 : ),
136 : );
137 : }
138 :
139 0 : return RefreshIndicator(
140 0 : onRefresh: _loadMembers,
141 0 : child: ListView.builder(
142 : padding: const EdgeInsets.only(bottom: 16),
143 0 : itemCount: filteredMembers.length,
144 0 : itemBuilder: (context, index) {
145 0 : final member = filteredMembers[index];
146 0 : return _buildMemberCard(member);
147 : },
148 : ),
149 : );
150 : },
151 : ),
152 : ),
153 : ],
154 : ),
155 0 : floatingActionButton: FloatingActionButton(
156 0 : onPressed: () {
157 : // 회원 추가 화면으로 이동 (추후 구현)
158 0 : _showAddMemberDialog();
159 : },
160 : backgroundColor: Colors.blue,
161 : child: const Icon(Icons.add, color: Colors.white),
162 : ),
163 : );
164 : }
165 :
166 0 : Widget _buildMemberCard(Member member) {
167 0 : final memberTypeText = _getMemberTypeDisplayName(member.memberType);
168 :
169 0 : return Card(
170 : margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
171 : elevation: 2,
172 0 : shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
173 0 : child: InkWell(
174 0 : onTap: () {
175 : // 회원 상세 정보 다이얼로그 표시
176 0 : _showMemberDetailDialog(member);
177 : },
178 0 : borderRadius: BorderRadius.circular(12),
179 0 : child: Padding(
180 : padding: const EdgeInsets.all(16.0),
181 0 : child: Row(
182 0 : children: [
183 : // 회원 정보
184 0 : Expanded(
185 0 : child: Column(
186 : crossAxisAlignment: CrossAxisAlignment.start,
187 0 : children: [
188 0 : Row(
189 0 : children: [
190 0 : Text(
191 0 : member.name,
192 : style: const TextStyle(
193 : fontSize: 18,
194 : fontWeight: FontWeight.bold,
195 : ),
196 : ),
197 : const SizedBox(width: 8),
198 : // 성별 아이콘
199 0 : Icon(
200 0 : member.gender == 'female' ? Icons.female : Icons.male,
201 : size: 18,
202 0 : color: member.gender == 'female' ? Colors.pink : Colors.blue,
203 : ),
204 : // 핸디캡
205 0 : if (member.handicap != null && member.handicap != 0)
206 0 : Container(
207 : margin: const EdgeInsets.only(left: 8),
208 : padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
209 0 : decoration: BoxDecoration(
210 0 : color: Colors.orange.shade100,
211 0 : borderRadius: BorderRadius.circular(12),
212 : ),
213 0 : child: Text(
214 0 : (member.handicap! > 0) ? "+${member.handicap}" : member.handicap.toString(),
215 0 : style: TextStyle(
216 : fontSize: 12,
217 0 : color: Colors.orange.shade800,
218 : fontWeight: FontWeight.bold,
219 : ),
220 : ),
221 : ),
222 : ],
223 : ),
224 0 : if (member.email.isNotEmpty || member.phone != null)
225 0 : const SizedBox(height: 4),
226 0 : if (member.phone != null)
227 0 : Text(
228 0 : member.phone!,
229 0 : style: TextStyle(
230 : fontSize: 14,
231 0 : color: Colors.grey.shade700,
232 : ),
233 : ),
234 0 : const SizedBox(width: 8),
235 0 : if (member.email.isNotEmpty)
236 0 : Text(
237 0 : member.email,
238 0 : style: TextStyle(
239 : fontSize: 14,
240 0 : color: Colors.grey.shade700,
241 : ),
242 : ),
243 0 : const SizedBox(height: 4),
244 0 : Row(
245 0 : children: [
246 0 : Text(
247 : memberTypeText, // 회원 유형으로 변경
248 0 : style: TextStyle(
249 : fontSize: 14,
250 0 : color: Colors.blue.shade700,
251 : fontWeight: FontWeight.w500,
252 : ),
253 : ),
254 : const SizedBox(width: 8),
255 : // 가입일 표시
256 0 : if (member.joinDate != null)
257 0 : Text(
258 0 : '가입: ${formatDate(member.joinDate!)}',
259 0 : style: TextStyle(
260 : fontSize: 12,
261 0 : color: Colors.grey.shade600,
262 : ),
263 : ),
264 0 : const SizedBox(width: 8),
265 : // 상태 표시
266 0 : Container(
267 : padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
268 0 : decoration: BoxDecoration(
269 0 : color: getStatusColor(member.status ?? 'inactive'),
270 0 : borderRadius: BorderRadius.circular(12),
271 : ),
272 0 : child: Text(
273 0 : getStatusLabel(member.status),
274 0 : style: TextStyle(
275 0 : color: getStatusTextColor(member.status ?? 'inactive'),
276 : fontWeight: FontWeight.bold,
277 : fontSize: 12,
278 : ),
279 : ),
280 : ),
281 : ],
282 : ),
283 : ],
284 : ),
285 : ),
286 : // 액션 버튼
287 0 : PopupMenuButton<String>(
288 : icon: const Icon(Icons.more_vert),
289 0 : onSelected: (value) {
290 0 : if (value == 'edit') {
291 0 : _showMemberDetailDialog(member);
292 0 : } else if (value == 'delete') {
293 0 : _showDeleteConfirmationDialog(member);
294 : }
295 : },
296 0 : itemBuilder: (context) => [
297 : const PopupMenuItem(
298 : value: 'delete',
299 : child: Row(
300 : children: [
301 : Icon(Icons.delete, size: 20, color: Colors.red),
302 : SizedBox(width: 8),
303 : Text('삭제', style: TextStyle(color: Colors.red)),
304 : ],
305 : ),
306 : ),
307 : ],
308 : ),
309 : ],
310 : ),
311 : ),
312 : ),
313 : );
314 : }
315 :
316 : // 회원 추가 다이얼로그
317 0 : void _showAddMemberDialog() {
318 0 : final nameController = TextEditingController();
319 0 : final emailController = TextEditingController();
320 0 : final phoneController = TextEditingController();
321 0 : final handicapController = TextEditingController(text: '0');
322 0 : final joinDateController = TextEditingController(text: formatDate(DateTime.now()));
323 :
324 : // 선택형 필드를 위한 값 초기화
325 : String selectedGender = 'male';
326 : String selectedMemberType = 'regular';
327 : String selectedStatus = 'active';
328 :
329 0 : showDialog(
330 0 : context: context,
331 0 : builder: (context) => StatefulBuilder(
332 0 : builder: (context, setState) {
333 0 : return AlertDialog(
334 : title: const Text('회원 추가'),
335 0 : content: SingleChildScrollView(
336 0 : child: Column(
337 : mainAxisSize: MainAxisSize.min,
338 0 : children: [
339 0 : TextField(
340 : controller: nameController,
341 : decoration: const InputDecoration(
342 : labelText: '이름',
343 : border: OutlineInputBorder(),
344 : ),
345 : ),
346 : const SizedBox(height: 16),
347 0 : TextField(
348 : controller: emailController,
349 : keyboardType: TextInputType.emailAddress,
350 : decoration: const InputDecoration(
351 : labelText: '이메일',
352 : border: OutlineInputBorder(),
353 : ),
354 : ),
355 : const SizedBox(height: 16),
356 0 : TextField(
357 : controller: phoneController,
358 : keyboardType: TextInputType.phone,
359 : decoration: const InputDecoration(
360 : labelText: '전화번호 (선택사항)',
361 : border: OutlineInputBorder(),
362 : ),
363 : ),
364 : const SizedBox(height: 16),
365 0 : TextField(
366 : controller: handicapController,
367 : keyboardType: TextInputType.number,
368 : decoration: const InputDecoration(
369 : labelText: '핸디캡',
370 : border: OutlineInputBorder(),
371 : ),
372 : ),
373 : const SizedBox(height: 16),
374 0 : TextField(
375 : controller: joinDateController,
376 : readOnly: true,
377 : decoration: const InputDecoration(
378 : labelText: '가입일',
379 : border: OutlineInputBorder(),
380 : suffixIcon: Icon(Icons.calendar_today),
381 : ),
382 0 : onTap: () async {
383 0 : final DateTime? picked = await showDatePicker(
384 : context: context,
385 0 : initialDate: DateTime.now(),
386 0 : firstDate: DateTime(2000),
387 0 : lastDate: DateTime(2101),
388 : );
389 : if (picked != null) {
390 0 : setState(() {
391 0 : joinDateController.text = formatDate(picked);
392 : });
393 : }
394 : },
395 : ),
396 : const SizedBox(height: 16),
397 0 : DropdownButtonFormField<String>(
398 : value: selectedGender,
399 : decoration: const InputDecoration(
400 : labelText: '성별',
401 : border: OutlineInputBorder(),
402 : ),
403 : items: const [
404 : DropdownMenuItem(value: 'male', child: Text('남성')),
405 : DropdownMenuItem(value: 'female', child: Text('여성')),
406 : ],
407 0 : onChanged: (value) {
408 : if (value != null) {
409 0 : setState(() {
410 : selectedGender = value;
411 : });
412 : }
413 : },
414 : ),
415 : const SizedBox(height: 16),
416 0 : DropdownButtonFormField<String>(
417 : value: selectedMemberType,
418 : decoration: const InputDecoration(
419 : labelText: '회원 유형',
420 : border: OutlineInputBorder(),
421 : ),
422 : items: const [
423 : DropdownMenuItem(value: 'regular', child: Text('정회원')),
424 : DropdownMenuItem(value: 'associate', child: Text('준회원')),
425 : DropdownMenuItem(value: 'guest', child: Text('게스트')),
426 : ],
427 0 : onChanged: (value) {
428 : if (value != null) {
429 0 : setState(() {
430 : selectedMemberType = value;
431 : });
432 : }
433 : },
434 : ),
435 : const SizedBox(height: 16),
436 0 : DropdownButtonFormField<String>(
437 : value: selectedStatus,
438 : decoration: const InputDecoration(
439 : labelText: '상태',
440 : border: OutlineInputBorder(),
441 : ),
442 : items: const [
443 : DropdownMenuItem(value: 'active', child: Text('활성')),
444 : DropdownMenuItem(value: 'inactive', child: Text('비활성')),
445 : ],
446 0 : onChanged: (value) {
447 : if (value != null) {
448 0 : setState(() {
449 : selectedStatus = value;
450 : });
451 : }
452 : },
453 : ),
454 : ],
455 : ),
456 : ),
457 0 : actions: [
458 0 : TextButton(
459 0 : onPressed: () => Navigator.of(context).pop(),
460 : child: const Text('취소'),
461 : ),
462 0 : ElevatedButton(
463 0 : onPressed: () async {
464 0 : if (nameController.text.isEmpty) {
465 0 : ScaffoldMessenger.of(context).showSnackBar(
466 : const SnackBar(content: Text('이름은 필수 입력 항목입니다')),
467 : );
468 : return;
469 : }
470 :
471 : try {
472 0 : final memberService = Provider.of<MemberService>(context, listen: false);
473 0 : final clubService = Provider.of<ClubService>(context, listen: false);
474 :
475 0 : if (clubService.currentClub != null) {
476 : // 회원 추가 데이터 준비
477 0 : final memberData = <String, dynamic>{
478 0 : 'name': nameController.text.trim(),
479 0 : 'clubId': clubService.currentClub!.id,
480 : 'gender': selectedGender,
481 : 'memberType': selectedMemberType,
482 : 'status': selectedStatus,
483 0 : 'handicap': int.tryParse(handicapController.text) ?? 0,
484 0 : 'isActive': selectedStatus == 'active',
485 : };
486 :
487 : // 가입일 추가
488 : try {
489 0 : final joinDate = parseDate(joinDateController.text);
490 : if (joinDate != null) {
491 0 : memberData['joinDate'] = joinDate.toIso8601String();
492 : }
493 : } catch (e) {
494 : // 가입일 파싱 오류 무시
495 : }
496 :
497 : // 이메일이 비어있으면 null로 설정, 그렇지 않으면 트림한 값 사용
498 0 : if (emailController.text.trim().isEmpty) {
499 0 : memberData['email'] = null;
500 : } else {
501 0 : memberData['email'] = emailController.text.trim();
502 : }
503 :
504 : // 전화번호가 비어있으면 null로 설정, 그렇지 않으면 트림한 값 사용
505 0 : if (phoneController.text.trim().isEmpty) {
506 0 : memberData['phone'] = null;
507 : } else {
508 0 : memberData['phone'] = phoneController.text.trim();
509 : }
510 :
511 0 : await memberService.addMember(memberData);
512 :
513 0 : if (mounted) {
514 0 : Navigator.of(context).pop();
515 0 : ScaffoldMessenger.of(context).showSnackBar(
516 : const SnackBar(content: Text('회원이 추가되었습니다')),
517 : );
518 : }
519 : }
520 : } catch (e) {
521 0 : ScaffoldMessenger.of(context).showSnackBar(
522 0 : SnackBar(content: Text('회원 추가에 실패했습니다: $e')),
523 : );
524 : }
525 : },
526 : child: const Text('추가'),
527 : ),
528 : ],
529 : );
530 : },
531 : ),
532 : );
533 : }
534 :
535 : // 날짜 포맷팅 함수
536 0 : String formatDate(DateTime? date) {
537 : if (date == null) return '정보 없음';
538 0 : return DateFormat('yyyy-MM-dd').format(date);
539 : }
540 :
541 : // 날짜 파싱 함수
542 0 : DateTime? parseDate(String? dateStr) {
543 : if (dateStr == null) return null;
544 0 : if (dateStr.isEmpty) return null;
545 : try {
546 : // 다양한 형식 지원
547 0 : if (dateStr.contains('T')) {
548 : // ISO 형식 (예: 2023-01-01T00:00:00.000Z)
549 0 : return DateTime.parse(dateStr);
550 0 : } else if (dateStr.contains('-')) {
551 : // yyyy-MM-dd 형식
552 0 : return DateFormat('yyyy-MM-dd').parse(dateStr);
553 : }
554 : return null;
555 : } catch (e) {
556 : return null;
557 : }
558 : }
559 :
560 : // 폼 필드 생성 헬퍼 메서드
561 : // 폼 필드 빌더 헬퍼 함수
562 0 : Widget buildFormField(String label, Widget field) {
563 0 : return Padding(
564 : padding: const EdgeInsets.only(bottom: 16.0),
565 0 : child: Column(
566 : crossAxisAlignment: CrossAxisAlignment.start,
567 0 : children: [
568 0 : Text(
569 : label,
570 : style: const TextStyle(
571 : fontSize: 14,
572 : fontWeight: FontWeight.w500,
573 : color: Colors.grey,
574 : ),
575 : ),
576 : const SizedBox(height: 4),
577 : field,
578 : ],
579 : ),
580 : );
581 : }
582 :
583 : // 성별 표시 이름 변환
584 0 : String _getGenderDisplayName(String? gender) {
585 0 : return getGenderLabel(gender);
586 : }
587 :
588 : // 회원 유형 표시 이름 변환
589 0 : String _getMemberTypeDisplayName(String? memberType) {
590 0 : return getMemberTypeLabel(memberType);
591 : }
592 :
593 : // 회원 상태에 따른 배경색 가져오기
594 0 : Color getStatusColor(String status) {
595 : switch (status) {
596 0 : case 'active': return Colors.green.shade100;
597 0 : case 'inactive': return Colors.grey.shade200;
598 0 : case 'suspended': return Colors.red.shade100;
599 0 : case 'deleted': return Colors.grey.shade300;
600 0 : default: return Colors.grey.shade200;
601 : }
602 : }
603 :
604 : // 회원 상태에 따른 텍스트 색상 가져오기
605 0 : Color getStatusTextColor(String status) {
606 : switch (status) {
607 0 : case 'active': return Colors.green.shade800;
608 0 : case 'inactive': return Colors.grey.shade700;
609 0 : case 'suspended': return Colors.red.shade800;
610 0 : case 'deleted': return Colors.grey.shade800;
611 0 : default: return Colors.grey.shade700;
612 : }
613 : }
614 :
615 : // 회원 상세 정보 다이얼로그
616 0 : void _showMemberDetailDialog(Member member) {
617 : bool isEditing = false;
618 :
619 : // 폼 컨트롤러 초기화
620 0 : final nameController = TextEditingController(text: member.name);
621 0 : final phoneController = TextEditingController(text: member.phone ?? '');
622 0 : final emailController = TextEditingController(text: member.email ?? '');
623 0 : final handicapController = TextEditingController(text: member.handicap?.toString() ?? '0');
624 0 : final joinDateController = TextEditingController(text: formatDate(member.joinDate));
625 :
626 : // 선택형 필드를 위한 값 초기화
627 0 : String selectedGender = member.gender ?? 'male';
628 0 : String selectedMemberType = member.memberType ?? 'regular';
629 0 : String selectedStatus = member.status ?? (member.isActive ? 'active' : 'inactive');
630 :
631 0 : showDialog(
632 0 : context: context,
633 0 : builder: (context) => StatefulBuilder(
634 0 : builder: (context, setState) {
635 0 : return AlertDialog(
636 0 : title: Text(isEditing ? '회원 정보 수정' : '회원 정보'),
637 0 : content: SingleChildScrollView(
638 0 : child: Column(
639 : mainAxisSize: MainAxisSize.min,
640 : crossAxisAlignment: CrossAxisAlignment.start,
641 0 : children: [
642 : // 회원 정보 헤더 (보기 모드에서만 표시)
643 0 : if (!isEditing) ...[
644 0 : Row(
645 : crossAxisAlignment: CrossAxisAlignment.center,
646 0 : children: [
647 : // 현재 회원 상태
648 0 : Expanded(
649 0 : child: Column(
650 : crossAxisAlignment: CrossAxisAlignment.start,
651 0 : children: [
652 0 : Row(
653 0 : children: [
654 0 : Text(
655 0 : member.name,
656 : style: const TextStyle(
657 : fontSize: 18,
658 : fontWeight: FontWeight.bold,
659 : ),
660 : ),
661 : ],
662 : ),
663 : const SizedBox(height: 4),
664 0 : Text(
665 0 : _getMemberTypeDisplayName(member.memberType),
666 0 : style: TextStyle(
667 : fontSize: 14,
668 0 : color: Colors.blue.shade700,
669 : fontWeight: FontWeight.w500,
670 : ),
671 : ),
672 0 : if (member.phone != null && member.phone!.isNotEmpty) ...[
673 : const SizedBox(height: 4),
674 0 : Text(
675 0 : member.phone!,
676 0 : style: TextStyle(
677 : fontSize: 14,
678 0 : color: Colors.grey.shade700,
679 : ),
680 : ),
681 : ],
682 0 : if (member.email.isNotEmpty) ...[
683 : const SizedBox(height: 2),
684 0 : Text(
685 0 : member.email,
686 0 : style: TextStyle(
687 : fontSize: 14,
688 0 : color: Colors.grey.shade700,
689 : ),
690 : ),
691 : ],
692 : ],
693 : ),
694 : ),
695 : ],
696 : ),
697 : const SizedBox(height: 16),
698 : const Divider(),
699 : ],
700 :
701 : // 폼 필드
702 0 : Container(
703 : width: double.infinity,
704 : padding: const EdgeInsets.symmetric(vertical: 8),
705 0 : child: Column(
706 : crossAxisAlignment: CrossAxisAlignment.start,
707 0 : children: [
708 : // 이름 필드 - 수정 모드에서만 표시 (상단에 이미 표시됨)
709 : if (isEditing)
710 0 : buildFormField('이름', TextField(
711 : controller: nameController,
712 : decoration: const InputDecoration(
713 : border: OutlineInputBorder(),
714 : contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
715 : ),
716 : )),
717 :
718 : // 성별 필드 - 상단에 없으므로 항상 표시
719 0 : buildFormField('성별', isEditing ?
720 0 : DropdownButtonFormField<String>(
721 : value: selectedGender,
722 : decoration: const InputDecoration(
723 : border: OutlineInputBorder(),
724 : contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
725 : ),
726 0 : items: [
727 0 : DropdownMenuItem<String>(
728 : value: 'male',
729 0 : child: Text('남성'),
730 : ),
731 0 : DropdownMenuItem<String>(
732 : value: 'female',
733 0 : child: Text('여성'),
734 : ),
735 : ],
736 0 : onChanged: (value) {
737 : if (value != null) {
738 : selectedGender = value;
739 : }
740 : },
741 : ) :
742 0 : Text(_getGenderDisplayName(member.gender), style: const TextStyle(fontSize: 16))
743 : ),
744 :
745 : // 회원 유형 필드 - 수정 모드에서만 표시 (상단에 이미 표시됨)
746 : if (isEditing)
747 0 : buildFormField('회원 유형', DropdownButtonFormField<String>(
748 : value: selectedMemberType,
749 : decoration: const InputDecoration(
750 : border: OutlineInputBorder(),
751 : contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
752 : ),
753 0 : items: [
754 0 : DropdownMenuItem<String>(
755 : value: 'regular',
756 0 : child: Text('정회원'),
757 : ),
758 0 : DropdownMenuItem<String>(
759 : value: 'associate',
760 0 : child: Text('준회원'),
761 : ),
762 0 : DropdownMenuItem<String>(
763 : value: 'guest',
764 0 : child: Text('게스트'),
765 : ),
766 0 : DropdownMenuItem<String>(
767 : value: 'manager',
768 0 : child: Text('운영진'),
769 : ),
770 0 : DropdownMenuItem<String>(
771 : value: 'owner',
772 0 : child: Text('모임장'),
773 : ),
774 : ],
775 0 : onChanged: (value) {
776 : if (value != null) {
777 : selectedMemberType = value;
778 : }
779 : },
780 : )),
781 :
782 : // 전화번호 필드 - 수정 모드에서만 표시 (상단에 이미 표시됨)
783 : if (isEditing)
784 0 : buildFormField('전화번호', TextField(
785 : controller: phoneController,
786 : keyboardType: TextInputType.phone,
787 : decoration: const InputDecoration(
788 : border: OutlineInputBorder(),
789 : contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
790 : ),
791 : )),
792 :
793 : // 이메일 필드 - 수정 모드에서만 표시 (상단에 이미 표시됨)
794 : if (isEditing)
795 0 : buildFormField('이메일', TextField(
796 : controller: emailController,
797 : keyboardType: TextInputType.emailAddress,
798 : decoration: const InputDecoration(
799 : border: OutlineInputBorder(),
800 : contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
801 : ),
802 : )),
803 :
804 : // 핸디캡 필드 - 상단에 없으므로 항상 표시
805 0 : buildFormField('핸디캡', isEditing ?
806 0 : TextField(
807 : controller: handicapController,
808 : keyboardType: TextInputType.number,
809 : decoration: const InputDecoration(
810 : border: OutlineInputBorder(),
811 : contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
812 : ),
813 : ) :
814 0 : Text(member.handicap?.toString() ?? '0', style: const TextStyle(fontSize: 16))
815 : ),
816 :
817 : // 상태 필드 - 수정 모드에서만 표시 (상단에 이미 표시됨)
818 : if (isEditing)
819 0 : buildFormField('상태', DropdownButtonFormField<String>(
820 : value: selectedStatus,
821 : decoration: const InputDecoration(
822 : border: OutlineInputBorder(),
823 : contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
824 : ),
825 0 : items: [
826 0 : DropdownMenuItem<String>(
827 : value: 'active',
828 0 : child: Text('활성'),
829 : ),
830 0 : DropdownMenuItem<String>(
831 : value: 'inactive',
832 0 : child: Text('비활성'),
833 : ),
834 0 : DropdownMenuItem<String>(
835 : value: 'suspended',
836 0 : child: Text('정지'),
837 : ),
838 : ],
839 0 : onChanged: (value) {
840 : if (value != null) {
841 : selectedStatus = value;
842 : }
843 : },
844 : )),
845 :
846 : // 가입일 필드
847 0 : buildFormField('가입일', isEditing ?
848 0 : InkWell(
849 0 : onTap: () async {
850 0 : final DateTime? picked = await showDatePicker(
851 : context: context,
852 0 : initialDate: member.joinDate ?? DateTime.now(),
853 0 : firstDate: DateTime(2000),
854 0 : lastDate: DateTime.now(),
855 : // locale은 앱 전체 설정을 사용합니다
856 : );
857 : if (picked != null) {
858 0 : setState(() {
859 : // 새로운 날짜로 업데이트
860 0 : joinDateController.text = formatDate(picked);
861 :
862 : // copyWith 메서드를 사용하여 효율적으로 업데이트
863 0 : member = member.copyWith(
864 : joinDate: picked, // 새로 선택한 날짜
865 : );
866 : });
867 : }
868 : },
869 0 : child: InputDecorator(
870 : decoration: const InputDecoration(
871 : border: OutlineInputBorder(),
872 : contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
873 : suffixIcon: Icon(Icons.calendar_today),
874 : ),
875 0 : child: Text(formatDate(member.joinDate), style: const TextStyle(fontSize: 16)),
876 : ),
877 : ) :
878 0 : Text(formatDate(member.joinDate), style: const TextStyle(fontSize: 16))
879 : ),
880 :
881 : // 시스템 정보 (수정불가)
882 0 : if (!isEditing) ...[
883 : const SizedBox(height: 16),
884 : const Divider(),
885 : const SizedBox(height: 8),
886 : const Text('시스템 정보', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
887 : const SizedBox(height: 8),
888 0 : buildFormField('회원 ID', Text(member.id, style: const TextStyle(fontSize: 14, fontFamily: 'monospace'))),
889 0 : if (member.userId.isNotEmpty)
890 0 : buildFormField('유저 ID', Text(member.userId, style: const TextStyle(fontSize: 14, fontFamily: 'monospace'))),
891 0 : if (member.clubId.isNotEmpty)
892 0 : buildFormField('클럽 ID', Text(member.clubId, style: const TextStyle(fontSize: 14, fontFamily: 'monospace'))),
893 : ],
894 : ],
895 : ),
896 : ),
897 : ],
898 : ),
899 : ),
900 0 : actions: [
901 : // 취소 버튼
902 0 : TextButton(
903 0 : onPressed: () => Navigator.of(context).pop(),
904 : child: const Text('취소'),
905 : ),
906 : // 수정/저장 버튼
907 0 : TextButton(
908 0 : onPressed: () {
909 : if (isEditing) {
910 : // 수정 모드에서 저장 버튼 클릭 시
911 0 : if (nameController.text.trim().isEmpty) {
912 0 : ScaffoldMessenger.of(context).showSnackBar(
913 : const SnackBar(content: Text('이름은 필수 입력 항목입니다.')),
914 : );
915 : return;
916 : }
917 :
918 : // copyWith 메서드를 사용하여 회원 정보 업데이트
919 0 : final updatedMember = member.copyWith(
920 0 : name: nameController.text.trim(),
921 0 : email: emailController.text.trim().isEmpty ? null : emailController.text.trim(),
922 0 : phone: phoneController.text.trim().isEmpty ? null : phoneController.text.trim(),
923 : memberType: selectedMemberType,
924 : gender: selectedGender,
925 : status: selectedStatus,
926 0 : handicap: int.tryParse(handicapController.text) ?? member.handicap,
927 0 : isActive: selectedStatus == 'active',
928 0 : updatedAt: DateTime.now(),
929 : // joinDate는 이미 날짜 선택기에서 업데이트되었으므로 여기서 다시 설정할 필요 없음
930 : );
931 :
932 : // 회원 정보 저장
933 : // toJson 결과에서 clubId 제거하여 MemberService에서 설정한 clubId만 사용
934 0 : final memberData = updatedMember.toJson();
935 0 : memberData.remove('clubId'); // clubId 필드 제거
936 :
937 : // 전화번호와 이메일 필드가 비어있을 때 null로 명시적 설정
938 0 : if (phoneController.text.trim().isEmpty) {
939 0 : memberData['phone'] = null;
940 : }
941 :
942 0 : if (emailController.text.trim().isEmpty) {
943 0 : memberData['email'] = null;
944 : }
945 :
946 : // 가입일 필드 명시적으로 추가 (ISO 형식으로 변환)
947 0 : if (member.joinDate != null) {
948 0 : memberData['joinDate'] = member.joinDate!.toIso8601String();
949 : }
950 :
951 0 : context.read<MemberService>().updateMember(
952 0 : updatedMember.id,
953 : memberData,
954 0 : ).then((_) {
955 0 : ScaffoldMessenger.of(context).showSnackBar(
956 : const SnackBar(content: Text('회원 정보가 업데이트되었습니다.')),
957 : );
958 0 : Navigator.of(context).pop();
959 0 : }).catchError((error) {
960 0 : ScaffoldMessenger.of(context).showSnackBar(
961 0 : SnackBar(content: Text('오류: ${error.toString()}')),
962 : );
963 : });
964 : } else {
965 : // 보기 모드에서 수정 버튼 클릭 시
966 0 : setState(() {
967 : isEditing = true;
968 : });
969 : }
970 : },
971 0 : child: Text(isEditing ? '저장' : '수정'),
972 : ),
973 : ],
974 : );
975 : },
976 : ),
977 : );
978 : }
979 :
980 : // 회원 삭제 확인 다이얼로그
981 0 : void _showDeleteConfirmationDialog(Member member) {
982 0 : showDialog(
983 0 : context: context,
984 0 : builder: (context) => AlertDialog(
985 : title: const Text('회원 삭제'),
986 0 : content: Text('${member.name} 회원을 정말 삭제하시겠습니까?'),
987 0 : actions: [
988 0 : TextButton(
989 0 : onPressed: () => Navigator.of(context).pop(),
990 : child: const Text('취소'),
991 : ),
992 0 : TextButton(
993 0 : onPressed: () async {
994 : try {
995 0 : final memberService = Provider.of<MemberService>(context, listen: false);
996 :
997 0 : await memberService.deleteMember(member.id);
998 :
999 0 : if (mounted) {
1000 0 : Navigator.of(context).pop();
1001 0 : ScaffoldMessenger.of(context).showSnackBar(
1002 : const SnackBar(content: Text('회원이 삭제되었습니다')),
1003 : );
1004 : }
1005 : } catch (e) {
1006 0 : ScaffoldMessenger.of(context).showSnackBar(
1007 0 : SnackBar(content: Text('회원 삭제에 실패했습니다: $e')),
1008 : );
1009 : }
1010 : },
1011 0 : style: TextButton.styleFrom(foregroundColor: Colors.red),
1012 : child: const Text('삭제'),
1013 : ),
1014 : ],
1015 : ),
1016 : );
1017 : }
1018 : }
|