이벤트 개발완료
This commit is contained in:
@@ -5,6 +5,97 @@ const Map<String, String> roleOptions = {
|
||||
'user': '일반 사용자',
|
||||
};
|
||||
|
||||
// -------------------------------
|
||||
// Strong enums for client usage
|
||||
// -------------------------------
|
||||
|
||||
/// 참가자 상태 (서버 enum과 1:1 매칭)
|
||||
enum ParticipantStatus { pending, confirmed, canceled, attended }
|
||||
|
||||
extension ParticipantStatusX on ParticipantStatus {
|
||||
String get apiValue {
|
||||
switch (this) {
|
||||
case ParticipantStatus.pending:
|
||||
return 'pending';
|
||||
case ParticipantStatus.confirmed:
|
||||
return 'confirmed';
|
||||
case ParticipantStatus.canceled:
|
||||
return 'canceled';
|
||||
case ParticipantStatus.attended:
|
||||
return 'attended';
|
||||
}
|
||||
}
|
||||
|
||||
String get labelKo {
|
||||
switch (this) {
|
||||
case ParticipantStatus.pending:
|
||||
return '대기';
|
||||
case ParticipantStatus.confirmed:
|
||||
return '확인됨';
|
||||
case ParticipantStatus.canceled:
|
||||
return '취소됨';
|
||||
case ParticipantStatus.attended:
|
||||
return '참석';
|
||||
}
|
||||
}
|
||||
|
||||
static ParticipantStatus? from(String? value) {
|
||||
switch ((value ?? '').toLowerCase()) {
|
||||
case 'pending':
|
||||
return ParticipantStatus.pending;
|
||||
case 'confirmed':
|
||||
return ParticipantStatus.confirmed;
|
||||
case 'canceled':
|
||||
return ParticipantStatus.canceled;
|
||||
case 'attended':
|
||||
return ParticipantStatus.attended;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 결제 상태 (서버 enum과 1:1 매칭)
|
||||
enum PaymentStatus { unpaid, paid }
|
||||
|
||||
extension PaymentStatusX on PaymentStatus {
|
||||
String get apiValue {
|
||||
switch (this) {
|
||||
case PaymentStatus.unpaid:
|
||||
return 'unpaid';
|
||||
case PaymentStatus.paid:
|
||||
return 'paid';
|
||||
}
|
||||
}
|
||||
|
||||
String get labelKo {
|
||||
switch (this) {
|
||||
case PaymentStatus.unpaid:
|
||||
return '미납';
|
||||
case PaymentStatus.paid:
|
||||
return '납부';
|
||||
}
|
||||
}
|
||||
|
||||
static PaymentStatus? from(String? value) {
|
||||
switch ((value ?? '').toLowerCase()) {
|
||||
case 'unpaid':
|
||||
return PaymentStatus.unpaid;
|
||||
case 'paid':
|
||||
return PaymentStatus.paid;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backward-compatible helpers for non-enum callsites
|
||||
String toKoreanParticipantStatus(String? value) =>
|
||||
(ParticipantStatusX.from(value) ?? ParticipantStatus.pending).labelKo;
|
||||
|
||||
String toKoreanPaymentStatus(String? value) =>
|
||||
(PaymentStatusX.from(value) ?? PaymentStatus.unpaid).labelKo;
|
||||
|
||||
// 성별 옵션
|
||||
const Map<String, String> genderOptions = {
|
||||
'male': '남성',
|
||||
@@ -48,9 +139,10 @@ const Map<String, String> eventStatusOptions = {
|
||||
const Map<String, String> eventTypeOptions = {
|
||||
'regular': '정기전',
|
||||
'exchange': '교류전',
|
||||
'tournament': '토너먼트',
|
||||
'tournament': '대회',
|
||||
'lightning': '번개',
|
||||
'friendly': '친선전',
|
||||
'event': '이벤트',
|
||||
'other': '기타',
|
||||
};
|
||||
|
||||
|
||||
@@ -23,11 +23,39 @@ class EventCsvParser {
|
||||
};
|
||||
}
|
||||
|
||||
final headers = _parseCsvLine(lines.first)
|
||||
.map((e) => e.trim().toLowerCase())
|
||||
final headersRaw = _parseCsvLine(lines.first);
|
||||
final headers = headersRaw
|
||||
.map((e) => e.trim())
|
||||
.toList();
|
||||
|
||||
if (!headers.contains('title') || !headers.contains('startdate')) {
|
||||
// 한글 헤더를 영문 키로 정규화하기 위한 매핑
|
||||
String _normalizeHeader(String raw) {
|
||||
final v = raw.trim().toLowerCase();
|
||||
// 공백 제거 버전도 별도로 준비
|
||||
final vn = v.replaceAll(' ', '');
|
||||
// 한국어 -> 영문 매핑
|
||||
if (v == '제목') return 'title';
|
||||
if (v == '시작일') return 'startDate';
|
||||
if (v == '종료일') return 'endDate';
|
||||
if (v == '설명') return 'description';
|
||||
if (v == '장소') return 'location';
|
||||
if (v == '상태') return 'status';
|
||||
if (v == '유형') return 'type';
|
||||
if (v == '게임 수' || vn == '게임수') return 'gameCount';
|
||||
if (v == '참가비') return 'participantFee';
|
||||
if (v == '신청마감' || v == '신청 마감') return 'registrationDeadline';
|
||||
if (v == '최대 참가자' || vn == '최대참가자') return 'maxParticipants';
|
||||
|
||||
// 기존 영문 키들 정규화
|
||||
if (vn == 'maxparticipants') return 'maxParticipants';
|
||||
if (vn == 'startdate') return 'startDate';
|
||||
if (vn == 'enddate') return 'endDate';
|
||||
return raw.trim();
|
||||
}
|
||||
|
||||
final normalizedHeaders = headers.map(_normalizeHeader).toList();
|
||||
|
||||
if (!normalizedHeaders.contains('title') || !normalizedHeaders.contains('startDate')) {
|
||||
return {
|
||||
'processedRows': 0,
|
||||
'validEvents': <Map<String, dynamic>>[],
|
||||
@@ -52,26 +80,20 @@ class EventCsvParser {
|
||||
processedRows++;
|
||||
|
||||
final event = <String, dynamic>{};
|
||||
for (var j = 0; j < headers.length && j < row.length; j++) {
|
||||
final header = headers[j];
|
||||
for (var j = 0; j < normalizedHeaders.length && j < row.length; j++) {
|
||||
final header = normalizedHeaders[j];
|
||||
var value = row[j].trim();
|
||||
|
||||
// 헤더 정규화
|
||||
var normalizedHeader = header;
|
||||
if (header == 'maxparticipants') normalizedHeader = 'maxParticipants';
|
||||
if (header == 'startdate') normalizedHeader = 'startDate';
|
||||
if (header == 'enddate') normalizedHeader = 'endDate';
|
||||
|
||||
if (normalizedHeader == 'startDate' || normalizedHeader == 'endDate') {
|
||||
if (header == 'startDate' || header == 'endDate') {
|
||||
// 날짜 문자열은 그대로 전달하고, 화면/서버 측에서 ISO 변환/검증 처리
|
||||
// 비어있으면 startDate는 필수, endDate는 null
|
||||
if (normalizedHeader == 'startDate') {
|
||||
if (header == 'startDate') {
|
||||
event['startDate'] = value.isEmpty ? DateTime.now().toIso8601String() : value;
|
||||
} else {
|
||||
event['endDate'] = value.isEmpty ? null : value;
|
||||
}
|
||||
} else {
|
||||
event[normalizedHeader] = value.isEmpty ? null : value;
|
||||
event[header] = value.isEmpty ? null : value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -310,5 +310,44 @@ class TieBreakerUtil {
|
||||
|
||||
return ranks;
|
||||
}
|
||||
|
||||
|
||||
/// 재사용 가능한: 점수 리스트에 대한 순위 계산 (외부에서 쉽게 호출)
|
||||
static Map<String, int> computeScoreRanks(
|
||||
List<Score> scores, {
|
||||
required bool includeHandicap,
|
||||
List<TieBreakerOption>? options,
|
||||
List<Member>? members,
|
||||
}) {
|
||||
return calculateRanks(
|
||||
scores,
|
||||
includeHandicap,
|
||||
options: options,
|
||||
members: members,
|
||||
);
|
||||
}
|
||||
|
||||
/// 재사용 가능한: 참가자 요약(Map) 리스트에 정렬/순위 부여
|
||||
/// - summaries: 각 항목은 최소 'total'과 'totalH' 키를 포함해야 함
|
||||
/// - includeHandicap: 어떤 합계를 정렬 기준으로 사용할지 결정
|
||||
/// - totalKey/totalHKey: 키명이 다른 경우 오버라이드 가능
|
||||
static List<Map<String, dynamic>> rankParticipantSummaries(
|
||||
List<Map<String, dynamic>> summaries, {
|
||||
required bool includeHandicap,
|
||||
String totalKey = 'total',
|
||||
String totalHKey = 'totalH',
|
||||
}) {
|
||||
final items = List<Map<String, dynamic>>.of(summaries);
|
||||
items.sort((a, b) {
|
||||
final at = includeHandicap ? (a[totalHKey] as int) : (a[totalKey] as int);
|
||||
final bt = includeHandicap ? (b[totalHKey] as int) : (b[totalKey] as int);
|
||||
return bt.compareTo(at); // desc
|
||||
});
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
items[i] = {
|
||||
...items[i],
|
||||
'rank': i + 1,
|
||||
};
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import 'test_utils.dart';
|
||||
|
||||
/// URL 관련 유틸리티 기능을 제공하는 클래스
|
||||
class UrlUtils {
|
||||
/// 이벤트 공개 URL의 기본 도메인
|
||||
static const String eventBaseUrl = 'https://bowling.example.com/events/';
|
||||
/// 이벤트 공개 URL의 기본 도메인 (프로덕션)
|
||||
static const String eventBaseUrl = 'https://lanebow.com/events/';
|
||||
|
||||
/// 공개 URL 해시 생성
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user