tdd 진행
This commit is contained in:
@@ -0,0 +1,511 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'dart:math';
|
||||
|
||||
import '../../models/participant_model.dart';
|
||||
import '../../models/team_model.dart';
|
||||
import '../../services/event_service.dart';
|
||||
import '../../widgets/loading_indicator.dart';
|
||||
|
||||
class TeamGeneratorScreen extends StatefulWidget {
|
||||
final String eventId;
|
||||
final List<Participant> participants;
|
||||
final List<Team>? existingTeams;
|
||||
|
||||
const TeamGeneratorScreen({
|
||||
Key? key,
|
||||
required this.eventId,
|
||||
required this.participants,
|
||||
this.existingTeams,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TeamGeneratorScreen> createState() => _TeamGeneratorScreenState();
|
||||
}
|
||||
|
||||
class _TeamGeneratorScreenState extends State<TeamGeneratorScreen> {
|
||||
bool _isLoading = false;
|
||||
int _teamCount = 2;
|
||||
bool _balanceTeams = true;
|
||||
List<Team> _generatedTeams = [];
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// 참가자 선택 상태 관리
|
||||
final Map<String, bool> _selectedParticipants = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// 모든 참가자 기본 선택
|
||||
for (var participant in widget.participants) {
|
||||
if (participant.status == '참석함' || participant.status == '확인됨') {
|
||||
_selectedParticipants[participant.memberId ?? ''] = true;
|
||||
} else {
|
||||
_selectedParticipants[participant.memberId ?? ''] = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 기존 팀이 있으면 팀 수 설정
|
||||
if (widget.existingTeams != null && widget.existingTeams!.isNotEmpty) {
|
||||
_teamCount = widget.existingTeams!.length;
|
||||
_generatedTeams = List.from(widget.existingTeams!);
|
||||
}
|
||||
}
|
||||
|
||||
// 팀 생성
|
||||
void _generateTeams() {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 선택된 참가자 목록 생성
|
||||
final selectedParticipants = widget.participants
|
||||
.where((p) => _selectedParticipants[p.memberId] == true)
|
||||
.toList();
|
||||
|
||||
if (selectedParticipants.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('최소 한 명 이상의 참가자를 선택해주세요')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_teamCount <= 0 || _teamCount > selectedParticipants.length) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('유효한 팀 수를 입력해주세요')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// 참가자 섞기
|
||||
final shuffledParticipants = List.from(selectedParticipants)..shuffle();
|
||||
|
||||
// 팀 생성
|
||||
final teams = List.generate(_teamCount, (index) => <Participant>[]);
|
||||
|
||||
if (_balanceTeams) {
|
||||
// 균등하게 팀 배분
|
||||
for (int i = 0; i < shuffledParticipants.length; i++) {
|
||||
teams[i % _teamCount].add(shuffledParticipants[i]);
|
||||
}
|
||||
} else {
|
||||
// 랜덤 배분
|
||||
final random = Random();
|
||||
for (var participant in shuffledParticipants) {
|
||||
teams[random.nextInt(_teamCount)].add(participant);
|
||||
}
|
||||
}
|
||||
|
||||
// Team 객체로 변환
|
||||
_generatedTeams = List.generate(
|
||||
_teamCount,
|
||||
(index) => Team(
|
||||
id: widget.existingTeams != null && index < widget.existingTeams!.length
|
||||
? widget.existingTeams![index].id
|
||||
: '',
|
||||
eventId: widget.eventId,
|
||||
name: '${index + 1}',
|
||||
memberIds: teams[index].map((p) => p.memberId ?? '').toList(),
|
||||
description: '자동 생성된 팀',
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('팀 생성 중 오류가 발생했습니다: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 팀 저장
|
||||
Future<void> _saveTeams() async {
|
||||
if (_generatedTeams.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('먼저 팀을 생성해주세요')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final eventService = Provider.of<EventService>(context, listen: false);
|
||||
|
||||
// 팀 데이터 준비
|
||||
final teamsData = _generatedTeams.map((team) => {
|
||||
'eventId': widget.eventId,
|
||||
'name': team.name,
|
||||
'memberIds': team.memberIds,
|
||||
'description': team.description,
|
||||
}).toList();
|
||||
|
||||
// 팀 저장
|
||||
await eventService.saveTeams(widget.eventId, teamsData);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('팀이 저장되었습니다')),
|
||||
);
|
||||
Navigator.of(context).pop(true); // 성공 결과와 함께 이전 화면으로 돌아가기
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('팀 저장에 실패했습니다: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('팀 생성'),
|
||||
actions: [
|
||||
if (_generatedTeams.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.save),
|
||||
onPressed: _saveTeams,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _isLoading
|
||||
? const LoadingIndicator()
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 팀 생성 설정 섹션
|
||||
_buildSectionTitle('팀 생성 설정'),
|
||||
|
||||
// 팀 수 설정
|
||||
TextFormField(
|
||||
initialValue: _teamCount.toString(),
|
||||
decoration: const InputDecoration(
|
||||
labelText: '팀 수',
|
||||
hintText: '생성할 팀 수를 입력하세요',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '팀 수를 입력해주세요';
|
||||
}
|
||||
final teamCount = int.tryParse(value);
|
||||
if (teamCount == null || teamCount <= 0) {
|
||||
return '유효한 팀 수를 입력해주세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_teamCount = int.tryParse(value) ?? 2;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 균등 배분 설정
|
||||
SwitchListTile(
|
||||
title: const Text('균등하게 팀 배분'),
|
||||
subtitle: const Text('참가자를 팀에 균등하게 배분합니다'),
|
||||
value: _balanceTeams,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_balanceTeams = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 팀 생성 버튼
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _generateTeams,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: const Text(
|
||||
'팀 생성하기',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 참가자 선택 섹션
|
||||
_buildSectionTitle('참가자 선택'),
|
||||
|
||||
// 전체 선택/해제 버튼
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
for (var key in _selectedParticipants.keys) {
|
||||
_selectedParticipants[key] = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
child: const Text('전체 선택'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
for (var key in _selectedParticipants.keys) {
|
||||
_selectedParticipants[key] = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
child: const Text('전체 해제'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 참가자 목록
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: widget.participants.length,
|
||||
itemBuilder: (context, index) {
|
||||
final participant = widget.participants[index];
|
||||
return CheckboxListTile(
|
||||
title: Text(participant.name ?? '이름 없음'),
|
||||
subtitle: Text(participant.status ?? '상태 없음'),
|
||||
value: _selectedParticipants[participant.memberId] ?? false,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedParticipants[participant.memberId ?? ''] = value ?? false;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
if (_generatedTeams.isNotEmpty) ...[
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 생성된 팀 섹션
|
||||
_buildSectionTitle('생성된 팀'),
|
||||
|
||||
// 팀 목록
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _generatedTeams.length,
|
||||
itemBuilder: (context, teamIndex) {
|
||||
final team = _generatedTeams[teamIndex];
|
||||
|
||||
// 팀원 목록 생성
|
||||
final teamMembers = widget.participants
|
||||
.where((p) => team.memberIds.contains(p.memberId))
|
||||
.toList();
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(
|
||||
'팀 ${team.name}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: Text('${teamMembers.length}명'),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
// 팀 이름 수정 다이얼로그
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
String newName = team.name;
|
||||
return AlertDialog(
|
||||
title: const Text('팀 이름 수정'),
|
||||
content: TextField(
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '팀 이름',
|
||||
),
|
||||
onChanged: (value) {
|
||||
newName = value;
|
||||
},
|
||||
controller: TextEditingController(text: team.name),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_generatedTeams[teamIndex] = Team(
|
||||
id: team.id,
|
||||
eventId: team.eventId,
|
||||
name: newName,
|
||||
memberIds: team.memberIds,
|
||||
description: team.description,
|
||||
);
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('저장'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
...teamMembers.map((member) => ListTile(
|
||||
leading: const Icon(Icons.person),
|
||||
title: Text(member.name ?? '이름 없음'),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.swap_horiz),
|
||||
onPressed: () {
|
||||
// 팀원 이동 다이얼로그
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
int targetTeamIndex = teamIndex;
|
||||
return AlertDialog(
|
||||
title: const Text('팀원 이동'),
|
||||
content: DropdownButton<int>(
|
||||
value: targetTeamIndex,
|
||||
items: List.generate(
|
||||
_generatedTeams.length,
|
||||
(index) => DropdownMenuItem(
|
||||
value: index,
|
||||
child: Text('팀 ${_generatedTeams[index].name}'),
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
targetTeamIndex = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (targetTeamIndex != teamIndex) {
|
||||
setState(() {
|
||||
// 현재 팀에서 제거
|
||||
_generatedTeams[teamIndex] = Team(
|
||||
id: team.id,
|
||||
eventId: team.eventId,
|
||||
name: team.name,
|
||||
memberIds: team.memberIds
|
||||
.where((id) => id != member.memberId)
|
||||
.toList(),
|
||||
description: team.description,
|
||||
);
|
||||
|
||||
// 대상 팀에 추가
|
||||
final targetTeam = _generatedTeams[targetTeamIndex];
|
||||
_generatedTeams[targetTeamIndex] = Team(
|
||||
id: targetTeam.id,
|
||||
eventId: targetTeam.eventId,
|
||||
name: targetTeam.name,
|
||||
memberIds: [
|
||||
...targetTeam.memberIds,
|
||||
member.memberId ?? '',
|
||||
],
|
||||
description: targetTeam.description,
|
||||
);
|
||||
});
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('이동'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// 팀 저장 버튼
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _saveTeams,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
child: const Text(
|
||||
'팀 저장하기',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 섹션 제목 위젯
|
||||
Widget _buildSectionTitle(String title) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user