ui수정
This commit is contained in:
@@ -9,6 +9,27 @@ const averageService = require('../services/averageService');
|
|||||||
exports.publicEventEntry = async (req, res) => {
|
exports.publicEventEntry = async (req, res) => {
|
||||||
const { publicHash } = req.params;
|
const { publicHash } = req.params;
|
||||||
const { password } = req.body || {};
|
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;
|
const auth = req.headers.authorization;
|
||||||
let event;
|
let event;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -12,4 +12,7 @@ router.post('/:publicHash/guest', publicAuth, publicController.addPublicGuest);
|
|||||||
// 공개 점수 수정
|
// 공개 점수 수정
|
||||||
router.put('/:publicHash/score', publicAuth, publicController.updatePublicScore);
|
router.put('/:publicHash/score', publicAuth, publicController.updatePublicScore);
|
||||||
|
|
||||||
|
// 팀 핸디캡 수정
|
||||||
|
router.put('/:publicHash/team/:teamNumber/handicap', publicAuth, publicController.updateTeamHandicap);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -67,3 +67,38 @@ class PublicEvent {
|
|||||||
bool get isActive => (status == 'active');
|
bool get isActive => (status == 'active');
|
||||||
bool get isFinished => (status == 'completed');
|
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 = {};
|
final Map<String, Timer> _postTimers = {};
|
||||||
_ResultSort _resultSort = _ResultSort.total;
|
_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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -385,10 +458,13 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
|||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
if (event.status != null)
|
Chip(
|
||||||
Chip(key: const Key('status_chip'), label: Text(event.status!)),
|
key: const Key('status_chip'),
|
||||||
|
label: Text(_statusLabel(event.statusEnum)),
|
||||||
|
backgroundColor: _statusColor(event.statusEnum),
|
||||||
|
),
|
||||||
if (event.eventType != null && event.eventType!.isNotEmpty)
|
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)
|
if (event.startDate != null || event.endDate != null)
|
||||||
Chip(
|
Chip(
|
||||||
key: const Key('period_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)),
|
Chip(key: const Key('deadline_chip'), label: Text('신청마감 ' + event.registrationDeadline!.toLocal().toString().split('.').first)),
|
||||||
if (event.participantFee != null)
|
if (event.participantFee != null)
|
||||||
Chip(key: const Key('fee_chip'), label: Text('참가비 ₩' + event.participantFee!.toString())),
|
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),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
|
if (teams.isEmpty) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text('참가자', style: TextStyle(fontWeight: FontWeight.bold), key: Key('section_participants')),
|
const Text('참가자', style: TextStyle(fontWeight: FontWeight.bold), key: Key('section_participants')),
|
||||||
Text('총 ' + participants.length.toString() + '명', key: const Key('participants_count')),
|
Text('총 ' + participants.length.toString() + '명', key: const Key('participants_count')),
|
||||||
@@ -425,9 +504,26 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
|||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Wrap(
|
||||||
(p.status ?? '-') + ' · avg:' + (p.average?.toString() ?? '-') + ' · hc:' + p.handicap.toString(),
|
spacing: 6,
|
||||||
key: Key('participant_info_' + p.participantId),
|
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)
|
if (canInputScore && event != null)
|
||||||
Padding(
|
Padding(
|
||||||
@@ -438,7 +534,7 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 6),
|
padding: const EdgeInsets.only(right: 6),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 54,
|
width: 60,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
@@ -466,7 +562,7 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
|||||||
isDense: true,
|
isDense: true,
|
||||||
hintText: '---',
|
hintText: '---',
|
||||||
counterText: '',
|
counterText: '',
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 6, horizontal: 6),
|
contentPadding: EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||||
),
|
),
|
||||||
maxLength: 3,
|
maxLength: 3,
|
||||||
onChanged: (v) => _onScoreChanged(p, g, v),
|
onChanged: (v) => _onScoreChanged(p, g, v),
|
||||||
@@ -480,11 +576,26 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing: Text(
|
trailing: Column(
|
||||||
'R ' + (partRankMap[p.participantId]?['rank']?.toString() ?? '-') +
|
mainAxisSize: MainAxisSize.min,
|
||||||
' · 총 ' + PublicRankingUtils.participantTotalWithHc(p).toString(),
|
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),
|
key: Key('participant_total_' + p.participantId),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (!canInputScore) {
|
if (!canInputScore) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('완료 상태에서는 점수 입력이 불가합니다')));
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('완료 상태에서는 점수 입력이 불가합니다')));
|
||||||
@@ -496,6 +607,7 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
const Text('팀', style: TextStyle(fontWeight: FontWeight.bold), key: Key('section_teams')),
|
const Text('팀', style: TextStyle(fontWeight: FontWeight.bold), key: Key('section_teams')),
|
||||||
Text('총 ' + teams.length.toString() + '팀', key: const Key('teams_count')),
|
Text('총 ' + teams.length.toString() + '팀', key: const Key('teams_count')),
|
||||||
@@ -509,8 +621,12 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
|||||||
: Column(
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
for (final t in teams)
|
for (final t in teams)
|
||||||
ListTile(
|
Padding(
|
||||||
title: Row(
|
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('팀 ' + t.teamNumber.toString()),
|
Text('팀 ' + t.teamNumber.toString()),
|
||||||
const SizedBox(width: 6),
|
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)),
|
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;
|
int membersTotal = 0;
|
||||||
for (final m in t.members) {
|
for (final m in t.members) {
|
||||||
int mt = 0;
|
int mt = 0;
|
||||||
@@ -544,6 +689,20 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
|
|||||||
key: Key('team_summary_' + t.teamNumber.toString()),
|
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),
|
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