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