공개페이지

This commit is contained in:
2025-10-29 00:25:03 +09:00
parent 19f99989b8
commit 63d0d383b0
28 changed files with 2691 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
import '../models/public_participant.dart';
import '../models/public_team.dart';
class PublicRankingUtils {
static int capScore(int scoreWithHandicap) {
if (scoreWithHandicap > 300) return 300;
if (scoreWithHandicap < 0) return 0;
return scoreWithHandicap;
}
// 평균(핸디 포함), 소수 1자리. 입력된 게임 수 기준으로 계산
static double participantAverageWithHc(PublicParticipant p) {
int total = 0;
int cnt = 0;
for (int i = 0; i < p.rawScores.length; i++) {
final raw = p.rawScores[i];
int hc = (i < p.handicaps.length) ? p.handicaps[i] : 0;
if (hc == 0) {
hc = p.handicap;
}
if (raw == null) continue;
total += capScore(raw + hc);
cnt += 1;
}
if (cnt == 0) return 0.0;
return total / cnt;
}
// 개인별 총점(핸디캡 포함) 계산
static int participantTotalWithHc(PublicParticipant p) {
int total = 0;
for (int i = 0; i < p.rawScores.length; i++) {
final raw = p.rawScores[i];
int hc = (i < p.handicaps.length) ? p.handicaps[i] : 0;
if (hc == 0) {
// per-game 핸디캡이 없으면 참가자 기본 핸디캡 사용
hc = p.handicap;
}
if (raw == null) continue;
total += capScore(raw + hc);
}
return total;
}
// 동점자 순위 처리(1,1,3 방식)
static List<int> ranksFor(List<int> valuesDesc) {
if (valuesDesc.isEmpty) return const [];
final ranks = List<int>.filled(valuesDesc.length, 0);
int rank = 1;
for (int i = 0; i < valuesDesc.length; i++) {
if (i > 0 && valuesDesc[i] == valuesDesc[i - 1]) {
ranks[i] = ranks[i - 1];
} else {
ranks[i] = rank;
}
rank = i + 2; // 다음 베이스 랭크
}
return ranks;
}
static List<Map<String, dynamic>> individualRanking(List<PublicParticipant> list) {
// 내림차순 정렬
final items = list
.map((p) => {
'p': p,
'total': participantTotalWithHc(p),
})
.toList();
items.sort((a, b) => (b['total'] as int).compareTo(a['total'] as int));
final totals = items.map<int>((e) => e['total'] as int).toList();
final rs = ranksFor(totals);
for (int i = 0; i < items.length; i++) {
items[i]['rank'] = rs[i];
}
return items;
}
// 동점자 순위 처리 for double 리스트 (1,1,3 방식)
static List<int> ranksForDoubles(List<double> valuesDesc) {
if (valuesDesc.isEmpty) return const [];
final ranks = List<int>.filled(valuesDesc.length, 0);
int rank = 1;
const eps = 1e-9;
for (int i = 0; i < valuesDesc.length; i++) {
if (i > 0 && (valuesDesc[i] - valuesDesc[i - 1]).abs() < eps) {
ranks[i] = ranks[i - 1];
} else {
ranks[i] = rank;
}
rank = i + 2;
}
return ranks;
}
// 평균(핸디 포함) 기준 개인 랭킹 (내림차순). item: {p, total, avg, rank}
static List<Map<String, dynamic>> individualRankingByAvg(List<PublicParticipant> list) {
final items = list
.map((p) => {
'p': p,
'total': participantTotalWithHc(p),
'avg': participantAverageWithHc(p),
})
.toList();
items.sort((a, b) => (b['avg'] as double).compareTo(a['avg'] as double));
final avgs = items.map<double>((e) => e['avg'] as double).toList();
final rs = ranksForDoubles(avgs);
for (int i = 0; i < items.length; i++) {
items[i]['rank'] = rs[i];
}
return items;
}
static List<Map<String, dynamic>> teamRanking(List<PublicTeam> teams) {
// 팀 총점 = 구성원 총점 합 + 팀 핸디캡 합(여기서는 팀 handicap만 단일 값으로 가정)
final items = teams
.map((t) {
int membersTotal = 0;
for (final m in t.members) {
int mt = 0;
for (int i = 0; i < m.rawScores.length; i++) {
final raw = m.rawScores[i];
final hc = (i < m.handicaps.length) ? m.handicaps[i] : 0;
if (raw == null) continue;
mt += capScore(raw + hc);
}
membersTotal += mt;
}
final total = membersTotal + t.handicap;
return {'team': t, 'total': total};
})
.toList();
items.sort((a, b) => (b['total'] as int).compareTo(a['total'] as int));
final totals = items.map<int>((e) => e['total'] as int).toList();
final rs = ranksFor(totals);
for (int i = 0; i < items.length; i++) {
items[i]['rank'] = rs[i];
}
return items;
}
// 참가자별 총점과 랭크 맵 생성 (participantId -> {total, rank})
static Map<String, Map<String, int>> buildParticipantRankMap(List<PublicParticipant> list) {
final ranked = individualRanking(list);
final map = <String, Map<String, int>>{};
for (final item in ranked) {
final p = item['p'] as PublicParticipant;
map[p.participantId] = {
'total': item['total'] as int,
'rank': item['rank'] as int,
};
}
return map;
}
// 팀별 총점 맵 (teamNumber -> {total, rank})
static Map<int, Map<String, int>> buildTeamRankMap(List<PublicTeam> teams) {
final ranked = teamRanking(teams);
final map = <int, Map<String, int>>{};
for (final item in ranked) {
final t = item['team'] as PublicTeam;
map[t.teamNumber] = {
'total': item['total'] as int,
'rank': item['rank'] as int,
};
}
return map;
}
// 미배정 참가자 목록 계산
static List<PublicParticipant> unassignedParticipants(List<PublicParticipant> participants, List<PublicTeam> teams) {
final assigned = <String>{};
for (final t in teams) {
for (final m in t.members) {
final pid = m.participantId;
if (pid != null && pid.isNotEmpty) assigned.add(pid);
}
}
return participants.where((p) => !assigned.contains(p.participantId)).toList();
}
}
+19
View File
@@ -0,0 +1,19 @@
// Intentionally minimal; no platform imports to keep tests stable
// Minimal web storage wrapper. On web uses window.localStorage; otherwise in-memory.
// Avoids flutter_secure_storage_web to keep WebAssembly warnings out.
class WebStorage {
static final Map<String, String> _memory = <String, String>{};
static String? getItem(String key) {
return _memory[key];
}
static void setItem(String key, String value) {
_memory[key] = value;
}
static void removeItem(String key) {
_memory.remove(key);
}
}