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 : }
|