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

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
+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: {...}} 형태로 반환될 수 있음