이벤트 개발완료
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../models/event_model.dart';
|
||||
import '../../../utils/url_utils.dart';
|
||||
import '../../../utils/enum_mappings.dart';
|
||||
import '../../../theme/badges.dart';
|
||||
|
||||
class BasicInfoTab extends StatelessWidget {
|
||||
final Event event;
|
||||
final int participantsCount;
|
||||
final VoidCallback onShareEvent;
|
||||
final VoidCallback onSetEventReminders;
|
||||
|
||||
const BasicInfoTab({
|
||||
super.key,
|
||||
required this.event,
|
||||
required this.participantsCount,
|
||||
required this.onShareEvent,
|
||||
required this.onSetEventReminders,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
_badge(
|
||||
text: getEventTypeLabel(event.type),
|
||||
color: _getTypeColor(getEventTypeLabel(event.type)),
|
||||
icon: _getTypeIcon(getEventTypeLabel(event.type)),
|
||||
),
|
||||
if (event.status != null)
|
||||
BadgeStyles.eventStatus(
|
||||
getEventStatusLabel(event.status),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
onPressed: onShareEvent,
|
||||
tooltip: '이벤트 공유',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (event.description != null && event.description!.isNotEmpty) ...[
|
||||
const Text('설명', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(event.description ?? ''),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
const Text('이벤트 정보', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
_infoRow(Icons.calendar_today, '시작: ${DateFormat('yyyy-MM-dd HH:mm').format(event.startDate)}'),
|
||||
if (event.endDate != null)
|
||||
_infoRow(Icons.calendar_today, '종료: ${DateFormat('yyyy-MM-dd HH:mm').format(event.endDate!)}'),
|
||||
if (event.location != null && event.location!.isNotEmpty)
|
||||
_infoRow(Icons.location_on, '장소: ${event.location}'),
|
||||
if (event.maxParticipants != null)
|
||||
_infoRow(Icons.people, '최대 참가자: ${event.maxParticipants}명'),
|
||||
if (event.gameCount != null)
|
||||
_infoRow(Icons.sports, '게임 수: ${event.gameCount}게임'),
|
||||
if (event.participantFee != null)
|
||||
_infoRow(
|
||||
Icons.attach_money,
|
||||
event.participantFee == 0
|
||||
? '참가비: 무료'
|
||||
: '참가비: ${NumberFormat.currency(locale: 'ko_KR', symbol: '₩', decimalDigits: 0).format(event.participantFee)}',
|
||||
),
|
||||
if (event.registrationDeadline != null)
|
||||
_infoRow(Icons.timer, '신청마감: ${DateFormat('yyyy-MM-dd HH:mm').format(event.registrationDeadline!)}'),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
const Text('공개 접근', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
if (event.publicHash != null && event.publicHash!.isNotEmpty) ...[
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _infoRow(
|
||||
Icons.link,
|
||||
UrlUtils.getEventPublicUrl(event.publicHash!),
|
||||
isSelectable: true,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy),
|
||||
onPressed: () {
|
||||
final publicUrl = UrlUtils.getEventPublicUrl(event.publicHash!);
|
||||
UrlUtils.copyUrlToClipboard(context, publicUrl);
|
||||
},
|
||||
tooltip: 'URL 복사',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (event.accessPassword != null && event.accessPassword!.isNotEmpty)
|
||||
_infoRow(Icons.lock, '접근 비밀번호: ${event.accessPassword}'),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const Text('참가자 정보', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
_infoRow(Icons.people, '현재 참가자: $participantsCount명'),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onSetEventReminders,
|
||||
icon: const Icon(Icons.notifications_active),
|
||||
label: const Text('알림 설정'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.green,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoRow(IconData icon, String text, {bool isSelectable = false}) {
|
||||
final textWidget = isSelectable
|
||||
? SelectableText(
|
||||
text,
|
||||
maxLines: 3,
|
||||
onTap: () {},
|
||||
)
|
||||
: Text(text);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, size: 20, color: Colors.blueGrey),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: textWidget),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _badge({required String text, required Color color, required IconData icon}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(fontWeight: FontWeight.w600, color: color),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getTypeColor(String type) {
|
||||
switch (type) {
|
||||
case '정기전':
|
||||
return Colors.indigo;
|
||||
case '연습':
|
||||
return Colors.teal;
|
||||
case '대회':
|
||||
return Colors.deepOrange;
|
||||
default:
|
||||
return Colors.blueGrey;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getTypeIcon(String type) {
|
||||
switch (type) {
|
||||
case '정기전':
|
||||
return Icons.event;
|
||||
case '연습':
|
||||
return Icons.fitness_center;
|
||||
case '대회':
|
||||
return Icons.emoji_events;
|
||||
default:
|
||||
return Icons.info;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../models/participant_model.dart';
|
||||
import '../../../models/score_model.dart';
|
||||
import '../../../utils/enum_mappings.dart';
|
||||
import '../../../theme/badges.dart';
|
||||
|
||||
class ParticipantsTab extends StatefulWidget {
|
||||
final List<Participant> participants;
|
||||
final List<Score> scores;
|
||||
final void Function(Participant participant) onEditParticipant;
|
||||
|
||||
const ParticipantsTab({
|
||||
super.key,
|
||||
required this.participants,
|
||||
required this.scores,
|
||||
required this.onEditParticipant,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ParticipantsTab> createState() => _ParticipantsTabState();
|
||||
}
|
||||
|
||||
class _ParticipantsTabState extends State<ParticipantsTab> {
|
||||
String? _filterMemberType; // null: 전체
|
||||
String? _filterStatus; // null: 전체
|
||||
bool? _filterPaid; // null: 전체
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _buildFilterSummary() {
|
||||
final parts = <String>[];
|
||||
if (_filterStatus != null && _filterStatus!.isNotEmpty)
|
||||
parts.add('상태: ${_filterStatus!}');
|
||||
if (_filterMemberType != null && _filterMemberType!.isNotEmpty)
|
||||
parts.add('유형: ${_filterMemberType!}');
|
||||
if (_filterPaid != null) parts.add('결제: ${_filterPaid! ? '완료' : '미결제'}');
|
||||
final q = _searchController.text.trim();
|
||||
if (q.isNotEmpty) parts.add("검색: '$q'");
|
||||
return parts.isEmpty ? '필터 없음' : parts.join(' · ');
|
||||
}
|
||||
|
||||
bool _hasActiveFilters() {
|
||||
return (_filterStatus != null && _filterStatus!.isNotEmpty) ||
|
||||
(_filterMemberType != null && _filterMemberType!.isNotEmpty) ||
|
||||
(_filterPaid != null) ||
|
||||
(_searchController.text.trim().isNotEmpty);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Set<String> memberTypeOptions = widget.participants
|
||||
.where((p) => (p.memberType ?? '').isNotEmpty)
|
||||
.map((p) => p.memberType!)
|
||||
.toSet();
|
||||
final Set<String> statusOptions = widget.participants
|
||||
.where((p) => (p.status ?? '').isNotEmpty)
|
||||
.map((p) => p.status!)
|
||||
.toSet();
|
||||
|
||||
final List<Participant> filtered = widget.participants.where((p) {
|
||||
final matchesType =
|
||||
_filterMemberType == null ||
|
||||
(_filterMemberType!.isEmpty) ||
|
||||
p.memberType == _filterMemberType;
|
||||
final matchesStatus =
|
||||
_filterStatus == null ||
|
||||
(_filterStatus!.isEmpty) ||
|
||||
p.status == _filterStatus;
|
||||
final matchesPaid = _filterPaid == null || p.isPaid == _filterPaid;
|
||||
final q = _searchController.text.trim().toLowerCase();
|
||||
final matchesQuery =
|
||||
q.isEmpty || (p.name ?? '').toLowerCase().contains(q);
|
||||
return matchesType && matchesStatus && matchesPaid && matchesQuery;
|
||||
}).toList();
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: filtered.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ExpansionTile(
|
||||
initiallyExpanded: false,
|
||||
leading: Icon(
|
||||
Icons.filter_list,
|
||||
color: _hasActiveFilters()
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
title: Text(
|
||||
'필터/검색',
|
||||
style: TextStyle(
|
||||
color: _hasActiveFilters()
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
fontWeight: _hasActiveFilters()
|
||||
? FontWeight.w600
|
||||
: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
_buildFilterSummary(),
|
||||
style: TextStyle(
|
||||
color: _hasActiveFilters()
|
||||
? Colors.black87
|
||||
: Colors.black54,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
childrenPadding: const EdgeInsets.only(
|
||||
bottom: 8,
|
||||
left: 8,
|
||||
right: 8,
|
||||
),
|
||||
children: [
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: '이름 검색',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
setState(() => _searchController.clear());
|
||||
},
|
||||
tooltip: '검색 지우기',
|
||||
)
|
||||
: null,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
FilterChip(
|
||||
selected: _filterStatus == null,
|
||||
label: const Text('상태: 전체'),
|
||||
onSelected: (_) =>
|
||||
setState(() => _filterStatus = null),
|
||||
),
|
||||
...statusOptions.map(
|
||||
(s) => FilterChip(
|
||||
selected: _filterStatus == s,
|
||||
label: Text('상태: $s'),
|
||||
onSelected: (_) =>
|
||||
setState(() => _filterStatus = s),
|
||||
),
|
||||
),
|
||||
FilterChip(
|
||||
selected: _filterMemberType == null,
|
||||
label: const Text('유형: 전체'),
|
||||
onSelected: (_) =>
|
||||
setState(() => _filterMemberType = null),
|
||||
),
|
||||
...memberTypeOptions.map(
|
||||
(t) => FilterChip(
|
||||
selected: _filterMemberType == t,
|
||||
label: Text('유형: $t'),
|
||||
onSelected: (_) =>
|
||||
setState(() => _filterMemberType = t),
|
||||
),
|
||||
),
|
||||
FilterChip(
|
||||
selected: _filterPaid == null,
|
||||
label: const Text('결제: 전체'),
|
||||
onSelected: (_) => setState(() => _filterPaid = null),
|
||||
),
|
||||
FilterChip(
|
||||
selected: _filterPaid == true,
|
||||
label: const Text('결제: 완료'),
|
||||
onSelected: (_) => setState(() => _filterPaid = true),
|
||||
),
|
||||
FilterChip(
|
||||
selected: _filterPaid == false,
|
||||
label: const Text('결제: 미결제'),
|
||||
onSelected: (_) =>
|
||||
setState(() => _filterPaid = false),
|
||||
),
|
||||
ActionChip(
|
||||
label: const Text('필터 초기화'),
|
||||
avatar: const Icon(Icons.refresh, size: 16),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_filterMemberType = null;
|
||||
_filterStatus = null;
|
||||
_filterPaid = null;
|
||||
_searchController.clear();
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final participant = filtered[index - 1];
|
||||
// handicap: participant.handicap 없으면 가장 최근 score.handicap 사용 (부호 무관)
|
||||
int? handicap = participant.handicap;
|
||||
if (handicap == null) {
|
||||
Score? latest = widget.scores
|
||||
.where((s) => s.memberId == participant.memberId && s.handicap != null)
|
||||
.fold<Score?>(
|
||||
null,
|
||||
(prev, s) => prev == null || s.date.isAfter(prev.date) ? s : prev,
|
||||
);
|
||||
handicap = latest?.handicap;
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: ListTile(
|
||||
leading: Builder(
|
||||
builder: (_) {
|
||||
final gender = (participant.gender ?? '').toLowerCase();
|
||||
final bool isFemale =
|
||||
gender == 'female' ||
|
||||
gender == 'f' ||
|
||||
gender == '여' ||
|
||||
gender == '여성';
|
||||
final icon = isFemale ? Icons.female : Icons.male;
|
||||
final color = isFemale ? Colors.pink : Colors.blue;
|
||||
return CircleAvatar(
|
||||
backgroundColor: color.withValues(alpha: 0.1),
|
||||
child: Icon(icon, size: 18, color: color),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
participant.name ?? '알 수 없음',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
// Guest small badge
|
||||
if ((participant.memberType ?? '').toLowerCase() == 'guest')
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 2),
|
||||
child: BadgeStyles.guestSmall(size: 16),
|
||||
),
|
||||
if (handicap != null && handicap != 0)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.deepPurple.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.deepPurple.withValues(alpha: 0.25),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
handicap > 0 ? '+$handicap' : '$handicap',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.deepPurple,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (participant.average != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.indigo.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.indigo.withValues(alpha: 0.25),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
participant.average!.toStringAsFixed(1),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (participant.memberType != null &&
|
||||
participant.memberType!.isNotEmpty &&
|
||||
participant.memberType!.toLowerCase() != 'guest')
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: BadgeStyles.memberType(getMemberTypeLabel(participant.memberType)),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (participant.status != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: BadgeStyles.participantStatus(
|
||||
toKoreanParticipantStatus(participant.status),
|
||||
),
|
||||
),
|
||||
BadgeStyles.payment(participant.isPaid),
|
||||
],
|
||||
),
|
||||
),
|
||||
if ((participant.notes != null) &&
|
||||
participant.notes!.trim().isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: const [
|
||||
Icon(Icons.notes, size: 14, color: Colors.black54),
|
||||
SizedBox(width: 4),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
onTap: () => widget.onEditParticipant(participant),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../models/participant_model.dart';
|
||||
import '../../../models/score_model.dart';
|
||||
import '../../../models/member_model.dart';
|
||||
import '../../../models/club_model.dart';
|
||||
import '../../../models/tie_breaker_option.dart';
|
||||
import '../../../utils/tie_breaker_util.dart';
|
||||
import '../../../theme/badges.dart';
|
||||
|
||||
class ScoresTab extends StatefulWidget {
|
||||
final String eventId;
|
||||
final List<Participant> participants;
|
||||
final List<Score> scores;
|
||||
final List<Member> members;
|
||||
final List<Member> clubMembers; // for gender icon lookup
|
||||
final Club? club;
|
||||
|
||||
final bool includeHandicap;
|
||||
final ValueChanged<bool> onIncludeHandicapChanged;
|
||||
|
||||
final bool groupByParticipant;
|
||||
final ValueChanged<bool> onGroupByParticipantChanged;
|
||||
|
||||
final Future<void> Function(Participant participant, List<Score> scores) onOpenBatchEdit;
|
||||
final Future<void> Function(Score score) onEditScore;
|
||||
final void Function(Score score) onDeleteScore;
|
||||
|
||||
const ScoresTab({
|
||||
super.key,
|
||||
required this.eventId,
|
||||
required this.participants,
|
||||
required this.scores,
|
||||
required this.members,
|
||||
required this.clubMembers,
|
||||
required this.club,
|
||||
required this.includeHandicap,
|
||||
required this.onIncludeHandicapChanged,
|
||||
required this.groupByParticipant,
|
||||
required this.onGroupByParticipantChanged,
|
||||
required this.onOpenBatchEdit,
|
||||
required this.onEditScore,
|
||||
required this.onDeleteScore,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ScoresTab> createState() => _ScoresTabState();
|
||||
}
|
||||
|
||||
class _ScoresTabState extends State<ScoresTab> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.scores.isEmpty) {
|
||||
return const Center(child: Text('등록된 점수가 없습니다.'));
|
||||
}
|
||||
|
||||
// 동점자 처리 옵션
|
||||
List<TieBreakerOption>? tieBreakerOptions;
|
||||
if (widget.club != null &&
|
||||
widget.club!.tieBreakerOptions != null &&
|
||||
widget.club!.tieBreakerOptions!.isNotEmpty) {
|
||||
tieBreakerOptions = widget.club!.tieBreakerOptions;
|
||||
}
|
||||
|
||||
// 정렬/순위 계산
|
||||
final sortedScores = TieBreakerUtil.sortScoresByOptions(
|
||||
widget.scores,
|
||||
tieBreakerOptions,
|
||||
widget.members,
|
||||
widget.includeHandicap,
|
||||
);
|
||||
|
||||
final ranks = TieBreakerUtil.computeScoreRanks(
|
||||
sortedScores,
|
||||
includeHandicap: widget.includeHandicap,
|
||||
options: tieBreakerOptions,
|
||||
members: widget.members,
|
||||
);
|
||||
|
||||
// 평균/최고/최저
|
||||
final totalScoreSum = sortedScores.fold<int>(
|
||||
0,
|
||||
(sum, s) => sum + (widget.includeHandicap ? s.totalScore + (s.handicap ?? 0) : s.totalScore),
|
||||
);
|
||||
final averageScore = sortedScores.isNotEmpty ? totalScoreSum / sortedScores.length : 0.0;
|
||||
final int? maxScore = sortedScores.isNotEmpty
|
||||
? sortedScores
|
||||
.map((s) => widget.includeHandicap ? s.totalScore + (s.handicap ?? 0) : s.totalScore)
|
||||
.reduce((a, b) => a > b ? a : b)
|
||||
: null;
|
||||
final int? minScore = sortedScores.isNotEmpty
|
||||
? sortedScores
|
||||
.map((s) => widget.includeHandicap ? s.totalScore + (s.handicap ?? 0) : s.totalScore)
|
||||
.reduce((a, b) => a < b ? a : b)
|
||||
: null;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Summary cards
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _summaryCard(
|
||||
color: Colors.blue.shade50,
|
||||
icon: const Icon(Icons.analytics, color: Colors.blue),
|
||||
title: '평균',
|
||||
value: averageScore.toStringAsFixed(1),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _summaryCard(
|
||||
color: Colors.green.shade50,
|
||||
icon: const Icon(Icons.trending_up, color: Colors.green),
|
||||
title: '최고',
|
||||
value: maxScore?.toString() ?? '-',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _summaryCard(
|
||||
color: Colors.red.shade50,
|
||||
icon: const Icon(Icons.trending_down, color: Colors.red),
|
||||
title: '최저',
|
||||
value: minScore?.toString() ?? '-',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.shade200),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.score, color: Colors.blue),
|
||||
const SizedBox(width: 8),
|
||||
Text('평균 점수: ${averageScore.toStringAsFixed(1)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (tieBreakerOptions != null && tieBreakerOptions.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.green.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('동점자 처리 우선순위:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: tieBreakerOptions.map((o) => Chip(
|
||||
label: Text(_tieBreakerLabel(o)),
|
||||
backgroundColor: Colors.green.shade100,
|
||||
)).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
ToggleButtons(
|
||||
isSelected: [widget.groupByParticipant, !widget.groupByParticipant],
|
||||
onPressed: (i) => widget.onGroupByParticipantChanged(i == 0),
|
||||
constraints: const BoxConstraints(minHeight: 32, minWidth: 100),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
children: const [
|
||||
Padding(padding: EdgeInsets.symmetric(horizontal: 8), child: Text('참가자별')),
|
||||
Padding(padding: EdgeInsets.symmetric(horizontal: 8), child: Text('점수별')),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
const Text('핸디캡 포함'),
|
||||
Switch(
|
||||
value: widget.includeHandicap,
|
||||
onChanged: widget.onIncludeHandicapChanged,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: widget.groupByParticipant
|
||||
? _buildGroupedList(sortedScores)
|
||||
: ListView.builder(
|
||||
itemCount: sortedScores.length,
|
||||
itemBuilder: (context, index) {
|
||||
final score = sortedScores[index];
|
||||
final rank = ranks[score.id] ?? (index + 1);
|
||||
var participant = widget.participants.firstWhere(
|
||||
(p) => p.id == (score.participantId ?? ''),
|
||||
orElse: () => Participant(
|
||||
id: '',
|
||||
eventId: '',
|
||||
memberId: score.memberId,
|
||||
name: '알 수 없음',
|
||||
),
|
||||
);
|
||||
if (participant.id.isEmpty) {
|
||||
final byMember = widget.participants.where((p) => p.memberId == score.memberId);
|
||||
if (byMember.isNotEmpty) participant = byMember.first;
|
||||
}
|
||||
return _buildScoreListItem(context, score, rank, participant);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _summaryCard({required Color color, required Icon icon, required String title, required String value}) {
|
||||
return Card(
|
||||
color: color,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(title, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: Colors.black54)),
|
||||
Text(value, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _tieBreakerLabel(TieBreakerOption option) {
|
||||
switch (option) {
|
||||
case TieBreakerOption.lowerHandicap:
|
||||
return '핸디캡 낮은 순';
|
||||
case TieBreakerOption.lowerScoreGap:
|
||||
return '점수 격차 작은 순';
|
||||
case TieBreakerOption.olderAge:
|
||||
return '연장자 우선';
|
||||
case TieBreakerOption.none:
|
||||
return '없음';
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildGroupedList(List<Score> sortedScores) {
|
||||
final summaries = widget.participants.map((p) {
|
||||
final pScores = sortedScores.where((s) => ((s.participantId != null && s.participantId!.isNotEmpty && s.participantId == p.id) || ((s.participantId == null || s.participantId!.isEmpty) && s.memberId == p.memberId))).toList();
|
||||
if (pScores.isEmpty) return null;
|
||||
pScores.sort((a, b) => (a.gameNumber ?? 0).compareTo(b.gameNumber ?? 0));
|
||||
final totals = pScores.map((s) => s.totalScore).toList();
|
||||
final totalsH = pScores.map((s) => s.totalScore + (s.handicap ?? 0)).toList();
|
||||
final total = totals.fold<int>(0, (a, b) => a + b);
|
||||
final totalH = totalsH.fold<int>(0, (a, b) => a + b);
|
||||
final avg = totals.isNotEmpty ? (total / totals.length) : 0.0;
|
||||
final avgH = totalsH.isNotEmpty ? (totalH / totalsH.length) : 0.0;
|
||||
final high = totals.isNotEmpty ? totals.reduce((a, b) => a > b ? a : b) : 0;
|
||||
final highH = totalsH.isNotEmpty ? totalsH.reduce((a, b) => a > b ? a : b) : 0;
|
||||
final low = totals.isNotEmpty ? totals.reduce((a, b) => a < b ? a : b) : 0;
|
||||
final lowH = totalsH.isNotEmpty ? totalsH.reduce((a, b) => a < b ? a : b) : 0;
|
||||
final handicapSum = pScores.fold<int>(0, (a, s) => a + (s.handicap ?? 0));
|
||||
return {
|
||||
'p': p,
|
||||
'scores': pScores,
|
||||
'total': total,
|
||||
'totalH': totalH,
|
||||
'avg': avg,
|
||||
'avgH': avgH,
|
||||
'high': high,
|
||||
'highH': highH,
|
||||
'low': low,
|
||||
'lowH': lowH,
|
||||
'handicapSum': handicapSum,
|
||||
};
|
||||
}).whereType<Map<String, dynamic>>().toList();
|
||||
|
||||
final ranked = TieBreakerUtil.rankParticipantSummaries(
|
||||
summaries,
|
||||
includeHandicap: widget.includeHandicap,
|
||||
);
|
||||
|
||||
return ListView(
|
||||
children: ranked.map((data) {
|
||||
final rank = data['rank'] as int;
|
||||
final p = data['p'] as Participant;
|
||||
final pScores = data['scores'] as List<Score>;
|
||||
final total = data['total'] as int;
|
||||
final totalH = data['totalH'] as int;
|
||||
final avg = data['avg'] as double;
|
||||
final avgH = data['avgH'] as double;
|
||||
final high = data['high'] as int;
|
||||
final highH = data['highH'] as int;
|
||||
final low = data['low'] as int;
|
||||
final lowH = data['lowH'] as int;
|
||||
final handicapSum = data['handicapSum'] as int;
|
||||
final displayTotal = widget.includeHandicap ? totalH : total;
|
||||
final displayAvg = widget.includeHandicap ? avgH : avg;
|
||||
final subtitle = widget.includeHandicap
|
||||
? '게임 ${pScores.length} · 최저 $lowH · 최고 $highH'
|
||||
: '게임 ${pScores.length} · 최저 $low · 최고 $high';
|
||||
|
||||
Icon? genderIcon;
|
||||
try {
|
||||
final m = widget.clubMembers.firstWhere((cm) => cm.id == p.memberId);
|
||||
final g = m.gender?.toLowerCase();
|
||||
if (g == 'male' || g == 'm' || g == '남' || g == '남성') {
|
||||
genderIcon = const Icon(Icons.male, size: 16, color: Colors.blue);
|
||||
} else if (g == 'female' || g == 'f' || g == '여' || g == '여성') {
|
||||
genderIcon = const Icon(Icons.female, size: 16, color: Colors.pink);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
return ExpansionTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: _getRankColor(rank),
|
||||
child: Text('$rank', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Flexible(
|
||||
fit: FlexFit.loose,
|
||||
child: Text(
|
||||
p.name ?? '알 수 없음',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Small guest badge next to name (grouped)
|
||||
if ((p.memberType ?? '').toLowerCase() == 'guest')
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: BadgeStyles.guestSmall(size: 16),
|
||||
),
|
||||
if (genderIcon != null)
|
||||
Icon(genderIcon.icon, size: 20, color: genderIcon.color),
|
||||
if (handicapSum != 0) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.indigo.shade100,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Text('+$handicapSum', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700)),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: displayAvg >= 200 ? Colors.red.shade400 : Colors.blue.shade100,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Text(
|
||||
displayAvg.toStringAsFixed(1),
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: displayAvg >= 200 ? Colors.white : Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(subtitle, style: const TextStyle(fontSize: 13), overflow: TextOverflow.ellipsis, maxLines: 1),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
|
||||
decoration: BoxDecoration(color: Colors.blue.shade100, borderRadius: BorderRadius.circular(14)),
|
||||
child: Text('$displayTotal', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: Colors.black87)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.edit, size: 18),
|
||||
label: const Text('전체 수정'),
|
||||
onPressed: () => widget.onOpenBatchEdit(p, pScores),
|
||||
style: OutlinedButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
...pScores.map((s) {
|
||||
final scoreText = widget.includeHandicap
|
||||
? '${s.totalScore + (s.handicap ?? 0)} (${s.totalScore}+${s.handicap ?? 0})'
|
||||
: '${s.totalScore}';
|
||||
final int displayVal = widget.includeHandicap ? (s.totalScore + (s.handicap ?? 0)) : s.totalScore;
|
||||
final gameNo = s.gameNumber?.toString() ?? '';
|
||||
return Dismissible(
|
||||
key: ValueKey('score_${s.id}_${s.gameNumber ?? ''}'),
|
||||
direction: DismissDirection.endToStart,
|
||||
background: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
color: Colors.red.shade400,
|
||||
child: const Icon(Icons.delete, color: Colors.white),
|
||||
),
|
||||
onDismissed: (_) => widget.onDeleteScore(s),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 50),
|
||||
leading: CircleAvatar(
|
||||
radius: 18,
|
||||
child: Text(gameNo.isEmpty ? '-' : gameNo, style: const TextStyle(fontSize: 15)),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (displayVal >= 200)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
|
||||
decoration: BoxDecoration(color: Colors.red.shade400, borderRadius: BorderRadius.circular(12)),
|
||||
child: Text(
|
||||
scoreText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: Colors.white),
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
scoreText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: Colors.black87),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(DateFormat('yyyy-MM-dd').format(s.date), style: const TextStyle(color: Colors.black54, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
onTap: () => widget.onEditScore(s),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScoreListItem(BuildContext context, Score score, int rank, Participant participant) {
|
||||
return Dismissible(
|
||||
key: Key('score_${score.id}'),
|
||||
direction: DismissDirection.endToStart,
|
||||
background: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
color: Colors.red,
|
||||
child: const Icon(Icons.delete, color: Colors.white),
|
||||
),
|
||||
confirmDismiss: (direction) async {
|
||||
// confirm handled by parent via onDeleteScore as needed; keep swipe immediate
|
||||
return true;
|
||||
},
|
||||
onDismissed: (_) => widget.onDeleteScore(score),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: _getRankColor(rank),
|
||||
child: Text(
|
||||
rank.toString(),
|
||||
key: Key('rank_${rank}_text'),
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
participant.name ?? '알 수 없음',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
// Small guest badge next to name
|
||||
if ((participant.memberType ?? '').toLowerCase() == 'guest')
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 2),
|
||||
child: BadgeStyles.guestSmall(size: 16),
|
||||
),
|
||||
Builder(
|
||||
builder: (ctx) {
|
||||
Member? m;
|
||||
try {
|
||||
m = widget.clubMembers.firstWhere((cm) => cm.id == (participant.memberId.isNotEmpty ? participant.memberId : score.memberId));
|
||||
} catch (_) {}
|
||||
final gender = m?.gender?.toLowerCase();
|
||||
IconData? icon;
|
||||
Color color = Colors.grey;
|
||||
if (gender == 'male' || gender == 'm' || gender == '남' || gender == '남성') {
|
||||
icon = Icons.male;
|
||||
color = Colors.blue;
|
||||
} else if (gender == 'female' || gender == 'f' || gender == '여' || gender == '여성') {
|
||||
icon = Icons.female;
|
||||
color = Colors.pink;
|
||||
}
|
||||
if (icon == null) return const SizedBox.shrink();
|
||||
return Icon(icon, size: 16, color: color);
|
||||
},
|
||||
),
|
||||
if ((score.handicap ?? 0) != 0) ...[
|
||||
const SizedBox(width: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(color: Colors.indigo.shade100, borderRadius: BorderRadius.circular(12)),
|
||||
child: Text(
|
||||
'+${score.handicap}',
|
||||
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Builder(
|
||||
builder: (ctx) {
|
||||
final displayedTotal = widget.includeHandicap ? (score.totalScore + (score.handicap ?? 0)) : score.totalScore;
|
||||
final bool isHigh = displayedTotal >= 200;
|
||||
final bg = isHigh ? Colors.red.shade400 : Colors.blue.shade100;
|
||||
final fg = isHigh ? Colors.white : Colors.black87;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(12)),
|
||||
child: Text('$displayedTotal', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14, color: fg)),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('날짜: ${DateFormat('yyyy-MM-dd').format(score.date)}'),
|
||||
],
|
||||
),
|
||||
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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () => widget.onEditScore(score),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// helpers (duplicated from original for isolation)
|
||||
Color _getRankColor(int rank) {
|
||||
switch (rank) {
|
||||
case 1:
|
||||
return Colors.amber.shade700;
|
||||
case 2:
|
||||
return Colors.blueGrey.shade400;
|
||||
case 3:
|
||||
return Colors.brown.shade400;
|
||||
default:
|
||||
return Colors.blue.shade700;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getScoreColor(int score) {
|
||||
if (score == 10) return Colors.green;
|
||||
if (score == 0) return Colors.red;
|
||||
return Colors.blue;
|
||||
}
|
||||
|
||||
String _getFrameDisplay(int frameIndex, int score, List<int> frames) {
|
||||
if (score == 10) return 'X';
|
||||
if (frameIndex > 0 && frames[frameIndex - 1] + score == 10) return '/';
|
||||
return score.toString();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user