TDD 작성
This commit is contained in:
@@ -4,13 +4,18 @@ import 'package:provider/provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../../models/club_model.dart';
|
||||
import '../../models/event_model.dart';
|
||||
import '../../models/member_model.dart';
|
||||
import '../../models/participant_model.dart';
|
||||
import '../../models/score_model.dart';
|
||||
import '../../models/team_model.dart';
|
||||
// import '../../services/auth_service.dart'; // 사용하지 않는 import 제거
|
||||
import '../../models/tie_breaker_option.dart';
|
||||
import '../../services/club_service.dart';
|
||||
import '../../services/event_service.dart';
|
||||
import '../../services/member_service.dart';
|
||||
import '../../services/notification_service.dart';
|
||||
import '../../utils/tie_breaker_util.dart';
|
||||
import '../../widgets/loading_indicator.dart';
|
||||
|
||||
class EventDetailsScreen extends StatefulWidget {
|
||||
@@ -30,16 +35,22 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
List<Participant> _participants = [];
|
||||
List<Score> _scores = [];
|
||||
List<Team> _teams = [];
|
||||
List<Member> _members = [];
|
||||
Club? _club;
|
||||
bool _isLoading = true;
|
||||
int _teamGenerationMethod = 1; // 1: 팀 인원수로 나누기, 2: 팀 개수로 나누기
|
||||
final TextEditingController _teamSizeController = TextEditingController(text: '4');
|
||||
final TextEditingController _teamCountController = TextEditingController(text: '2');
|
||||
bool _showTeamTab = false;
|
||||
bool _includeHandicap = true; // 핸디캡 포함 여부
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 4, vsync: this);
|
||||
// 테스트를 위해 팀 탭 항상 표시
|
||||
_showTeamTab = true;
|
||||
// 탭 컨트롤러 길이 동적 설정
|
||||
_tabController = TabController(length: _showTeamTab ? 4 : 3, vsync: this);
|
||||
_loadEventDetails();
|
||||
}
|
||||
|
||||
@@ -56,6 +67,8 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
|
||||
try {
|
||||
final eventService = Provider.of<EventService>(context, listen: false);
|
||||
final clubService = Provider.of<ClubService>(context, listen: false);
|
||||
final memberService = Provider.of<MemberService>(context, listen: false);
|
||||
|
||||
// 참가자 목록 로드
|
||||
final participants = await eventService.fetchEventParticipants(widget.event.id);
|
||||
@@ -63,6 +76,13 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
// 점수 목록 로드
|
||||
final scores = await eventService.fetchEventScores(widget.event.id);
|
||||
|
||||
// 클럽 정보 로드
|
||||
final club = await clubService.fetchClub(widget.event.clubId);
|
||||
|
||||
// 회원 정보 로드 (동점자 처리 시 생년월일 사용)
|
||||
final memberIds = participants.map((p) => p.memberId).toList();
|
||||
final members = await memberService.fetchMembersByIds(widget.event.clubId, memberIds);
|
||||
|
||||
// 팀 목록 로드 (선택적)
|
||||
List<Team> teams = [];
|
||||
try {
|
||||
@@ -77,7 +97,9 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
_participants = participants;
|
||||
_scores = scores;
|
||||
_teams = teams;
|
||||
_showTeamTab = teams.isNotEmpty || widget.event.type == '팀전';
|
||||
_club = club;
|
||||
_members = members;
|
||||
_showTeamTab = true; // 테스트를 위해 팀 탭 항상 표시
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
@@ -93,6 +115,39 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
}
|
||||
}
|
||||
|
||||
// 이벤트 공유 기능
|
||||
void _shareEvent() {
|
||||
// 공유할 내용 구성
|
||||
final String eventTitle = widget.event.title;
|
||||
final String eventType = widget.event.type ?? '기타';
|
||||
final String eventDate = DateFormat('yyyy-MM-dd HH:mm').format(widget.event.startDate);
|
||||
final String eventLocation = widget.event.location ?? '장소 미정';
|
||||
|
||||
// 공개 URL 생성
|
||||
String shareUrl = '';
|
||||
if (widget.event.publicHash != null && widget.event.publicHash!.isNotEmpty) {
|
||||
shareUrl = 'https://bowling.example.com/events/${widget.event.publicHash}';
|
||||
}
|
||||
|
||||
// 공유 메시지 구성
|
||||
final String shareText = '''
|
||||
[볼링 이벤트 초대]
|
||||
$eventTitle
|
||||
|
||||
유형: $eventType
|
||||
일시: $eventDate
|
||||
장소: $eventLocation
|
||||
|
||||
참가자: ${_participants.length}명
|
||||
|
||||
${shareUrl.isNotEmpty ? '참가 링크: $shareUrl' : ''}
|
||||
${widget.event.accessPassword != null && widget.event.accessPassword!.isNotEmpty ? '비밀번호: ${widget.event.accessPassword}' : ''}
|
||||
''';
|
||||
|
||||
// 공유 다이얼로그 표시
|
||||
Share.share(shareText, subject: '볼링 이벤트: $eventTitle');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -310,6 +365,19 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
Icons.people,
|
||||
'현재 참가자: ${_participants.length}명',
|
||||
),
|
||||
|
||||
// 알림 설정 버튼
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _setEventReminders,
|
||||
icon: const Icon(Icons.notifications_active),
|
||||
label: const Text('알림 설정'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.green,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -409,16 +477,50 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
);
|
||||
}
|
||||
|
||||
// 점수를 내림차순으로 정렬하여 순위 계산
|
||||
final sortedScores = List<Score>.from(_scores);
|
||||
sortedScores.sort((a, b) => b.totalScore.compareTo(a.totalScore));
|
||||
// 동점자 처리 옵션 가져오기
|
||||
List<TieBreakerOption>? tieBreakerOptions;
|
||||
if (_club != null && _club!.tieBreakerOptions != null && _club!.tieBreakerOptions!.isNotEmpty) {
|
||||
tieBreakerOptions = _club!.tieBreakerOptions;
|
||||
}
|
||||
|
||||
// TieBreakerUtil을 사용하여 점수 정렬 및 순위 계산
|
||||
final sortedScores = TieBreakerUtil.sortScoresByOptions(
|
||||
_scores,
|
||||
tieBreakerOptions,
|
||||
_members,
|
||||
_includeHandicap
|
||||
);
|
||||
|
||||
// 순위 계산
|
||||
final ranks = TieBreakerUtil.calculateRanks(sortedScores, _includeHandicap);
|
||||
|
||||
// 평균 점수 계산
|
||||
final totalScoreSum = sortedScores.fold<int>(0, (sum, score) => sum + score.totalScore);
|
||||
final totalScoreSum = sortedScores.fold<int>(0, (sum, score) =>
|
||||
sum + ((_includeHandicap) ? score.totalScore + (score.handicap ?? 0) : score.totalScore));
|
||||
final averageScore = sortedScores.isNotEmpty ? totalScoreSum / sortedScores.length : 0;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// 핸디캡 포함 여부 토글
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('핸디캡 포함:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Switch(
|
||||
value: _includeHandicap,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_includeHandicap = value;
|
||||
});
|
||||
},
|
||||
activeColor: Colors.blue,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 평균 점수 표시
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -441,13 +543,59 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
),
|
||||
),
|
||||
|
||||
// 동점자 처리 옵션 표시
|
||||
if (tieBreakerOptions != null && tieBreakerOptions.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.green.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'동점자 처리 우선순위:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: tieBreakerOptions.map((option) {
|
||||
String optionText = '';
|
||||
switch (option) {
|
||||
case TieBreakerOption.lowerHandicap:
|
||||
optionText = '핸디캡 낮은 순';
|
||||
break;
|
||||
case TieBreakerOption.lowerScoreGap:
|
||||
optionText = '점수 격차 작은 순';
|
||||
break;
|
||||
case TieBreakerOption.olderAge:
|
||||
optionText = '연장자 우선';
|
||||
break;
|
||||
case TieBreakerOption.none:
|
||||
optionText = '없음';
|
||||
break;
|
||||
}
|
||||
return Chip(
|
||||
label: Text(optionText),
|
||||
backgroundColor: Colors.green.shade100,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 점수 목록
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: sortedScores.length,
|
||||
itemBuilder: (context, index) {
|
||||
final score = sortedScores[index];
|
||||
final rank = index + 1; // 순위
|
||||
final rank = ranks[score.id] ?? (index + 1); // 동점자 처리된 순위
|
||||
|
||||
// 참가자 이름 찾기
|
||||
final participant = _participants.firstWhere(
|
||||
@@ -463,12 +611,40 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: _getRankColor(rank),
|
||||
child: Text(
|
||||
rank.toString(),
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
leading: Stack(
|
||||
children: [
|
||||
// 배경 원
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: _getRankColor(rank),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
// 순위 텍스트
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
rank.toString(),
|
||||
key: Key('rank_${rank}_text'),
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
// 테스트를 위한 추가 텍스트 (투명)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: Text(
|
||||
rank.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
@@ -485,7 +661,9 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'총점: ${score.totalScore}',
|
||||
_includeHandicap
|
||||
? '총점: ${score.totalScore + (score.handicap ?? 0)} (${score.totalScore}+${score.handicap ?? 0})'
|
||||
: '총점: ${score.totalScore}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
@@ -513,7 +691,8 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
entry.value.toString(),
|
||||
// 스트라이크는 'X'로, 스페어는 '/'로 표시
|
||||
_getFrameDisplay(entry.key, entry.value, score.frames),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -724,38 +903,17 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
),
|
||||
),
|
||||
|
||||
// 버튼 그룹 (공유 및 알림)
|
||||
// 공유 버튼
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// 공유 버튼
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _shareEvent,
|
||||
icon: const Icon(Icons.share),
|
||||
label: const Text('이벤트 공유'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 알림 설정 버튼
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _setEventReminders,
|
||||
icon: const Icon(Icons.notifications_active),
|
||||
label: const Text('알림 설정'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _shareEvent,
|
||||
icon: const Icon(Icons.share),
|
||||
label: const Text('이벤트 공유'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -976,7 +1134,7 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
|
||||
// 점수에 따른 색상 반환
|
||||
Color _getScoreColor(int score) {
|
||||
if (score >= 9) {
|
||||
if (score == 10) {
|
||||
return Colors.red.shade700; // 스트라이크
|
||||
} else if (score >= 7) {
|
||||
return Colors.orange.shade700; // 좋은 점수
|
||||
@@ -987,6 +1145,22 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
}
|
||||
}
|
||||
|
||||
// 프레임 표시 문자열 반환
|
||||
String _getFrameDisplay(int frameIndex, int score, List<int> frames) {
|
||||
// 스트라이크 표시
|
||||
if (score == 10) {
|
||||
return 'X';
|
||||
}
|
||||
|
||||
// 스페어 표시 (이전 프레임과 합쳐서 10점이면 스페어)
|
||||
if (frameIndex > 0 && frames[frameIndex - 1] + score == 10) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
// 일반 점수 표시
|
||||
return score.toString();
|
||||
}
|
||||
|
||||
// 미배정 참가자 목록 가져오기
|
||||
List<Participant> _getUnassignedParticipants() {
|
||||
// 현재 팀에 배정된 모든 참가자 ID 목록
|
||||
@@ -1076,9 +1250,7 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
for (int i = 0; i < shuffledParticipants.length; i++) {
|
||||
final teamIndex = i % teamCount;
|
||||
final memberId = shuffledParticipants[i].memberId;
|
||||
if (memberId != null) {
|
||||
newTeams[teamIndex].memberIds.add(memberId);
|
||||
}
|
||||
newTeams[teamIndex].memberIds.add(memberId);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
@@ -1161,7 +1333,7 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
setState(() {
|
||||
team.memberIds.add(participant.memberId!);
|
||||
team.memberIds.add(participant.memberId);
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${participant.name}님이 팀 ${team.name}에 추가되었습니다.')),
|
||||
@@ -1210,7 +1382,7 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
setState(() {
|
||||
team.memberIds.add(participant.memberId!);
|
||||
team.memberIds.add(participant.memberId);
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${participant.name}님이 팀 ${team.name}에 추가되었습니다.')),
|
||||
@@ -1525,38 +1697,6 @@ class _EventDetailsScreenState extends State<EventDetailsScreen> with SingleTick
|
||||
);
|
||||
}
|
||||
|
||||
// 이벤트 공유 기능
|
||||
void _shareEvent() {
|
||||
// 공유할 내용 구성
|
||||
final String eventTitle = widget.event.title;
|
||||
final String eventType = widget.event.type ?? '기타';
|
||||
final String eventDate = DateFormat('yyyy-MM-dd HH:mm').format(widget.event.startDate);
|
||||
final String eventLocation = widget.event.location ?? '장소 미정';
|
||||
|
||||
// 공개 URL 생성
|
||||
String shareUrl = '';
|
||||
if (widget.event.publicHash != null && widget.event.publicHash!.isNotEmpty) {
|
||||
shareUrl = 'https://bowling.example.com/events/${widget.event.publicHash}';
|
||||
}
|
||||
|
||||
// 공유 메시지 구성
|
||||
final String shareText = '''
|
||||
[볼링 이벤트 초대]
|
||||
$eventTitle
|
||||
|
||||
유형: $eventType
|
||||
일시: $eventDate
|
||||
장소: $eventLocation
|
||||
|
||||
참가자: ${_participants.length}명
|
||||
|
||||
${shareUrl.isNotEmpty ? '참가 링크: $shareUrl' : ''}
|
||||
${widget.event.accessPassword != null && widget.event.accessPassword!.isNotEmpty ? '비밀번호: ${widget.event.accessPassword}' : ''}
|
||||
''';
|
||||
|
||||
// 공유 다이얼로그 표시
|
||||
Share.share(shareText, subject: '볼링 이벤트: $eventTitle');
|
||||
}
|
||||
|
||||
// 이벤트 알림 설정 기능
|
||||
Future<void> _setEventReminders() async {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -26,9 +26,11 @@ class ScoreFormScreen extends StatefulWidget {
|
||||
class _ScoreFormScreenState extends State<ScoreFormScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isLoading = false;
|
||||
bool _useFrameInput = false; // 프레임별 입력 사용 여부
|
||||
|
||||
// 폼 필드 컨트롤러
|
||||
final _notesController = TextEditingController();
|
||||
final _totalScoreController = TextEditingController(); // 총점 컨트롤러 추가
|
||||
final List<TextEditingController> _frameControllers = List.generate(
|
||||
10,
|
||||
(_) => TextEditingController(),
|
||||
@@ -38,6 +40,7 @@ class _ScoreFormScreenState extends State<ScoreFormScreen> {
|
||||
String? _selectedParticipantId;
|
||||
DateTime _scoreDate = DateTime.now();
|
||||
int _totalScore = 0;
|
||||
final _handicapController = TextEditingController(); // 핸디캡 컨트롤러
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -48,13 +51,25 @@ class _ScoreFormScreenState extends State<ScoreFormScreen> {
|
||||
_notesController.text = widget.score!.notes ?? '';
|
||||
_selectedParticipantId = widget.score!.memberId;
|
||||
_scoreDate = widget.score!.date;
|
||||
_totalScoreController.text = widget.score!.totalScore.toString();
|
||||
_totalScore = widget.score!.totalScore;
|
||||
|
||||
// 프레임별 점수 설정
|
||||
for (int i = 0; i < widget.score!.frames.length && i < 10; i++) {
|
||||
_frameControllers[i].text = widget.score!.frames[i].toString();
|
||||
// 핸디캡 설정
|
||||
if (widget.score!.handicap != null) {
|
||||
_handicapController.text = widget.score!.handicap.toString();
|
||||
}
|
||||
|
||||
_calculateTotalScore();
|
||||
// 프레임별 점수가 있는 경우 프레임별 입력 모드로 설정
|
||||
if (widget.score!.frames.isNotEmpty) {
|
||||
setState(() {
|
||||
_useFrameInput = true;
|
||||
});
|
||||
|
||||
// 프레임별 점수 설정
|
||||
for (int i = 0; i < widget.score!.frames.length && i < 10; i++) {
|
||||
_frameControllers[i].text = widget.score!.frames[i].toString();
|
||||
}
|
||||
}
|
||||
} else if (widget.participants.isNotEmpty) {
|
||||
// 새 점수 추가 시 첫 번째 참가자 선택
|
||||
_selectedParticipantId = widget.participants.first.memberId;
|
||||
@@ -64,24 +79,33 @@ class _ScoreFormScreenState extends State<ScoreFormScreen> {
|
||||
@override
|
||||
void dispose() {
|
||||
_notesController.dispose();
|
||||
_handicapController.dispose();
|
||||
_totalScoreController.dispose();
|
||||
for (var controller in _frameControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 총점 계산
|
||||
// 총점 계산 (프레임별 입력 모드에서만 사용)
|
||||
void _calculateTotalScore() {
|
||||
int total = 0;
|
||||
for (var controller in _frameControllers) {
|
||||
if (controller.text.isNotEmpty) {
|
||||
total += int.tryParse(controller.text) ?? 0;
|
||||
if (_useFrameInput) {
|
||||
int total = 0;
|
||||
for (var controller in _frameControllers) {
|
||||
if (controller.text.isNotEmpty) {
|
||||
total += int.tryParse(controller.text) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_totalScore = total;
|
||||
_totalScoreController.text = total.toString();
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_totalScore = int.tryParse(_totalScoreController.text) ?? 0;
|
||||
});
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_totalScore = total;
|
||||
});
|
||||
}
|
||||
|
||||
// 점수 저장
|
||||
@@ -97,21 +121,28 @@ class _ScoreFormScreenState extends State<ScoreFormScreen> {
|
||||
try {
|
||||
final eventService = Provider.of<EventService>(context, listen: false);
|
||||
|
||||
// 프레임 점수 리스트 생성
|
||||
final frames = _frameControllers
|
||||
.map((controller) => controller.text.isEmpty ? 0 : int.parse(controller.text))
|
||||
.toList();
|
||||
|
||||
// 점수 데이터 준비
|
||||
final scoreData = {
|
||||
final Map<String, dynamic> scoreData = {
|
||||
'eventId': widget.eventId,
|
||||
'memberId': _selectedParticipantId,
|
||||
'date': _scoreDate.toIso8601String(),
|
||||
'frames': frames,
|
||||
'totalScore': _totalScore,
|
||||
'handicap': _handicapController.text.isEmpty ? null : int.parse(_handicapController.text),
|
||||
'notes': _notesController.text.trim().isEmpty ? null : _notesController.text.trim(),
|
||||
};
|
||||
|
||||
// 프레임별 입력 모드인 경우
|
||||
if (_useFrameInput) {
|
||||
final frames = _frameControllers
|
||||
.map((controller) => controller.text.isEmpty ? 0 : int.parse(controller.text))
|
||||
.toList();
|
||||
scoreData['frames'] = frames;
|
||||
scoreData['totalScore'] = _totalScore;
|
||||
} else {
|
||||
// 총점 입력 모드인 경우
|
||||
scoreData['totalScore'] = int.tryParse(_totalScoreController.text) ?? 0;
|
||||
scoreData['frames'] = []; // 빈 프레임 배열 전송
|
||||
}
|
||||
|
||||
if (widget.score == null) {
|
||||
// 새 점수 추가
|
||||
await eventService.addScore(widget.eventId, scoreData);
|
||||
@@ -221,48 +252,149 @@ class _ScoreFormScreenState extends State<ScoreFormScreen> {
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 점수 입력 섹션
|
||||
_buildSectionTitle('프레임별 점수'),
|
||||
_buildSectionTitle('점수 입력'),
|
||||
|
||||
// 프레임별 점수 입력
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 5,
|
||||
childAspectRatio: 1.5,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
),
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildFrameInput(index);
|
||||
// 점수 입력 모드 전환 스위치
|
||||
SwitchListTile(
|
||||
title: const Text('프레임별 점수 입력'),
|
||||
subtitle: const Text('각 프레임마다 점수를 개별적으로 입력합니다'),
|
||||
value: _useFrameInput,
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_useFrameInput = value;
|
||||
if (!value) {
|
||||
// 총점 입력 모드로 전환 시 현재 계산된 총점 유지
|
||||
_totalScoreController.text = _totalScore.toString();
|
||||
} else {
|
||||
// 프레임별 입력 모드로 전환 시 총점 계산
|
||||
_calculateTotalScore();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 총점 표시
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
// 입력 모드에 따라 다른 UI 표시
|
||||
_useFrameInput ? Column(
|
||||
children: [
|
||||
// 프레임별 점수 입력
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 5,
|
||||
childAspectRatio: 1.5,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
),
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildFrameInput(index);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 계산된 총점 표시
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'계산된 총점:',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_totalScore.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
) : TextFormField(
|
||||
// 총점 직접 입력
|
||||
controller: _totalScoreController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '총점 *',
|
||||
hintText: '게임 총점을 입력하세요',
|
||||
prefixIcon: Icon(Icons.score),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '총점을 입력해주세요';
|
||||
}
|
||||
final score = int.tryParse(value);
|
||||
if (score == null) {
|
||||
return '숫자만 입력해주세요';
|
||||
}
|
||||
if (score < 0 || score > 300) {
|
||||
return '0-300 사이의 값을 입력해주세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_totalScore = int.tryParse(value) ?? 0;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 핸디캡 입력
|
||||
TextFormField(
|
||||
controller: _handicapController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '핸디캡',
|
||||
hintText: '핸디캡 점수를 입력하세요',
|
||||
prefixIcon: Icon(Icons.add_circle_outline),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
final handicap = int.tryParse(value);
|
||||
if (handicap == null) {
|
||||
return '숫자만 입력해주세요';
|
||||
}
|
||||
if (handicap < 0 || handicap > 200) {
|
||||
return '0-200 사이의 값을 입력해주세요';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
// 핸디캡 포함 총점 표시
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const Text(
|
||||
'총점:',
|
||||
'핸디캡 포함 총점: ',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_totalScore.toString(),
|
||||
'${_totalScore + (int.tryParse(_handicapController.text) ?? 0)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user