회원목록
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user