1382 lines
56 KiB
Dart
1382 lines
56 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:lanebow/models/event_model.dart';
|
|
import 'package:lanebow/models/score_model.dart';
|
|
import 'package:lanebow/models/participant_model.dart';
|
|
import 'package:lanebow/models/member_model.dart';
|
|
import 'package:lanebow/utils/tie_breaker_util.dart';
|
|
import 'package:lanebow/models/tie_breaker_option.dart';
|
|
import 'package:lanebow/screens/club/team_generator_screen.dart';
|
|
import 'package:lanebow/services/event_service.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
/// 테스트용 이벤트 상세 화면 위젯
|
|
/// 실제 EventDetailsScreen의 핵심 기능만 구현하여 테스트에 사용
|
|
class TestEventDetailsScreen extends StatefulWidget {
|
|
final Event event;
|
|
final List<Score> scores;
|
|
final List<Participant> participants;
|
|
|
|
const TestEventDetailsScreen({
|
|
super.key,
|
|
required this.event,
|
|
this.scores = const [],
|
|
this.participants = const [],
|
|
});
|
|
|
|
@override
|
|
State<TestEventDetailsScreen> createState() => _TestEventDetailsScreenState();
|
|
}
|
|
|
|
class _TestEventDetailsScreenState extends State<TestEventDetailsScreen> {
|
|
bool _includeHandicap = false;
|
|
List<Member> _members = [];
|
|
bool _isLoading = false;
|
|
String? _scoreError; // 점수 탭 전용 에러 메시지 상태
|
|
String? _participantError; // 참가자 탭 전용 에러 메시지 상태
|
|
String _participantFilter = '전체'; // 참가자 필터 드롭다운 상태
|
|
|
|
// 점수에 따른 색상 반환
|
|
Color _getScoreColor(int score) {
|
|
if (score == 10) {
|
|
return Colors.red.shade700; // 스트라이크
|
|
} else if (score >= 7) {
|
|
return Colors.orange.shade700; // 좋은 점수
|
|
} else if (score >= 5) {
|
|
return Colors.green.shade700; // 평균 점수
|
|
} else {
|
|
return Colors.blue.shade700; // 낮은 점수
|
|
}
|
|
}
|
|
|
|
// 프레임 표시 문자열 반환
|
|
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();
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// 테스트용 멤버 데이터 초기화
|
|
_members = widget.scores.map((score) {
|
|
return Member(
|
|
id: score.memberId,
|
|
clubId: score.clubId,
|
|
name: score.participantName ?? '알 수 없음',
|
|
birthDate: DateTime(1990, 1, 1), // 테스트용 기본 생년월일
|
|
userId: 'test_user_${score.memberId}',
|
|
email: 'test_${score.memberId}@example.com',
|
|
isActive: true,
|
|
);
|
|
}).toList();
|
|
|
|
// 실제 화면과 유사하게 초기 로드시 팀 데이터 조회 호출
|
|
// (Provider는 build 이후 사용 가능하므로 microtask로 지연)
|
|
Future.microtask(() {
|
|
if (mounted) {
|
|
final eventService = Provider.of<EventService>(context, listen: false);
|
|
eventService.fetchEventTeams(widget.event.id);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 점수 탭 위젯 구현
|
|
Widget _buildScoresTab() {
|
|
if (widget.scores.isEmpty) {
|
|
return const Center(child: Text('등록된 점수가 없습니다'));
|
|
}
|
|
|
|
// TieBreakerUtil을 사용하여 점수 정렬 및 순위 계산
|
|
final tieBreakerOptions = [
|
|
TieBreakerOption.lowerHandicap, // 핸디캡이 적은 사람 우선
|
|
TieBreakerOption.olderAge, // 연장자 우선
|
|
TieBreakerOption.lowerScoreGap, // 점수 격차가 적은 사람 우선
|
|
];
|
|
|
|
final sortedScores = TieBreakerUtil.sortScoresByOptions(
|
|
widget.scores,
|
|
tieBreakerOptions,
|
|
_members,
|
|
_includeHandicap,
|
|
);
|
|
|
|
// 순위 계산
|
|
final ranks = TieBreakerUtil.calculateRanks(sortedScores, _includeHandicap);
|
|
|
|
// 에러 상태면 에러 UI 표시
|
|
if (_scoreError != null) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(_scoreError!, key: const Key('score_error_text')),
|
|
const SizedBox(height: 8),
|
|
ElevatedButton(
|
|
key: const Key('score_retry_button'),
|
|
onPressed: () async {
|
|
final eventService = Provider.of<EventService>(
|
|
context,
|
|
listen: false,
|
|
);
|
|
try {
|
|
setState(() {
|
|
_scoreError = null;
|
|
_isLoading = true;
|
|
});
|
|
await eventService.fetchEventScores(widget.event.id);
|
|
} catch (_) {
|
|
setState(() {
|
|
_scoreError = '점수 조회 실패';
|
|
});
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
},
|
|
child: const Text('재시도'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
children: [
|
|
// 상단 제어 영역: 새로고침 + 핸디캡 토글
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Row(
|
|
children: [
|
|
ElevatedButton.icon(
|
|
key: const Key('score_refresh_button'),
|
|
onPressed: () async {
|
|
final eventService = Provider.of<EventService>(
|
|
context,
|
|
listen: false,
|
|
);
|
|
try {
|
|
setState(() {
|
|
_scoreError = null;
|
|
_isLoading = true;
|
|
});
|
|
await eventService.fetchEventScores(widget.event.id);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('점수가 업데이트되었습니다')),
|
|
);
|
|
}
|
|
} catch (_) {
|
|
setState(() {
|
|
_scoreError = '점수 조회 실패';
|
|
});
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text('점수 갱신 실패')));
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
},
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('새로고침'),
|
|
),
|
|
const Spacer(),
|
|
const Text(
|
|
'핸디캡 포함:',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
Switch(
|
|
value: _includeHandicap,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_includeHandicap = value;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: ListView.builder(
|
|
itemCount: sortedScores.length,
|
|
itemBuilder: (context, index) {
|
|
final score = sortedScores[index];
|
|
final rank = ranks[score.id] ?? (index + 1);
|
|
final participant = widget.participants.firstWhere(
|
|
(p) => p.memberId == score.memberId,
|
|
orElse: () => Participant(
|
|
id: 'unknown_${score.memberId}',
|
|
eventId: widget.event.id,
|
|
memberId: score.memberId,
|
|
name: '알 수 없음',
|
|
),
|
|
);
|
|
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 24,
|
|
height: 24,
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade700,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
'$rank',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
participant.name ?? '알 수 없음',
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
const Spacer(),
|
|
Builder(
|
|
builder: (context) {
|
|
Future<void> confirmAndDeleteScore() async {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('삭제 확인'),
|
|
content: const Text('정말로 이 점수를 삭제하시겠습니까?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () =>
|
|
Navigator.pop(ctx, false),
|
|
child: const Text('취소'),
|
|
),
|
|
TextButton(
|
|
onPressed: () =>
|
|
Navigator.pop(ctx, true),
|
|
child: const Text('삭제'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
if (confirmed == true) {
|
|
final eventService =
|
|
Provider.of<EventService>(
|
|
context,
|
|
listen: false,
|
|
);
|
|
try {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = true;
|
|
});
|
|
}
|
|
await eventService.fetchEventScores(
|
|
widget.event.id,
|
|
);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('점수가 삭제되었습니다'),
|
|
),
|
|
);
|
|
}
|
|
} catch (_) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('점수 삭제 실패'),
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Align(
|
|
alignment: Alignment.topRight,
|
|
child: IconButton(
|
|
tooltip: '점수 삭제',
|
|
icon: const Icon(
|
|
Icons.delete,
|
|
color: Colors.redAccent,
|
|
),
|
|
onPressed: confirmAndDeleteScore,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade100,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
_includeHandicap
|
|
? '총점: ${score.totalScore + (score.handicap ?? 0)} (${score.totalScore}+${score.handicap ?? 0})'
|
|
: '총점: ${score.totalScore}',
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: score.frames.asMap().entries.map((entry) {
|
|
return Container(
|
|
width: 30,
|
|
height: 30,
|
|
margin: const EdgeInsets.only(right: 4),
|
|
decoration: BoxDecoration(
|
|
color: _getScoreColor(entry.value),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
_getFrameDisplay(
|
|
entry.key,
|
|
entry.value,
|
|
score.frames,
|
|
),
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// 참가자 탭 위젯 구현
|
|
Widget _buildParticipantsTab() {
|
|
// 에러 상태면 에러 UI 표시
|
|
if (_participantError != null) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(_participantError!, key: const Key('participant_error_text')),
|
|
const SizedBox(height: 8),
|
|
ElevatedButton(
|
|
key: const Key('participant_retry_button'),
|
|
onPressed: () async {
|
|
final service = Provider.of<EventService>(
|
|
context,
|
|
listen: false,
|
|
);
|
|
try {
|
|
setState(() {
|
|
_participantError = null;
|
|
_isLoading = true;
|
|
});
|
|
await service.fetchEventParticipants(widget.event.id);
|
|
} catch (_) {
|
|
setState(() {
|
|
_participantError = '참가자 조회 실패';
|
|
});
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
},
|
|
child: const Text('재시도'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
Future<void> openGuestPreInput() async {
|
|
// 모달을 열고 저장 시 true 반환
|
|
final result = await showModalBottomSheet<bool?>(
|
|
context: context,
|
|
builder: (ctx) {
|
|
return SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Text(
|
|
'게스트 사전입력',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
IconButton(
|
|
key: const Key('guest_preinput_close_button'),
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => Navigator.of(ctx).pop(null),
|
|
),
|
|
],
|
|
),
|
|
const Divider(height: 1),
|
|
const SizedBox(height: 12),
|
|
const Text('상태: 등록됨'),
|
|
const SizedBox(height: 8),
|
|
const Text('결제: 미납'),
|
|
const SizedBox(height: 16),
|
|
Align(
|
|
alignment: Alignment.centerRight,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ElevatedButton(
|
|
key: const Key('guest_preinput_save_fail_button'),
|
|
onPressed: () => Navigator.of(ctx).pop(false),
|
|
child: const Text('저장 실패'),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton(
|
|
key: const Key('guest_preinput_save_button'),
|
|
onPressed: () => Navigator.of(ctx).pop(true),
|
|
child: const Text('저장'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
if (!mounted) return;
|
|
if (result == true) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text('게스트 기본값이 적용되었습니다')));
|
|
} else if (result == false) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text('게스트 기본값 적용에 실패했습니다')));
|
|
}
|
|
}
|
|
|
|
// 공통 헤더: 필터 드롭다운 + 새로고침 + 게스트 사전입력 버튼
|
|
final header = Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
child: Row(
|
|
children: [
|
|
DropdownButton<String>(
|
|
key: const Key('participant_filter_dropdown'),
|
|
value: _participantFilter,
|
|
items: const [
|
|
DropdownMenuItem(value: '전체', child: Text('전체')),
|
|
DropdownMenuItem(value: '확인됨', child: Text('확인됨')),
|
|
DropdownMenuItem(value: '대기', child: Text('대기')),
|
|
DropdownMenuItem(value: '취소', child: Text('취소')),
|
|
],
|
|
onChanged: (value) async {
|
|
if (value == null) return;
|
|
setState(() {
|
|
_participantFilter = value;
|
|
});
|
|
final service = Provider.of<EventService>(context, listen: false);
|
|
try {
|
|
setState(() {
|
|
_participantError = null;
|
|
_isLoading = true;
|
|
});
|
|
await service.fetchEventParticipants(widget.event.id);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('참가자가 업데이트되었습니다')),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
final msg = e.toString().contains('404')
|
|
? '참가자 정보가 없습니다 (404)'
|
|
: '참가자 조회 실패';
|
|
setState(() {
|
|
_participantError = msg;
|
|
});
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text('참가자 갱신 실패')));
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
},
|
|
),
|
|
const Spacer(),
|
|
ElevatedButton.icon(
|
|
key: const Key('participant_refresh_button'),
|
|
onPressed: () async {
|
|
final service = Provider.of<EventService>(context, listen: false);
|
|
try {
|
|
setState(() {
|
|
_participantError = null;
|
|
_isLoading = true;
|
|
});
|
|
await service.fetchEventParticipants(widget.event.id);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('참가자가 업데이트되었습니다')),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
final msg = e.toString().contains('404')
|
|
? '참가자 정보가 없습니다 (404)'
|
|
: '참가자 조회 실패';
|
|
setState(() {
|
|
_participantError = msg;
|
|
});
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text('참가자 갱신 실패')));
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
},
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('새로고침'),
|
|
),
|
|
IconButton(
|
|
tooltip: '게스트 사전입력',
|
|
key: const Key('guest_preinput_button'),
|
|
icon: const Icon(Icons.person_outline),
|
|
onPressed: openGuestPreInput,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (widget.participants.isEmpty) {
|
|
return Column(
|
|
children: [
|
|
header,
|
|
const Expanded(child: Center(child: Text('등록된 참가자가 없습니다'))),
|
|
],
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
children: [
|
|
header,
|
|
Expanded(
|
|
child: ListView.builder(
|
|
itemCount: widget.participants.length,
|
|
itemBuilder: (context, index) {
|
|
final participant = widget.participants[index];
|
|
return ListTile(
|
|
title: Text(
|
|
participant.name ?? '알 수 없음',
|
|
key: Key('participant_name_$index'),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
subtitle: Text('회원 ID: ${participant.memberId}'),
|
|
trailing: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Builder(
|
|
builder: (context) {
|
|
Future<void> confirmAndDeleteParticipant() async {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('삭제 확인'),
|
|
content: Text(
|
|
'정말로 ${participant.name ?? '참가자'}를 삭제하시겠습니까?',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx, false),
|
|
child: const Text('취소'),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx, true),
|
|
child: const Text('삭제'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
if (confirmed == true) {
|
|
final service = Provider.of<EventService>(
|
|
context,
|
|
listen: false,
|
|
);
|
|
try {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = true;
|
|
});
|
|
}
|
|
// 실제 삭제 호출은 생략하고 재조회로 시뮬레이션
|
|
await service.fetchEventParticipants(
|
|
widget.event.id,
|
|
);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('참가자가 삭제되었습니다')),
|
|
);
|
|
}
|
|
} catch (_) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('참가자 삭제 실패')),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return IconButton(
|
|
tooltip: '참가자 삭제',
|
|
icon: const Icon(
|
|
Icons.delete,
|
|
color: Colors.redAccent,
|
|
),
|
|
onPressed: confirmAndDeleteParticipant,
|
|
);
|
|
},
|
|
),
|
|
const Icon(Icons.chevron_right),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// 팀 탭 위젯 구현
|
|
Widget _buildTeamsTab() {
|
|
// 팀 생성 옵션 상태 관리
|
|
final teamSizeController = TextEditingController(text: '3');
|
|
final teamCountController = TextEditingController(text: '2');
|
|
int selectedOption = 0; // 0: 팀 인원수로 나누기, 1: 팀 개수로 나누기
|
|
|
|
return StatefulBuilder(
|
|
builder: (context, setState) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 팀 관리 버튼 (실제 화면과 유사하게 구성)
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
ElevatedButton.icon(
|
|
onPressed: () async {
|
|
// TeamGeneratorScreen으로 이동 후 결과 처리
|
|
final result = await Navigator.of(context).push<bool>(
|
|
MaterialPageRoute(
|
|
builder: (_) => TeamGeneratorScreen(
|
|
eventId: widget.event.id,
|
|
participants: widget.participants,
|
|
existingTeams: const [],
|
|
),
|
|
),
|
|
);
|
|
if (result == true) {
|
|
// 리프레시: 팀 데이터 재조회
|
|
final eventService = Provider.of<EventService>(
|
|
context,
|
|
listen: false,
|
|
);
|
|
await eventService.fetchEventTeams(widget.event.id);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('팀 구성이 업데이트되었습니다')),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('팀 재생성'),
|
|
),
|
|
ElevatedButton.icon(
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.save),
|
|
label: const Text('팀 저장'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
// 팀 생성 옵션 카드
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'팀 생성 옵션',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
// 팀 인원수로 나누기 옵션
|
|
Row(
|
|
children: [
|
|
Radio<int>(
|
|
value: 0,
|
|
groupValue: selectedOption,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
selectedOption = value!;
|
|
});
|
|
},
|
|
),
|
|
const Text('팀 인원수로 나누기'),
|
|
],
|
|
),
|
|
// 팀 개수로 나누기 옵션
|
|
Row(
|
|
children: [
|
|
Radio<int>(
|
|
value: 1,
|
|
groupValue: selectedOption,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
selectedOption = value!;
|
|
});
|
|
},
|
|
),
|
|
const Text('팀 개수로 나누기'),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
// 선택된 옵션에 따른 입력 필드
|
|
if (selectedOption == 0) ...[
|
|
// 팀 인원수로 나누기
|
|
Row(
|
|
children: [
|
|
const Text('인원수:'),
|
|
const SizedBox(width: 8),
|
|
SizedBox(
|
|
width: 60,
|
|
child: TextField(
|
|
controller: teamSizeController,
|
|
keyboardType: TextInputType.number,
|
|
decoration: const InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
contentPadding: EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 8,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
] else ...[
|
|
// 팀 개수로 나누기
|
|
Row(
|
|
children: [
|
|
const Text('팀 수:'),
|
|
const SizedBox(width: 8),
|
|
SizedBox(
|
|
width: 60,
|
|
child: TextField(
|
|
controller: teamCountController,
|
|
keyboardType: TextInputType.number,
|
|
decoration: const InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
contentPadding: EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 8,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
const SizedBox(height: 8),
|
|
// 참가자 수 표시
|
|
Text('총 ${widget.participants.length}명 참가자'),
|
|
const SizedBox(height: 16),
|
|
// 팀 생성 버튼
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
// 실제 팀 생성 로직은 생략하고 스낵바만 표시
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('팀이 생성되었습니다'),
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
},
|
|
child: const Text('팀 생성하기'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
// 팀 목록 표시 (테스트용 더미 데이터)
|
|
const Text(
|
|
'팀 목록',
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Expanded(
|
|
child: ListView(
|
|
children: [
|
|
// 팀 A 카드
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Text(
|
|
'팀 A',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
const Text('팀 에버리지: 180'),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text('팀원: 참가자 1, 참가자 2, 참가자 3'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// 미배정 참가자 섹션
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'미배정 참가자',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text('참가자 4'),
|
|
const Text('참가자 5'),
|
|
const Text('참가자 6'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return DefaultTabController(
|
|
length: 4,
|
|
child: Builder(
|
|
builder: (context) {
|
|
final tabController = DefaultTabController.of(context);
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(widget.event.title),
|
|
bottom: const TabBar(
|
|
tabs: [
|
|
Tab(text: '기본 정보'),
|
|
Tab(text: '참가자'),
|
|
Tab(text: '점수'),
|
|
Tab(text: '팀'),
|
|
],
|
|
),
|
|
),
|
|
body: Stack(
|
|
children: [
|
|
TabBarView(
|
|
children: [
|
|
// 기본 정보 탭
|
|
SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 이벤트 유형 및 상태
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 6,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Text(
|
|
widget.event.type ?? '기타',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 6,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.green,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Text(
|
|
widget.event.status ?? '상태 없음',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
// 공개 URL
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.withValues(alpha: 0.05),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: Colors.blue.withValues(alpha: 0.3),
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(
|
|
Icons.link,
|
|
size: 18,
|
|
color: Colors.blue,
|
|
),
|
|
const SizedBox(width: 8),
|
|
const Text(
|
|
'공개 URL',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
final publicUrl =
|
|
'https://bowling.example.com/events/${widget.event.publicHash}';
|
|
Clipboard.setData(
|
|
ClipboardData(text: publicUrl),
|
|
);
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(
|
|
SnackBar(
|
|
content: const Text(
|
|
'공개 URL이 클립보드에 복사되었습니다',
|
|
),
|
|
action: SnackBarAction(
|
|
label: '확인',
|
|
onPressed: () {},
|
|
),
|
|
duration: const Duration(
|
|
seconds: 2,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
icon: const Icon(Icons.copy, size: 16),
|
|
label: const Text('복사'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 0,
|
|
),
|
|
minimumSize: const Size(0, 32),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'https://lanebow.app/events/${widget.event.publicHash}',
|
|
style: const TextStyle(color: Colors.blue),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
// 접근 비밀번호
|
|
if (widget.event.accessPassword != null &&
|
|
widget.event.accessPassword!.isNotEmpty) ...[
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.orange.withValues(alpha: 0.05),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: Colors.orange.withValues(alpha: 0.3),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Icon(
|
|
Icons.lock,
|
|
size: 18,
|
|
color: Colors.orange,
|
|
),
|
|
const SizedBox(width: 8),
|
|
const Text(
|
|
'접근 비밀번호: ',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
widget.event.accessPassword ?? '비밀번호 없음',
|
|
),
|
|
const Spacer(),
|
|
IconButton(
|
|
icon: const Icon(Icons.copy, size: 18),
|
|
onPressed: () {
|
|
Clipboard.setData(
|
|
ClipboardData(
|
|
text: widget.event.accessPassword!,
|
|
),
|
|
);
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(
|
|
SnackBar(
|
|
content: const Text(
|
|
'비밀번호가 클립보드에 복사되었습니다',
|
|
),
|
|
action: SnackBarAction(
|
|
label: '확인',
|
|
onPressed: () {},
|
|
),
|
|
duration: const Duration(seconds: 2),
|
|
),
|
|
);
|
|
},
|
|
tooltip: '비밀번호 복사',
|
|
color: Colors.orange,
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
// 공유 버튼
|
|
const SizedBox(height: 16),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
builder: (context) => Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
children: [
|
|
const Text(
|
|
'이벤트 공유',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () =>
|
|
Navigator.pop(context),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Divider(height: 1),
|
|
ListTile(
|
|
title: const Text('기본 정보만 공유'),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
},
|
|
),
|
|
ListTile(
|
|
title: const Text('참가 링크 포함 공유'),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
},
|
|
),
|
|
ListTile(
|
|
title: const Text('상세 정보 포함 공유'),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
icon: const Icon(Icons.share),
|
|
label: const Text('이벤트 공유'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.green,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// 참가자 탭
|
|
_buildParticipantsTab(),
|
|
|
|
// 점수 탭
|
|
_buildScoresTab(),
|
|
|
|
// 팀 탭
|
|
_buildTeamsTab(),
|
|
],
|
|
),
|
|
if (_isLoading)
|
|
Positioned.fill(
|
|
child: Container(
|
|
key: const Key('loading_overlay'),
|
|
color: Colors.black38,
|
|
child: const Center(child: CircularProgressIndicator()),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
floatingActionButton: AnimatedBuilder(
|
|
animation: tabController,
|
|
builder: (_, __) {
|
|
final fab = _buildFabForTab(context, tabController.index);
|
|
return fab ?? const SizedBox.shrink();
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// 로딩 상태 토글 헬퍼: 외부(top-level)에서 setState 직접 호출 경고를 피하기 위해 제공
|
|
void toggleLoading(bool value) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_isLoading = value;
|
|
});
|
|
}
|
|
}
|
|
|
|
Widget? _buildFabForTab(BuildContext context, int tabIndex) {
|
|
switch (tabIndex) {
|
|
case 1: // 참가자
|
|
return FloatingActionButton(
|
|
tooltip: '참가자 추가',
|
|
onPressed: () async {
|
|
final result = await Navigator.of(context).push<bool>(
|
|
MaterialPageRoute(builder: (_) => const _DummyParticipantForm()),
|
|
);
|
|
if (result == true) {
|
|
final state = context
|
|
.findAncestorStateOfType<_TestEventDetailsScreenState>();
|
|
if (state == null) return;
|
|
final service = Provider.of<EventService>(context, listen: false);
|
|
try {
|
|
state.toggleLoading(true);
|
|
await service.fetchEventParticipants(state.widget.event.id);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text('참가자가 업데이트되었습니다')));
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text('참가자 갱신 실패')));
|
|
}
|
|
} finally {
|
|
state.toggleLoading(false);
|
|
}
|
|
}
|
|
},
|
|
child: const Icon(Icons.person_add),
|
|
);
|
|
case 2: // 점수
|
|
return FloatingActionButton(
|
|
tooltip: '점수 추가',
|
|
onPressed: () async {
|
|
final result = await Navigator.of(context).push<bool>(
|
|
MaterialPageRoute(builder: (_) => const _DummyScoreForm()),
|
|
);
|
|
if (result == true) {
|
|
final state = context
|
|
.findAncestorStateOfType<_TestEventDetailsScreenState>();
|
|
if (state == null) return;
|
|
final service = Provider.of<EventService>(context, listen: false);
|
|
try {
|
|
state.toggleLoading(true);
|
|
await service.fetchEventScores(state.widget.event.id);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text('점수가 업데이트되었습니다')));
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text('점수 갱신 실패')));
|
|
}
|
|
} finally {
|
|
state.toggleLoading(false);
|
|
}
|
|
}
|
|
},
|
|
child: const Icon(Icons.add_chart),
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
class _DummyParticipantForm extends StatelessWidget {
|
|
const _DummyParticipantForm();
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('참가자 폼')),
|
|
body: Center(
|
|
child: ElevatedButton(
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
child: const Text('저장'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DummyScoreForm extends StatelessWidget {
|
|
const _DummyScoreForm();
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('점수 폼')),
|
|
body: Center(
|
|
child: ElevatedButton(
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
child: const Text('저장'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|