회원목록

This commit is contained in:
2025-08-01 16:45:26 +09:00
parent a996def67a
commit ed8c7eacba
191 changed files with 17491 additions and 0 deletions
@@ -0,0 +1,501 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../services/auth_service.dart';
import '../../services/club_service.dart';
import '../../services/member_service.dart';
import '../../services/score_service.dart';
import '../../models/member_model.dart';
import '../../models/score_model.dart';
import 'member_statistics_screen.dart';
import 'score_entry_screen.dart';
class ClubStatisticsScreen extends StatefulWidget {
const ClubStatisticsScreen({super.key});
@override
State<ClubStatisticsScreen> createState() => _ClubStatisticsScreenState();
}
class _ClubStatisticsScreenState extends State<ClubStatisticsScreen> {
bool _isLoading = false;
bool _isInit = false;
Map<String, ScoreStatistics> _clubStatistics = {};
List<Member> _members = [];
Map<String, List<Score>> _recentScores = {};
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_isInit) {
_loadClubStatistics();
_isInit = true;
}
}
Future<void> _loadClubStatistics() async {
setState(() {
_isLoading = true;
});
try {
final authService = Provider.of<AuthService>(context, listen: false);
final clubService = Provider.of<ClubService>(context, listen: false);
final memberService = Provider.of<MemberService>(context, listen: false);
final scoreService = Provider.of<ScoreService>(context, listen: false);
if (clubService.currentClub != null) {
// 점수 서비스 초기화
scoreService.initialize(authService.token!);
// 회원 목록 로드
await memberService.fetchClubMembers();
_members = memberService.members;
// 클럽 통계 로드
_clubStatistics = await scoreService.fetchClubStatistics();
// 각 회원별 최근 점수 로드 (최대 3개)
_recentScores = {};
for (final member in _members) {
try {
final scores = await scoreService.fetchMemberScores(member.id);
// 날짜 기준 정렬 (최신순)
scores.sort((a, b) => b.date.compareTo(a.date));
_recentScores[member.id] = scores.take(3).toList();
} catch (e) {
// 개별 회원 점수 로드 실패 시 빈 배열로 처리
_recentScores[member.id] = [];
}
}
}
} catch (e) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('데이터 로드 중 오류가 발생했습니다: $e')));
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('클럽 통계')),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: RefreshIndicator(
onRefresh: _loadClubStatistics,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 클럽 전체 통계
_buildClubOverallStats(),
const SizedBox(height: 24),
// 회원별 통계
const Text(
'회원별 통계',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// 회원 목록
..._members.map((member) => _buildMemberStatsCard(member)),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const ScoreEntryScreen()),
);
// 화면으로 돌아왔을 때 데이터 새로고침
_loadClubStatistics();
},
backgroundColor: Colors.blue,
child: const Icon(Icons.add, color: Colors.white),
),
);
}
Widget _buildClubOverallStats() {
// 클럽 전체 통계가 없는 경우
if (!_clubStatistics.containsKey('overall')) {
return const Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(child: Text('클럽 전체 통계 데이터가 없습니다')),
),
);
}
final overallStats = _clubStatistics['overall']!;
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
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: [
Expanded(
child: _buildStatItem(
label: '클럽 평균',
value: overallStats.average.toStringAsFixed(1),
icon: Icons.score,
color: Colors.blue,
),
),
Expanded(
child: _buildStatItem(
label: '최고 점수',
value: overallStats.highScore.toString(),
icon: Icons.emoji_events,
color: Colors.amber,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatItem(
label: '최저 점수',
value: overallStats.lowScore.toString(),
icon: Icons.arrow_downward,
color: Colors.red,
),
),
Expanded(
child: _buildStatItem(
label: '총 게임 수',
value: overallStats.gamesPlayed.toString(),
icon: Icons.sports,
color: Colors.green,
),
),
],
),
// 클럽 평균 점수 추이 그래프
const SizedBox(height: 24),
const Text(
'클럽 평균 점수 추이',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
SizedBox(height: 180, child: _buildClubAverageChart()),
// 추가 통계 정보
if (overallStats.additionalStats != null) ...[
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 8),
...overallStats.additionalStats!.entries.map((entry) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(entry.key, style: const TextStyle(fontSize: 14)),
Text(
entry.value.toString(),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
);
}),
],
],
),
),
);
}
Widget _buildStatItem({
required String label,
required String value,
required IconData icon,
required Color color,
}) {
return Column(
children: [
Icon(icon, size: 28, color: color),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
],
);
}
Widget _buildMemberStatsCard(Member member) {
// 회원 통계 정보
final memberStats = _clubStatistics[member.id];
// 회원 최근 점수
final recentScores = _recentScores[member.id] ?? [];
return Card(
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => MemberStatisticsScreen(memberId: member.id),
),
);
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 회원 정보
Row(
children: [
CircleAvatar(
radius: 24,
backgroundColor: Colors.blue.shade100,
backgroundImage: member.profileImage != null
? NetworkImage(member.profileImage!)
: null,
child: member.profileImage == null
? Text(
member.name.isNotEmpty
? member.name[0].toUpperCase()
: '?',
style: const TextStyle(
fontSize: 20,
color: Colors.blue,
fontWeight: FontWeight.bold,
),
)
: null,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
member.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
if (memberStats != null)
Text(
'평균: ${memberStats.average.toStringAsFixed(1)} | 최고: ${memberStats.highScore} | 게임: ${memberStats.gamesPlayed}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
const Icon(Icons.chevron_right),
],
),
// 최근 점수 표시
if (recentScores.isNotEmpty) ...[
const SizedBox(height: 12),
const Divider(),
const SizedBox(height: 8),
const Text(
'최근 점수',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: recentScores.map((score) {
return Column(
children: [
Text(
score.totalScore.toString(),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
Text(
DateFormat('MM/dd').format(score.date),
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
),
),
],
);
}).toList(),
),
],
],
),
),
),
);
}
Widget _buildClubAverageChart() {
// 클럽 통계가 없는 경우
if (!_clubStatistics.containsKey('overall')) {
return const Center(child: Text('통계 데이터가 없습니다'));
}
// 회원별 평균 점수 데이터 추출
final memberAverages = <String, double>{};
// 회원별 통계 정보에서 평균 점수 추출
_clubStatistics.forEach((key, stats) {
if (key != 'overall') {
// 회원 ID에 해당하는 회원 찾기
final member = _members.firstWhere(
(m) => m.id == key,
orElse: () => Member(
id: key,
userId: key,
name: 'Unknown',
email: '',
clubId: '',
isActive: true,
),
);
memberAverages[member.name] = stats.average;
}
});
// 평균 점수 기준으로 정렬 (내림차순)
final sortedEntries = memberAverages.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
// 최대 8명만 표시
final displayEntries = sortedEntries.length > 8
? sortedEntries.sublist(0, 8)
: sortedEntries;
// 그래프 데이터 생성
final barGroups = <BarChartGroupData>[];
for (int i = 0; i < displayEntries.length; i++) {
barGroups.add(
BarChartGroupData(
x: i,
barRods: [
BarChartRodData(
toY: displayEntries[i].value,
color: Colors.blue,
width: 16,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
),
),
],
),
);
}
// 최소/최대 점수 계산 (Y축 범위 설정용)
final minScore = displayEntries.isEmpty
? 0.0
: displayEntries.map((e) => e.value).reduce((a, b) => a < b ? a : b) -
10;
final maxScore = displayEntries.isEmpty
? 300.0
: displayEntries.map((e) => e.value).reduce((a, b) => a > b ? a : b) +
10;
return BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: maxScore,
minY: minScore < 0 ? 0 : minScore,
gridData: const FlGridData(show: true, horizontalInterval: 50),
borderData: FlBorderData(
show: true,
border: Border.all(color: Colors.grey.shade300),
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
final index = value.toInt();
if (index >= 0 && index < displayEntries.length) {
// 회원 이름 표시 (최대 5글자)
final name = displayEntries[index].key;
final shortName = name.length > 5
? '${name.substring(0, 4)}...'
: name;
return Text(shortName, style: const TextStyle(fontSize: 10));
}
return const SizedBox();
},
reservedSize: 30,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 50,
getTitlesWidget: (value, meta) {
return Text(
value.toInt().toString(),
style: const TextStyle(fontSize: 10),
);
},
reservedSize: 40,
),
),
),
barGroups: barGroups,
),
);
}
}
@@ -0,0 +1,662 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../services/auth_service.dart';
import '../../services/club_service.dart';
import '../../services/member_service.dart';
import '../../services/score_service.dart';
import '../../models/member_model.dart';
import '../../models/score_model.dart';
import 'score_entry_screen.dart';
class MemberStatisticsScreen extends StatefulWidget {
final String memberId;
const MemberStatisticsScreen({super.key, required this.memberId});
@override
State<MemberStatisticsScreen> createState() => _MemberStatisticsScreenState();
}
class _MemberStatisticsScreenState extends State<MemberStatisticsScreen>
with SingleTickerProviderStateMixin {
bool _isLoading = false;
bool _isInit = false;
late TabController _tabController;
Member? _member;
List<Score> _scores = [];
ScoreStatistics? _statistics;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_isInit) {
_loadMemberData();
_isInit = true;
}
}
Future<void> _loadMemberData() async {
setState(() {
_isLoading = true;
});
try {
final authService = Provider.of<AuthService>(context, listen: false);
final clubService = Provider.of<ClubService>(context, listen: false);
final memberService = Provider.of<MemberService>(context, listen: false);
final scoreService = Provider.of<ScoreService>(context, listen: false);
if (clubService.currentClub != null) {
// 회원 정보 로드
_member = await memberService.fetchMemberById(widget.memberId);
// 점수 서비스 초기화
scoreService.initialize(authService.token!);
// 회원 점수 및 통계 로드
await Future.wait([
scoreService.fetchMemberScores(widget.memberId).then((scores) {
_scores = scores;
}),
scoreService.fetchMemberStatistics(widget.memberId).then((stats) {
_statistics = stats;
}),
]);
}
} catch (e) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('데이터 로드 중 오류가 발생했습니다: $e')));
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_member?.name ?? '회원 통계'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: '통계'),
Tab(text: '점수 기록'),
],
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: TabBarView(
controller: _tabController,
children: [_buildStatisticsTab(), _buildScoresTab()],
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ScoreEntryScreen(memberId: widget.memberId),
),
);
// 화면으로 돌아왔을 때 데이터 새로고침
_loadMemberData();
},
backgroundColor: Colors.blue,
child: const Icon(Icons.add, color: Colors.white),
),
);
}
Widget _buildStatisticsTab() {
if (_statistics == null) {
return const Center(child: Text('통계 데이터가 없습니다'));
}
return RefreshIndicator(
onRefresh: _loadMemberData,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 회원 정보 카드
if (_member != null) _buildMemberInfoCard(),
const SizedBox(height: 24),
// 주요 통계 카드
_buildMainStatsCard(),
const SizedBox(height: 24),
// 추가 통계 정보
if (_statistics!.additionalStats != null)
_buildAdditionalStatsCard(),
// 통계 그래프
const SizedBox(height: 24),
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: 16),
SizedBox(height: 200, child: _buildScoreChart()),
],
),
),
),
],
),
),
);
}
Widget _buildMemberInfoCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
CircleAvatar(
radius: 30,
backgroundColor: Colors.blue.shade100,
backgroundImage: _member!.profileImage != null
? NetworkImage(_member!.profileImage!)
: null,
child: _member!.profileImage == null
? Text(
_member!.name.isNotEmpty
? _member!.name[0].toUpperCase()
: '?',
style: const TextStyle(
fontSize: 24,
color: Colors.blue,
fontWeight: FontWeight.bold,
),
)
: null,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_member!.name,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
_member!.email,
style: TextStyle(color: Colors.grey[600]),
),
if (_member!.phone != null)
Text(
_member!.phone!,
style: TextStyle(color: Colors.grey[600]),
),
],
),
),
],
),
),
);
}
Widget _buildMainStatsCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
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: [
Expanded(
child: _buildStatItem(
label: '평균 점수',
value: _statistics!.average.toStringAsFixed(1),
icon: Icons.score,
color: Colors.blue,
),
),
Expanded(
child: _buildStatItem(
label: '최고 점수',
value: _statistics!.highScore.toString(),
icon: Icons.emoji_events,
color: Colors.amber,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatItem(
label: '최저 점수',
value: _statistics!.lowScore.toString(),
icon: Icons.arrow_downward,
color: Colors.red,
),
),
Expanded(
child: _buildStatItem(
label: '게임 수',
value: _statistics!.gamesPlayed.toString(),
icon: Icons.sports,
color: Colors.green,
),
),
],
),
],
),
),
);
}
Widget _buildStatItem({
required String label,
required String value,
required IconData icon,
required Color color,
}) {
return Column(
children: [
Icon(icon, size: 32, color: color),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(label, style: TextStyle(fontSize: 14, color: Colors.grey[600])),
],
);
}
Widget _buildAdditionalStatsCard() {
final additionalStats = _statistics!.additionalStats!;
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
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),
...additionalStats.entries.map((entry) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(entry.key, style: const TextStyle(fontSize: 16)),
Text(
entry.value.toString(),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
);
}),
],
),
),
);
}
Widget _buildScoresTab() {
if (_scores.isEmpty) {
return const Center(child: Text('기록된 점수가 없습니다'));
}
// 날짜 기준으로 정렬 (최신순)
final sortedScores = [..._scores];
sortedScores.sort((a, b) => b.date.compareTo(a.date));
return RefreshIndicator(
onRefresh: _loadMemberData,
child: ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: sortedScores.length,
itemBuilder: (context, index) {
final score = sortedScores[index];
return _buildScoreCard(score);
},
),
);
}
Widget _buildScoreCard(Score score) {
final dateFormat = DateFormat('yyyy년 MM월 dd일');
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ExpansionTile(
title: Row(
children: [
Text(
score.totalScore.toString(),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
dateFormat.format(score.date),
style: const TextStyle(fontSize: 14),
),
if (score.notes != null && score.notes!.isNotEmpty)
Text(
score.notes!,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'프레임 점수',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
_buildFrameScores(score.frames),
if (score.notes != null && score.notes!.isNotEmpty) ...[
const SizedBox(height: 16),
const Text(
'메모',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(score.notes!),
],
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
icon: const Icon(Icons.edit, size: 16),
label: const Text('수정'),
onPressed: () {
// 점수 수정 기능 (추후 구현)
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('점수 수정 기능은 아직 개발 중입니다')),
);
},
),
const SizedBox(width: 8),
TextButton.icon(
icon: const Icon(
Icons.delete,
size: 16,
color: Colors.red,
),
label: const Text(
'삭제',
style: TextStyle(color: Colors.red),
),
onPressed: () {
_showDeleteConfirmationDialog(score);
},
),
],
),
],
),
),
],
),
);
}
Widget _buildFrameScores(List<int> frames) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: List.generate(10, (index) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
),
child: Column(
children: [
Text(
'${index + 1}',
style: TextStyle(fontSize: 10, color: Colors.grey[600]),
),
const SizedBox(height: 4),
Text(
index < frames.length ? frames[index].toString() : '-',
style: const TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
],
),
),
);
}),
),
);
}
Widget _buildScoreChart() {
if (_scores.isEmpty) {
return const Center(child: Text('점수 데이터가 없습니다'));
}
// 날짜 기준으로 정렬 (오래된 순)
final sortedScores = [..._scores];
sortedScores.sort((a, b) => a.date.compareTo(b.date));
// 최대 10개의 최근 점수만 표시
final displayScores = sortedScores.length > 10
? sortedScores.sublist(sortedScores.length - 10)
: sortedScores;
// 최소/최대 점수 계산 (Y축 범위 설정용)
final minScore =
displayScores.map((s) => s.totalScore).reduce((a, b) => a < b ? a : b) -
10;
final maxScore =
displayScores.map((s) => s.totalScore).reduce((a, b) => a > b ? a : b) +
10;
// 그래프 데이터 포인트 생성
final spots = <FlSpot>[];
for (int i = 0; i < displayScores.length; i++) {
spots.add(FlSpot(i.toDouble(), displayScores[i].totalScore.toDouble()));
}
return LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: true,
horizontalInterval: 20,
verticalInterval: 1,
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: 1,
getTitlesWidget: (value, meta) {
final index = value.toInt();
if (index >= 0 && index < displayScores.length) {
return Text(
DateFormat('MM/dd').format(displayScores[index].date),
style: const TextStyle(fontSize: 10),
);
}
return const SizedBox();
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 50,
getTitlesWidget: (value, meta) {
return Text(
value.toInt().toString(),
style: const TextStyle(fontSize: 10),
);
},
reservedSize: 40,
),
),
),
borderData: FlBorderData(
show: true,
border: Border.all(color: Colors.grey.shade300),
),
minX: 0,
maxX: displayScores.length - 1.0,
minY: minScore.toDouble(),
maxY: maxScore.toDouble(),
lineBarsData: [
LineChartBarData(
spots: spots,
isCurved: false,
color: Colors.blue,
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: true),
belowBarData: BarAreaData(
show: true,
color: Colors.blue.withOpacity(0.2),
),
),
],
),
);
}
void _showDeleteConfirmationDialog(Score score) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('점수 삭제'),
content: const Text('이 점수 기록을 정말 삭제하시겠습니까?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('취소'),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
try {
final scoreService = Provider.of<ScoreService>(
context,
listen: false,
);
await scoreService.deleteScore(score.id);
// 데이터 새로고침
_loadMemberData();
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('점수가 삭제되었습니다')));
}
} catch (e) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('점수 삭제에 실패했습니다: $e')));
}
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('삭제'),
),
],
),
);
}
}
@@ -0,0 +1,378 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../../services/club_service.dart';
import '../../services/member_service.dart';
import '../../services/event_service.dart';
import '../../services/score_service.dart';
class ScoreEntryScreen extends StatefulWidget {
final String? memberId;
final String? eventId;
const ScoreEntryScreen({
super.key,
this.memberId,
this.eventId,
});
@override
State<ScoreEntryScreen> createState() => _ScoreEntryScreenState();
}
class _ScoreEntryScreenState extends State<ScoreEntryScreen> {
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
// 점수 입력 관련 상태
final List<TextEditingController> _frameControllers = List.generate(
10,
(_) => TextEditingController(),
);
final TextEditingController _notesController = TextEditingController();
// 선택된 회원 및 이벤트
String? _selectedMemberId;
String? _selectedEventId;
DateTime _selectedDate = DateTime.now();
@override
void initState() {
super.initState();
_selectedMemberId = widget.memberId;
_selectedEventId = widget.eventId;
}
@override
void dispose() {
for (var controller in _frameControllers) {
controller.dispose();
}
_notesController.dispose();
super.dispose();
}
// 총점 계산
int _calculateTotalScore() {
int total = 0;
for (var controller in _frameControllers) {
final value = int.tryParse(controller.text) ?? 0;
total += value;
}
return total;
}
// 점수 저장
Future<void> _saveScore() async {
if (!_formKey.currentState!.validate()) {
return;
}
if (_selectedMemberId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('회원을 선택해주세요')),
);
return;
}
setState(() {
_isLoading = true;
});
try {
final scoreService = Provider.of<ScoreService>(context, listen: false);
final clubService = Provider.of<ClubService>(context, listen: false);
if (clubService.currentClub != null) {
// 프레임 점수 배열 생성
final frames = _frameControllers
.map((controller) => int.tryParse(controller.text) ?? 0)
.toList();
// 총점 계산
final totalScore = _calculateTotalScore();
// 점수 데이터 생성
final scoreData = {
'memberId': _selectedMemberId,
'eventId': _selectedEventId,
'clubId': clubService.currentClub!.id,
'frames': frames,
'totalScore': totalScore,
'date': _selectedDate.toIso8601String(),
'notes': _notesController.text.isNotEmpty ? _notesController.text : null,
};
// 점수 저장
await scoreService.addScore(scoreData);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('점수가 저장되었습니다')),
);
Navigator.of(context).pop();
}
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('점수 저장에 실패했습니다: $e')),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('점수 입력'),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: Form(
key: _formKey,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 회원 선택
_buildMemberSelector(),
const SizedBox(height: 16),
// 이벤트 선택
_buildEventSelector(),
const SizedBox(height: 16),
// 날짜 선택
_buildDateSelector(),
const SizedBox(height: 24),
// 프레임 점수 입력
_buildFrameInputs(),
const SizedBox(height: 24),
// 총점 표시
_buildTotalScore(),
const SizedBox(height: 16),
// 메모 입력
TextFormField(
controller: _notesController,
decoration: const InputDecoration(
labelText: '메모',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 24),
// 저장 버튼
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _saveScore,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
child: const Text('점수 저장'),
),
),
],
),
),
),
);
}
Widget _buildMemberSelector() {
return Consumer<MemberService>(
builder: (context, memberService, _) {
final members = memberService.members;
return DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: '회원 선택',
border: OutlineInputBorder(),
),
value: _selectedMemberId,
items: members.map((member) {
return DropdownMenuItem<String>(
value: member.id,
child: Text(member.name),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedMemberId = value;
});
},
validator: (value) {
if (value == null || value.isEmpty) {
return '회원을 선택해주세요';
}
return null;
},
);
},
);
}
Widget _buildEventSelector() {
return Consumer<EventService>(
builder: (context, eventService, _) {
final events = eventService.events;
return DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: '이벤트 선택 (선택사항)',
border: OutlineInputBorder(),
),
value: _selectedEventId,
items: [
const DropdownMenuItem<String>(
value: null,
child: Text('이벤트 없음'),
),
...events.map((event) {
return DropdownMenuItem<String>(
value: event.id,
child: Text(event.title),
);
}),
],
onChanged: (value) {
setState(() {
_selectedEventId = value;
});
},
);
},
);
}
Widget _buildDateSelector() {
return InkWell(
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime.now().subtract(const Duration(days: 365)),
lastDate: DateTime.now(),
);
if (pickedDate != null) {
setState(() {
_selectedDate = pickedDate;
});
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: '날짜',
border: OutlineInputBorder(),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(DateFormat('yyyy년 MM월 dd일').format(_selectedDate)),
const Icon(Icons.calendar_today),
],
),
),
);
}
Widget _buildFrameInputs() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'프레임 점수',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
childAspectRatio: 1.5,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: 10,
itemBuilder: (context, index) {
return TextFormField(
controller: _frameControllers[index],
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
decoration: InputDecoration(
labelText: '${index + 1}프레임',
border: const OutlineInputBorder(),
),
onChanged: (_) {
setState(() {});
},
validator: (value) {
if (value == null || value.isEmpty) {
return '입력';
}
final score = int.tryParse(value);
if (score == null) {
return '숫자';
}
if (score < 0 || score > 30) {
return '0-30';
}
return null;
},
);
},
),
],
);
}
Widget _buildTotalScore() {
final totalScore = _calculateTotalScore();
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'총점',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'$totalScore',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
);
}
}