이벤트 가져오기 위젯 테스트 추가 및 테스트 훅 도입
This commit is contained in:
@@ -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,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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: {...}} 형태로 반환될 수 있음
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user