이벤트 개발완료

This commit is contained in:
2025-10-23 22:04:03 +09:00
parent ce1aee67d6
commit cd0e139b5e
48 changed files with 7855 additions and 3459 deletions
@@ -0,0 +1,234 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../../models/score_model.dart';
import '../../models/participant_model.dart';
import '../../services/event_service.dart';
class ScoreBatchEditScreen extends StatefulWidget {
final String eventId;
final Participant participant;
final List<Score> scores; // 해당 참가자의 기존 점수들 (게임번호 기준 정렬 권장)
final int gameCount; // 이벤트 전체 게임 수
const ScoreBatchEditScreen({
super.key,
required this.eventId,
required this.participant,
required this.scores,
required this.gameCount,
});
@override
State<ScoreBatchEditScreen> createState() => _ScoreBatchEditScreenState();
}
class _ScoreBatchEditScreenState extends State<ScoreBatchEditScreen> {
final _formKey = GlobalKey<FormState>();
bool _isSaving = false;
late List<TextEditingController> _scoreCtrls;
late List<TextEditingController> _handicapCtrls;
@override
void initState() {
super.initState();
// 기존 점수 맵: gameNumber -> Score
final byGame = <int, Score>{};
for (final s in widget.scores) {
final gn = s.gameNumber ?? 0;
if (gn > 0) byGame[gn] = s;
}
final count = widget.gameCount > 0 ? widget.gameCount : (byGame.keys.isEmpty ? 1 : byGame.keys.reduce((a,b)=>a>b?a:b));
_scoreCtrls = List.generate(count, (i) {
final gn = i + 1;
final s = byGame[gn];
return TextEditingController(text: s != null ? s.totalScore.toString() : '');
});
_handicapCtrls = List.generate(count, (i) {
final gn = i + 1;
final s = byGame[gn];
return TextEditingController(text: s != null ? (s.handicap ?? 0).toString() : '');
});
}
@override
void dispose() {
for (final c in _scoreCtrls) {
c.dispose();
}
for (final c in _handicapCtrls) {
c.dispose();
}
super.dispose();
}
Future<void> _saveAll() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isSaving = true);
try {
final eventService = Provider.of<EventService>(context, listen: false);
// 기존 점수 맵 구성
final byGame = <int, Score>{};
for (final s in widget.scores) {
final gn = s.gameNumber ?? 0;
if (gn > 0) byGame[gn] = s;
}
final count = _scoreCtrls.length;
for (int i = 0; i < count; i++) {
final gameNo = i + 1;
final scoreText = _scoreCtrls[i].text.trim();
final hText = _handicapCtrls[i].text.trim();
final hasAny = scoreText.isNotEmpty || hText.isNotEmpty;
if (!hasAny) continue; // 완전 비어있으면 건너뜀
final newScore = int.tryParse(scoreText) ?? 0;
final newHdc = int.tryParse(hText.isEmpty ? '0' : hText) ?? 0;
final existing = byGame[gameNo];
if (existing != null) {
// 업데이트: 날짜 보존
await eventService.updateScore(widget.eventId, existing.id, {
'gameNumber': gameNo,
'score': newScore,
'totalScore': newScore, // 호환 목적
'handicap': newHdc,
'date': existing.date.toIso8601String(),
});
} else {
// 신규 추가
await eventService.addScore(widget.eventId, {
'eventId': widget.eventId,
'participantId': widget.participant.id,
'gameNumber': gameNo,
'score': newScore,
'handicap': newHdc,
});
}
}
if (mounted) Navigator.of(context).pop(true);
} catch (e) {
if (!mounted) return;
setState(() => _isSaving = false);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('일괄 수정 실패: $e')));
}
}
@override
Widget build(BuildContext context) {
final count = _scoreCtrls.length;
return Scaffold(
appBar: AppBar(
title: Text('${widget.participant.name ?? '참가자'} - 점수 일괄 수정'),
actions: [
IconButton(
onPressed: _isSaving ? null : _saveAll,
icon: const Icon(Icons.save),
tooltip: '저장',
)
],
),
body: _isSaving
? const Center(child: CircularProgressIndicator())
: Form(
key: _formKey,
child: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: count,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, i) {
// 기존 점수 찾기
final gn = i + 1;
final s = widget.scores.firstWhere(
(e) => (e.gameNumber ?? 0) == gn,
orElse: () => Score(
id: '',
memberId: widget.participant.memberId,
eventId: widget.eventId,
clubId: '',
frames: const [],
totalScore: 0,
handicap: 0,
gameNumber: gn,
date: DateTime.now(),
),
);
return Card(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 14,
child: Text(gn.toString()),
),
const SizedBox(width: 8),
Text('기록일: ${DateFormat('yyyy-MM-dd').format(s.date)}'),
const Spacer(),
Text('합계: ${(_scoreCtrls[i].text.isEmpty ? 0 : int.tryParse(_scoreCtrls[i].text) ?? 0) + (int.tryParse(_handicapCtrls[i].text.isEmpty ? '0' : _handicapCtrls[i].text) ?? 0)}'),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextFormField(
controller: _scoreCtrls[i],
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '점수 (0-300)',
prefixIcon: Icon(Icons.score),
),
validator: (v) {
if (v == null || v.isEmpty) return null; // 신규 입력은 비워둘 수 있음
final n = int.tryParse(v);
if (n == null) return '숫자만';
if (n < 0 || n > 300) return '0-300';
return null;
},
),
),
const SizedBox(width: 8),
Expanded(
child: TextFormField(
controller: _handicapCtrls[i],
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '핸디캡',
prefixIcon: Icon(Icons.add_circle_outline),
),
validator: (v) {
if (v == null || v.isEmpty) return null;
final n = int.tryParse(v);
if (n == null) return '숫자만';
return null;
},
),
),
],
),
],
),
),
);
},
),
),
bottomNavigationBar: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton.icon(
icon: const Icon(Icons.save),
label: const Text('모든 게임 저장'),
onPressed: _isSaving ? null : _saveAll,
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 14)),
),
),
),
);
}
}