LCOV - code coverage report

Current view: | top level - lib/screens/score - club_statistics_screen.dart |  |  | Coverage | Total | Hit
Test: | lcov.info |  | Lines: | 0.6 % | 177 | 1
Test Date: | 2025-08-12 03:21:51 |  | Functions: | - | 0 | 0

                Line data    Source code

           1              : import 'package:flutter/material.dart';
           2              : import 'package:provider/provider.dart';
           3              : import 'package:intl/intl.dart';
           4              : import 'package:fl_chart/fl_chart.dart';
           5              :
           6              : import '../../services/auth_service.dart';
           7              : import '../../services/club_service.dart';
           8              : import '../../services/member_service.dart';
           9              : import '../../services/score_service.dart';
          10              : import '../../models/member_model.dart';
          11              : import '../../models/score_model.dart';
          12              : import 'member_statistics_screen.dart';
          13              : import 'score_entry_screen.dart';
          14              :
          15              : class ClubStatisticsScreen extends StatefulWidget {
          16           24 :   const ClubStatisticsScreen({super.key});
          17              :
          18            0 :   @override
          19            0 :   State<ClubStatisticsScreen> createState() => _ClubStatisticsScreenState();
          20              : }
          21              :
          22              : class _ClubStatisticsScreenState extends State<ClubStatisticsScreen> {
          23              :   bool _isLoading = false;
          24              :   bool _isInit = false;
          25              :
          26              :   Map<String, ScoreStatistics> _clubStatistics = {};
          27              :   List<Member> _members = [];
          28              :   Map<String, List<Score>> _recentScores = {};
          29              :
          30            0 :   @override
          31              :   void didChangeDependencies() {
          32            0 :     super.didChangeDependencies();
          33            0 :     if (!_isInit) {
          34            0 :       _loadClubStatistics();
          35            0 :       _isInit = true;
          36              :     }
          37              :   }
          38              :
          39            0 :   Future<void> _loadClubStatistics() async {
          40            0 :     setState(() {
          41            0 :       _isLoading = true;
          42              :     });
          43              :
          44              :     try {
          45            0 :       final authService = Provider.of<AuthService>(context, listen: false);
          46            0 :       final clubService = Provider.of<ClubService>(context, listen: false);
          47            0 :       final memberService = Provider.of<MemberService>(context, listen: false);
          48            0 :       final scoreService = Provider.of<ScoreService>(context, listen: false);
          49              :
          50            0 :       if (clubService.currentClub != null) {
          51              :         // 점수 서비스 초기화
          52            0 :         scoreService.initialize(authService.token!);
          53              :
          54              :         // 회원 목록 로드
          55            0 :         await memberService.fetchClubMembers();
          56            0 :         _members = memberService.members;
          57              :
          58              :         // 클럽 통계 로드
          59            0 :         _clubStatistics = await scoreService.fetchClubStatistics();
          60              :
          61              :         // 각 회원별 최근 점수 로드 (최대 3개)
          62            0 :         _recentScores = {};
          63            0 :         for (final member in _members) {
          64              :           try {
          65            0 :             final scores = await scoreService.fetchMemberScores(member.id);
          66              :             // 날짜 기준 정렬 (최신순)
          67            0 :             scores.sort((a, b) => b.date.compareTo(a.date));
          68            0 :             _recentScores[member.id] = scores.take(3).toList();
          69              :           } catch (e) {
          70              :             // 개별 회원 점수 로드 실패 시 빈 배열로 처리
          71            0 :             _recentScores[member.id] = [];
          72              :           }
          73              :         }
          74              :       }
          75              :     } catch (e) {
          76            0 :       ScaffoldMessenger.of(
          77            0 :         context,
          78            0 :       ).showSnackBar(SnackBar(content: Text('데이터 로드 중 오류가 발생했습니다: $e')));
          79              :     } finally {
          80            0 :       setState(() {
          81            0 :         _isLoading = false;
          82              :       });
          83              :     }
          84              :   }
          85              :
          86            0 :   @override
          87              :   Widget build(BuildContext context) {
          88            0 :     return Scaffold(
          89            0 :       appBar: AppBar(title: const Text('클럽 통계')),
          90            0 :       body: _isLoading
          91              :           ? const Center(child: CircularProgressIndicator())
          92            0 :           : RefreshIndicator(
          93            0 :               onRefresh: _loadClubStatistics,
          94            0 :               child: SingleChildScrollView(
          95              :                 physics: const AlwaysScrollableScrollPhysics(),
          96              :                 padding: const EdgeInsets.all(16.0),
          97            0 :                 child: Column(
          98              :                   crossAxisAlignment: CrossAxisAlignment.start,
          99            0 :                   children: [
         100              :                     // 클럽 전체 통계
         101            0 :                     _buildClubOverallStats(),
         102              :                     const SizedBox(height: 24),
         103              :
         104              :                     // 회원별 통계
         105              :                     const Text(
         106              :                       '회원별 통계',
         107              :                       style: TextStyle(
         108              :                         fontSize: 18,
         109              :                         fontWeight: FontWeight.bold,
         110              :                       ),
         111              :                     ),
         112              :                     const SizedBox(height: 16),
         113              :
         114              :                     // 회원 목록
         115            0 :                     ..._members.map((member) => _buildMemberStatsCard(member)),
         116              :                   ],
         117              :                 ),
         118              :               ),
         119              :             ),
         120            0 :       floatingActionButton: FloatingActionButton(
         121            0 :         onPressed: () async {
         122            0 :           await Navigator.of(context).push(
         123            0 :             MaterialPageRoute(builder: (context) => const ScoreEntryScreen()),
         124              :           );
         125              :           // 화면으로 돌아왔을 때 데이터 새로고침
         126            0 :           _loadClubStatistics();
         127              :         },
         128              :         backgroundColor: Colors.blue,
         129              :         child: const Icon(Icons.add, color: Colors.white),
         130              :       ),
         131              :     );
         132              :   }
         133              :
         134            0 :   Widget _buildClubOverallStats() {
         135              :     // 클럽 전체 통계가 없는 경우
         136            0 :     if (!_clubStatistics.containsKey('overall')) {
         137              :       return const Card(
         138              :         child: Padding(
         139              :           padding: EdgeInsets.all(16.0),
         140              :           child: Center(child: Text('클럽 전체 통계 데이터가 없습니다')),
         141              :         ),
         142              :       );
         143              :     }
         144              :
         145            0 :     final overallStats = _clubStatistics['overall']!;
         146              :
         147            0 :     return Card(
         148              :       elevation: 2,
         149            0 :       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
         150            0 :       child: Padding(
         151              :         padding: const EdgeInsets.all(16.0),
         152            0 :         child: Column(
         153              :           crossAxisAlignment: CrossAxisAlignment.start,
         154            0 :           children: [
         155              :             const Text(
         156              :               '클럽 전체 통계',
         157              :               style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
         158              :             ),
         159              :             const SizedBox(height: 16),
         160            0 :             Row(
         161            0 :               children: [
         162            0 :                 Expanded(
         163            0 :                   child: _buildStatItem(
         164              :                     label: '클럽 평균',
         165            0 :                     value: overallStats.average.toStringAsFixed(1),
         166              :                     icon: Icons.score,
         167              :                     color: Colors.blue,
         168              :                   ),
         169              :                 ),
         170            0 :                 Expanded(
         171            0 :                   child: _buildStatItem(
         172              :                     label: '최고 점수',
         173            0 :                     value: overallStats.highScore.toString(),
         174              :                     icon: Icons.emoji_events,
         175              :                     color: Colors.amber,
         176              :                   ),
         177              :                 ),
         178              :               ],
         179              :             ),
         180              :             const SizedBox(height: 16),
         181            0 :             Row(
         182            0 :               children: [
         183            0 :                 Expanded(
         184            0 :                   child: _buildStatItem(
         185              :                     label: '최저 점수',
         186            0 :                     value: overallStats.lowScore.toString(),
         187              :                     icon: Icons.arrow_downward,
         188              :                     color: Colors.red,
         189              :                   ),
         190              :                 ),
         191            0 :                 Expanded(
         192            0 :                   child: _buildStatItem(
         193              :                     label: '총 게임 수',
         194            0 :                     value: overallStats.gamesPlayed.toString(),
         195              :                     icon: Icons.sports,
         196              :                     color: Colors.green,
         197              :                   ),
         198              :                 ),
         199              :               ],
         200              :             ),
         201              :
         202              :             // 클럽 평균 점수 추이 그래프
         203              :             const SizedBox(height: 24),
         204              :             const Text(
         205              :               '클럽 평균 점수 추이',
         206              :               style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
         207              :             ),
         208              :             const SizedBox(height: 8),
         209            0 :             SizedBox(height: 180, child: _buildClubAverageChart()),
         210              :
         211              :             // 추가 통계 정보
         212            0 :             if (overallStats.additionalStats != null) ...[
         213              :               const SizedBox(height: 16),
         214              :               const Divider(),
         215              :               const SizedBox(height: 8),
         216            0 :               ...overallStats.additionalStats!.entries.map((entry) {
         217            0 :                 return Padding(
         218              :                   padding: const EdgeInsets.only(bottom: 8.0),
         219            0 :                   child: Row(
         220              :                     mainAxisAlignment: MainAxisAlignment.spaceBetween,
         221            0 :                     children: [
         222            0 :                       Text(entry.key, style: const TextStyle(fontSize: 14)),
         223            0 :                       Text(
         224            0 :                         entry.value.toString(),
         225              :                         style: const TextStyle(
         226              :                           fontSize: 14,
         227              :                           fontWeight: FontWeight.bold,
         228              :                         ),
         229              :                       ),
         230              :                     ],
         231              :                   ),
         232              :                 );
         233              :               }),
         234              :             ],
         235              :           ],
         236              :         ),
         237              :       ),
         238              :     );
         239              :   }
         240              :
         241            0 :   Widget _buildStatItem({
         242              :     required String label,
         243              :     required String value,
         244              :     required IconData icon,
         245              :     required Color color,
         246              :   }) {
         247            0 :     return Column(
         248            0 :       children: [
         249            0 :         Icon(icon, size: 28, color: color),
         250              :         const SizedBox(height: 8),
         251            0 :         Text(
         252              :           value,
         253            0 :           style: TextStyle(
         254              :             fontSize: 18,
         255              :             fontWeight: FontWeight.bold,
         256              :             color: color,
         257              :           ),
         258              :         ),
         259            0 :         Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
         260              :       ],
         261              :     );
         262              :   }
         263              :
         264            0 :   Widget _buildMemberStatsCard(Member member) {
         265              :     // 회원 통계 정보
         266            0 :     final memberStats = _clubStatistics[member.id];
         267              :     // 회원 최근 점수
         268            0 :     final recentScores = _recentScores[member.id] ?? [];
         269              :
         270            0 :     return Card(
         271              :       margin: const EdgeInsets.only(bottom: 12),
         272            0 :       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
         273            0 :       child: InkWell(
         274            0 :         onTap: () {
         275            0 :           Navigator.of(context).push(
         276            0 :             MaterialPageRoute(
         277            0 :               builder: (context) => MemberStatisticsScreen(memberId: member.id),
         278              :             ),
         279              :           );
         280              :         },
         281            0 :         borderRadius: BorderRadius.circular(12),
         282            0 :         child: Padding(
         283              :           padding: const EdgeInsets.all(16.0),
         284            0 :           child: Column(
         285              :             crossAxisAlignment: CrossAxisAlignment.start,
         286            0 :             children: [
         287              :               // 회원 정보
         288            0 :               Row(
         289            0 :                 children: [
         290            0 :                   CircleAvatar(
         291              :                     radius: 24,
         292            0 :                     backgroundColor: Colors.blue.shade100,
         293            0 :                     backgroundImage: member.profileImage != null
         294            0 :                         ? NetworkImage(member.profileImage!)
         295              :                         : null,
         296            0 :                     child: member.profileImage == null
         297            0 :                         ? Text(
         298            0 :                             member.name.isNotEmpty
         299            0 :                                 ? member.name[0].toUpperCase()
         300              :                                 : '?',
         301              :                             style: const TextStyle(
         302              :                               fontSize: 20,
         303              :                               color: Colors.blue,
         304              :                               fontWeight: FontWeight.bold,
         305              :                             ),
         306              :                           )
         307              :                         : null,
         308              :                   ),
         309              :                   const SizedBox(width: 12),
         310            0 :                   Expanded(
         311            0 :                     child: Column(
         312              :                       crossAxisAlignment: CrossAxisAlignment.start,
         313            0 :                       children: [
         314            0 :                         Text(
         315            0 :                           member.name,
         316              :                           style: const TextStyle(
         317              :                             fontSize: 16,
         318              :                             fontWeight: FontWeight.bold,
         319              :                           ),
         320              :                         ),
         321              :                         if (memberStats != null)
         322            0 :                           Text(
         323            0 :                             '평균: ${memberStats.average.toStringAsFixed(1)} | 최고: ${memberStats.highScore} | 게임: ${memberStats.gamesPlayed}',
         324            0 :                             style: TextStyle(
         325              :                               fontSize: 12,
         326            0 :                               color: Colors.grey[600],
         327              :                             ),
         328              :                           ),
         329              :                       ],
         330              :                     ),
         331              :                   ),
         332              :                   const Icon(Icons.chevron_right),
         333              :                 ],
         334              :               ),
         335              :
         336              :               // 최근 점수 표시
         337            0 :               if (recentScores.isNotEmpty) ...[
         338              :                 const SizedBox(height: 12),
         339              :                 const Divider(),
         340              :                 const SizedBox(height: 8),
         341              :                 const Text(
         342              :                   '최근 점수',
         343              :                   style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
         344              :                 ),
         345              :                 const SizedBox(height: 8),
         346            0 :                 Row(
         347              :                   mainAxisAlignment: MainAxisAlignment.spaceAround,
         348            0 :                   children: recentScores.map((score) {
         349            0 :                     return Column(
         350            0 :                       children: [
         351            0 :                         Text(
         352            0 :                           score.totalScore.toString(),
         353              :                           style: const TextStyle(
         354              :                             fontSize: 16,
         355              :                             fontWeight: FontWeight.bold,
         356              :                             color: Colors.blue,
         357              :                           ),
         358              :                         ),
         359            0 :                         Text(
         360            0 :                           DateFormat('MM/dd').format(score.date),
         361            0 :                           style: TextStyle(
         362              :                             fontSize: 10,
         363            0 :                             color: Colors.grey[600],
         364              :                           ),
         365              :                         ),
         366              :                       ],
         367              :                     );
         368            0 :                   }).toList(),
         369              :                 ),
         370              :               ],
         371              :             ],
         372              :           ),
         373              :         ),
         374              :       ),
         375              :     );
         376              :   }
         377              :
         378            0 :   Widget _buildClubAverageChart() {
         379              :     // 클럽 통계가 없는 경우
         380            0 :     if (!_clubStatistics.containsKey('overall')) {
         381              :       return const Center(child: Text('통계 데이터가 없습니다'));
         382              :     }
         383              :
         384              :     // 회원별 평균 점수 데이터 추출
         385            0 :     final memberAverages = <String, double>{};
         386              :
         387              :     // 회원별 통계 정보에서 평균 점수 추출
         388            0 :     _clubStatistics.forEach((key, stats) {
         389            0 :       if (key != 'overall') {
         390              :         // 회원 ID에 해당하는 회원 찾기
         391            0 :         final member = _members.firstWhere(
         392            0 :           (m) => m.id == key,
         393            0 :           orElse: () => Member(
         394              :             id: key,
         395              :             userId: key,
         396              :             name: 'Unknown',
         397              :             email: '',
         398              :             clubId: '',
         399              :             isActive: true,
         400              :           ),
         401              :         );
         402            0 :         memberAverages[member.name] = stats.average;
         403              :       }
         404              :     });
         405              :
         406              :     // 평균 점수 기준으로 정렬 (내림차순)
         407            0 :     final sortedEntries = memberAverages.entries.toList()
         408            0 :       ..sort((a, b) => b.value.compareTo(a.value));
         409              :
         410              :     // 최대 8명만 표시
         411            0 :     final displayEntries = sortedEntries.length > 8
         412            0 :         ? sortedEntries.sublist(0, 8)
         413              :         : sortedEntries;
         414              :
         415              :     // 그래프 데이터 생성
         416            0 :     final barGroups = <BarChartGroupData>[];
         417            0 :     for (int i = 0; i < displayEntries.length; i++) {
         418            0 :       barGroups.add(
         419            0 :         BarChartGroupData(
         420              :           x: i,
         421            0 :           barRods: [
         422            0 :             BarChartRodData(
         423            0 :               toY: displayEntries[i].value,
         424              :               color: Colors.blue,
         425              :               width: 16,
         426              :               borderRadius: const BorderRadius.only(
         427              :                 topLeft: Radius.circular(4),
         428              :                 topRight: Radius.circular(4),
         429              :               ),
         430              :             ),
         431              :           ],
         432              :         ),
         433              :       );
         434              :     }
         435              :
         436              :     // 최소/최대 점수 계산 (Y축 범위 설정용)
         437            0 :     final minScore = displayEntries.isEmpty
         438              :         ? 0.0
         439            0 :         : displayEntries.map((e) => e.value).reduce((a, b) => a < b ? a : b) -
         440              :               10;
         441            0 :     final maxScore = displayEntries.isEmpty
         442              :         ? 300.0
         443            0 :         : displayEntries.map((e) => e.value).reduce((a, b) => a > b ? a : b) +
         444              :               10;
         445              :
         446            0 :     return BarChart(
         447            0 :       BarChartData(
         448              :         alignment: BarChartAlignment.spaceAround,
         449              :         maxY: maxScore,
         450            0 :         minY: minScore < 0 ? 0 : minScore,
         451              :         gridData: const FlGridData(show: true, horizontalInterval: 50),
         452            0 :         borderData: FlBorderData(
         453              :           show: true,
         454            0 :           border: Border.all(color: Colors.grey.shade300),
         455              :         ),
         456            0 :         titlesData: FlTitlesData(
         457              :           show: true,
         458              :           rightTitles: const AxisTitles(
         459              :             sideTitles: SideTitles(showTitles: false),
         460              :           ),
         461              :           topTitles: const AxisTitles(
         462              :             sideTitles: SideTitles(showTitles: false),
         463              :           ),
         464            0 :           bottomTitles: AxisTitles(
         465            0 :             sideTitles: SideTitles(
         466              :               showTitles: true,
         467            0 :               getTitlesWidget: (value, meta) {
         468            0 :                 final index = value.toInt();
         469            0 :                 if (index >= 0 && index < displayEntries.length) {
         470              :                   // 회원 이름 표시 (최대 5글자)
         471            0 :                   final name = displayEntries[index].key;
         472            0 :                   final shortName = name.length > 5
         473            0 :                       ? '${name.substring(0, 4)}...'
         474            0 :                       : name;
         475              :
         476            0 :                   return Text(shortName, style: const TextStyle(fontSize: 10));
         477              :                 }
         478              :                 return const SizedBox();
         479              :               },
         480              :               reservedSize: 30,
         481              :             ),
         482              :           ),
         483            0 :           leftTitles: AxisTitles(
         484            0 :             sideTitles: SideTitles(
         485              :               showTitles: true,
         486              :               interval: 50,
         487            0 :               getTitlesWidget: (value, meta) {
         488            0 :                 return Text(
         489            0 :                   value.toInt().toString(),
         490              :                   style: const TextStyle(fontSize: 10),
         491              :                 );
         492              :               },
         493              :               reservedSize: 40,
         494              :             ),
         495              :           ),
         496              :         ),
         497              :         barGroups: barGroups,
         498              :       ),
         499              :     );
         500              :   }
         501              : }

Generated by: LCOV version 2.3.1-1