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 : }
|