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