ui수정
This commit is contained in:
@@ -9,6 +9,27 @@ const averageService = require('../services/averageService');
|
||||
exports.publicEventEntry = async (req, res) => {
|
||||
const { publicHash } = req.params;
|
||||
const { password } = req.body || {};
|
||||
|
||||
// 팀 핸디캡 수정
|
||||
exports.updateTeamHandicap = async (req, res) => {
|
||||
const { publicHash } = req.params;
|
||||
const { teamNumber, handicap } = req.body || {};
|
||||
try {
|
||||
const event = await Event.findOne({ where: { publicHash } });
|
||||
if (!event) return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' });
|
||||
if (event.status !== 'ready' && event.status !== 'active') return res.status(403).json({ message: '팀 핸디캡 수정이 불가능한 상태입니다.' });
|
||||
|
||||
const team = await EventTeam.findOne({ where: { eventId: event.id, teamNumber } });
|
||||
if (!team) return res.status(404).json({ message: '팀을 찾을 수 없습니다.' });
|
||||
|
||||
const hc = Number(handicap) || 0;
|
||||
await team.update({ handicap: hc });
|
||||
return res.json({ message: '팀 핸디캡이 저장되었습니다.', data: { teamNumber, handicap: hc } });
|
||||
} catch (e) {
|
||||
console.error('팀 핸디캡 수정 오류:', e);
|
||||
res.status(500).json({ message: '팀 핸디캡 저장 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
const auth = req.headers.authorization;
|
||||
let event;
|
||||
try {
|
||||
|
||||
@@ -12,4 +12,7 @@ router.post('/:publicHash/guest', publicAuth, publicController.addPublicGuest);
|
||||
// 공개 점수 수정
|
||||
router.put('/:publicHash/score', publicAuth, publicController.updatePublicScore);
|
||||
|
||||
// 팀 핸디캡 수정
|
||||
router.put('/:publicHash/team/:teamNumber/handicap', publicAuth, publicController.updateTeamHandicap);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -67,3 +67,38 @@ class PublicEvent {
|
||||
bool get isActive => (status == 'active');
|
||||
bool get isFinished => (status == 'completed');
|
||||
}
|
||||
|
||||
enum EventStatus { ready, active, completed, canceled, unknown }
|
||||
enum EventType { regular, tournament, practice, league, unknown }
|
||||
|
||||
extension PublicEventEnums on PublicEvent {
|
||||
EventStatus get statusEnum {
|
||||
switch ((status ?? '').toLowerCase()) {
|
||||
case 'ready':
|
||||
return EventStatus.ready;
|
||||
case 'active':
|
||||
return EventStatus.active;
|
||||
case 'completed':
|
||||
return EventStatus.completed;
|
||||
case 'canceled':
|
||||
return EventStatus.canceled;
|
||||
default:
|
||||
return EventStatus.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
EventType get typeEnum {
|
||||
switch ((eventType ?? '').toLowerCase()) {
|
||||
case 'regular':
|
||||
return EventType.regular;
|
||||
case 'tournament':
|
||||
return EventType.tournament;
|
||||
case 'practice':
|
||||
return EventType.practice;
|
||||
case 'league':
|
||||
return EventType.league;
|
||||
default:
|
||||
return EventType.unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,3 +88,50 @@ class PublicParticipant {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ParticipantStatus { pending, confirmed, canceled, unknown }
|
||||
enum PaymentStatus { unpaid, paid, refunded, partial, unknown }
|
||||
enum MemberType { member, guest, staff, unknown }
|
||||
|
||||
extension PublicParticipantEnums on PublicParticipant {
|
||||
ParticipantStatus get statusEnum {
|
||||
switch ((status ?? '').toLowerCase()) {
|
||||
case 'pending':
|
||||
return ParticipantStatus.pending;
|
||||
case 'confirmed':
|
||||
return ParticipantStatus.confirmed;
|
||||
case 'canceled':
|
||||
return ParticipantStatus.canceled;
|
||||
default:
|
||||
return ParticipantStatus.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
PaymentStatus get paymentEnum {
|
||||
switch ((paymentStatus ?? '').toLowerCase()) {
|
||||
case 'paid':
|
||||
return PaymentStatus.paid;
|
||||
case 'unpaid':
|
||||
return PaymentStatus.unpaid;
|
||||
case 'refunded':
|
||||
return PaymentStatus.refunded;
|
||||
case 'partial':
|
||||
return PaymentStatus.partial;
|
||||
default:
|
||||
return PaymentStatus.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
MemberType get memberTypeEnum {
|
||||
switch ((memberType ?? '').toLowerCase()) {
|
||||
case 'member':
|
||||
return MemberType.member;
|
||||
case 'guest':
|
||||
return MemberType.guest;
|
||||
case 'staff':
|
||||
return MemberType.staff;
|
||||
default:
|
||||
return MemberType.unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,79 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
||||
final Map<String, Timer> _postTimers = {};
|
||||
_ResultSort _resultSort = _ResultSort.total;
|
||||
|
||||
String _statusLabel(EventStatus s) {
|
||||
switch (s) {
|
||||
case EventStatus.ready:
|
||||
return 'ready';
|
||||
case EventStatus.active:
|
||||
return 'active';
|
||||
case EventStatus.completed:
|
||||
return 'completed';
|
||||
case EventStatus.canceled:
|
||||
return 'canceled';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
Color _statusColor(EventStatus s) {
|
||||
switch (s) {
|
||||
case EventStatus.ready:
|
||||
return Colors.blue.shade50;
|
||||
case EventStatus.active:
|
||||
return Colors.green.shade50;
|
||||
case EventStatus.completed:
|
||||
return Colors.grey.shade200;
|
||||
case EventStatus.canceled:
|
||||
return Colors.red.shade50;
|
||||
default:
|
||||
return Colors.grey.shade100;
|
||||
}
|
||||
}
|
||||
|
||||
String _typeLabel(EventType t) {
|
||||
switch (t) {
|
||||
case EventType.regular:
|
||||
return 'regular';
|
||||
case EventType.tournament:
|
||||
return 'tournament';
|
||||
case EventType.practice:
|
||||
return 'practice';
|
||||
case EventType.league:
|
||||
return 'league';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
String _participantStatusLabel(ParticipantStatus s) {
|
||||
switch (s) {
|
||||
case ParticipantStatus.pending:
|
||||
return 'pending';
|
||||
case ParticipantStatus.confirmed:
|
||||
return 'confirmed';
|
||||
case ParticipantStatus.canceled:
|
||||
return 'canceled';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
String _paymentLabel(PaymentStatus s) {
|
||||
switch (s) {
|
||||
case PaymentStatus.paid:
|
||||
return 'paid';
|
||||
case PaymentStatus.unpaid:
|
||||
return 'unpaid';
|
||||
case PaymentStatus.refunded:
|
||||
return 'refunded';
|
||||
case PaymentStatus.partial:
|
||||
return 'partial';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -385,10 +458,13 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
if (event.status != null)
|
||||
Chip(key: const Key('status_chip'), label: Text(event.status!)),
|
||||
Chip(
|
||||
key: const Key('status_chip'),
|
||||
label: Text(_statusLabel(event.statusEnum)),
|
||||
backgroundColor: _statusColor(event.statusEnum),
|
||||
),
|
||||
if (event.eventType != null && event.eventType!.isNotEmpty)
|
||||
Chip(key: const Key('type_chip'), label: Text(event.eventType!)),
|
||||
Chip(key: const Key('type_chip'), label: Text(_typeLabel(event.typeEnum))),
|
||||
if (event.startDate != null || event.endDate != null)
|
||||
Chip(
|
||||
key: const Key('period_chip'),
|
||||
@@ -402,10 +478,13 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
||||
Chip(key: const Key('deadline_chip'), label: Text('신청마감 ' + event.registrationDeadline!.toLocal().toString().split('.').first)),
|
||||
if (event.participantFee != null)
|
||||
Chip(key: const Key('fee_chip'), label: Text('참가비 ₩' + event.participantFee!.toString())),
|
||||
if (event.location != null && event.location!.isNotEmpty)
|
||||
Chip(key: const Key('location_chip'), label: Text(event.location!)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (teams.isEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
const Text('참가자', style: TextStyle(fontWeight: FontWeight.bold), key: Key('section_participants')),
|
||||
Text('총 ' + participants.length.toString() + '명', key: const Key('participants_count')),
|
||||
@@ -425,9 +504,26 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
(p.status ?? '-') + ' · avg:' + (p.average?.toString() ?? '-') + ' · hc:' + p.handicap.toString(),
|
||||
key: Key('participant_info_' + p.participantId),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
Chip(
|
||||
key: Key('participant_status_' + p.participantId),
|
||||
label: Text(_participantStatusLabel(p.statusEnum)),
|
||||
),
|
||||
if (p.paymentStatus != null && p.paymentStatus!.isNotEmpty)
|
||||
Chip(
|
||||
key: Key('participant_payment_' + p.participantId),
|
||||
label: Text(_paymentLabel(p.paymentEnum)),
|
||||
),
|
||||
if (p.memberType != null && p.memberType!.isNotEmpty)
|
||||
Chip(
|
||||
key: Key('participant_type_' + p.participantId),
|
||||
label: Text(p.memberType!),
|
||||
),
|
||||
Text('avg:' + (p.average?.toString() ?? '-') + ' · hc:' + p.handicap.toString(), key: Key('participant_info_' + p.participantId)),
|
||||
],
|
||||
),
|
||||
if (canInputScore && event != null)
|
||||
Padding(
|
||||
@@ -438,7 +534,7 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: SizedBox(
|
||||
width: 54,
|
||||
width: 60,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
@@ -466,7 +562,7 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
||||
isDense: true,
|
||||
hintText: '---',
|
||||
counterText: '',
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 6, horizontal: 6),
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||
),
|
||||
maxLength: 3,
|
||||
onChanged: (v) => _onScoreChanged(p, g, v),
|
||||
@@ -480,11 +576,26 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Text(
|
||||
'R ' + (partRankMap[p.participantId]?['rank']?.toString() ?? '-') +
|
||||
' · 총 ' + PublicRankingUtils.participantTotalWithHc(p).toString(),
|
||||
trailing: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
key: Key('participant_rank_' + p.participantId),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text('R ' + (partRankMap[p.participantId]?['rank']?.toString() ?? '-'), style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'총 ' + PublicRankingUtils.participantTotalWithHc(p).toString(),
|
||||
key: Key('participant_total_' + p.participantId),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
if (!canInputScore) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('완료 상태에서는 점수 입력이 불가합니다')));
|
||||
@@ -496,6 +607,7 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
const Text('팀', style: TextStyle(fontWeight: FontWeight.bold), key: Key('section_teams')),
|
||||
Text('총 ' + teams.length.toString() + '팀', key: const Key('teams_count')),
|
||||
@@ -509,8 +621,12 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
||||
: Column(
|
||||
children: [
|
||||
for (final t in teams)
|
||||
ListTile(
|
||||
title: Row(
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text('팀 ' + t.teamNumber.toString()),
|
||||
const SizedBox(width: 6),
|
||||
@@ -523,9 +639,38 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
||||
),
|
||||
child: Text('R ' + (teamRankMap[t.teamNumber]?['rank']?.toString() ?? '-'), style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: TextFormField(
|
||||
key: Key('team_hc_input_' + t.teamNumber.toString()),
|
||||
initialValue: t.handicap.toString(),
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
isDense: true,
|
||||
labelText: '팀 HC',
|
||||
counterText: '',
|
||||
),
|
||||
maxLength: 3,
|
||||
onChanged: (v) async {
|
||||
final val = int.tryParse(v);
|
||||
if (val == null || val < 0 || val > 300) return;
|
||||
try {
|
||||
await _service.updateTeamHandicap(widget.publicHash, teamNumber: t.teamNumber, handicap: val);
|
||||
// 로컬 반영
|
||||
final idx = _teams.indexWhere((tt) => tt.teamNumber == t.teamNumber);
|
||||
if (idx >= 0) {
|
||||
_teams[idx] = PublicTeam(teamId: _teams[idx].teamId, teamNumber: _teams[idx].teamNumber, handicap: val, members: _teams[idx].members);
|
||||
setState(() {});
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Builder(builder: (context) {
|
||||
const SizedBox(height: 6),
|
||||
Builder(builder: (context) {
|
||||
int membersTotal = 0;
|
||||
for (final m in t.members) {
|
||||
int mt = 0;
|
||||
@@ -544,6 +689,20 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
||||
key: Key('team_summary_' + t.teamNumber.toString()),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 6),
|
||||
// 팀 구성원 리스트
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final m in t.members)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Text('• ' + m.name),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -166,4 +166,16 @@ class PublicEventService {
|
||||
headers: _authHeader(token),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateTeamHandicap(String publicHash, {
|
||||
required int teamNumber,
|
||||
required int handicap,
|
||||
}) async {
|
||||
final token = getSavedToken(publicHash);
|
||||
await _http.put(
|
||||
'/public/' + publicHash + '/team/' + teamNumber.toString() + '/handicap',
|
||||
data: { 'teamNumber': teamNumber, 'handicap': handicap },
|
||||
headers: _authHeader(token),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user