Line data Source code
1 : import 'package:flutter/material.dart';
2 : import 'package:flutter/services.dart';
3 : import 'package:provider/provider.dart';
4 : import 'package:intl/intl.dart';
5 : import 'package:share_plus/share_plus.dart';
6 :
7 : import '../../models/club_model.dart';
8 : import '../../models/event_model.dart';
9 : import '../../models/member_model.dart';
10 : import '../../models/participant_model.dart';
11 : import '../../models/score_model.dart';
12 : import '../../models/team_model.dart';
13 : import '../../models/tie_breaker_option.dart';
14 : import '../../services/club_service.dart';
15 : import '../../services/event_service.dart';
16 : import '../../services/member_service.dart';
17 : import '../../services/notification_service.dart';
18 : import '../../utils/tie_breaker_util.dart';
19 : import '../../widgets/loading_indicator.dart';
20 :
21 : class EventDetailsScreen extends StatefulWidget {
22 : final Event event;
23 :
24 0 : const EventDetailsScreen({
25 : Key? key,
26 : required this.event,
27 0 : }) : super(key: key);
28 :
29 0 : @override
30 0 : State<EventDetailsScreen> createState() => _EventDetailsScreenState();
31 : }
32 :
33 : class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTickerProviderStateMixin {
34 : late TabController _tabController;
35 : List<Participant> _participants = [];
36 : List<Score> _scores = [];
37 : List<Team> _teams = [];
38 : List<Member> _members = [];
39 : Club? _club;
40 : bool _isLoading = true;
41 : int _teamGenerationMethod = 1; // 1: 팀 인원수로 나누기, 2: 팀 개수로 나누기
42 : final TextEditingController _teamSizeController = TextEditingController(text: '4');
43 : final TextEditingController _teamCountController = TextEditingController(text: '2');
44 : bool _showTeamTab = false;
45 : bool _includeHandicap = true; // 핸디캡 포함 여부
46 :
47 0 : @override
48 : void initState() {
49 0 : super.initState();
50 : // 테스트를 위해 팀 탭 항상 표시
51 0 : _showTeamTab = true;
52 : // 탭 컨트롤러 길이 동적 설정
53 0 : _tabController = TabController(length: _showTeamTab ? 4 : 3, vsync: this);
54 0 : _loadEventDetails();
55 : }
56 :
57 0 : @override
58 : void dispose() {
59 0 : _tabController.dispose();
60 0 : super.dispose();
61 : }
62 :
63 0 : Future<void> _loadEventDetails() async {
64 0 : setState(() {
65 0 : _isLoading = true;
66 : });
67 :
68 : try {
69 0 : final eventService = Provider.of<EventService>(context, listen: false);
70 0 : final clubService = Provider.of<ClubService>(context, listen: false);
71 0 : final memberService = Provider.of<MemberService>(context, listen: false);
72 :
73 : // 참가자 목록 로드
74 0 : final participants = await eventService.fetchEventParticipants(widget.event.id);
75 :
76 : // 점수 목록 로드
77 0 : final scores = await eventService.fetchEventScores(widget.event.id);
78 :
79 : // 클럽 정보 로드
80 0 : final club = await clubService.fetchClub(widget.event.clubId);
81 :
82 : // 회원 정보 로드 (동점자 처리 시 생년월일 사용)
83 0 : final memberIds = participants.map((p) => p.memberId).toList();
84 0 : final members = await memberService.fetchMembersByIds(widget.event.clubId, memberIds);
85 :
86 : // 팀 목록 로드 (선택적)
87 0 : List<Team> teams = [];
88 : try {
89 0 : teams = await eventService.fetchEventTeams(widget.event.id);
90 : } catch (e) {
91 : // 팀 기능이 없거나 오류 발생 시 무시
92 0 : print('팀 로드 실패: $e');
93 : }
94 :
95 0 : if (mounted) {
96 0 : setState(() {
97 0 : _participants = participants;
98 0 : _scores = scores;
99 0 : _teams = teams;
100 0 : _club = club;
101 0 : _members = members;
102 0 : _showTeamTab = true; // 테스트를 위해 팀 탭 항상 표시
103 0 : _isLoading = false;
104 : });
105 : }
106 : } catch (e) {
107 0 : if (mounted) {
108 0 : setState(() {
109 0 : _isLoading = false;
110 : });
111 0 : ScaffoldMessenger.of(context).showSnackBar(
112 0 : SnackBar(content: Text('이벤트 정보를 불러오는데 실패했습니다: $e')),
113 : );
114 : }
115 : }
116 : }
117 :
118 : // 이벤트 공유 기능
119 0 : void _shareEvent() {
120 : // 공유할 내용 구성
121 0 : final String eventTitle = widget.event.title;
122 0 : final String eventType = widget.event.type ?? '기타';
123 0 : final String eventDate = DateFormat('yyyy-MM-dd HH:mm').format(widget.event.startDate);
124 0 : final String eventLocation = widget.event.location ?? '장소 미정';
125 :
126 : // 공개 URL 생성
127 : String shareUrl = '';
128 0 : if (widget.event.publicHash != null && widget.event.publicHash!.isNotEmpty) {
129 0 : shareUrl = 'https://bowling.example.com/events/${widget.event.publicHash}';
130 : }
131 :
132 : // 공유 메시지 구성
133 : final String shareText = '''
134 : [볼링 이벤트 초대]
135 : $eventTitle
136 :
137 : 유형: $eventType
138 : 일시: $eventDate
139 : 장소: $eventLocation
140 :
141 0 : 참가자: ${_participants.length}명
142 :
143 0 : ${shareUrl.isNotEmpty ? '참가 링크: $shareUrl' : ''}
144 0 : ${widget.event.accessPassword != null && widget.event.accessPassword!.isNotEmpty ? '비밀번호: ${widget.event.accessPassword}' : ''}
145 0 : ''';
146 :
147 : // 공유 다이얼로그 표시
148 0 : Share.share(shareText, subject: '볼링 이벤트: $eventTitle');
149 : }
150 :
151 0 : @override
152 : Widget build(BuildContext context) {
153 0 : return Scaffold(
154 0 : appBar: AppBar(
155 0 : title: Text(widget.event.title),
156 0 : bottom: TabBar(
157 0 : controller: _tabController,
158 0 : tabs: [
159 : const Tab(text: '기본 정보'),
160 : const Tab(text: '참가자'),
161 : const Tab(text: '점수'),
162 0 : if (_showTeamTab) const Tab(text: '팀'),
163 : ],
164 : ),
165 : ),
166 0 : body: _isLoading
167 : ? const LoadingIndicator()
168 0 : : TabBarView(
169 0 : controller: _tabController,
170 0 : children: [
171 0 : _buildBasicInfoTab(),
172 0 : _buildParticipantsTab(),
173 0 : _buildScoresTab(),
174 0 : if (_showTeamTab) _buildTeamsTab(),
175 : ],
176 : ),
177 0 : floatingActionButton: _buildFloatingActionButton(),
178 : );
179 : }
180 :
181 0 : Widget _buildFloatingActionButton() {
182 0 : final currentTab = _tabController.index;
183 :
184 : switch (currentTab) {
185 0 : case 0: // 기본 정보 탭
186 0 : return FloatingActionButton(
187 0 : onPressed: _editEvent,
188 : child: const Icon(Icons.edit),
189 : );
190 0 : case 1: // 참가자 탭
191 0 : return FloatingActionButton(
192 0 : onPressed: _addParticipant,
193 : child: const Icon(Icons.person_add),
194 : );
195 0 : case 2: // 점수 탭
196 0 : return FloatingActionButton(
197 0 : onPressed: _addScore,
198 : child: const Icon(Icons.add_chart),
199 : );
200 0 : case 3: // 팀 탭 (표시되는 경우)
201 0 : if (_showTeamTab) {
202 0 : return FloatingActionButton(
203 0 : onPressed: _generateTeams,
204 : child: const Icon(Icons.group_add),
205 : );
206 : }
207 0 : return Container();
208 : default:
209 0 : return Container();
210 : }
211 : }
212 :
213 : // 기본 정보 탭
214 0 : Widget _buildBasicInfoTab() {
215 0 : return SingleChildScrollView(
216 : padding: const EdgeInsets.all(16),
217 0 : child: Column(
218 : crossAxisAlignment: CrossAxisAlignment.start,
219 0 : children: [
220 : // 이벤트 유형 및 상태 뱃지
221 0 : Row(
222 0 : children: [
223 0 : Container(
224 : padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
225 0 : decoration: BoxDecoration(
226 0 : color: _getTypeColor(widget.event.type ?? '기타'),
227 0 : borderRadius: BorderRadius.circular(16),
228 : ),
229 0 : child: Text(
230 0 : widget.event.type ?? '기타',
231 : style: const TextStyle(
232 : color: Colors.white,
233 : fontWeight: FontWeight.bold,
234 : ),
235 : ),
236 : ),
237 : const SizedBox(width: 8),
238 0 : Container(
239 : padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
240 0 : decoration: BoxDecoration(
241 0 : color: _getStatusColor(widget.event.status ?? '준비중'),
242 0 : borderRadius: BorderRadius.circular(16),
243 : ),
244 0 : child: Text(
245 0 : widget.event.status ?? '준비중',
246 : style: const TextStyle(
247 : color: Colors.white,
248 : fontWeight: FontWeight.bold,
249 : ),
250 : ),
251 : ),
252 : const Spacer(),
253 : // 공유 버튼 추가
254 0 : IconButton(
255 : icon: const Icon(Icons.share),
256 0 : onPressed: _shareEvent,
257 : tooltip: '이벤트 공유',
258 : ),
259 : ],
260 : ),
261 : const SizedBox(height: 16),
262 :
263 : // 이벤트 설명
264 0 : if (widget.event.description != null && widget.event.description!.isNotEmpty) ...[
265 : const Text(
266 : '설명',
267 : style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
268 : ),
269 : const SizedBox(height: 8),
270 0 : Container(
271 : width: double.infinity,
272 : padding: const EdgeInsets.all(12),
273 0 : decoration: BoxDecoration(
274 0 : color: Colors.grey[100],
275 0 : borderRadius: BorderRadius.circular(8),
276 : ),
277 0 : child: Text(widget.event.description ?? ''),
278 : ),
279 : const SizedBox(height: 16),
280 : ],
281 :
282 : // 이벤트 정보
283 0 : const Text(
284 : '이벤트 정보',
285 : style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
286 : ),
287 0 : const SizedBox(height: 8),
288 0 : _buildInfoRow(
289 : Icons.calendar_today,
290 0 : '시작: ${DateFormat('yyyy-MM-dd HH:mm').format(widget.event.startDate)}',
291 : ),
292 0 : if (widget.event.endDate != null)
293 0 : _buildInfoRow(
294 : Icons.calendar_today,
295 0 : '종료: ${DateFormat('yyyy-MM-dd HH:mm').format(widget.event.endDate!)}',
296 : ),
297 0 : if (widget.event.location != null && widget.event.location!.isNotEmpty)
298 0 : _buildInfoRow(Icons.location_on, '장소: ${widget.event.location}'),
299 0 : if (widget.event.maxParticipants != null)
300 0 : _buildInfoRow(
301 : Icons.people,
302 0 : '최대 참가자: ${widget.event.maxParticipants}명',
303 : ),
304 0 : if (widget.event.gameCount != null)
305 0 : _buildInfoRow(
306 : Icons.sports,
307 0 : '게임 수: ${widget.event.gameCount}게임',
308 : ),
309 0 : if (widget.event.participantFee != null)
310 0 : _buildInfoRow(
311 : Icons.attach_money,
312 0 : '참가비: ${NumberFormat.currency(locale: 'ko_KR', symbol: '₩', decimalDigits: 0).format(widget.event.participantFee)}',
313 : ),
314 0 : if (widget.event.registrationDeadline != null)
315 0 : _buildInfoRow(
316 : Icons.timer,
317 0 : '신청마감: ${DateFormat('yyyy-MM-dd HH:mm').format(widget.event.registrationDeadline!)}',
318 : ),
319 0 : const SizedBox(height: 16),
320 :
321 : // 공개 접근 정보
322 0 : const Text(
323 : '공개 접근',
324 : style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
325 : ),
326 0 : const SizedBox(height: 8),
327 0 : if (widget.event.publicHash != null && widget.event.publicHash!.isNotEmpty) ...[
328 0 : Row(
329 0 : children: [
330 0 : Expanded(
331 0 : child: _buildInfoRow(
332 : Icons.link,
333 0 : 'https://bowling.example.com/events/${widget.event.publicHash}',
334 : isSelectable: true,
335 : ),
336 : ),
337 0 : IconButton(
338 : icon: const Icon(Icons.copy),
339 0 : onPressed: () {
340 0 : final publicUrl = 'https://bowling.example.com/events/${widget.event.publicHash}';
341 0 : Clipboard.setData(ClipboardData(text: publicUrl));
342 0 : ScaffoldMessenger.of(context).showSnackBar(
343 : const SnackBar(content: Text('공개 URL이 클립보드에 복사되었습니다')),
344 : );
345 : },
346 : tooltip: 'URL 복사',
347 : ),
348 : ],
349 : ),
350 : ],
351 0 : if (widget.event.accessPassword != null && widget.event.accessPassword!.isNotEmpty)
352 0 : _buildInfoRow(
353 : Icons.lock,
354 0 : '접근 비밀번호: ${widget.event.accessPassword}',
355 : ),
356 :
357 : // 참가자 정보
358 0 : const SizedBox(height: 16),
359 0 : const Text(
360 : '참가자 정보',
361 : style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
362 : ),
363 0 : const SizedBox(height: 8),
364 0 : _buildInfoRow(
365 : Icons.people,
366 0 : '현재 참가자: ${_participants.length}명',
367 : ),
368 :
369 : // 알림 설정 버튼
370 0 : const SizedBox(height: 24),
371 0 : ElevatedButton.icon(
372 0 : onPressed: _setEventReminders,
373 : icon: const Icon(Icons.notifications_active),
374 : label: const Text('알림 설정'),
375 0 : style: ElevatedButton.styleFrom(
376 : foregroundColor: Colors.white,
377 : backgroundColor: Colors.green,
378 : minimumSize: const Size(double.infinity, 48),
379 : ),
380 : ),
381 : ],
382 : ),
383 : );
384 : }
385 :
386 : // 참가자 탭
387 0 : Widget _buildParticipantsTab() {
388 0 : if (_participants.isEmpty) {
389 : return const Center(
390 : child: Text('참가자가 없습니다.'),
391 : );
392 : }
393 :
394 0 : return ListView.builder(
395 0 : itemCount: _participants.length,
396 0 : itemBuilder: (context, index) {
397 0 : final participant = _participants[index];
398 0 : return Card(
399 : margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
400 0 : child: ListTile(
401 0 : leading: CircleAvatar(
402 0 : backgroundColor: Colors.blue.shade100,
403 0 : child: Text(
404 0 : participant.name?.substring(0, 1) ?? '?',
405 : style: const TextStyle(color: Colors.blue),
406 : ),
407 : ),
408 0 : title: Text(
409 0 : participant.name ?? '이름 없음',
410 : style: const TextStyle(fontWeight: FontWeight.bold),
411 : ),
412 0 : subtitle: Row(
413 0 : children: [
414 : // 참가 상태 뱃지
415 0 : if (participant.status != null)
416 0 : Container(
417 : margin: const EdgeInsets.only(right: 8),
418 : padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
419 0 : decoration: BoxDecoration(
420 0 : color: _getParticipantStatusColor(participant.status!).withOpacity(0.2),
421 0 : borderRadius: BorderRadius.circular(12),
422 0 : border: Border.all(color: _getParticipantStatusColor(participant.status!)),
423 : ),
424 0 : child: Text(
425 0 : participant.status!,
426 0 : style: TextStyle(
427 : fontSize: 12,
428 0 : color: _getParticipantStatusColor(participant.status!),
429 : ),
430 : ),
431 : ),
432 : // 결제 상태 뱃지
433 0 : Container(
434 : padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
435 0 : decoration: BoxDecoration(
436 0 : color: participant.isPaid ? Colors.green.withOpacity(0.2) : Colors.red.withOpacity(0.2),
437 0 : borderRadius: BorderRadius.circular(12),
438 0 : border: Border.all(color: participant.isPaid ? Colors.green : Colors.red),
439 : ),
440 0 : child: Text(
441 0 : participant.isPaid ? '결제완료' : '미결제',
442 0 : style: TextStyle(
443 : fontSize: 12,
444 0 : color: participant.isPaid ? Colors.green : Colors.red,
445 : ),
446 : ),
447 : ),
448 : ],
449 : ),
450 0 : trailing: Row(
451 : mainAxisSize: MainAxisSize.min,
452 0 : children: [
453 0 : IconButton(
454 : icon: const Icon(Icons.edit, size: 20),
455 : tooltip: '참가자 정보 수정',
456 0 : onPressed: () => _editParticipant(participant),
457 : ),
458 0 : IconButton(
459 : icon: const Icon(Icons.delete, size: 20),
460 : tooltip: '참가자 삭제',
461 0 : onPressed: () => _removeParticipant(participant),
462 : ),
463 : ],
464 : ),
465 0 : onTap: () => _showParticipantDetails(participant),
466 : ),
467 : );
468 : },
469 : );
470 : }
471 :
472 : // 점수 탭
473 0 : Widget _buildScoresTab() {
474 0 : if (_scores.isEmpty) {
475 : return const Center(
476 : child: Text('등록된 점수가 없습니다.'),
477 : );
478 : }
479 :
480 : // 동점자 처리 옵션 가져오기
481 : List<TieBreakerOption>? tieBreakerOptions;
482 0 : if (_club != null && _club!.tieBreakerOptions != null && _club!.tieBreakerOptions!.isNotEmpty) {
483 0 : tieBreakerOptions = _club!.tieBreakerOptions;
484 : }
485 :
486 : // TieBreakerUtil을 사용하여 점수 정렬 및 순위 계산
487 0 : final sortedScores = TieBreakerUtil.sortScoresByOptions(
488 0 : _scores,
489 : tieBreakerOptions,
490 0 : _members,
491 0 : _includeHandicap
492 : );
493 :
494 : // 순위 계산
495 0 : final ranks = TieBreakerUtil.calculateRanks(sortedScores, _includeHandicap);
496 :
497 : // 평균 점수 계산
498 0 : final totalScoreSum = sortedScores.fold<int>(0, (sum, score) =>
499 0 : sum + ((_includeHandicap) ? score.totalScore + (score.handicap ?? 0) : score.totalScore));
500 0 : final averageScore = sortedScores.isNotEmpty ? totalScoreSum / sortedScores.length : 0;
501 :
502 0 : return Column(
503 0 : children: [
504 : // 핸디캡 포함 여부 토글
505 0 : Container(
506 : padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
507 0 : child: Row(
508 : mainAxisAlignment: MainAxisAlignment.center,
509 0 : children: [
510 : const Text('핸디캡 포함:', style: TextStyle(fontWeight: FontWeight.bold)),
511 0 : Switch(
512 0 : value: _includeHandicap,
513 0 : onChanged: (value) {
514 0 : setState(() {
515 0 : _includeHandicap = value;
516 : });
517 : },
518 : activeColor: Colors.blue,
519 : ),
520 : ],
521 : ),
522 : ),
523 :
524 : // 평균 점수 표시
525 0 : Container(
526 : padding: const EdgeInsets.all(8),
527 : margin: const EdgeInsets.all(8),
528 0 : decoration: BoxDecoration(
529 0 : color: Colors.blue.shade50,
530 0 : borderRadius: BorderRadius.circular(8),
531 0 : border: Border.all(color: Colors.blue.shade200),
532 : ),
533 0 : child: Row(
534 : mainAxisAlignment: MainAxisAlignment.center,
535 0 : children: [
536 : const Icon(Icons.score, color: Colors.blue),
537 : const SizedBox(width: 8),
538 0 : Text(
539 0 : '평균 점수: ${averageScore.toStringAsFixed(1)}',
540 : style: const TextStyle(fontWeight: FontWeight.bold),
541 : ),
542 : ],
543 : ),
544 : ),
545 :
546 : // 동점자 처리 옵션 표시
547 0 : if (tieBreakerOptions != null && tieBreakerOptions.isNotEmpty)
548 0 : Container(
549 : padding: const EdgeInsets.all(8),
550 : margin: const EdgeInsets.symmetric(horizontal: 8),
551 0 : decoration: BoxDecoration(
552 0 : color: Colors.green.shade50,
553 0 : borderRadius: BorderRadius.circular(8),
554 0 : border: Border.all(color: Colors.green.shade200),
555 : ),
556 0 : child: Column(
557 : crossAxisAlignment: CrossAxisAlignment.start,
558 0 : children: [
559 : const Text(
560 : '동점자 처리 우선순위:',
561 : style: TextStyle(fontWeight: FontWeight.bold),
562 : ),
563 : const SizedBox(height: 4),
564 0 : Wrap(
565 : spacing: 8,
566 0 : children: tieBreakerOptions.map((option) {
567 : String optionText = '';
568 : switch (option) {
569 0 : case TieBreakerOption.lowerHandicap:
570 : optionText = '핸디캡 낮은 순';
571 : break;
572 0 : case TieBreakerOption.lowerScoreGap:
573 : optionText = '점수 격차 작은 순';
574 : break;
575 0 : case TieBreakerOption.olderAge:
576 : optionText = '연장자 우선';
577 : break;
578 0 : case TieBreakerOption.none:
579 : optionText = '없음';
580 : break;
581 : }
582 0 : return Chip(
583 0 : label: Text(optionText),
584 0 : backgroundColor: Colors.green.shade100,
585 : );
586 0 : }).toList(),
587 : ),
588 : ],
589 : ),
590 : ),
591 :
592 : // 점수 목록
593 0 : Expanded(
594 0 : child: ListView.builder(
595 0 : itemCount: sortedScores.length,
596 0 : itemBuilder: (context, index) {
597 0 : final score = sortedScores[index];
598 0 : final rank = ranks[score.id] ?? (index + 1); // 동점자 처리된 순위
599 :
600 : // 참가자 이름 찾기
601 0 : final participant = _participants.firstWhere(
602 0 : (p) => p.memberId == score.memberId,
603 0 : orElse: () => Participant(
604 : id: '',
605 : eventId: '',
606 0 : memberId: score.memberId,
607 : name: '알 수 없음',
608 : ),
609 : );
610 :
611 0 : return Card(
612 : margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
613 0 : child: ListTile(
614 0 : leading: Stack(
615 0 : children: [
616 : // 배경 원
617 0 : Container(
618 : width: 40,
619 : height: 40,
620 0 : decoration: BoxDecoration(
621 0 : color: _getRankColor(rank),
622 : shape: BoxShape.circle,
623 : ),
624 : ),
625 : // 순위 텍스트
626 0 : Container(
627 : width: 40,
628 : height: 40,
629 : alignment: Alignment.center,
630 0 : child: Text(
631 0 : rank.toString(),
632 0 : key: Key('rank_${rank}_text'),
633 : style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
634 : ),
635 : ),
636 : // 테스트를 위한 추가 텍스트 (투명)
637 0 : Positioned(
638 : left: 0,
639 : right: 0,
640 : top: 0,
641 0 : child: Text(
642 0 : rank.toString(),
643 : textAlign: TextAlign.center,
644 : style: const TextStyle(color: Colors.transparent),
645 : ),
646 : ),
647 : ],
648 : ),
649 0 : title: Row(
650 0 : children: [
651 0 : Expanded(
652 0 : child: Text(
653 0 : participant.name ?? '알 수 없음',
654 : style: const TextStyle(fontWeight: FontWeight.bold),
655 : ),
656 : ),
657 0 : Container(
658 : padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
659 0 : decoration: BoxDecoration(
660 0 : color: Colors.blue.shade100,
661 0 : borderRadius: BorderRadius.circular(12),
662 : ),
663 0 : child: Text(
664 0 : _includeHandicap
665 0 : ? '총점: ${score.totalScore + (score.handicap ?? 0)} (${score.totalScore}+${score.handicap ?? 0})'
666 0 : : '총점: ${score.totalScore}',
667 : style: const TextStyle(fontWeight: FontWeight.bold),
668 : ),
669 : ),
670 : ],
671 : ),
672 0 : subtitle: Column(
673 : crossAxisAlignment: CrossAxisAlignment.start,
674 0 : children: [
675 : const SizedBox(height: 4),
676 0 : Text(
677 0 : '날짜: ${DateFormat('yyyy-MM-dd').format(score.date)}',
678 : ),
679 : const SizedBox(height: 4),
680 0 : SingleChildScrollView(
681 : scrollDirection: Axis.horizontal,
682 0 : child: Row(
683 0 : children: score.frames.asMap().entries.map((entry) {
684 0 : return Container(
685 : width: 30,
686 : height: 30,
687 : margin: const EdgeInsets.only(right: 4),
688 0 : decoration: BoxDecoration(
689 0 : color: _getScoreColor(entry.value),
690 0 : borderRadius: BorderRadius.circular(4),
691 : ),
692 0 : child: Center(
693 0 : child: Text(
694 : // 스트라이크는 'X'로, 스페어는 '/'로 표시
695 0 : _getFrameDisplay(entry.key, entry.value, score.frames),
696 : style: const TextStyle(
697 : color: Colors.white,
698 : fontWeight: FontWeight.bold,
699 : ),
700 : ),
701 : ),
702 : );
703 0 : }).toList(),
704 : ),
705 : ),
706 : ],
707 : ),
708 0 : trailing: Row(
709 : mainAxisSize: MainAxisSize.min,
710 0 : children: [
711 0 : IconButton(
712 : icon: const Icon(Icons.edit, size: 20),
713 : tooltip: '점수 수정',
714 0 : onPressed: () => _editScore(score),
715 : ),
716 0 : IconButton(
717 : icon: const Icon(Icons.delete, size: 20),
718 : tooltip: '점수 삭제',
719 0 : onPressed: () => _deleteScore(score),
720 : ),
721 : ],
722 : ),
723 0 : onTap: () => _showScoreDetails(score),
724 : ),
725 : );
726 : },
727 : ),
728 : ),
729 : ],
730 : );
731 : }
732 :
733 : // 팀 탭
734 0 : Widget _buildTeamsTab() {
735 0 : if (_teams.isEmpty) {
736 0 : return Center(
737 0 : child: Padding(
738 : padding: const EdgeInsets.all(16.0),
739 0 : child: Column(
740 : mainAxisAlignment: MainAxisAlignment.center,
741 0 : children: [
742 : const Icon(Icons.groups, size: 64, color: Colors.blue),
743 : const SizedBox(height: 16),
744 : const Text('생성된 팀이 없습니다.', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
745 : const SizedBox(height: 24),
746 : // 팀 생성 옵션
747 0 : Card(
748 0 : child: Padding(
749 : padding: const EdgeInsets.all(16.0),
750 0 : child: Column(
751 : crossAxisAlignment: CrossAxisAlignment.start,
752 0 : children: [
753 : const Text('팀 생성 옵션', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
754 : const SizedBox(height: 16),
755 : // 팀 생성 방법 선택
756 0 : Row(
757 0 : children: [
758 0 : Radio<int>(
759 : value: 1,
760 0 : groupValue: _teamGenerationMethod,
761 0 : onChanged: (value) {
762 0 : setState(() {
763 0 : _teamGenerationMethod = value!;
764 : });
765 : },
766 : ),
767 : const Text('팀 인원수로 나누기'),
768 : ],
769 : ),
770 0 : if (_teamGenerationMethod == 1)
771 0 : Padding(
772 : padding: const EdgeInsets.only(left: 32.0),
773 0 : child: Row(
774 0 : children: [
775 0 : SizedBox(
776 : width: 80,
777 0 : child: TextFormField(
778 0 : controller: _teamSizeController,
779 : decoration: const InputDecoration(
780 : labelText: '인원수',
781 : suffixText: '명',
782 : ),
783 : keyboardType: TextInputType.number,
784 0 : inputFormatters: [FilteringTextInputFormatter.digitsOnly],
785 : ),
786 : ),
787 : const SizedBox(width: 16),
788 0 : Text('총 ${_participants.length}명 참가자'),
789 : ],
790 : ),
791 : ),
792 0 : const SizedBox(height: 8),
793 0 : Row(
794 0 : children: [
795 0 : Radio<int>(
796 : value: 2,
797 0 : groupValue: _teamGenerationMethod,
798 0 : onChanged: (value) {
799 0 : setState(() {
800 0 : _teamGenerationMethod = value!;
801 : });
802 : },
803 : ),
804 : const Text('팀 개수로 나누기'),
805 : ],
806 : ),
807 0 : if (_teamGenerationMethod == 2)
808 0 : Padding(
809 : padding: const EdgeInsets.only(left: 32.0),
810 0 : child: Row(
811 0 : children: [
812 0 : SizedBox(
813 : width: 80,
814 0 : child: TextFormField(
815 0 : controller: _teamCountController,
816 : decoration: const InputDecoration(
817 : labelText: '팀 수',
818 : suffixText: '개',
819 : ),
820 : keyboardType: TextInputType.number,
821 0 : inputFormatters: [FilteringTextInputFormatter.digitsOnly],
822 : ),
823 : ),
824 : const SizedBox(width: 16),
825 0 : Text('총 ${_participants.length}명 참가자'),
826 : ],
827 : ),
828 : ),
829 0 : const SizedBox(height: 16),
830 : // 팀 생성 버튼
831 0 : Row(
832 : mainAxisAlignment: MainAxisAlignment.center,
833 0 : children: [
834 0 : ElevatedButton.icon(
835 0 : onPressed: _generateTeams,
836 : icon: const Icon(Icons.group_add),
837 : label: const Text('팀 생성하기'),
838 0 : style: ElevatedButton.styleFrom(
839 : backgroundColor: Colors.blue,
840 : foregroundColor: Colors.white,
841 : padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
842 : ),
843 : ),
844 : ],
845 : ),
846 : ],
847 : ),
848 : ),
849 : ),
850 : ],
851 : ),
852 : ),
853 : );
854 : }
855 :
856 : // 미배정 참가자 목록 조회
857 0 : final List<Participant> unassignedParticipants = _getUnassignedParticipants();
858 :
859 0 : return Column(
860 0 : children: [
861 : // 팀 관리 버튼
862 0 : Padding(
863 : padding: const EdgeInsets.all(8.0),
864 0 : child: Row(
865 : mainAxisAlignment: MainAxisAlignment.spaceEvenly,
866 0 : children: [
867 0 : ElevatedButton.icon(
868 0 : onPressed: _generateTeams,
869 : icon: const Icon(Icons.refresh),
870 : label: const Text('팀 재생성'),
871 0 : style: ElevatedButton.styleFrom(
872 : backgroundColor: Colors.blue,
873 : foregroundColor: Colors.white,
874 : ),
875 : ),
876 0 : ElevatedButton.icon(
877 0 : onPressed: _saveTeams,
878 : icon: const Icon(Icons.save),
879 : label: const Text('팀 저장'),
880 0 : style: ElevatedButton.styleFrom(
881 : backgroundColor: Colors.green,
882 : foregroundColor: Colors.white,
883 : ),
884 : ),
885 : ],
886 : ),
887 : ),
888 :
889 : // 이벤트 설명
890 0 : if (widget.event.description != null && widget.event.description!.isNotEmpty)
891 0 : Padding(
892 : padding: const EdgeInsets.symmetric(vertical: 16.0),
893 0 : child: Column(
894 : crossAxisAlignment: CrossAxisAlignment.start,
895 0 : children: [
896 : const Text(
897 : '이벤트 설명',
898 : style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
899 : ),
900 : const SizedBox(height: 8),
901 0 : Text(widget.event.description!),
902 : ],
903 : ),
904 : ),
905 :
906 : // 공유 버튼
907 0 : Padding(
908 : padding: const EdgeInsets.symmetric(vertical: 16.0),
909 0 : child: ElevatedButton.icon(
910 0 : onPressed: _shareEvent,
911 : icon: const Icon(Icons.share),
912 : label: const Text('이벤트 공유'),
913 0 : style: ElevatedButton.styleFrom(
914 : foregroundColor: Colors.white,
915 : backgroundColor: Colors.blue,
916 : ),
917 : ),
918 : ),
919 :
920 : // 미배정 참가자 섹션
921 0 : if (unassignedParticipants.isNotEmpty)
922 0 : Card(
923 : margin: const EdgeInsets.all(8),
924 0 : color: Colors.amber.shade50,
925 0 : child: Column(
926 : crossAxisAlignment: CrossAxisAlignment.start,
927 0 : children: [
928 0 : Padding(
929 : padding: const EdgeInsets.all(8.0),
930 0 : child: Row(
931 0 : children: [
932 : const Icon(Icons.person_off, color: Colors.amber),
933 : const SizedBox(width: 8),
934 0 : Text('미배정 참가자 (${unassignedParticipants.length}명)',
935 : style: const TextStyle(fontWeight: FontWeight.bold),
936 : ),
937 : ],
938 : ),
939 : ),
940 : const Divider(height: 1),
941 0 : Wrap(
942 0 : children: unassignedParticipants.map((participant) {
943 0 : return Padding(
944 : padding: const EdgeInsets.all(4.0),
945 0 : child: Chip(
946 : avatar: const CircleAvatar(
947 : child: Icon(Icons.person, size: 16),
948 : ),
949 0 : label: Text(participant.name ?? '알 수 없음'),
950 : deleteIcon: const Icon(Icons.add_circle, size: 18),
951 0 : onDeleted: () => _showTeamSelectionDialog(participant),
952 : ),
953 : );
954 0 : }).toList(),
955 : ),
956 : ],
957 : ),
958 : ),
959 :
960 : // 팀 목록
961 0 : Expanded(
962 0 : child: ListView.builder(
963 0 : itemCount: _teams.length,
964 0 : itemBuilder: (context, index) {
965 0 : final team = _teams[index];
966 0 : final teamMembers = _getTeamMembers(team);
967 0 : final teamAverage = _calculateTeamAverage(team);
968 :
969 0 : return Card(
970 : margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
971 0 : child: ExpansionTile(
972 0 : leading: CircleAvatar(
973 0 : backgroundColor: Colors.blue.shade700,
974 0 : child: Text('${index + 1}', style: const TextStyle(color: Colors.white)),
975 : ),
976 0 : title: Row(
977 0 : children: [
978 0 : Text('팀 ${team.name}', style: const TextStyle(fontWeight: FontWeight.bold)),
979 : const SizedBox(width: 8),
980 0 : Container(
981 : padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
982 0 : decoration: BoxDecoration(
983 0 : color: Colors.blue.shade100,
984 0 : borderRadius: BorderRadius.circular(12),
985 : ),
986 0 : child: Text('${team.memberIds.length}명'),
987 : ),
988 : ],
989 : ),
990 0 : subtitle: Row(
991 0 : children: [
992 : const Icon(Icons.bar_chart, size: 16, color: Colors.blue),
993 : const SizedBox(width: 4),
994 0 : Text('팀 에버리지: ${teamAverage.toStringAsFixed(1)}'),
995 : ],
996 : ),
997 0 : children: [
998 0 : ...teamMembers.map((participant) {
999 : // 참가자의 평균 점수 계산
1000 0 : final participantScores = _scores.where((s) => s.memberId == participant.memberId).toList();
1001 0 : final participantAverage = participantScores.isNotEmpty
1002 0 : ? participantScores.fold<int>(0, (sum, s) => sum + s.totalScore) / participantScores.length
1003 : : 0.0;
1004 :
1005 0 : return ListTile(
1006 : leading: const CircleAvatar(
1007 : child: Icon(Icons.person, size: 16),
1008 : ),
1009 0 : title: Text(participant.name ?? '알 수 없음'),
1010 0 : subtitle: Text('평균: ${participantAverage.toStringAsFixed(1)}'),
1011 0 : trailing: IconButton(
1012 : icon: const Icon(Icons.remove_circle, color: Colors.red),
1013 : tooltip: '팀에서 제외',
1014 0 : onPressed: () => _removeFromTeam(team, participant),
1015 : ),
1016 : );
1017 0 : }).toList(),
1018 : // 팀원 추가 버튼
1019 0 : if (unassignedParticipants.isNotEmpty)
1020 0 : ListTile(
1021 : leading: const Icon(Icons.add_circle, color: Colors.green),
1022 : title: const Text('팀원 추가하기'),
1023 0 : onTap: () => _showParticipantSelectionDialog(team),
1024 : ),
1025 : ],
1026 : ),
1027 : );
1028 : },
1029 : ),
1030 : ),
1031 : ],
1032 : );
1033 : }
1034 :
1035 : // 정보 행 위젯
1036 0 : Widget _buildInfoRow(IconData icon, String text, {bool isSelectable = false}) {
1037 0 : return Padding(
1038 : padding: const EdgeInsets.symmetric(vertical: 8.0),
1039 0 : child: Row(
1040 0 : children: [
1041 0 : Icon(icon, color: Colors.blue),
1042 : const SizedBox(width: 8),
1043 : isSelectable
1044 0 : ? Expanded(
1045 0 : child: SelectableText(
1046 : text,
1047 : style: const TextStyle(fontSize: 16),
1048 : ),
1049 : )
1050 0 : : Expanded(
1051 0 : child: Text(
1052 : text,
1053 : style: const TextStyle(fontSize: 16),
1054 : ),
1055 : ),
1056 : ],
1057 : ),
1058 : );
1059 : }
1060 :
1061 : // 이벤트 유형에 따른 색상 반환
1062 0 : Color _getTypeColor(String type) {
1063 0 : switch (type.toLowerCase()) {
1064 0 : case '개인전':
1065 0 : case 'individual':
1066 : return Colors.blue;
1067 0 : case '팀전':
1068 0 : case 'team':
1069 : return Colors.purple;
1070 0 : case '리그':
1071 0 : case 'league':
1072 : return Colors.green;
1073 0 : case '토너먼트':
1074 0 : case 'tournament':
1075 : return Colors.orange;
1076 : default:
1077 : return Colors.grey;
1078 : }
1079 : }
1080 :
1081 : // 이벤트 상태에 따른 색상 반환
1082 0 : Color _getStatusColor(String status) {
1083 0 : switch (status.toLowerCase()) {
1084 0 : case '활성':
1085 0 : case 'active':
1086 : return Colors.green;
1087 0 : case '대기':
1088 0 : case 'pending':
1089 : return Colors.orange;
1090 0 : case '취소':
1091 0 : case 'cancelled':
1092 : return Colors.red;
1093 0 : case '완료':
1094 0 : case 'completed':
1095 : return Colors.blue;
1096 : default:
1097 : return Colors.grey;
1098 : }
1099 : }
1100 :
1101 : // 참가자 상태에 따른 색상 반환
1102 0 : Color _getParticipantStatusColor(String status) {
1103 0 : switch (status.toLowerCase()) {
1104 0 : case 'registered':
1105 0 : case '등록됨':
1106 : return Colors.blue;
1107 0 : case 'confirmed':
1108 0 : case '확인됨':
1109 : return Colors.green;
1110 0 : case 'cancelled':
1111 0 : case '취소됨':
1112 : return Colors.red;
1113 0 : case 'attended':
1114 0 : case '참석함':
1115 : return Colors.purple;
1116 : default:
1117 : return Colors.grey;
1118 : }
1119 : }
1120 :
1121 : // 순위에 따른 색상 반환
1122 0 : Color _getRankColor(int rank) {
1123 : switch (rank) {
1124 0 : case 1:
1125 0 : return Colors.amber.shade700; // 금메달
1126 0 : case 2:
1127 0 : return Colors.blueGrey.shade400; // 은메달
1128 0 : case 3:
1129 0 : return Colors.brown.shade400; // 동메달
1130 : default:
1131 0 : return Colors.blue.shade700; // 기본 색상
1132 : }
1133 : }
1134 :
1135 : // 점수에 따른 색상 반환
1136 0 : Color _getScoreColor(int score) {
1137 0 : if (score == 10) {
1138 0 : return Colors.red.shade700; // 스트라이크
1139 0 : } else if (score >= 7) {
1140 0 : return Colors.orange.shade700; // 좋은 점수
1141 0 : } else if (score >= 5) {
1142 0 : return Colors.green.shade700; // 평균 점수
1143 : } else {
1144 0 : return Colors.blue.shade700; // 낮은 점수
1145 : }
1146 : }
1147 :
1148 : // 프레임 표시 문자열 반환
1149 0 : String _getFrameDisplay(int frameIndex, int score, List<int> frames) {
1150 : // 스트라이크 표시
1151 0 : if (score == 10) {
1152 : return 'X';
1153 : }
1154 :
1155 : // 스페어 표시 (이전 프레임과 합쳐서 10점이면 스페어)
1156 0 : if (frameIndex > 0 && frames[frameIndex - 1] + score == 10) {
1157 : return '/';
1158 : }
1159 :
1160 : // 일반 점수 표시
1161 0 : return score.toString();
1162 : }
1163 :
1164 : // 미배정 참가자 목록 가져오기
1165 0 : List<Participant> _getUnassignedParticipants() {
1166 : // 현재 팀에 배정된 모든 참가자 ID 목록
1167 0 : final List<String> assignedMemberIds = [];
1168 0 : for (final team in _teams) {
1169 0 : assignedMemberIds.addAll(team.memberIds);
1170 : }
1171 :
1172 : // 배정되지 않은 참가자 목록 반환
1173 0 : return _participants.where((p) => !assignedMemberIds.contains(p.memberId)).toList();
1174 : }
1175 :
1176 : // 팀원 목록 가져오기
1177 0 : List<Participant> _getTeamMembers(Team team) {
1178 0 : return _participants
1179 0 : .where((p) => team.memberIds.contains(p.memberId))
1180 0 : .toList();
1181 : }
1182 :
1183 : // 팀 평균 점수 계산
1184 0 : double _calculateTeamAverage(Team team) {
1185 0 : if (team.memberIds.isEmpty) return 0.0;
1186 :
1187 : double totalAverage = 0.0;
1188 : int memberCount = 0;
1189 :
1190 0 : for (final memberId in team.memberIds) {
1191 0 : final memberScores = _scores.where((s) => s.memberId == memberId).toList();
1192 0 : if (memberScores.isNotEmpty) {
1193 0 : final memberTotal = memberScores.fold<int>(0, (sum, score) => sum + score.totalScore);
1194 0 : totalAverage += memberTotal / memberScores.length;
1195 0 : memberCount++;
1196 : }
1197 : }
1198 :
1199 0 : return memberCount > 0 ? totalAverage / memberCount : 0.0;
1200 : }
1201 :
1202 : // 팀 생성 방법
1203 0 : void _generateTeams() {
1204 0 : if (_participants.isEmpty) {
1205 0 : ScaffoldMessenger.of(context).showSnackBar(
1206 : const SnackBar(content: Text('참가자가 없습니다.')),
1207 : );
1208 : return;
1209 : }
1210 :
1211 : // 기존 팀 삭제
1212 0 : setState(() {
1213 0 : _teams.clear();
1214 : });
1215 :
1216 : // 참가자 셔플
1217 0 : final shuffledParticipants = List<Participant>.from(_participants);
1218 0 : shuffledParticipants.shuffle();
1219 :
1220 : int teamCount;
1221 : int teamSize;
1222 :
1223 : // 팀 생성 방법에 따른 팀 개수 계산
1224 0 : if (_teamGenerationMethod == 1) {
1225 : // 팀 인원수로 나누기
1226 0 : teamSize = int.tryParse(_teamSizeController.text) ?? 4;
1227 0 : if (teamSize <= 0) teamSize = 4;
1228 0 : teamCount = (_participants.length / teamSize).ceil();
1229 : } else {
1230 : // 팀 개수로 나누기
1231 0 : teamCount = int.tryParse(_teamCountController.text) ?? 2;
1232 0 : if (teamCount <= 0) teamCount = 2;
1233 0 : teamSize = (_participants.length / teamCount).ceil();
1234 : }
1235 :
1236 : // 팀 생성
1237 0 : final List<Team> newTeams = [];
1238 0 : for (int i = 0; i < teamCount; i++) {
1239 0 : final teamName = String.fromCharCode(65 + i); // A, B, C, ...
1240 0 : final team = Team(
1241 0 : id: 'temp_${DateTime.now().millisecondsSinceEpoch}_$i',
1242 0 : eventId: widget.event.id,
1243 : name: teamName,
1244 0 : memberIds: [],
1245 : );
1246 0 : newTeams.add(team);
1247 : }
1248 :
1249 : // 참가자 배정
1250 0 : for (int i = 0; i < shuffledParticipants.length; i++) {
1251 0 : final teamIndex = i % teamCount;
1252 0 : final memberId = shuffledParticipants[i].memberId;
1253 0 : newTeams[teamIndex].memberIds.add(memberId);
1254 : }
1255 :
1256 0 : setState(() {
1257 0 : _teams = newTeams;
1258 : });
1259 :
1260 0 : ScaffoldMessenger.of(context).showSnackBar(
1261 0 : SnackBar(content: Text('팀이 생성되었습니다. ($teamCount개 팀)')),
1262 : );
1263 : }
1264 :
1265 : // 팀 저장
1266 0 : void _saveTeams() async {
1267 0 : if (_teams.isEmpty) {
1268 0 : ScaffoldMessenger.of(context).showSnackBar(
1269 : const SnackBar(content: Text('저장할 팀이 없습니다.')),
1270 : );
1271 : return;
1272 : }
1273 :
1274 0 : setState(() {
1275 0 : _isLoading = true;
1276 : });
1277 :
1278 : try {
1279 : // 기존 팀 삭제 및 새 팀 저장 로직 구현
1280 : // TODO: API 연동 구현
1281 :
1282 0 : ScaffoldMessenger.of(context).showSnackBar(
1283 : const SnackBar(content: Text('팀이 저장되었습니다.')),
1284 : );
1285 : } catch (e) {
1286 0 : ScaffoldMessenger.of(context).showSnackBar(
1287 0 : SnackBar(content: Text('팀 저장 중 오류가 발생했습니다: $e')),
1288 : );
1289 : } finally {
1290 0 : setState(() {
1291 0 : _isLoading = false;
1292 : });
1293 : }
1294 : }
1295 :
1296 : // 팀에서 참가자 제거
1297 0 : void _removeFromTeam(Team team, Participant participant) {
1298 0 : setState(() {
1299 0 : team.memberIds.remove(participant.memberId);
1300 : });
1301 :
1302 0 : ScaffoldMessenger.of(context).showSnackBar(
1303 0 : SnackBar(content: Text('${participant.name}님이 팀에서 제외되었습니다.')),
1304 : );
1305 : }
1306 :
1307 : // 참가자 선택 다이얼로그 표시
1308 0 : void _showParticipantSelectionDialog(Team team) {
1309 0 : final unassignedParticipants = _getUnassignedParticipants();
1310 0 : if (unassignedParticipants.isEmpty) {
1311 0 : ScaffoldMessenger.of(context).showSnackBar(
1312 : const SnackBar(content: Text('추가할 참가자가 없습니다.')),
1313 : );
1314 : return;
1315 : }
1316 :
1317 0 : showDialog(
1318 0 : context: context,
1319 0 : builder: (context) => AlertDialog(
1320 0 : title: Text('팀 ${team.name}에 참가자 추가'),
1321 0 : content: SizedBox(
1322 : width: double.maxFinite,
1323 0 : child: ListView.builder(
1324 : shrinkWrap: true,
1325 0 : itemCount: unassignedParticipants.length,
1326 0 : itemBuilder: (context, index) {
1327 0 : final participant = unassignedParticipants[index];
1328 0 : return ListTile(
1329 : leading: const CircleAvatar(
1330 : child: Icon(Icons.person, size: 16),
1331 : ),
1332 0 : title: Text(participant.name ?? '알 수 없음'),
1333 0 : onTap: () {
1334 0 : Navigator.of(context).pop();
1335 0 : setState(() {
1336 0 : team.memberIds.add(participant.memberId);
1337 : });
1338 0 : ScaffoldMessenger.of(context).showSnackBar(
1339 0 : SnackBar(content: Text('${participant.name}님이 팀 ${team.name}에 추가되었습니다.')),
1340 : );
1341 : },
1342 : );
1343 : },
1344 : ),
1345 : ),
1346 0 : actions: [
1347 0 : TextButton(
1348 0 : onPressed: () => Navigator.of(context).pop(),
1349 : child: const Text('취소'),
1350 : ),
1351 : ],
1352 : ),
1353 : );
1354 : }
1355 :
1356 : // 참가자를 추가할 팀 선택 다이얼로그 표시
1357 0 : void _showTeamSelectionDialog(Participant participant) {
1358 0 : if (_teams.isEmpty) {
1359 0 : ScaffoldMessenger.of(context).showSnackBar(
1360 : const SnackBar(content: Text('추가할 팀이 없습니다.')),
1361 : );
1362 : return;
1363 : }
1364 :
1365 0 : showDialog(
1366 0 : context: context,
1367 0 : builder: (context) => AlertDialog(
1368 0 : title: Text('${participant.name}님을 추가할 팀 선택'),
1369 0 : content: SizedBox(
1370 : width: double.maxFinite,
1371 0 : child: ListView.builder(
1372 : shrinkWrap: true,
1373 0 : itemCount: _teams.length,
1374 0 : itemBuilder: (context, index) {
1375 0 : final team = _teams[index];
1376 0 : return ListTile(
1377 0 : leading: CircleAvatar(
1378 0 : backgroundColor: Colors.blue.shade700,
1379 0 : child: Text('${index + 1}', style: const TextStyle(color: Colors.white)),
1380 : ),
1381 0 : title: Text('팀 ${team.name} (${team.memberIds.length}명)'),
1382 0 : onTap: () {
1383 0 : Navigator.of(context).pop();
1384 0 : setState(() {
1385 0 : team.memberIds.add(participant.memberId);
1386 : });
1387 0 : ScaffoldMessenger.of(context).showSnackBar(
1388 0 : SnackBar(content: Text('${participant.name}님이 팀 ${team.name}에 추가되었습니다.')),
1389 : );
1390 : },
1391 : );
1392 : },
1393 : ),
1394 : ),
1395 0 : actions: [
1396 0 : TextButton(
1397 0 : onPressed: () => Navigator.of(context).pop(),
1398 : child: const Text('취소'),
1399 : ),
1400 : ],
1401 : ),
1402 : );
1403 : }
1404 :
1405 : // 이벤트 수정
1406 0 : void _editEvent() {
1407 : // 이벤트 수정 다이얼로그 또는 화면으로 이동
1408 0 : Navigator.of(context).pop(true); // 수정을 위해 이전 화면으로 돌아가기
1409 : }
1410 :
1411 : // 참가자 추가
1412 0 : void _addParticipant() {
1413 : // 참가자 추가 다이얼로그 표시
1414 0 : showDialog(
1415 0 : context: context,
1416 0 : builder: (context) => AlertDialog(
1417 : title: const Text('참가자 추가'),
1418 : content: const Text('참가자 추가 기능은 아직 구현 중입니다.'),
1419 0 : actions: [
1420 0 : TextButton(
1421 0 : onPressed: () => Navigator.of(context).pop(),
1422 : child: const Text('확인'),
1423 : ),
1424 : ],
1425 : ),
1426 : );
1427 : }
1428 :
1429 : // 참가자 수정
1430 0 : void _editParticipant(Participant participant) {
1431 : // 참가자 수정 다이얼로그 표시
1432 0 : showDialog(
1433 0 : context: context,
1434 0 : builder: (context) => AlertDialog(
1435 : title: const Text('참가자 수정'),
1436 0 : content: Text('${participant.name} 참가자 수정 기능은 아직 구현 중입니다.'),
1437 0 : actions: [
1438 0 : TextButton(
1439 0 : onPressed: () => Navigator.of(context).pop(),
1440 : child: const Text('확인'),
1441 : ),
1442 : ],
1443 : ),
1444 : );
1445 : }
1446 :
1447 : // 참가자 삭제
1448 0 : void _removeParticipant(Participant participant) {
1449 : // 참가자 삭제 확인 다이얼로그
1450 0 : showDialog(
1451 0 : context: context,
1452 0 : builder: (context) => AlertDialog(
1453 : title: const Text('참가자 삭제'),
1454 0 : content: Text('${participant.name} 참가자를 정말 삭제하시겠습니까?'),
1455 0 : actions: [
1456 0 : TextButton(
1457 0 : onPressed: () => Navigator.of(context).pop(),
1458 : child: const Text('취소'),
1459 : ),
1460 0 : TextButton(
1461 0 : onPressed: () async {
1462 0 : Navigator.of(context).pop();
1463 :
1464 0 : setState(() {
1465 0 : _isLoading = true;
1466 : });
1467 :
1468 : try {
1469 0 : final eventService = Provider.of<EventService>(context, listen: false);
1470 0 : await eventService.removeParticipant(widget.event.id, participant.id);
1471 :
1472 0 : if (mounted) {
1473 0 : setState(() {
1474 0 : _participants.removeWhere((p) => p.id == participant.id);
1475 0 : _isLoading = false;
1476 : });
1477 :
1478 0 : ScaffoldMessenger.of(context).showSnackBar(
1479 : const SnackBar(content: Text('참가자가 삭제되었습니다')),
1480 : );
1481 : }
1482 : } catch (e) {
1483 0 : if (mounted) {
1484 0 : setState(() {
1485 0 : _isLoading = false;
1486 : });
1487 :
1488 0 : ScaffoldMessenger.of(context).showSnackBar(
1489 0 : SnackBar(content: Text('참가자 삭제에 실패했습니다: $e')),
1490 : );
1491 : }
1492 : }
1493 : },
1494 0 : style: TextButton.styleFrom(foregroundColor: Colors.red),
1495 : child: const Text('삭제'),
1496 : ),
1497 : ],
1498 : ),
1499 : );
1500 : }
1501 :
1502 : // 참가자 상세 정보 표시
1503 0 : void _showParticipantDetails(Participant participant) {
1504 : // 참가자 상세 정보 다이얼로그
1505 0 : showDialog(
1506 0 : context: context,
1507 0 : builder: (context) => AlertDialog(
1508 0 : title: Text(participant.name ?? '이름 없음'),
1509 0 : content: Column(
1510 : mainAxisSize: MainAxisSize.min,
1511 : crossAxisAlignment: CrossAxisAlignment.start,
1512 0 : children: [
1513 0 : if (participant.email != null)
1514 0 : _buildInfoRow(Icons.email, participant.email!),
1515 0 : if (participant.phoneNumber != null)
1516 0 : _buildInfoRow(Icons.phone, participant.phoneNumber!),
1517 0 : _buildInfoRow(Icons.info, '상태: ${participant.status ?? "없음"}'),
1518 0 : _buildInfoRow(
1519 : Icons.payment,
1520 0 : '결제: ${participant.isPaid ? "완료" : "미완료"}${participant.paidAmount != null ? " (${participant.paidAmount}원)" : ""}',
1521 : ),
1522 0 : if (participant.notes != null) ...[
1523 : const Divider(),
1524 : const Text('메모:', style: TextStyle(fontWeight: FontWeight.bold)),
1525 0 : Text(participant.notes!),
1526 : ],
1527 : ],
1528 : ),
1529 0 : actions: [
1530 0 : TextButton(
1531 0 : onPressed: () => Navigator.of(context).pop(),
1532 : child: const Text('닫기'),
1533 : ),
1534 : ],
1535 : ),
1536 : );
1537 : }
1538 :
1539 : // 점수 추가
1540 0 : void _addScore() {
1541 : // 점수 추가 다이얼로그 표시
1542 0 : showDialog(
1543 0 : context: context,
1544 0 : builder: (context) => AlertDialog(
1545 : title: const Text('점수 추가'),
1546 : content: const Text('점수 추가 기능은 아직 구현 중입니다.'),
1547 0 : actions: [
1548 0 : TextButton(
1549 0 : onPressed: () => Navigator.of(context).pop(),
1550 : child: const Text('확인'),
1551 : ),
1552 : ],
1553 : ),
1554 : );
1555 : }
1556 :
1557 : // 점수 수정
1558 0 : void _editScore(Score score) {
1559 : // 점수 수정 다이얼로그 표시
1560 0 : showDialog(
1561 0 : context: context,
1562 0 : builder: (context) => AlertDialog(
1563 : title: const Text('점수 수정'),
1564 0 : content: Text('점수 ID: ${score.id} 수정 기능은 아직 구현 중입니다.'),
1565 0 : actions: [
1566 0 : TextButton(
1567 0 : onPressed: () => Navigator.of(context).pop(),
1568 : child: const Text('확인'),
1569 : ),
1570 : ],
1571 : ),
1572 : );
1573 : }
1574 :
1575 : // 점수 삭제
1576 0 : void _deleteScore(Score score) {
1577 : // 점수 삭제 확인 다이얼로그
1578 0 : showDialog(
1579 0 : context: context,
1580 0 : builder: (context) => AlertDialog(
1581 : title: const Text('점수 삭제'),
1582 : content: const Text('이 점수를 정말 삭제하시겠습니까?'),
1583 0 : actions: [
1584 0 : TextButton(
1585 0 : onPressed: () => Navigator.of(context).pop(),
1586 : child: const Text('취소'),
1587 : ),
1588 0 : TextButton(
1589 0 : onPressed: () async {
1590 0 : Navigator.of(context).pop();
1591 :
1592 0 : setState(() {
1593 0 : _isLoading = true;
1594 : });
1595 :
1596 : try {
1597 0 : final eventService = Provider.of<EventService>(context, listen: false);
1598 0 : await eventService.deleteScore(widget.event.id, score.id);
1599 :
1600 0 : if (mounted) {
1601 0 : setState(() {
1602 0 : _scores.removeWhere((s) => s.id == score.id);
1603 0 : _isLoading = false;
1604 : });
1605 :
1606 0 : ScaffoldMessenger.of(context).showSnackBar(
1607 : const SnackBar(content: Text('점수가 삭제되었습니다')),
1608 : );
1609 : }
1610 : } catch (e) {
1611 0 : if (mounted) {
1612 0 : setState(() {
1613 0 : _isLoading = false;
1614 : });
1615 :
1616 0 : ScaffoldMessenger.of(context).showSnackBar(
1617 0 : SnackBar(content: Text('점수 삭제에 실패했습니다: $e')),
1618 : );
1619 : }
1620 : }
1621 : },
1622 0 : style: TextButton.styleFrom(foregroundColor: Colors.red),
1623 : child: const Text('삭제'),
1624 : ),
1625 : ],
1626 : ),
1627 : );
1628 : }
1629 :
1630 : // 점수 상세 정보 표시
1631 0 : void _showScoreDetails(Score score) {
1632 : // 참가자 이름 찾기
1633 0 : final participant = _participants.firstWhere(
1634 0 : (p) => p.memberId == score.memberId,
1635 0 : orElse: () => Participant(
1636 : id: '',
1637 : eventId: '',
1638 0 : memberId: score.memberId,
1639 : name: '알 수 없음',
1640 : ),
1641 : );
1642 :
1643 : // 점수 상세 정보 다이얼로그
1644 0 : showDialog(
1645 0 : context: context,
1646 0 : builder: (context) => AlertDialog(
1647 0 : title: Text('${participant.name} 점수'),
1648 0 : content: Column(
1649 : mainAxisSize: MainAxisSize.min,
1650 : crossAxisAlignment: CrossAxisAlignment.start,
1651 0 : children: [
1652 0 : _buildInfoRow(Icons.score, '총점: ${score.totalScore}'),
1653 0 : _buildInfoRow(
1654 : Icons.calendar_today,
1655 0 : '날짜: ${DateFormat('yyyy-MM-dd').format(score.date)}',
1656 : ),
1657 : const Divider(),
1658 : const Text('프레임별 점수:', style: TextStyle(fontWeight: FontWeight.bold)),
1659 : const SizedBox(height: 8),
1660 0 : Wrap(
1661 : spacing: 8,
1662 : runSpacing: 8,
1663 0 : children: score.frames.asMap().entries.map((entry) {
1664 0 : return Container(
1665 : width: 40,
1666 : height: 40,
1667 0 : decoration: BoxDecoration(
1668 0 : border: Border.all(color: Colors.blue),
1669 0 : borderRadius: BorderRadius.circular(4),
1670 : ),
1671 0 : child: Center(
1672 0 : child: Text(
1673 0 : entry.value.toString(),
1674 : style: const TextStyle(
1675 : fontSize: 16,
1676 : fontWeight: FontWeight.bold,
1677 : ),
1678 : ),
1679 : ),
1680 : );
1681 0 : }).toList(),
1682 : ),
1683 0 : if (score.notes != null) ...[
1684 : const Divider(),
1685 : const Text('메모:', style: TextStyle(fontWeight: FontWeight.bold)),
1686 0 : Text(score.notes ?? ''),
1687 : ],
1688 : ],
1689 : ),
1690 0 : actions: [
1691 0 : TextButton(
1692 0 : onPressed: () => Navigator.of(context).pop(),
1693 : child: const Text('닫기'),
1694 : ),
1695 : ],
1696 : ),
1697 : );
1698 : }
1699 :
1700 :
1701 : // 이벤트 알림 설정 기능
1702 0 : Future<void> _setEventReminders() async {
1703 0 : final notificationService = NotificationService();
1704 :
1705 : // 알림 권한 확인
1706 0 : final hasPermission = await notificationService.requestPermission();
1707 :
1708 : if (!hasPermission) {
1709 0 : if (mounted) {
1710 0 : ScaffoldMessenger.of(context).showSnackBar(
1711 : const SnackBar(content: Text('알림 권한이 없습니다. 설정에서 권한을 허용해주세요.')),
1712 : );
1713 : }
1714 : return;
1715 : }
1716 :
1717 : // 알림 설정 다이얼로그 표시
1718 0 : if (!mounted) return;
1719 :
1720 0 : showDialog(
1721 0 : context: context,
1722 0 : builder: (context) => AlertDialog(
1723 : title: const Text('이벤트 알림 설정'),
1724 0 : content: Column(
1725 : mainAxisSize: MainAxisSize.min,
1726 0 : children: [
1727 0 : ListTile(
1728 : leading: const Icon(Icons.notifications_active),
1729 : title: const Text('이벤트 시작 알림'),
1730 0 : subtitle: Text('${DateFormat('yyyy-MM-dd HH:mm').format(widget.event.startDate)} 1시간 전'),
1731 0 : onTap: () async {
1732 0 : Navigator.of(context).pop();
1733 0 : await notificationService.scheduleEventStartReminder(widget.event);
1734 :
1735 0 : if (mounted) {
1736 0 : ScaffoldMessenger.of(context).showSnackBar(
1737 : const SnackBar(content: Text('이벤트 시작 알림이 설정되었습니다')),
1738 : );
1739 : }
1740 : },
1741 : ),
1742 0 : if (widget.event.registrationDeadline != null) ListTile(
1743 : leading: const Icon(Icons.timer),
1744 : title: const Text('등록 마감 알림'),
1745 0 : subtitle: Text('${DateFormat('yyyy-MM-dd HH:mm').format(widget.event.registrationDeadline!)} 1일 전'),
1746 0 : onTap: () async {
1747 0 : Navigator.of(context).pop();
1748 0 : await notificationService.scheduleRegistrationDeadlineReminder(widget.event);
1749 :
1750 0 : if (mounted) {
1751 0 : ScaffoldMessenger.of(context).showSnackBar(
1752 : const SnackBar(content: Text('등록 마감 알림이 설정되었습니다')),
1753 : );
1754 : }
1755 : },
1756 : ),
1757 0 : ListTile(
1758 : leading: const Icon(Icons.notifications_off),
1759 : title: const Text('알림 취소'),
1760 : subtitle: const Text('이 이벤트에 대한 모든 알림 취소'),
1761 0 : onTap: () async {
1762 0 : Navigator.of(context).pop();
1763 0 : await notificationService.cancelEventNotifications(widget.event.id);
1764 :
1765 0 : if (mounted) {
1766 0 : ScaffoldMessenger.of(context).showSnackBar(
1767 : const SnackBar(content: Text('이벤트 알림이 취소되었습니다')),
1768 : );
1769 : }
1770 : },
1771 : ),
1772 : ],
1773 : ),
1774 0 : actions: [
1775 0 : TextButton(
1776 0 : onPressed: () => Navigator.of(context).pop(),
1777 : child: const Text('닫기'),
1778 : ),
1779 : ],
1780 : ),
1781 : );
1782 : }
1783 :
1784 : }
|