TDD 작성
This commit is contained in:
@@ -0,0 +1,640 @@
|
||||
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';
|
||||
|
||||
/// 테스트용 이벤트 상세 화면 위젯
|
||||
/// 실제 EventDetailsScreen의 핵심 기능만 구현하여 테스트에 사용
|
||||
class TestEventDetailsScreen extends StatefulWidget {
|
||||
final Event event;
|
||||
final List<Score> scores;
|
||||
final List<Participant> participants;
|
||||
|
||||
const TestEventDetailsScreen({
|
||||
Key? key,
|
||||
required this.event,
|
||||
this.scores = const [],
|
||||
this.participants = const [],
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TestEventDetailsScreen> createState() => _TestEventDetailsScreenState();
|
||||
}
|
||||
|
||||
class _TestEventDetailsScreenState extends State<TestEventDetailsScreen> {
|
||||
bool _includeHandicap = false;
|
||||
List<Member> _members = [];
|
||||
|
||||
// 점수에 따른 색상 반환
|
||||
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();
|
||||
}
|
||||
|
||||
// 점수 탭 위젯 구현
|
||||
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);
|
||||
|
||||
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;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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(),
|
||||
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() {
|
||||
if (widget.participants.isEmpty) {
|
||||
return const Center(child: Text('등록된 참가자가 없습니다'));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: widget.participants.length,
|
||||
itemBuilder: (context, index) {
|
||||
final participant = widget.participants[index];
|
||||
return ListTile(
|
||||
title: Text(participant.name ?? '알 수 없음'),
|
||||
subtitle: Text('회원 ID: ${participant.memberId}'),
|
||||
trailing: 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: [
|
||||
// 팀 생성 옵션 카드
|
||||
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: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.event.title),
|
||||
bottom: const TabBar(
|
||||
tabs: [
|
||||
Tab(text: '기본 정보'),
|
||||
Tab(text: '참가자'),
|
||||
Tab(text: '점수'),
|
||||
Tab(text: '팀'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: 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.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.withOpacity(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.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange.withOpacity(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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user