이벤트 가져오기 위젯 테스트 추가 및 테스트 훅 도입

This commit is contained in:
2025-09-12 06:33:44 +09:00
parent 107abc963f
commit a5f2544d11
121 changed files with 39277 additions and 3911 deletions
+7 -2
View File
@@ -14,7 +14,7 @@ class ApiService {
late Dio _dio;
final CookieJar _cookieJar = CookieJar();
String? _token;
ApiService._internal() {
_dio = Dio(BaseOptions(
@@ -36,9 +36,14 @@ class ApiService {
));
}
// 테스트를 위한 Dio 주입 지점
@visibleForTesting
void setDio(Dio dio) {
_dio = dio;
}
// 토큰 설정
void setToken(String token) {
_token = token;
_dio.options.headers['Authorization'] = 'Bearer $token';
}
+7 -2
View File
@@ -7,12 +7,17 @@ import '../models/user_model.dart';
import './api_service.dart';
class AuthService with ChangeNotifier {
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
// 주입 가능하도록 생성자에서 설정, 기본값 유지
final FlutterSecureStorage _secureStorage;
final ApiService _apiService;
User? _currentUser;
String? _token;
bool _isLoading = false;
bool _isInitialized = false;
final ApiService _apiService = ApiService();
AuthService({ApiService? apiService, FlutterSecureStorage? secureStorage})
: _apiService = apiService ?? ApiService(),
_secureStorage = secureStorage ?? const FlutterSecureStorage();
User? get currentUser => _currentUser;
String? get token => _token;
+36 -24
View File
@@ -13,6 +13,7 @@ class ClubService with ChangeNotifier {
bool _isLoading = false;
String? _token;
ApiService _apiService;
bool _disposed = false;
// 기본 생성자
ClubService() : _apiService = ApiService();
@@ -26,6 +27,7 @@ class ClubService with ChangeNotifier {
// 초기화 함수
Future<void> initialize(String token) async {
if (_disposed) return;
_token = token;
_apiService.setToken(token);
@@ -34,16 +36,17 @@ class ClubService with ChangeNotifier {
final clubId = prefs.getString(ApiConfig.clubIdKey);
if (clubId != null) {
if (_disposed) return;
await fetchClubById(clubId);
}
}
// 사용자의 모든 클럽 가져오기
Future<void> fetchUserClubs() async {
if (_token == null) return;
if (_disposed || _token == null) return;
_isLoading = true;
notifyListeners();
if (!_disposed) notifyListeners();
try {
final data = await _apiService.post('${ApiConfig.clubs}/user');
@@ -53,24 +56,24 @@ class ClubService with ChangeNotifier {
_clubs = clubsData.map((clubData) => Club.fromJson(clubData)).toList();
_isLoading = false;
notifyListeners();
if (!_disposed) notifyListeners();
} catch (e) {
_isLoading = false;
notifyListeners();
if (!_disposed) notifyListeners();
throw Exception('클럽 목록을 불러오는데 실패했습니다: $e');
}
}
// ID로 클럽 정보 가져오기
Future<void> fetchClubById(String clubId) async {
if (_token == null) return;
if (_disposed || _token == null) return;
_isLoading = true;
notifyListeners();
if (!_disposed) notifyListeners();
try {
final data = await _apiService.post(
'${ApiConfig.clubs}',
ApiConfig.clubs,
data: {'clubId': clubId},
);
@@ -82,10 +85,10 @@ class ClubService with ChangeNotifier {
await prefs.setString(ApiConfig.clubIdKey, clubId);
_isLoading = false;
notifyListeners();
if (!_disposed) notifyListeners();
} catch (e) {
_isLoading = false;
notifyListeners();
if (!_disposed) notifyListeners();
throw Exception('클럽 정보를 불러오는데 실패했습니다: $e');
}
}
@@ -97,10 +100,10 @@ class ClubService with ChangeNotifier {
// 백엔드에 클럽 선택 요청 (세션에 clubId 저장)
Future<void> selectClub(String clubId) async {
if (_token == null) return;
if (_disposed || _token == null) return;
_isLoading = true;
notifyListeners();
if (!_disposed) notifyListeners();
try {
await _apiService.post(
@@ -109,6 +112,7 @@ class ClubService with ChangeNotifier {
);
// 클럽 선택 성공 후 클럽 정보 가져오기
if (_disposed) return;
await fetchClubById(clubId);
// 클럽 ID를 로컬에 저장
@@ -121,13 +125,15 @@ class ClubService with ChangeNotifier {
}
// 클럽 변경 이벤트 발생 - 다른 서비스들에게 알림
EventBus().fire(ClubChangedEvent(clubId));
if (!_disposed) {
EventBus().fire(ClubChangedEvent(clubId));
}
_isLoading = false;
notifyListeners();
if (!_disposed) notifyListeners();
} catch (e) {
_isLoading = false;
notifyListeners();
if (!_disposed) notifyListeners();
throw Exception('클럽 선택에 실패했습니다: $e');
}
}
@@ -139,11 +145,11 @@ class ClubService with ChangeNotifier {
}
_isLoading = true;
notifyListeners();
if (!_disposed) notifyListeners();
try {
final data = await _apiService.post(
'${ApiConfig.clubs}',
ApiConfig.clubs,
data: club.toJson(),
);
@@ -157,24 +163,24 @@ class ClubService with ChangeNotifier {
await prefs.setString(ApiConfig.clubIdKey, newClub.id);
_isLoading = false;
notifyListeners();
if (!_disposed) notifyListeners();
return newClub;
} catch (e) {
_isLoading = false;
notifyListeners();
if (!_disposed) notifyListeners();
throw Exception('클럽 생성에 실패했습니다: $e');
}
}
// 클럽 정보 업데이트
Future<Club> updateClub(String clubId, Map<String, dynamic> updates) async {
if (_token == null) {
if (_disposed || _token == null) {
throw Exception('인증이 필요합니다');
}
_isLoading = true;
notifyListeners();
if (!_disposed) notifyListeners();
try {
final data = await _apiService.put(
@@ -202,19 +208,19 @@ class ClubService with ChangeNotifier {
}
_isLoading = false;
notifyListeners();
if (!_disposed) notifyListeners();
return updatedClub;
} catch (e) {
_isLoading = false;
notifyListeners();
if (!_disposed) notifyListeners();
throw Exception('클럽 정보 업데이트에 실패했습니다: $e');
}
}
// 클럽 정보 가져오기 (ID로)
Future<Club> fetchClub(String clubId) async {
if (_token == null) {
if (_disposed || _token == null) {
throw Exception('인증이 필요합니다');
}
@@ -225,7 +231,7 @@ class ClubService with ChangeNotifier {
}
final data = await _apiService.post(
'${ApiConfig.clubs}',
ApiConfig.clubs,
data: {'clubId': clubId},
);
@@ -235,4 +241,10 @@ class ClubService with ChangeNotifier {
throw Exception('클럽 정보를 불러오는데 실패했습니다: $e');
}
}
@override
void dispose() {
_disposed = true;
super.dispose();
}
}
+2 -2
View File
@@ -187,7 +187,7 @@ class EventBus {
if (kDebugMode) {
debugPrint('EventBus: 인스턴스 정리 중 - ${entry.key}');
}
entry.value.dispose(); // 완전히 dispose 호출
await entry.value.dispose(); // 완전히 dispose 호출
// 각 인스턴스 dispose 후 마이크로태스크 실행으로 비동기 작업 완료 보장
await Future.microtask(() {});
@@ -344,7 +344,7 @@ class EventBus {
// 테스트 ID가 있지만 인스턴스가 없는 경우 생성
if (!_testInstances.containsKey(_currentTestId!)) {
if (kDebugMode) {
debugPrint('EventBus: 테스트 ID ${_currentTestId} 대한 새 인스턴스 생성');
debugPrint('EventBus: 테스트 ID $_currentTestId에 대한 새 인스턴스 생성');
}
_testInstances[_currentTestId!] = EventBus._internal();
_testInstances[_currentTestId!]!.reset();
+74 -14
View File
@@ -16,12 +16,12 @@ class EventService with ChangeNotifier {
List<Team> _teams = [];
bool _isLoading = false;
String? _token;
ApiService _apiService;
final ApiService _apiService;
String? _clubId;
// 기본 생성자
EventService() : _apiService = ApiService();
// 테스트용 생성자
EventService.forTest(this._apiService);
@@ -210,12 +210,23 @@ class EventService with ChangeNotifier {
notifyListeners();
try {
// userId는 일부 라우트에서 필수이므로 prefs에서 읽어 포함
final prefs = await SharedPreferences.getInstance();
final userId = prefs.getString(ApiConfig.userIdKey);
final payload = {
'clubId': _clubId,
if (userId != null) 'userId': userId,
'eventId': eventId,
};
final data = await _apiService.post(
'${ApiConfig.clubs}/events/participants',
data: {'eventId': eventId},
'${ApiConfig.clubs}/events/$eventId/participants/list',
data: payload,
);
final List<dynamic> participantsData = data['participants'] ?? [];
// 응답이 리스트이거나 객체 내 participants 키로 올 수 있어 모두 처리
final List<dynamic> participantsData = (data is List)
? data
: (data['participants'] ?? []);
_participants = participantsData
.map((participantData) => Participant.fromJson(participantData))
.toList();
@@ -244,9 +255,12 @@ class EventService with ChangeNotifier {
notifyListeners();
try {
// 참가자 페이로드 정규화: 상태/결제 상태 표준화 및 빈 문자열 null 처리
final normalized = _sanitizeParticipantPayload(participantData);
final payload = {'clubId': _clubId, ...normalized};
final data = await _apiService.post(
'${ApiConfig.clubs}/events/$eventId/participants',
data: participantData,
data: payload,
);
final newParticipant = Participant.fromJson(data['participant']);
@@ -278,9 +292,11 @@ class EventService with ChangeNotifier {
notifyListeners();
try {
// 업데이트 페이로드도 동일한 규칙으로 정규화
final normalized = _sanitizeParticipantPayload(updates);
final data = await _apiService.put(
'${ApiConfig.clubs}/events/$eventId/participants/$participantId',
data: updates,
data: normalized,
);
final updatedParticipant = Participant.fromJson(data['participant']);
@@ -304,6 +320,52 @@ class EventService with ChangeNotifier {
}
}
// 참가자 페이로드 정규화 헬퍼
Map<String, dynamic> _sanitizeParticipantPayload(Map<String, dynamic> data) {
final Map<String, dynamic> out = Map<String, dynamic>.from(data);
// 상태 표준화: 구버전/철자 변형 호환
final statusRaw = (out['status']?.toString().trim() ?? '').toLowerCase();
if (statusRaw.isNotEmpty) {
String status = statusRaw;
if (status == 'cancelled') status = 'canceled';
// 허용 목록: registered, pending, confirmed, canceled
if (![
'registered',
'pending',
'confirmed',
'canceled',
].contains(status)) {
// 허용 외 값은 안전하게 pending 처리
status = 'pending';
}
out['status'] = status;
}
// 결제 상태 표준화: isPaid -> paymentStatus 변환 및 강제
if (out.containsKey('isPaid') &&
(out['paymentStatus'] == null ||
(out['paymentStatus'].toString().isEmpty))) {
final isPaidVal = out['isPaid'] == true;
out['paymentStatus'] = isPaidVal ? 'paid' : 'unpaid';
}
final payRaw = (out['paymentStatus']?.toString().trim() ?? '')
.toLowerCase();
if (payRaw.isNotEmpty) {
out['paymentStatus'] = (payRaw == 'paid') ? 'paid' : 'unpaid';
}
// 서버 스키마에 맞추기 위해 isPaid 키는 제거 (서버는 paymentStatus만 사용)
out.remove('isPaid');
// 빈 문자열을 명시적 null로 변환 (이름/이메일/전화/메모 등)
out.updateAll((key, value) {
if (value is String && value.trim().isEmpty) return null;
return value;
});
return out;
}
// 참가자 삭제
Future<bool> removeParticipant(String eventId, String participantId) async {
if (_token == null || _clubId == null) {
@@ -485,9 +547,7 @@ class EventService with ChangeNotifier {
);
final List<dynamic> teamsData = data['teams'] ?? [];
_teams = teamsData
.map((teamData) => Team.fromJson(teamData))
.toList();
_teams = teamsData.map((teamData) => Team.fromJson(teamData)).toList();
notifyListeners();
return _teams;
@@ -495,7 +555,7 @@ class EventService with ChangeNotifier {
throw Exception('팀 목록을 불러오는데 실패했습니다: $e');
}
}
// 이벤트에 팀이 있는지 확인 (간략 조회)
Future<bool> hasEventTeams(String eventId) async {
if (_token == null) {
@@ -622,7 +682,7 @@ class EventService with ChangeNotifier {
throw Exception('파일에서 이벤트 생성에 실패했습니다: $e');
}
}
// 이벤트 복제
Future<Event> cloneEvent(String eventId, {String? newName}) async {
if (_token == null || _clubId == null) {
@@ -635,7 +695,7 @@ class EventService with ChangeNotifier {
try {
// 원본 이벤트 정보 가져오기
final originalEvent = await fetchEventById(eventId);
// 복제할 이벤트 데이터 준비
final Map<String, dynamic> cloneData = {
'clubId': _clubId,
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
import '../config/api_config.dart';
import '../models/subscription_model.dart';
@@ -23,7 +23,6 @@ class InAppPurchaseService extends ChangeNotifier {
// 구매 검증 관련 변수
bool _purchaseVerified = false;
Map<String, dynamic>? _verifiedPurchase;
// 구독 상품 ID (스토어에 등록된 ID와 일치해야 함)
final Map<SubscriptionPlanType, String> _subscriptionIds = {
@@ -241,14 +240,9 @@ class InAppPurchaseService extends ChangeNotifier {
if (response.statusCode == 200) {
// 검증 성공
final data = jsonDecode(response.body);
jsonDecode(response.body);
// 성공 이벤트 발생 (SubscriptionService에서 구독 정보 업데이트)
_purchaseVerified = true;
_verifiedPurchase = {
'transactionId': transactionId,
'productId': productId,
'verificationData': data,
};
notifyListeners();
return true;
} else {
+59 -50
View File
@@ -9,12 +9,14 @@ import './event_bus.dart';
class MemberService with ChangeNotifier {
List<Member> _members = [];
Member? _currentMember;
bool _isLoading = false;
String? _token;
String? _clubId;
final ApiService _apiService;
StreamSubscription? _eventSubscription;
String? _lastError;
DateTime? _lastLoadedAt;
bool _disposed = false;
// 기본 생성자
MemberService() : _apiService = ApiService();
@@ -24,6 +26,8 @@ class MemberService with ChangeNotifier {
List<Member> get members => [..._members];
bool get isLoading => _isLoading;
String? get lastError => _lastError;
DateTime? get lastLoadedAt => _lastLoadedAt;
// 초기화 함수
void initialize(String token) {
@@ -33,80 +37,52 @@ class MemberService with ChangeNotifier {
// 이벤트 구독 - 기존 구독이 있으면 취소 후 다시 구독
_eventSubscription?.cancel();
_eventSubscription = EventBus().on<ClubChangedEvent>().listen((event) {
if (_disposed) return; // dispose 이후 방어
if (event.clubId.isNotEmpty) {
setClubId(event.clubId);
}
});
}
// 비동기 리소스 상태 로깅
Future<void> _logResourceState(String context) async {
if (kDebugMode) {
debugPrint('===== MemberService 비동기 리소스 상태 ($context) =====');
debugPrint('시간: ${DateTime.now().toIso8601String()}');
debugPrint('구독 상태: ${_eventSubscription != null ? '활성' : '비활성'}');
debugPrint('클럽 ID: $_clubId');
debugPrint('토큰 상태: ${_token != null ? '있음' : '없음'}');
debugPrint('로딩 상태: $_isLoading');
debugPrint('회원 수: ${_members.length}');
debugPrint('EventBus 활성 구독 수: ${EventBus.activeSubscriptionCount}');
debugPrint('===========================================');
}
}
// (개발 중 로깅 유틸 제거됨)
@override
Future<void> dispose() async {
void dispose() {
// dispose 시작 상태 로깅
if (kDebugMode) {
debugPrint('MemberService: dispose 시작');
}
await _logResourceState('dispose 시작');
// 이벤트 구독 취소 확실히 처리
// 비동기 상태 로깅은 테스트/런타임 안정성을 위해 동기 경로로 축소
// (필요 시 개발 중 수동 호출)
// 이벤트 구독 취소: 동기 dispose 규약을 지키기 위해 unawaited로 처리
_disposed = true; // 이후 작업 차단
if (_eventSubscription != null) {
try {
// 구독 취소 전 로깅
if (kDebugMode) {
debugPrint('MemberService: 구독 취소 시작');
}
// 구독 취소
_eventSubscription!.cancel();
// 마이크로태스크 실행으로 비동기 작업 완료 보장
await Future.microtask(() {});
// 구독 취소 후 로깅
if (kDebugMode) {
debugPrint('MemberService: 구독 취소 완료');
}
// 구독 참조 제거
unawaited(_eventSubscription!.cancel());
_eventSubscription = null;
if (kDebugMode) {
debugPrint('MemberService: 구독 취소 요청');
}
} catch (e) {
if (kDebugMode) {
debugPrint('MemberService: 구독 취소 중 오류 - $e');
}
}
}
// 추가 마이크로태스크 실행으로 비동기 작업 완료 보장 (타이머 생성 회피)
await Future.microtask(() {});
await Future.microtask(() {});
// dispose 완료 후 상태 로깅
await _logResourceState('dispose 완료');
// 구독 수 로깅 - 지연 없이 즉시 처리
if (kDebugMode) {
debugPrint('MemberService: dispose 완료 후 구독 수 - ${EventBus.activeSubscriptionCount}');
debugPrint('MemberService: dispose 종료');
}
super.dispose();
}
// 클럽 ID 설정
Future<void> setClubId(String clubId) async {
if (_disposed) return; // dispose 이후 무시
// 클럽 ID가 변경된 경우에만 처리
if (_clubId != clubId) {
_clubId = clubId;
@@ -126,9 +102,14 @@ class MemberService with ChangeNotifier {
// 클럽의 모든 회원 가져오기
Future<void> fetchClubMembers() async {
print('fetchClubMembers 호출됨: token=${_token}');
if (_disposed) return; // dispose 이후 무시
final sw = Stopwatch()..start();
if (kDebugMode) {
debugPrint('MemberService.fetchClubMembers: 시작 (token=${_token != null}, clubId=$_clubId)');
}
_isLoading = true;
_lastError = null;
notifyListeners();
try {
@@ -142,6 +123,9 @@ class MemberService with ChangeNotifier {
}
if (clubId == null) {
if (kDebugMode) {
debugPrint('MemberService.fetchClubMembers: clubId 없음 - SharedPreferences에도 없음');
}
throw Exception('선택된 클럽이 없습니다');
}
@@ -150,7 +134,9 @@ class MemberService with ChangeNotifier {
data: {'clubId': clubId},
);
print('회원 응답 데이터: $data');
if (kDebugMode) {
debugPrint('MemberService.fetchClubMembers: 응답 수신 (elapsed=${sw.elapsedMilliseconds}ms)');
}
// 응답 데이터가 리스트 형태로 직접 왔으므로 그대로 사용
if (data is List) {
@@ -158,21 +144,40 @@ class MemberService with ChangeNotifier {
_members = membersData
.map((memberData) => Member.fromJson(memberData))
.toList();
print('변환된 회원 리스트 길이: ${_members.length}');
if (kDebugMode) {
debugPrint('MemberService.fetchClubMembers: 리스트 파싱 완료 (count=${_members.length})');
if (_members.isEmpty) {
debugPrint('MemberService.fetchClubMembers: 회원 목록이 비어있음');
}
}
} else {
// 응답 데이터가 객체 형태로 왔을 경우 members 키를 통해 접근
final List<dynamic> membersData = data['members'] ?? [];
_members = membersData
.map((memberData) => Member.fromJson(memberData))
.toList();
print('변환된 회원 리스트 길이: ${_members.length}');
if (kDebugMode) {
debugPrint('MemberService.fetchClubMembers: 객체 파싱 완료 (count=${_members.length})');
if (_members.isEmpty) {
debugPrint('MemberService.fetchClubMembers: 회원 목록이 비어있음');
}
}
}
_isLoading = false;
_lastLoadedAt = DateTime.now();
notifyListeners();
} catch (e) {
if (kDebugMode) {
debugPrint('MemberService.fetchClubMembers: 성공 (total=${_members.length}, elapsed=${sw.elapsedMilliseconds}ms, loadedAt=${_lastLoadedAt?.toIso8601String()})');
}
} catch (e, st) {
_isLoading = false;
_lastError = e.toString();
notifyListeners();
if (kDebugMode) {
debugPrint('MemberService.fetchClubMembers: 실패 (${e.toString()})');
debugPrint('MemberService.fetchClubMembers: stacktrace -> $st');
}
throw Exception('회원 목록을 불러오는데 실패했습니다: $e');
}
}
@@ -210,9 +215,13 @@ class MemberService with ChangeNotifier {
notifyListeners();
try {
final payload = {
'clubId': _clubId,
...memberData,
};
final data = await _apiService.post(
'${ApiConfig.clubs}/members/add',
data: memberData,
data: payload,
);
// 백엔드 응답 처리: 직접 회원 객체가 반환되거나 {member: {...}} 형태로 반환될 수 있음
+23 -1
View File
@@ -13,6 +13,8 @@ class ScoreService with ChangeNotifier {
ApiService _apiService = ApiService();
String? _memberId;
String? _eventId;
DateTime? _lastLoadedAt;
String? _lastError;
// 테스트용 생성자
ScoreService.forTest(ApiService apiService, {String? clubId, String? memberId, String? eventId}) {
@@ -37,6 +39,8 @@ class ScoreService with ChangeNotifier {
List<Score> get scores => [..._scores];
bool get isLoading => _isLoading;
DateTime? get lastLoadedAt => _lastLoadedAt;
String? get lastError => _lastError;
// 초기화 함수
void initialize(String token) {
@@ -61,6 +65,7 @@ class ScoreService with ChangeNotifier {
print('fetchClubScores 호출됨: token=$_token');
_isLoading = true;
_lastError = null;
notifyListeners();
try {
@@ -92,9 +97,11 @@ class ScoreService with ChangeNotifier {
}
_isLoading = false;
_lastLoadedAt = DateTime.now();
notifyListeners();
} catch (e) {
_isLoading = false;
_lastError = e.toString();
notifyListeners();
throw Exception('점수 목록을 불러오는데 실패했습니다: $e');
}
@@ -107,6 +114,7 @@ class ScoreService with ChangeNotifier {
}
_isLoading = true;
_lastError = null;
notifyListeners();
try {
@@ -127,6 +135,7 @@ class ScoreService with ChangeNotifier {
return memberScores;
} catch (e) {
_isLoading = false;
_lastError = e.toString();
notifyListeners();
throw Exception('회원 점수를 불러오는데 실패했습니다: $e');
}
@@ -139,6 +148,7 @@ class ScoreService with ChangeNotifier {
}
_isLoading = true;
_lastError = null;
notifyListeners();
try {
@@ -159,6 +169,7 @@ class ScoreService with ChangeNotifier {
return eventScores;
} catch (e) {
_isLoading = false;
_lastError = e.toString();
notifyListeners();
throw Exception('이벤트 점수를 불러오는데 실패했습니다: $e');
}
@@ -171,6 +182,7 @@ class ScoreService with ChangeNotifier {
}
_isLoading = true;
_lastError = null;
notifyListeners();
try {
@@ -186,6 +198,7 @@ class ScoreService with ChangeNotifier {
return newScore;
} catch (e) {
_isLoading = false;
_lastError = e.toString();
notifyListeners();
throw Exception('점수 추가에 실패했습니다: $e');
}
@@ -223,6 +236,7 @@ class ScoreService with ChangeNotifier {
return updatedScore;
} catch (e) {
_isLoading = false;
_lastError = e.toString();
notifyListeners();
throw Exception('점수 수정에 실패했습니다: $e');
}
@@ -249,6 +263,7 @@ class ScoreService with ChangeNotifier {
return true;
} catch (e) {
_isLoading = false;
_lastError = e.toString();
notifyListeners();
throw Exception('점수 삭제에 실패했습니다: $e');
}
@@ -257,17 +272,21 @@ class ScoreService with ChangeNotifier {
// 회원 통계 가져오기
Future<ScoreStatistics> fetchMemberStatistics(String memberId) async {
if (_token == null || _clubId == null) {
throw Exception('인증 또는 클럽 정보가 필요합니다');
throw Exception('인증 정보가 필요합니다');
}
_lastError = null;
try {
final data = await _apiService.post(
'${ApiConfig.clubs}/members/stats',
data: {'memberId': memberId},
);
_lastLoadedAt = DateTime.now();
return ScoreStatistics.fromJson(data['statistics']);
} catch (e) {
_lastError = e.toString();
throw Exception('회원 통계를 불러오는데 실패했습니다: $e');
}
}
@@ -309,8 +328,11 @@ class ScoreService with ChangeNotifier {
});
}
_lastLoadedAt = DateTime.now();
_lastError = null;
return statistics;
} catch (e) {
_lastError = e.toString();
throw Exception('클럽 통계를 불러오는데 실패했습니다: $e');
}
}