TDD 작성
This commit is contained in:
@@ -211,4 +211,28 @@ class ClubService with ChangeNotifier {
|
||||
throw Exception('클럽 정보 업데이트에 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 클럽 정보 가져오기 (ID로)
|
||||
Future<Club> fetchClub(String clubId) async {
|
||||
if (_token == null) {
|
||||
throw Exception('인증이 필요합니다');
|
||||
}
|
||||
|
||||
try {
|
||||
// 현재 클럽이 요청한 클럽과 동일한 경우 캐시된 데이터 반환
|
||||
if (_currentClub != null && _currentClub!.id == clubId) {
|
||||
return _currentClub!;
|
||||
}
|
||||
|
||||
final data = await _apiService.post(
|
||||
'${ApiConfig.clubs}',
|
||||
data: {'clubId': clubId},
|
||||
);
|
||||
|
||||
final club = Club.fromJson(data);
|
||||
return club;
|
||||
} catch (e) {
|
||||
throw Exception('클럽 정보를 불러오는데 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,506 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer' as developer;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:lanebow/utils/test_utils.dart';
|
||||
import 'package:lanebow/utils/test_config.dart';
|
||||
|
||||
class EventBus {
|
||||
static final EventBus _instance = EventBus._internal();
|
||||
|
||||
factory EventBus() => _instance;
|
||||
|
||||
EventBus._internal();
|
||||
|
||||
final _streamController = StreamController.broadcast();
|
||||
|
||||
Stream get stream => _streamController.stream;
|
||||
|
||||
void fire(dynamic event) {
|
||||
_streamController.add(event);
|
||||
}
|
||||
|
||||
// 특정 타입의 이벤트를 구독하는 메서드
|
||||
Stream<T> on<T>() {
|
||||
return stream.where((event) => event is T).cast<T>();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_streamController.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 이벤트 클래스들
|
||||
// 이벤트 클래스들 - 클래스 외부로 이동
|
||||
class ClubChangedEvent {
|
||||
final String clubId;
|
||||
|
||||
ClubChangedEvent(this.clubId);
|
||||
|
||||
@override
|
||||
String toString() => 'ClubChangedEvent(clubId: $clubId)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is ClubChangedEvent && other.clubId == clubId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => clubId.hashCode;
|
||||
}
|
||||
|
||||
class EventBus {
|
||||
// 테스트 환경에서 사용할 별도의 인스턴스 맵
|
||||
static final Map<String, EventBus> _testInstances = {};
|
||||
|
||||
// 기본 싱글톤 인스턴스
|
||||
static final EventBus _instance = EventBus._internal();
|
||||
|
||||
// 테스트 ID를 저장하는 변수 (테스트 격리용)
|
||||
static String? _currentTestId;
|
||||
|
||||
// 테스트 환경 초기화 상태 추적
|
||||
static bool _isTestEnvironmentReady = false;
|
||||
|
||||
// 테스트 환경 초기화 관련 로깅
|
||||
static void _logTestEnvironmentInit() {
|
||||
_isTestEnvironmentReady = true;
|
||||
if (kDebugMode) {
|
||||
debugPrint('EventBus: 테스트 환경 초기화 완료');
|
||||
}
|
||||
}
|
||||
|
||||
// 활성 구독 수 추적 (디버깅 및 메모리 누수 감지용)
|
||||
static int _activeSubscriptionCount = 0;
|
||||
|
||||
/// 현재 활성화된 테스트 인스턴스 수 반환
|
||||
static int get testInstanceCount => _testInstances.length;
|
||||
|
||||
// 테스트 재시도 횟수 추적 및 관리
|
||||
static int _testRetryCount = 0;
|
||||
|
||||
// 테스트 재시도 횟수 가져오기
|
||||
static int get testRetryCount => _testRetryCount;
|
||||
|
||||
// 테스트 재시도 횟수 증가
|
||||
static void incrementRetryCount() {
|
||||
_testRetryCount++;
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: 테스트 재시도 횟수 증가 - $_testRetryCount');
|
||||
}
|
||||
}
|
||||
|
||||
// 활성 구독 수 접근 게터
|
||||
static int get activeSubscriptionCount => _activeSubscriptionCount;
|
||||
|
||||
// 현재 테스트 ID 설정 메서드
|
||||
static void setCurrentTestId(String testId) {
|
||||
// 테스트 설정 로드 확인 및 로깅
|
||||
final config = TestConfig.instance;
|
||||
if (config.isVerboseLogging) {
|
||||
debugPrint('EventBus: 테스트 설정 로드 - asyncTimeout=${config.asyncTimeout}ms, testDelay=${config.testDelay}ms');
|
||||
}
|
||||
// 이전 테스트 ID가 있으면 해당 인스턴스 정리
|
||||
if (_currentTestId != null && _currentTestId != testId) {
|
||||
if (_testInstances.containsKey(_currentTestId!)) {
|
||||
try {
|
||||
_testInstances[_currentTestId!]!.dispose();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('EventBus: 이전 인스턴스 dispose 중 오류 - $e');
|
||||
}
|
||||
} finally {
|
||||
_testInstances.remove(_currentTestId!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 새 테스트 ID 설정
|
||||
_currentTestId = testId;
|
||||
|
||||
// 새 테스트 ID에 대한 인스턴스가 없으면 생성
|
||||
if (!_testInstances.containsKey(testId)) {
|
||||
_testInstances[testId] = EventBus._internal();
|
||||
} else {
|
||||
// 이미 존재하는 인스턴스라도 초기화 수행
|
||||
_testInstances[testId]!.reset();
|
||||
}
|
||||
|
||||
// 테스트 환경 초기화 로깅
|
||||
_logTestEnvironmentInit();
|
||||
|
||||
// 활성 구독 수 초기화
|
||||
_activeSubscriptionCount = 0;
|
||||
|
||||
// 테스트 재시도 횟수 초기화
|
||||
_testRetryCount = 0;
|
||||
|
||||
if (kDebugMode) {
|
||||
debugPrint('EventBus: 테스트 ID 설정됨 - $testId');
|
||||
developer.log('EventBus 메모리 상태: $_testInstances', name: 'EventBus');
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 테스트 ID 초기화 메서드
|
||||
static Future<void> tearDownTestEnvironment() async {
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: 테스트 환경 초기화 해제 시작');
|
||||
}
|
||||
|
||||
// 테스트 환경 해제 전 비동기 리소스 상태 로깅
|
||||
await _logAsyncResourceState('테스트 환경 해제 전');
|
||||
|
||||
// 테스트 종료 전 남아있는 타이머가 완료될 시간 제공
|
||||
await _ensureAllTimersComplete();
|
||||
|
||||
// 현재 테스트 ID에 해당하는 인스턴스가 있는 경우 먼저 dispose 호출
|
||||
if (_currentTestId != null && _testInstances.containsKey(_currentTestId!)) {
|
||||
try {
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: 현재 테스트 ID($_currentTestId) 인스턴스 dispose 시도');
|
||||
}
|
||||
await _testInstances[_currentTestId!]!.dispose();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('EventBus: 현재 테스트 인스턴스 dispose 중 오류 - $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_currentTestId = null;
|
||||
// 테스트 환경 초기화 해제 로깅
|
||||
_isTestEnvironmentReady = false;
|
||||
if (kDebugMode) {
|
||||
debugPrint('EventBus: 테스트 환경 초기화 해제');
|
||||
}
|
||||
_activeSubscriptionCount = 0;
|
||||
|
||||
// 테스트 환경 해제 후 비동기 리소스 상태 로깅
|
||||
await _logAsyncResourceState('테스트 환경 해제 후');
|
||||
|
||||
// 메모리 누수 방지를 위해 GC 힌트 추가
|
||||
if (TestUtils.isInTest) {
|
||||
developer.log('EventBus 메모리 정리 요청', name: 'EventBus');
|
||||
}
|
||||
|
||||
// 추가 마이크로태스크 실행으로 비동기 작업 완료 보장
|
||||
await Future.microtask(() {});
|
||||
await Future.microtask(() {});
|
||||
}
|
||||
|
||||
// 테스트 ID 초기화 (후방 호환성을 위해 유지)
|
||||
static Future<void> clearCurrentTestId() async {
|
||||
// tearDownTestEnvironment로 대체되었지만 후방 호환성을 위해 유지
|
||||
await tearDownTestEnvironment();
|
||||
}
|
||||
|
||||
// 모든 테스트 인스턴스를 정리하는 정적 메서드
|
||||
static Future<void> resetAllTestInstances() async {
|
||||
if (_testInstances.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: 모든 테스트 인스턴스 초기화 시작 (인스턴스 수: ${_testInstances.length})');
|
||||
}
|
||||
|
||||
// 초기화 전 비동기 리소스 상태 로깅
|
||||
await _logAsyncResourceState('모든 테스트 인스턴스 초기화 전');
|
||||
|
||||
// 기존 인스턴스 모두 정리
|
||||
for (var entry in _testInstances.entries) {
|
||||
try {
|
||||
if (kDebugMode) {
|
||||
debugPrint('EventBus: 인스턴스 정리 중 - ${entry.key}');
|
||||
}
|
||||
entry.value.dispose(); // 완전히 dispose 호출
|
||||
|
||||
// 각 인스턴스 dispose 후 마이크로태스크 실행으로 비동기 작업 완료 보장
|
||||
await Future.microtask(() {});
|
||||
} catch (e) {
|
||||
// dispose 중 오류 무시
|
||||
if (kDebugMode) {
|
||||
debugPrint('EventBus: 인스턴스 정리 중 오류 - $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 인스턴스 정리 후 중간 상태 로깅
|
||||
await _logAsyncResourceState('테스트 인스턴스 dispose 후, 초기화 전');
|
||||
|
||||
// 모든 테스트 인스턴스 제거
|
||||
_testInstances.clear();
|
||||
|
||||
// 기본 인스턴스도 초기화
|
||||
try {
|
||||
_instance.reset();
|
||||
await Future.microtask(() {}); // 기본 인스턴스 초기화 후 마이크로태스크 실행
|
||||
} catch (e) {
|
||||
// 기본 인스턴스 초기화 중 오류 무시
|
||||
if (kDebugMode) {
|
||||
debugPrint('EventBus: 기본 인스턴스 초기화 중 오류 - $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 테스트 ID도 초기화
|
||||
_currentTestId = null;
|
||||
// 테스트 환경 초기화 해제 로깅
|
||||
if (_isTestEnvironmentReady) {
|
||||
_isTestEnvironmentReady = false;
|
||||
if (kDebugMode) {
|
||||
debugPrint('EventBus: 모든 테스트 환경 초기화 해제');
|
||||
}
|
||||
}
|
||||
_activeSubscriptionCount = 0;
|
||||
|
||||
// 비동기 작업이 완료될 시간을 제공하되, 타이머 생성 없이 동기화
|
||||
// 타이머 생성 없이 마이크로태스크 큐 완료 보장
|
||||
await Future.microtask(() {});
|
||||
await Future.microtask(() {});
|
||||
|
||||
// 테스트 종료 전 남아있는 타이머가 완료될 시간 제공
|
||||
// 테스트 환경에서 !timersPending assertion 실패 방지
|
||||
await _ensureAllTimersComplete();
|
||||
|
||||
// 초기화 완료 후 비동기 리소스 상태 로깅
|
||||
await _logAsyncResourceState('모든 테스트 인스턴스 초기화 후');
|
||||
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: 모든 테스트 인스턴스 초기화됨');
|
||||
developer.log('EventBus 메모리 상태 초기화 완료', name: 'EventBus');
|
||||
}
|
||||
}
|
||||
|
||||
// 비동기 리소스 상태 진단 및 로깅
|
||||
static Future<Map<String, dynamic>> _diagnoseAsyncResources() async {
|
||||
final diagnosticInfo = <String, dynamic>{
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
'activeSubscriptions': _activeSubscriptionCount,
|
||||
'testInstances': _testInstances.length,
|
||||
'currentTestId': _currentTestId,
|
||||
'isTestEnvironmentReady': _isTestEnvironmentReady,
|
||||
'testRetryCount': _testRetryCount,
|
||||
};
|
||||
|
||||
// 인스턴스별 상태 수집
|
||||
final instanceStates = <String, Map<String, dynamic>>{};
|
||||
for (final entry in _testInstances.entries) {
|
||||
instanceStates[entry.key] = {
|
||||
'isDisposed': entry.value._isDisposed,
|
||||
'isStreamControllerClosed': entry.value._streamController.isClosed,
|
||||
};
|
||||
}
|
||||
diagnosticInfo['instanceStates'] = instanceStates;
|
||||
|
||||
// 기본 인스턴스 상태
|
||||
diagnosticInfo['defaultInstance'] = {
|
||||
'isDisposed': _instance._isDisposed,
|
||||
'isStreamControllerClosed': _instance._streamController.isClosed,
|
||||
};
|
||||
|
||||
return diagnosticInfo;
|
||||
}
|
||||
|
||||
// 비동기 리소스 상태 로깅
|
||||
static Future<void> _logAsyncResourceState(String context) async {
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
final diagnosticInfo = await _diagnoseAsyncResources();
|
||||
debugPrint('===== EventBus 비동기 리소스 상태 ($context) =====');
|
||||
debugPrint('시간: ${diagnosticInfo['timestamp']}');
|
||||
debugPrint('활성 구독 수: ${diagnosticInfo['activeSubscriptions']}');
|
||||
debugPrint('테스트 인스턴스 수: ${diagnosticInfo['testInstances']}');
|
||||
debugPrint('현재 테스트 ID: ${diagnosticInfo['currentTestId']}');
|
||||
debugPrint('테스트 환경 준비 상태: ${diagnosticInfo['isTestEnvironmentReady']}');
|
||||
debugPrint('테스트 재시도 횟수: ${diagnosticInfo['testRetryCount']}');
|
||||
|
||||
// 인스턴스별 상태 출력
|
||||
final instanceStates = diagnosticInfo['instanceStates'] as Map<String, Map<String, dynamic>>;
|
||||
if (instanceStates.isNotEmpty) {
|
||||
debugPrint('--- 테스트 인스턴스 상태 ---');
|
||||
instanceStates.forEach((key, value) {
|
||||
debugPrint(' $key: disposed=${value['isDisposed']}, streamClosed=${value['isStreamControllerClosed']}');
|
||||
});
|
||||
}
|
||||
|
||||
// 기본 인스턴스 상태
|
||||
final defaultInstance = diagnosticInfo['defaultInstance'] as Map<String, dynamic>;
|
||||
debugPrint('--- 기본 인스턴스 상태 ---');
|
||||
debugPrint(' disposed=${defaultInstance['isDisposed']}, streamClosed=${defaultInstance['isStreamControllerClosed']}');
|
||||
|
||||
debugPrint('===========================================');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _ensureAllTimersComplete() async {
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: 타이머 완료 대기 시작');
|
||||
}
|
||||
|
||||
// 비동기 리소스 상태 로깅 (시작)
|
||||
await _logAsyncResourceState('타이머 완료 대기 시작');
|
||||
|
||||
// 타이머를 새로 만들지 않기 위해 microtask만 사용
|
||||
await Future.microtask(() {});
|
||||
await Future.microtask(() {});
|
||||
await Future.microtask(() {});
|
||||
|
||||
// 중간 상태 로깅
|
||||
await _logAsyncResourceState('타이머 완료 중간 상태');
|
||||
|
||||
// 테스트 설정에서 정의된 타이머 완료 대기 시간을 적용
|
||||
// 추가 대기 시간이 설정되어 있어도 실제 Timer 생성은 하지 않음(테스트 타이머 경합 방지)
|
||||
// 필요한 경우 호출자에서 fakeAsync 등을 사용해 제어하도록 위임
|
||||
|
||||
// 비동기 리소스 상태 로깅 (완료)
|
||||
await _logAsyncResourceState('타이머 완료 대기 완료');
|
||||
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: 타이머 완료 대기 완료');
|
||||
}
|
||||
}
|
||||
|
||||
factory EventBus() {
|
||||
// 테스트 환경에서는 테스트 ID 자동 생성 및 설정
|
||||
if (TestUtils.isInTest) {
|
||||
// 테스트 ID가 설정되지 않았다면 자동으로 생성하지 말고, 명시적 설정을 강제한다
|
||||
if (_currentTestId == null) {
|
||||
throw StateError('EventBus: 테스트 환경에서 currentTestId가 설정되지 않았습니다. setUp에서 EventBus.setCurrentTestId(...)를 호출하세요.');
|
||||
}
|
||||
|
||||
// 테스트 ID가 있지만 인스턴스가 없는 경우 생성
|
||||
if (!_testInstances.containsKey(_currentTestId!)) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('EventBus: 테스트 ID ${_currentTestId}에 대한 새 인스턴스 생성');
|
||||
}
|
||||
_testInstances[_currentTestId!] = EventBus._internal();
|
||||
_testInstances[_currentTestId!]!.reset();
|
||||
}
|
||||
|
||||
return _testInstances[_currentTestId]!;
|
||||
}
|
||||
|
||||
// 테스트 환경이 아닌 경우 기본 싱글톤 인스턴스 반환
|
||||
return _instance;
|
||||
}
|
||||
|
||||
EventBus._internal();
|
||||
|
||||
// 이벤트 버스 내부 상태
|
||||
StreamController _streamController = StreamController.broadcast();
|
||||
bool _isDisposed = false;
|
||||
|
||||
Stream get stream => _streamController.stream;
|
||||
|
||||
void fire(dynamic event) {
|
||||
if (!_isDisposed && !_streamController.isClosed) {
|
||||
_streamController.add(event);
|
||||
// 디버그 모드 또는 상세 로깅 설정 시 이벤트 발생 로그 추가
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: 이벤트 발생 - $event');
|
||||
}
|
||||
} else if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: 이벤트 발생 실패 (이미 dispose됨) - $event');
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 타입의 이벤트를 구독하는 메서드
|
||||
Stream<T> on<T>() {
|
||||
if (_isDisposed) {
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: dispose된 인스턴스에서 on<$T> 호출됨');
|
||||
}
|
||||
// 빈 스트림 반환
|
||||
return Stream<T>.empty();
|
||||
}
|
||||
|
||||
// 타입 필터링 및 변환
|
||||
final filteredStream = stream.where((event) => event is T).cast<T>();
|
||||
|
||||
// 구독 생성 시 카운터 증가 - 한 번만 증가
|
||||
_activeSubscriptionCount++;
|
||||
|
||||
if (TestUtils.isInTest) {
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: 새 구독 추가됨 <$T> (현재 구독 수: $_activeSubscriptionCount)');
|
||||
}
|
||||
|
||||
// 구독 취소 시 카운터 감소를 위한 처리
|
||||
// StreamController를 사용하여 구독 취소 시 카운터 감소 보장
|
||||
final controller = StreamController<T>();
|
||||
|
||||
// 원본 스트림에서 데이터 수신 및 전달
|
||||
final subscription = filteredStream.listen(
|
||||
(data) => controller.add(data),
|
||||
onError: (error) => controller.addError(error),
|
||||
onDone: () => controller.close(),
|
||||
);
|
||||
|
||||
// 컨트롤러 닫힐 때 구독 취소 및 카운터 감소
|
||||
controller.onCancel = () {
|
||||
subscription.cancel();
|
||||
_activeSubscriptionCount = (_activeSubscriptionCount > 0) ? _activeSubscriptionCount - 1 : 0;
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: 구독 취소됨 <$T> (남은 구독 수: $_activeSubscriptionCount)');
|
||||
}
|
||||
};
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
return filteredStream;
|
||||
}
|
||||
|
||||
Future<void> _logInstanceState() async {
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus 인스턴스 상태:');
|
||||
debugPrint(' isDisposed: $_isDisposed');
|
||||
debugPrint(' streamController.isClosed: ${_streamController.isClosed}');
|
||||
if (TestUtils.isInTest) {
|
||||
debugPrint(' 현재 테스트 ID: $_currentTestId');
|
||||
debugPrint(' 활성 구독 수: $_activeSubscriptionCount');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
if (!_isDisposed) {
|
||||
// dispose 전 상태 로깅
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: dispose 시작');
|
||||
await _logInstanceState();
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
if (!_streamController.isClosed) {
|
||||
try {
|
||||
await _streamController.close();
|
||||
|
||||
// 마이크로태스크 실행으로 비동기 작업 완료 보장
|
||||
await Future.microtask(() {});
|
||||
|
||||
if ((kDebugMode || TestConfig.instance.isVerboseLogging) && TestUtils.isInTest) {
|
||||
debugPrint('EventBus: 인스턴스 dispose 완료 (테스트 ID: $_currentTestId)');
|
||||
// dispose 후 상태 로깅
|
||||
await _logInstanceState();
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: dispose 중 오류 - $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: 이미 dispose된 인스턴스에 dispose 호출됨');
|
||||
}
|
||||
}
|
||||
|
||||
/// 테스트 환경에서 EventBus를 초기화하는 메서드
|
||||
/// 기존 StreamController를 닫고 새로운 것으로 교체
|
||||
Future<void> reset() async {
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: reset 시작');
|
||||
await _logInstanceState();
|
||||
}
|
||||
|
||||
if (!_isDisposed) {
|
||||
try {
|
||||
await dispose();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('EventBus: reset 중 dispose 호출 오류 - $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 새로운 StreamController 생성
|
||||
_streamController = StreamController.broadcast();
|
||||
_isDisposed = false;
|
||||
|
||||
// 활성 구독 수 초기화 (인스턴스 레벨)
|
||||
if (TestUtils.isInTest) {
|
||||
_activeSubscriptionCount = 0;
|
||||
}
|
||||
|
||||
// 마이크로태스크 실행으로 비동기 작업 완료 보장
|
||||
await Future.microtask(() {});
|
||||
|
||||
if (kDebugMode || TestConfig.instance.isVerboseLogging) {
|
||||
debugPrint('EventBus: 인스턴스 reset 완료 (테스트 ID: $_currentTestId)');
|
||||
await _logInstanceState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,32 +474,47 @@ class EventService with ChangeNotifier {
|
||||
// 팀 관리 기능
|
||||
// 이벤트 팀 목록 가져오기
|
||||
Future<List<Team>> fetchEventTeams(String eventId) async {
|
||||
if (_token == null || _clubId == null) {
|
||||
throw Exception('인증 또는 클럽 정보가 필요합니다');
|
||||
if (_token == null) {
|
||||
throw Exception('인증 정보가 필요합니다');
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final data = await _apiService.post(
|
||||
'${ApiConfig.clubs}/events/teams',
|
||||
'${ApiConfig.events}/teams',
|
||||
data: {'eventId': eventId},
|
||||
);
|
||||
|
||||
final List<dynamic> teamsData = data['teams'] ?? [];
|
||||
_teams = teamsData.map((teamData) => Team.fromJson(teamData)).toList();
|
||||
_teams = teamsData
|
||||
.map((teamData) => Team.fromJson(teamData))
|
||||
.toList();
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
return _teams;
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
throw Exception('팀 목록을 불러오는데 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 이벤트에 팀이 있는지 확인 (간략 조회)
|
||||
Future<bool> hasEventTeams(String eventId) async {
|
||||
if (_token == null) {
|
||||
throw Exception('인증 정보가 필요합니다');
|
||||
}
|
||||
|
||||
try {
|
||||
final data = await _apiService.post(
|
||||
'${ApiConfig.events}/teams',
|
||||
data: {'eventId': eventId},
|
||||
);
|
||||
|
||||
final List<dynamic> teamsData = data['teams'] ?? [];
|
||||
return teamsData.isNotEmpty;
|
||||
} catch (e) {
|
||||
print('팀 확인 실패: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 팀 생성
|
||||
Future<List<Team>> generateTeams(
|
||||
|
||||
@@ -30,17 +30,78 @@ class MemberService with ChangeNotifier {
|
||||
_token = token;
|
||||
_apiService.setToken(token);
|
||||
|
||||
// 이벤트 구독
|
||||
_eventSubscription = EventBus().stream.listen((event) {
|
||||
if (event is ClubChangedEvent) {
|
||||
// 이벤트 구독 - 기존 구독이 있으면 취소 후 다시 구독
|
||||
_eventSubscription?.cancel();
|
||||
_eventSubscription = EventBus().on<ClubChangedEvent>().listen((event) {
|
||||
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
|
||||
void dispose() {
|
||||
_eventSubscription?.cancel();
|
||||
Future<void> dispose() async {
|
||||
// dispose 시작 상태 로깅
|
||||
if (kDebugMode) {
|
||||
debugPrint('MemberService: dispose 시작');
|
||||
}
|
||||
await _logResourceState('dispose 시작');
|
||||
|
||||
// 이벤트 구독 취소 확실히 처리
|
||||
if (_eventSubscription != null) {
|
||||
try {
|
||||
// 구독 취소 전 로깅
|
||||
if (kDebugMode) {
|
||||
debugPrint('MemberService: 구독 취소 시작');
|
||||
}
|
||||
|
||||
// 구독 취소
|
||||
_eventSubscription!.cancel();
|
||||
|
||||
// 마이크로태스크 실행으로 비동기 작업 완료 보장
|
||||
await Future.microtask(() {});
|
||||
|
||||
// 구독 취소 후 로깅
|
||||
if (kDebugMode) {
|
||||
debugPrint('MemberService: 구독 취소 완료');
|
||||
}
|
||||
|
||||
// 구독 참조 제거
|
||||
_eventSubscription = null;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('MemberService: 구독 취소 중 오류 - $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 추가 마이크로태스크 실행으로 비동기 작업 완료 보장 (타이머 생성 회피)
|
||||
await Future.microtask(() {});
|
||||
await Future.microtask(() {});
|
||||
|
||||
// dispose 완료 후 상태 로깅
|
||||
await _logResourceState('dispose 완료');
|
||||
|
||||
// 구독 수 로깅 - 지연 없이 즉시 처리
|
||||
if (kDebugMode) {
|
||||
debugPrint('MemberService: dispose 완료 후 구독 수 - ${EventBus.activeSubscriptionCount}');
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -49,6 +110,11 @@ class MemberService with ChangeNotifier {
|
||||
// 클럽 ID가 변경된 경우에만 처리
|
||||
if (_clubId != clubId) {
|
||||
_clubId = clubId;
|
||||
|
||||
// SharedPreferences에 저장
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(ApiConfig.clubIdKey, clubId);
|
||||
|
||||
notifyListeners();
|
||||
|
||||
// 새 클럽의 회원 목록 가져오기
|
||||
@@ -237,4 +303,46 @@ class MemberService with ChangeNotifier {
|
||||
throw Exception('회원 삭제에 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 여러 회원 ID로 회원 정보 가져오기
|
||||
Future<List<Member>> fetchMembersByIds(String clubId, List<String> memberIds) async {
|
||||
if (_token == null) {
|
||||
throw Exception('인증이 필요합니다');
|
||||
}
|
||||
|
||||
if (memberIds.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
// 이미 로드된 회원 정보 중에서 필터링
|
||||
if (_members.isNotEmpty && _clubId == clubId) {
|
||||
final cachedMembers = _members.where((member) => memberIds.contains(member.id)).toList();
|
||||
|
||||
// 모든 회원 정보가 캐시에 있는 경우
|
||||
if (cachedMembers.length == memberIds.length) {
|
||||
return cachedMembers;
|
||||
}
|
||||
}
|
||||
|
||||
// API로 회원 정보 요청
|
||||
final data = await _apiService.post(
|
||||
'${ApiConfig.clubs}/members/batch',
|
||||
data: {
|
||||
'clubId': clubId,
|
||||
'memberIds': memberIds,
|
||||
},
|
||||
);
|
||||
|
||||
if (data is List) {
|
||||
return data.map((memberData) => Member.fromJson(memberData)).toList();
|
||||
} else if (data['members'] != null && data['members'] is List) {
|
||||
return (data['members'] as List).map((memberData) => Member.fromJson(memberData)).toList();
|
||||
} else {
|
||||
throw Exception('회원 정보 형식이 올바르지 않습니다');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('회원 정보를 불러오는데 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
import 'package:timezone/data/latest.dart' as tz_data;
|
||||
@@ -8,14 +9,23 @@ import '../models/event_model.dart';
|
||||
class NotificationService {
|
||||
static NotificationService _instance = NotificationService._internal();
|
||||
factory NotificationService() => _instance;
|
||||
NotificationService._internal();
|
||||
|
||||
// 생성자에서 플러그인을 주입받을 수 있도록 변경
|
||||
NotificationService._internal([FlutterLocalNotificationsPlugin? plugin])
|
||||
: _notificationsPlugin = plugin ?? FlutterLocalNotificationsPlugin();
|
||||
|
||||
// 테스트용 인스턴스 설정 메서드
|
||||
static void setTestInstance(NotificationService instance) {
|
||||
_instance = instance;
|
||||
}
|
||||
|
||||
// 테스트용 인스턴스 생성 포함한 새 메서드
|
||||
@visibleForTesting
|
||||
static NotificationService createTestInstance(FlutterLocalNotificationsPlugin mockPlugin) {
|
||||
return NotificationService._internal(mockPlugin);
|
||||
}
|
||||
|
||||
final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
final FlutterLocalNotificationsPlugin _notificationsPlugin;
|
||||
bool _isInitialized = false;
|
||||
|
||||
// 알림 서비스 초기화
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../models/subscription_model.dart';
|
||||
import '../utils/test_utils.dart';
|
||||
|
||||
/// 구독 알림 서비스
|
||||
class SubscriptionNotificationService extends ChangeNotifier {
|
||||
@@ -33,10 +34,16 @@ class SubscriptionNotificationService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
SubscriptionNotificationService() {
|
||||
// 주기적으로 구독 상태 확인 (1시간마다)
|
||||
_checkTimer = Timer.periodic(const Duration(hours: 1), (_) {
|
||||
// 테스트 환경에서는 Timer를 생성하지 않음
|
||||
if (!TestUtils.isInTest) {
|
||||
// 주기적으로 구독 상태 확인 (1시간마다)
|
||||
_checkTimer = Timer.periodic(const Duration(hours: 1), (_) {
|
||||
_checkExpirationStatus();
|
||||
});
|
||||
} else {
|
||||
// 테스트 환경에서는 즉시 한 번만 체크
|
||||
_checkExpirationStatus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 구독 정보 업데이트
|
||||
@@ -84,7 +91,11 @@ class SubscriptionNotificationService extends ChangeNotifier {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_checkTimer?.cancel();
|
||||
// Timer가 있으면 취소
|
||||
if (_checkTimer != null) {
|
||||
_checkTimer!.cancel();
|
||||
_checkTimer = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user