import 'package:dio/dio.dart'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import '../config/api_config.dart'; import '../services/auth_service.dart'; import '../screens/auth/login_screen.dart'; import '../main.dart'; // navigatorKey를 사용하기 위한 import class ApiService { static final ApiService _instance = ApiService._internal(); factory ApiService() => _instance; late Dio _dio; final CookieJar _cookieJar = CookieJar(); ApiService._internal() { _dio = Dio(BaseOptions( baseUrl: ApiConfig.baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), contentType: 'application/json', )); // 쿠키 관리자 설정 (웹 환경에서는 사용하지 않음) if (!kIsWeb) { _dio.interceptors.add(CookieManager(_cookieJar)); } // 로그 인터셉터 (디버깅용) _dio.interceptors.add(LogInterceptor( requestBody: true, responseBody: true, )); } // 테스트를 위한 Dio 주입 지점 @visibleForTesting void setDio(Dio dio) { _dio = dio; } // 토큰 설정 void setToken(String token) { _dio.options.headers['Authorization'] = 'Bearer $token'; } // GET 요청 Future get(String path, {Map? queryParameters}) async { try { final response = await _dio.get( path, queryParameters: queryParameters, ); return response.data; } on DioException catch (e) { _handleError(e); rethrow; } } // POST 요청 Future post(String path, {dynamic data}) async { try { final isForm = data is FormData; final response = await _dio.post( path, data: data ?? {}, options: isForm ? Options(contentType: 'multipart/form-data') : null, ); return response.data; } on DioException catch (e) { _handleError(e); rethrow; } } // PUT 요청 Future put(String path, {dynamic data}) async { try { final response = await _dio.put( path, data: data, ); return response.data; } on DioException catch (e) { _handleError(e); rethrow; } } // DELETE 요청 Future delete(String path) async { try { final response = await _dio.delete(path); return response.data; } on DioException catch (e) { _handleError(e); rethrow; } } // DELETE 요청 (body 데이터 포함이 필요한 경우 사용) Future deleteWithData(String path, {dynamic data}) async { try { final response = await _dio.delete(path, data: data); return response.data; } on DioException catch (e) { _handleError(e); rethrow; } } // 에러 처리 void _handleError(DioException e) { if (e.response != null) { final status = e.response?.statusCode; final dataStr = e.response?.data?.toString() ?? ''; // 초기 진입 등에서 발생 가능한 EXPECTED 400은 콘솔에 노이즈로 찍지 않음 // 예: { error: '클럽 정보가 필요합니다.' } if (status == 400 && (dataStr.contains('클럽 정보가 필요합니다') || dataStr.contains('club'))) { // 무음 처리: 상위 호출부에서 사용자 경험을 고려해 처리함 return; } print('API 에러: $status - ${e.response?.data}'); // 401 오류 (인증 실패) 처리 if (status == 401) { print('인증 오류: 토큰이 유효하지 않습니다. 로그아웃 처리합니다.'); _handleUnauthorized(); } } else { print('API 요청 에러: ${e.message}'); } } // 인증 오류 처리 (401) void _handleUnauthorized() { // 다음 프레임에서 실행하여 현재 API 호출 스택이 완료되도록 함 Future.microtask(() async { try { // AuthService 인스턴스 가져오기 final authService = AuthService(); // 로그아웃 처리 await authService.logout(); // 전역 네비게이터 키를 사용하여 로그인 화면으로 이동 if (navigatorKey.currentContext != null) { Navigator.of(navigatorKey.currentContext!).pushAndRemoveUntil( MaterialPageRoute(builder: (context) => const LoginScreen()), (route) => false, ); } } catch (e) { print('로그아웃 처리 중 오류 발생: $e'); } }); } }