LCOV - code coverage report
Current view: top level - lib/services - member_service.dart Coverage Total Hit
Test: lcov.info Lines: 71.1 % 142 101
Test Date: 2025-08-12 03:21:51 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:async';
       2              : import 'package:flutter/foundation.dart';
       3              : import 'package:shared_preferences/shared_preferences.dart';
       4              : 
       5              : import '../config/api_config.dart';
       6              : import '../models/member_model.dart';
       7              : import './api_service.dart';
       8              : import './event_bus.dart';
       9              : 
      10              : class MemberService with ChangeNotifier {
      11              :   List<Member> _members = [];
      12              :   Member? _currentMember;
      13              :   bool _isLoading = false;
      14              :   String? _token;
      15              :   String? _clubId;
      16              :   final ApiService _apiService;
      17              :   StreamSubscription? _eventSubscription;
      18              :   
      19              :   // 기본 생성자
      20            0 :   MemberService() : _apiService = ApiService();
      21              :   
      22              :   // 테스트용 생성자
      23            2 :   MemberService.forTest(this._apiService);
      24              : 
      25            6 :   List<Member> get members => [..._members];
      26            0 :   bool get isLoading => _isLoading;
      27              : 
      28              :   // 초기화 함수
      29            2 :   void initialize(String token) {
      30            2 :     _token = token;
      31            4 :     _apiService.setToken(token);
      32              :     
      33              :     // 이벤트 구독 - 기존 구독이 있으면 취소 후 다시 구독
      34            2 :     _eventSubscription?.cancel();
      35           10 :     _eventSubscription = EventBus().on<ClubChangedEvent>().listen((event) {
      36            4 :       if (event.clubId.isNotEmpty) {
      37            4 :         setClubId(event.clubId);
      38              :       }
      39              :     });
      40              :   }
      41              :   
      42              :   // 비동기 리소스 상태 로깅
      43            2 :   Future<void> _logResourceState(String context) async {
      44              :     if (kDebugMode) {
      45            6 :       debugPrint('===== MemberService 비동기 리소스 상태 ($context) =====');
      46           10 :       debugPrint('시간: ${DateTime.now().toIso8601String()}');
      47            8 :       debugPrint('구독 상태: ${_eventSubscription != null ? '활성' : '비활성'}');
      48            8 :       debugPrint('클럽 ID: $_clubId');
      49            8 :       debugPrint('토큰 상태: ${_token != null ? '있음' : '없음'}');
      50            8 :       debugPrint('로딩 상태: $_isLoading');
      51           10 :       debugPrint('회원 수: ${_members.length}');
      52            8 :       debugPrint('EventBus 활성 구독 수: ${EventBus.activeSubscriptionCount}');
      53            4 :       debugPrint('===========================================');
      54              :     }
      55              :   }
      56              :   
      57            2 :   @override
      58              :   Future<void> dispose() async {
      59              :     // dispose 시작 상태 로깅
      60              :     if (kDebugMode) {
      61            4 :       debugPrint('MemberService: dispose 시작');
      62              :     }
      63            2 :     await _logResourceState('dispose 시작');
      64              :     
      65              :     // 이벤트 구독 취소 확실히 처리
      66            2 :     if (_eventSubscription != null) {
      67              :       try {
      68              :         // 구독 취소 전 로깅
      69              :         if (kDebugMode) {
      70            4 :           debugPrint('MemberService: 구독 취소 시작');
      71              :         }
      72              :         
      73              :         // 구독 취소
      74            4 :         _eventSubscription!.cancel();
      75              :         
      76              :         // 마이크로태스크 실행으로 비동기 작업 완료 보장
      77            4 :         await Future.microtask(() {});
      78              :         
      79              :         // 구독 취소 후 로깅
      80              :         if (kDebugMode) {
      81            4 :           debugPrint('MemberService: 구독 취소 완료');
      82              :         }
      83              :         
      84              :         // 구독 참조 제거
      85            2 :         _eventSubscription = null;
      86              :       } catch (e) {
      87              :         if (kDebugMode) {
      88            0 :           debugPrint('MemberService: 구독 취소 중 오류 - $e');
      89              :         }
      90              :       }
      91              :     }
      92              :     
      93              :     // 추가 마이크로태스크 실행으로 비동기 작업 완료 보장 (타이머 생성 회피)
      94            4 :     await Future.microtask(() {});
      95            4 :     await Future.microtask(() {});
      96              :     
      97              :     // dispose 완료 후 상태 로깅
      98            2 :     await _logResourceState('dispose 완료');
      99              :     
     100              :     // 구독 수 로깅 - 지연 없이 즉시 처리
     101              :     if (kDebugMode) {
     102            8 :       debugPrint('MemberService: dispose 완료 후 구독 수 - ${EventBus.activeSubscriptionCount}');
     103              :     }
     104              :     
     105            2 :     super.dispose();
     106              :   }
     107              : 
     108              :   // 클럽 ID 설정
     109            2 :   Future<void> setClubId(String clubId) async {
     110              :     // 클럽 ID가 변경된 경우에만 처리
     111            4 :     if (_clubId != clubId) {
     112            2 :       _clubId = clubId;
     113              :       
     114              :       // SharedPreferences에 저장
     115            2 :       final prefs = await SharedPreferences.getInstance();
     116            2 :       await prefs.setString(ApiConfig.clubIdKey, clubId);
     117              :       
     118            2 :       notifyListeners();
     119              :       
     120              :       // 새 클럽의 회원 목록 가져오기
     121            2 :       if (_token != null) {
     122            2 :         await fetchClubMembers();
     123              :       }
     124              :     }
     125              :   }
     126              : 
     127              :   // 클럽의 모든 회원 가져오기
     128            2 :   Future<void> fetchClubMembers() async {
     129            6 :     print('fetchClubMembers 호출됨: token=${_token}');
     130              : 
     131            2 :     _isLoading = true;
     132            2 :     notifyListeners();
     133              : 
     134              :     try {
     135              :       // 클럽 ID 가져오기 - 메모리에 저장된 값을 우선 사용
     136            2 :       String? clubId = _clubId;
     137              :       
     138              :       // 메모리에 없으면 SharedPreferences에서 가져오기
     139              :       if (clubId == null) {
     140            0 :         final prefs = await SharedPreferences.getInstance();
     141            0 :         clubId = prefs.getString(ApiConfig.clubIdKey);
     142              :       }
     143              : 
     144              :       if (clubId == null) {
     145            0 :         throw Exception('선택된 클럽이 없습니다');
     146              :       }
     147              : 
     148            4 :       final data = await _apiService.post(
     149              :         '${ApiConfig.clubs}/members',
     150            2 :         data: {'clubId': clubId},
     151              :       );
     152              : 
     153            4 :       print('회원 응답 데이터: $data');
     154              : 
     155              :       // 응답 데이터가 리스트 형태로 직접 왔으므로 그대로 사용
     156            2 :       if (data is List) {
     157              :         final List<dynamic> membersData = data;
     158            2 :         _members = membersData
     159            6 :             .map((memberData) => Member.fromJson(memberData))
     160            2 :             .toList();
     161            8 :         print('변환된 회원 리스트 길이: ${_members.length}');
     162              :       } else {
     163              :         // 응답 데이터가 객체 형태로 왔을 경우 members 키를 통해 접근
     164            0 :         final List<dynamic> membersData = data['members'] ?? [];
     165            0 :         _members = membersData
     166            0 :             .map((memberData) => Member.fromJson(memberData))
     167            0 :             .toList();
     168            0 :         print('변환된 회원 리스트 길이: ${_members.length}');
     169              :       }
     170              : 
     171            2 :       _isLoading = false;
     172            2 :       notifyListeners();
     173              :     } catch (e) {
     174            1 :       _isLoading = false;
     175            1 :       notifyListeners();
     176            2 :       throw Exception('회원 목록을 불러오는데 실패했습니다: $e');
     177              :     }
     178              :   }
     179              : 
     180              :   // ID로 회원 정보 가져오기
     181            1 :   Future<Member> fetchMemberById(String memberId) async {
     182            2 :     if (_token == null || _clubId == null) {
     183            0 :       throw Exception('인증 또는 클럽 정보가 필요합니다');
     184              :     }
     185              : 
     186              :     try {
     187            2 :       final data = await _apiService.post(
     188              :         '${ApiConfig.clubs}/members/detail',
     189            1 :         data: {'memberId': memberId},
     190              :       );
     191              : 
     192            2 :       if (data['statusCode'] == 200) {
     193            1 :         final memberData = data['member'];
     194            1 :         return Member.fromJson(memberData);
     195              :       } else {
     196            0 :         throw Exception('회원 정보를 불러오는데 실패했습니다');
     197              :       }
     198              :     } catch (e) {
     199            2 :       throw Exception('회원 정보를 불러오는데 실패했습니다: $e');
     200              :     }
     201              :   }
     202              : 
     203              :   // 회원 추가
     204            2 :   Future<Member> addMember(Map<String, dynamic> memberData) async {
     205            4 :     if (_token == null || _clubId == null) {
     206            0 :       throw Exception('인증 또는 클럽 정보가 필요합니다');
     207              :     }
     208              : 
     209            2 :     _isLoading = true;
     210            2 :     notifyListeners();
     211              : 
     212              :     try {
     213            4 :       final data = await _apiService.post(
     214              :         '${ApiConfig.clubs}/members/add',
     215              :         data: memberData,
     216              :       );
     217              : 
     218              :       // 백엔드 응답 처리: 직접 회원 객체가 반환되거나 {member: {...}} 형태로 반환될 수 있음
     219            4 :       final newMember = data is Map && data.containsKey('member')
     220            2 :           ? Member.fromJson(data['member'])
     221            1 :           : Member.fromJson(data);
     222              : 
     223            4 :       _members.add(newMember);
     224              : 
     225            2 :       _isLoading = false;
     226            2 :       notifyListeners();
     227              : 
     228              :       return newMember;
     229              :     } catch (e) {
     230            0 :       _isLoading = false;
     231            0 :       notifyListeners();
     232            0 :       throw Exception('회원 추가에 실패했습니다: $e');
     233              :     }
     234              :   }
     235              : 
     236              :   // 회원 정보 업데이트
     237            2 :   Future<Member> updateMember(
     238              :     String memberId,
     239              :     Map<String, dynamic> updates,
     240              :   ) async {
     241            4 :     if (_token == null || _clubId == null) {
     242            0 :       throw Exception('인증 또는 클럽 정보가 필요합니다');
     243              :     }
     244              : 
     245            2 :     _isLoading = true;
     246            2 :     notifyListeners();
     247              : 
     248              :     try {
     249            4 :       final data = await _apiService.post(
     250              :         '${ApiConfig.clubs}/members/update',
     251           10 :         data: {'clubId': _clubId, 'memberId': memberId, ...updates},
     252              :       );
     253              : 
     254              :       // 응답 데이터가 { member: {...} } 형태인지 또는 직접 회원 객체인지 확인
     255            2 :       final Map<String, dynamic> memberData = data['member'] != null
     256            2 :           ? data['member'] as Map<String, dynamic>
     257              :           : data as Map<String, dynamic>;
     258              : 
     259            2 :       final updatedMember = Member.fromJson(memberData);
     260              : 
     261              :       // 회원 목록 업데이트
     262           10 :       final index = _members.indexWhere((member) => member.id == memberId);
     263            2 :       if (index >= 0) {
     264            2 :         _members[index] = updatedMember;
     265              :       }
     266              : 
     267            2 :       _isLoading = false;
     268            2 :       notifyListeners();
     269              : 
     270              :       return updatedMember;
     271              :     } catch (e) {
     272            0 :       _isLoading = false;
     273            0 :       notifyListeners();
     274            0 :       throw Exception('회원 정보 업데이트에 실패했습니다: $e');
     275              :     }
     276              :   }
     277              : 
     278              :   // 회원 삭제 (또는 비활성화)
     279            2 :   Future<bool> deleteMember(String memberId) async {
     280            4 :     if (_token == null || _clubId == null) {
     281            0 :       throw Exception('인증 또는 클럽 정보가 필요합니다');
     282              :     }
     283              : 
     284            2 :     _isLoading = true;
     285            2 :     notifyListeners();
     286              : 
     287              :     try {
     288            4 :       await _apiService.post(
     289              :         '${ApiConfig.clubs}/members/delete',
     290            4 :         data: {'memberId': memberId, 'clubId': _clubId},
     291              :       );
     292              : 
     293              :       // 회원 목록에서 제거
     294           10 :       _members.removeWhere((member) => member.id == memberId);
     295              : 
     296            2 :       _isLoading = false;
     297            2 :       notifyListeners();
     298              : 
     299              :       return true;
     300              :     } catch (e) {
     301            0 :       _isLoading = false;
     302            0 :       notifyListeners();
     303            0 :       throw Exception('회원 삭제에 실패했습니다: $e');
     304              :     }
     305              :   }
     306              :   
     307              :   // 여러 회원 ID로 회원 정보 가져오기
     308            0 :   Future<List<Member>> fetchMembersByIds(String clubId, List<String> memberIds) async {
     309            0 :     if (_token == null) {
     310            0 :       throw Exception('인증이 필요합니다');
     311              :     }
     312              :     
     313            0 :     if (memberIds.isEmpty) {
     314            0 :       return [];
     315              :     }
     316              : 
     317              :     try {
     318              :       // 이미 로드된 회원 정보 중에서 필터링
     319            0 :       if (_members.isNotEmpty && _clubId == clubId) {
     320            0 :         final cachedMembers = _members.where((member) => memberIds.contains(member.id)).toList();
     321              :         
     322              :         // 모든 회원 정보가 캐시에 있는 경우
     323            0 :         if (cachedMembers.length == memberIds.length) {
     324              :           return cachedMembers;
     325              :         }
     326              :       }
     327              :       
     328              :       // API로 회원 정보 요청
     329            0 :       final data = await _apiService.post(
     330              :         '${ApiConfig.clubs}/members/batch',
     331            0 :         data: {
     332              :           'clubId': clubId,
     333              :           'memberIds': memberIds,
     334              :         },
     335              :       );
     336              : 
     337            0 :       if (data is List) {
     338            0 :         return data.map((memberData) => Member.fromJson(memberData)).toList();
     339            0 :       } else if (data['members'] != null && data['members'] is List) {
     340            0 :         return (data['members'] as List).map((memberData) => Member.fromJson(memberData)).toList();
     341              :       } else {
     342            0 :         throw Exception('회원 정보 형식이 올바르지 않습니다');
     343              :       }
     344              :     } catch (e) {
     345            0 :       throw Exception('회원 정보를 불러오는데 실패했습니다: $e');
     346              :     }
     347              :   }
     348              : }
        

Generated by: LCOV version 2.3.1-1