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