LCOV - code coverage report
Current view: top level - lib/screens/club - score_form_screen.dart Coverage Total Hit
Test: lcov.info Lines: 85.6 % 180 154
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              : 
       5              : import '../../models/score_model.dart';
       6              : import '../../models/participant_model.dart';
       7              : import '../../services/event_service.dart';
       8              : import '../../widgets/loading_indicator.dart';
       9              : 
      10              : class ScoreFormScreen extends StatefulWidget {
      11              :   final String eventId;
      12              :   final Score? score; // null이면 새 점수 추가, 아니면 점수 수정
      13              :   final List<Participant> participants;
      14              : 
      15            1 :   const ScoreFormScreen({
      16              :     Key? key,
      17              :     required this.eventId,
      18              :     this.score,
      19              :     required this.participants,
      20            1 :   }) : super(key: key);
      21              : 
      22            1 :   @override
      23            1 :   State<ScoreFormScreen> createState() => _ScoreFormScreenState();
      24              : }
      25              : 
      26              : class _ScoreFormScreenState extends State<ScoreFormScreen> {
      27              :   final _formKey = GlobalKey<FormState>();
      28              :   bool _isLoading = false;
      29              :   bool _useFrameInput = false; // 프레임별 입력 사용 여부
      30              : 
      31              :   // 폼 필드 컨트롤러
      32              :   final _notesController = TextEditingController();
      33              :   final _totalScoreController = TextEditingController(); // 총점 컨트롤러 추가
      34              :   final List<TextEditingController> _frameControllers = List.generate(
      35              :     10,
      36            2 :     (_) => TextEditingController(),
      37              :   );
      38              : 
      39              :   // 점수 데이터
      40              :   String? _selectedParticipantId;
      41              :   DateTime _scoreDate = DateTime.now();
      42              :   int _totalScore = 0;
      43              :   final _handicapController = TextEditingController(); // 핸디캡 컨트롤러
      44              : 
      45            1 :   @override
      46              :   void initState() {
      47            1 :     super.initState();
      48              :     
      49              :     // 수정 모드인 경우 기존 데이터 로드
      50            2 :     if (widget.score != null) {
      51            5 :       _notesController.text = widget.score!.notes ?? '';
      52            4 :       _selectedParticipantId = widget.score!.memberId;
      53            4 :       _scoreDate = widget.score!.date;
      54            6 :       _totalScoreController.text = widget.score!.totalScore.toString();
      55            4 :       _totalScore = widget.score!.totalScore;
      56              :       
      57              :       // 핸디캡 설정
      58            3 :       if (widget.score!.handicap != null) {
      59            6 :         _handicapController.text = widget.score!.handicap.toString();
      60              :       }
      61              :       
      62              :       // 프레임별 점수가 있는 경우 프레임별 입력 모드로 설정
      63            4 :       if (widget.score!.frames.isNotEmpty) {
      64            2 :         setState(() {
      65            1 :           _useFrameInput = true;
      66              :         });
      67              :         
      68              :         // 프레임별 점수 설정
      69            7 :         for (int i = 0; i < widget.score!.frames.length && i < 10; i++) {
      70            8 :           _frameControllers[i].text = widget.score!.frames[i].toString();
      71              :         }
      72              :       }
      73            3 :     } else if (widget.participants.isNotEmpty) {
      74              :       // 새 점수 추가 시 첫 번째 참가자 선택
      75            5 :       _selectedParticipantId = widget.participants.first.memberId;
      76              :     }
      77              :   }
      78              : 
      79            1 :   @override
      80              :   void dispose() {
      81            2 :     _notesController.dispose();
      82            2 :     _handicapController.dispose();
      83            2 :     _totalScoreController.dispose();
      84            2 :     for (var controller in _frameControllers) {
      85            1 :       controller.dispose();
      86              :     }
      87            1 :     super.dispose();
      88              :   }
      89              : 
      90              :   // 총점 계산 (프레임별 입력 모드에서만 사용)
      91            1 :   void _calculateTotalScore() {
      92            1 :     if (_useFrameInput) {
      93              :       int total = 0;
      94            2 :       for (var controller in _frameControllers) {
      95            2 :         if (controller.text.isNotEmpty) {
      96            0 :           total += int.tryParse(controller.text) ?? 0;
      97              :         }
      98              :       }
      99              :       
     100            2 :       setState(() {
     101            1 :         _totalScore = total;
     102            3 :         _totalScoreController.text = total.toString();
     103              :       });
     104              :     } else {
     105            0 :       setState(() {
     106            0 :         _totalScore = int.tryParse(_totalScoreController.text) ?? 0;
     107              :       });
     108              :     }
     109              :   }
     110              : 
     111              :   // 점수 저장
     112            1 :   Future<void> _saveScore() async {
     113            3 :     if (!_formKey.currentState!.validate()) {
     114              :       return;
     115              :     }
     116              :     
     117            2 :     setState(() {
     118            1 :       _isLoading = true;
     119              :     });
     120              :     
     121              :     try {
     122            2 :       final eventService = Provider.of<EventService>(context, listen: false);
     123              :       
     124              :       // 점수 데이터 준비
     125            1 :       final Map<String, dynamic> scoreData = {
     126            2 :         'eventId': widget.eventId,
     127            1 :         'memberId': _selectedParticipantId,
     128            2 :         'date': _scoreDate.toIso8601String(),
     129            3 :         'handicap': _handicapController.text.isEmpty ? null : int.parse(_handicapController.text),
     130            4 :         'notes': _notesController.text.trim().isEmpty ? null : _notesController.text.trim(),
     131              :       };
     132              :       
     133              :       // 프레임별 입력 모드인 경우
     134            1 :       if (_useFrameInput) {
     135            1 :         final frames = _frameControllers
     136            4 :             .map((controller) => controller.text.isEmpty ? 0 : int.parse(controller.text))
     137            1 :             .toList();
     138            1 :         scoreData['frames'] = frames;
     139            2 :         scoreData['totalScore'] = _totalScore;
     140              :       } else {
     141              :         // 총점 입력 모드인 경우
     142            4 :         scoreData['totalScore'] = int.tryParse(_totalScoreController.text) ?? 0;
     143            2 :         scoreData['frames'] = []; // 빈 프레임 배열 전송
     144              :       }
     145              :       
     146            2 :       if (widget.score == null) {
     147              :         // 새 점수 추가
     148            3 :         await eventService.addScore(widget.eventId, scoreData);
     149            0 :         if (mounted) {
     150            0 :           ScaffoldMessenger.of(context).showSnackBar(
     151              :             const SnackBar(content: Text('점수가 추가되었습니다')),
     152              :           );
     153              :         }
     154              :       } else {
     155              :         // 기존 점수 수정
     156            0 :         await eventService.updateScore(widget.eventId, widget.score!.id, scoreData);
     157            0 :         if (mounted) {
     158            0 :           ScaffoldMessenger.of(context).showSnackBar(
     159              :             const SnackBar(content: Text('점수가 수정되었습니다')),
     160              :           );
     161              :         }
     162              :       }
     163              :       
     164            0 :       if (mounted) {
     165            0 :         Navigator.of(context).pop(true); // 성공 결과와 함께 이전 화면으로 돌아가기
     166              :       }
     167              :     } catch (e) {
     168            1 :       if (mounted) {
     169            2 :         setState(() {
     170            1 :           _isLoading = false;
     171              :         });
     172              :         
     173            3 :         ScaffoldMessenger.of(context).showSnackBar(
     174            3 :           SnackBar(content: Text('점수 저장에 실패했습니다: $e')),
     175              :         );
     176              :       }
     177              :     }
     178              :   }
     179              : 
     180            1 :   @override
     181              :   Widget build(BuildContext context) {
     182            1 :     return Scaffold(
     183            1 :       appBar: AppBar(
     184            3 :         title: Text(widget.score == null ? '점수 추가' : '점수 수정'),
     185            1 :         actions: [
     186            1 :           IconButton(
     187              :             icon: const Icon(Icons.save),
     188            1 :             onPressed: _saveScore,
     189              :           ),
     190              :         ],
     191              :       ),
     192            1 :       body: _isLoading
     193              :           ? const LoadingIndicator()
     194            1 :           : SingleChildScrollView(
     195              :               padding: const EdgeInsets.all(16.0),
     196            1 :               child: Form(
     197            1 :                 key: _formKey,
     198            1 :                 child: Column(
     199              :                   crossAxisAlignment: CrossAxisAlignment.start,
     200            1 :                   children: [
     201              :                     // 기본 정보 섹션
     202            1 :                     _buildSectionTitle('기본 정보'),
     203              :                     
     204              :                     // 참가자 선택
     205            1 :                     DropdownButtonFormField<String>(
     206            1 :                       value: _selectedParticipantId,
     207              :                       decoration: const InputDecoration(
     208              :                         labelText: '참가자 *',
     209              :                       ),
     210            4 :                       items: widget.participants.map((participant) {
     211            1 :                         return DropdownMenuItem(
     212            1 :                           value: participant.memberId,
     213            2 :                           child: Text(participant.name ?? '이름 없음'),
     214              :                         );
     215            1 :                       }).toList(),
     216            0 :                       onChanged: (value) {
     217            0 :                         setState(() {
     218            0 :                           _selectedParticipantId = value;
     219              :                         });
     220              :                       },
     221            1 :                       validator: (value) {
     222            1 :                         if (value == null || value.isEmpty) {
     223              :                           return '참가자를 선택해주세요';
     224              :                         }
     225              :                         return null;
     226              :                       },
     227              :                     ),
     228              :                     const SizedBox(height: 16),
     229              :                     
     230              :                     // 날짜 선택
     231            1 :                     ListTile(
     232              :                       title: const Text('점수 기록 날짜 *'),
     233            1 :                       subtitle: Text(
     234            3 :                         DateFormat('yyyy년 MM월 dd일').format(_scoreDate),
     235              :                       ),
     236              :                       trailing: const Icon(Icons.calendar_today),
     237            0 :                       onTap: () async {
     238            0 :                         final date = await showDatePicker(
     239              :                           context: context,
     240            0 :                           initialDate: _scoreDate,
     241            0 :                           firstDate: DateTime(2020),
     242            0 :                           lastDate: DateTime(2030),
     243              :                         );
     244              :                         
     245            0 :                         if (date != null && mounted) {
     246            0 :                           setState(() {
     247            0 :                             _scoreDate = date;
     248              :                           });
     249              :                         }
     250              :                       },
     251              :                     ),
     252              :                     const SizedBox(height: 24),
     253              :                     
     254              :                     // 점수 입력 섹션
     255            1 :                     _buildSectionTitle('점수 입력'),
     256              :                     
     257              :                     // 점수 입력 모드 전환 스위치
     258            1 :                     SwitchListTile(
     259              :                       title: const Text('프레임별 점수 입력'),
     260              :                       subtitle: const Text('각 프레임마다 점수를 개별적으로 입력합니다'),
     261            1 :                       value: _useFrameInput,
     262            2 :                       activeColor: Theme.of(context).primaryColor,
     263            1 :                       onChanged: (value) {
     264            2 :                         setState(() {
     265            1 :                           _useFrameInput = value;
     266              :                           if (!value) {
     267              :                             // 총점 입력 모드로 전환 시 현재 계산된 총점 유지
     268            4 :                             _totalScoreController.text = _totalScore.toString();
     269              :                           } else {
     270              :                             // 프레임별 입력 모드로 전환 시 총점 계산
     271            1 :                             _calculateTotalScore();
     272              :                           }
     273              :                         });
     274              :                       },
     275              :                     ),
     276              :                     const SizedBox(height: 16),
     277              :                     
     278              :                     // 입력 모드에 따라 다른 UI 표시
     279            2 :                     _useFrameInput ? Column(
     280            1 :                       children: [
     281              :                         // 프레임별 점수 입력
     282            1 :                         GridView.builder(
     283              :                           shrinkWrap: true,
     284              :                           physics: const NeverScrollableScrollPhysics(),
     285              :                           gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
     286              :                             crossAxisCount: 5,
     287              :                             childAspectRatio: 1.5,
     288              :                             crossAxisSpacing: 10,
     289              :                             mainAxisSpacing: 10,
     290              :                           ),
     291              :                           itemCount: 10,
     292            1 :                           itemBuilder: (context, index) {
     293            1 :                             return _buildFrameInput(index);
     294              :                           },
     295              :                         ),
     296              :                         const SizedBox(height: 16),
     297              :                         
     298              :                         // 계산된 총점 표시
     299            1 :                         Container(
     300              :                           padding: const EdgeInsets.all(16),
     301            1 :                           decoration: BoxDecoration(
     302            1 :                             color: Colors.blue.shade50,
     303            1 :                             borderRadius: BorderRadius.circular(8),
     304              :                           ),
     305            1 :                           child: Row(
     306              :                             mainAxisAlignment: MainAxisAlignment.spaceBetween,
     307            1 :                             children: [
     308              :                               const Text(
     309              :                                 '계산된 총점:',
     310              :                                 style: TextStyle(
     311              :                                   fontSize: 18,
     312              :                                   fontWeight: FontWeight.bold,
     313              :                                 ),
     314              :                               ),
     315            1 :                               Text(
     316            2 :                                 _totalScore.toString(),
     317              :                                 style: const TextStyle(
     318              :                                   fontSize: 24,
     319              :                                   fontWeight: FontWeight.bold,
     320              :                                   color: Colors.blue,
     321              :                                 ),
     322              :                               ),
     323              :                             ],
     324              :                           ),
     325              :                         ),
     326              :                       ],
     327            1 :                     ) : TextFormField(
     328              :                       // 총점 직접 입력
     329            1 :                       controller: _totalScoreController,
     330              :                       keyboardType: TextInputType.number,
     331              :                       decoration: const InputDecoration(
     332              :                         labelText: '총점 *',
     333              :                         hintText: '게임 총점을 입력하세요',
     334              :                         prefixIcon: Icon(Icons.score),
     335              :                       ),
     336            1 :                       validator: (value) {
     337            1 :                         if (value == null || value.isEmpty) {
     338              :                           return '총점을 입력해주세요';
     339              :                         }
     340            1 :                         final score = int.tryParse(value);
     341              :                         if (score == null) {
     342              :                           return '숫자만 입력해주세요';
     343              :                         }
     344            2 :                         if (score < 0 || score > 300) {
     345              :                           return '0-300 사이의 값을 입력해주세요';
     346              :                         }
     347              :                         return null;
     348              :                       },
     349            1 :                       onChanged: (value) {
     350            2 :                         setState(() {
     351            2 :                           _totalScore = int.tryParse(value) ?? 0;
     352              :                         });
     353              :                       },
     354              :                     ),
     355              :                     const SizedBox(height: 16),
     356              :                     
     357              :                     const SizedBox(height: 16),
     358              :                     
     359              :                     // 핸디캡 입력
     360            1 :                     TextFormField(
     361            1 :                       controller: _handicapController,
     362              :                       keyboardType: TextInputType.number,
     363              :                       decoration: const InputDecoration(
     364              :                         labelText: '핸디캡',
     365              :                         hintText: '핸디캡 점수를 입력하세요',
     366              :                         prefixIcon: Icon(Icons.add_circle_outline),
     367              :                       ),
     368            1 :                       validator: (value) {
     369            1 :                         if (value != null && value.isNotEmpty) {
     370            0 :                           final handicap = int.tryParse(value);
     371              :                           if (handicap == null) {
     372              :                             return '숫자만 입력해주세요';
     373              :                           }
     374            0 :                           if (handicap < 0 || handicap > 200) {
     375              :                             return '0-200 사이의 값을 입력해주세요';
     376              :                           }
     377              :                         }
     378              :                         return null;
     379              :                       },
     380              :                     ),
     381              :                     // 핸디캡 포함 총점 표시
     382            1 :                     Padding(
     383              :                       padding: const EdgeInsets.symmetric(vertical: 8.0),
     384            1 :                       child: Row(
     385              :                         mainAxisAlignment: MainAxisAlignment.end,
     386            1 :                         children: [
     387              :                           const Text(
     388              :                             '핸디캡 포함 총점: ',
     389              :                             style: TextStyle(
     390              :                               fontWeight: FontWeight.bold,
     391              :                             ),
     392              :                           ),
     393            1 :                           Text(
     394            6 :                             '${_totalScore + (int.tryParse(_handicapController.text) ?? 0)}',
     395              :                             style: const TextStyle(
     396              :                               fontWeight: FontWeight.bold,
     397              :                               color: Colors.green,
     398              :                             ),
     399              :                           ),
     400              :                         ],
     401              :                       ),
     402              :                     ),
     403              :                     const SizedBox(height: 24),
     404              :                     
     405              :                     // 메모 섹션
     406            1 :                     _buildSectionTitle('추가 정보'),
     407              :                     
     408              :                     // 메모
     409            1 :                     TextFormField(
     410            1 :                       controller: _notesController,
     411              :                       decoration: const InputDecoration(
     412              :                         labelText: '메모',
     413              :                         hintText: '점수에 대한 메모를 입력하세요',
     414              :                       ),
     415              :                       maxLines: 3,
     416              :                     ),
     417              :                     const SizedBox(height: 32),
     418              :                     
     419              :                     // 저장 버튼
     420            1 :                     SizedBox(
     421              :                       width: double.infinity,
     422            1 :                       child: ElevatedButton(
     423            1 :                         onPressed: _saveScore,
     424            1 :                         style: ElevatedButton.styleFrom(
     425              :                           padding: const EdgeInsets.symmetric(vertical: 16),
     426              :                         ),
     427            1 :                         child: Text(
     428            2 :                           widget.score == null ? '점수 추가' : '점수 수정',
     429              :                           style: const TextStyle(fontSize: 16),
     430              :                         ),
     431              :                       ),
     432              :                     ),
     433              :                   ],
     434              :                 ),
     435              :               ),
     436              :             ),
     437              :     );
     438              :   }
     439              : 
     440              :   // 프레임 입력 위젯
     441            1 :   Widget _buildFrameInput(int frameIndex) {
     442            1 :     return Column(
     443            1 :       children: [
     444            1 :         Text(
     445            2 :           '${frameIndex + 1}',
     446              :           style: const TextStyle(fontWeight: FontWeight.bold),
     447              :         ),
     448            1 :         Expanded(
     449            1 :           child: TextFormField(
     450            2 :             controller: _frameControllers[frameIndex],
     451              :             keyboardType: TextInputType.number,
     452              :             textAlign: TextAlign.center,
     453            1 :             decoration: InputDecoration(
     454            1 :               border: OutlineInputBorder(
     455            1 :                 borderRadius: BorderRadius.circular(8),
     456              :               ),
     457              :               contentPadding: const EdgeInsets.symmetric(horizontal: 8),
     458              :             ),
     459            1 :             validator: (value) {
     460            1 :               if (value != null && value.isNotEmpty) {
     461            0 :                 final score = int.tryParse(value);
     462              :                 if (score == null) {
     463              :                   return '숫자만';
     464              :                 }
     465            0 :                 if (score < 0 || score > 300) {
     466              :                   return '0-300';
     467              :                 }
     468              :               }
     469              :               return null;
     470              :             },
     471            0 :             onChanged: (_) => _calculateTotalScore(),
     472              :           ),
     473              :         ),
     474              :       ],
     475              :     );
     476              :   }
     477              : 
     478              :   // 섹션 제목 위젯
     479            1 :   Widget _buildSectionTitle(String title) {
     480            1 :     return Column(
     481              :       crossAxisAlignment: CrossAxisAlignment.start,
     482            1 :       children: [
     483            1 :         Text(
     484              :           title,
     485              :           style: const TextStyle(
     486              :             fontSize: 18,
     487              :             fontWeight: FontWeight.bold,
     488              :           ),
     489              :         ),
     490              :         const Divider(),
     491              :         const SizedBox(height: 8),
     492              :       ],
     493              :     );
     494              :   }
     495              : }
        

Generated by: LCOV version 2.3.1-1