import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import '../../../core/config/environment.dart'; import 'interceptors/auth_interceptor.dart'; import 'interceptors/error_interceptor.dart'; import 'interceptors/logging_interceptor.dart'; import 'interceptors/response_interceptor.dart'; /// API 클라이언트 클래스 class ApiClient { late final Dio _dio; static ApiClient? _instance; factory ApiClient() { _instance ??= ApiClient._internal(); return _instance!; } ApiClient._internal() { try { if (Environment.enableLogging && kDebugMode) { debugPrint('[ApiClient] 초기화 시작'); } _dio = Dio(_baseOptions); if (Environment.enableLogging && kDebugMode) { debugPrint('[ApiClient] Dio 인스턴스 생성 완료'); debugPrint('[ApiClient] Base URL: ${_dio.options.baseUrl}'); debugPrint('[ApiClient] Connect Timeout: ${_dio.options.connectTimeout}'); debugPrint('[ApiClient] Receive Timeout: ${_dio.options.receiveTimeout}'); } _setupInterceptors(); if (Environment.enableLogging && kDebugMode) { debugPrint('[ApiClient] 인터셉터 설정 완료'); } } catch (e, stackTrace) { if (kDebugMode) { debugPrint('[ApiClient] ⚠️ 에러 발생: $e'); debugPrint('[ApiClient] Stack trace: $stackTrace'); } // 기본값으로 초기화 _dio = Dio(BaseOptions( baseUrl: 'http://43.201.34.104:8080/api/v1', connectTimeout: const Duration(seconds: 30), receiveTimeout: const Duration(seconds: 30), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, )); _setupInterceptors(); if (kDebugMode) { debugPrint('[ApiClient] 기본값으로 초기화 완료'); } } } /// Dio 인스턴스 getter Dio get dio => _dio; /// 기본 옵션 설정 BaseOptions get _baseOptions { try { return BaseOptions( baseUrl: Environment.apiBaseUrl, connectTimeout: Duration(milliseconds: Environment.apiTimeout), receiveTimeout: Duration(milliseconds: Environment.apiTimeout), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, validateStatus: (status) { return status != null && status < 500; }, ); } catch (e) { // Environment가 초기화되지 않은 경우 기본값 사용 return BaseOptions( baseUrl: 'http://43.201.34.104:8080/api/v1', connectTimeout: const Duration(seconds: 30), receiveTimeout: const Duration(seconds: 30), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, validateStatus: (status) { return status != null && status < 500; }, ); } } /// 인터셉터 설정 void _setupInterceptors() { _dio.interceptors.clear(); // 로깅 인터셉터 (개발 환경에서만) - 가장 먼저 추가하여 모든 요청/응답을 로깅 try { if (Environment.enableLogging && kDebugMode) { _dio.interceptors.add(LoggingInterceptor()); } } catch (e) { // Environment 접근 실패 시 디버그 모드에서만 로깅 활성화 if (kDebugMode) { _dio.interceptors.add(LoggingInterceptor()); } } // 인증 인터셉터 - 요청에 토큰 추가 및 401 처리 _dio.interceptors.add(AuthInterceptor(_dio)); // 응답 정규화 인터셉터 - 성공 응답을 일관된 형식으로 변환 _dio.interceptors.add(ResponseInterceptor()); // 에러 처리 인터셉터 - 마지막에 추가하여 모든 에러를 캐치 _dio.interceptors.add(ErrorInterceptor()); } /// 토큰 업데이트 void updateAuthToken(String token) { _dio.options.headers['Authorization'] = 'Bearer $token'; } /// 토큰 제거 void removeAuthToken() { _dio.options.headers.remove('Authorization'); } /// GET 요청 Future> get( String path, { Map? queryParameters, Options? options, CancelToken? cancelToken, ProgressCallback? onReceiveProgress, }) { return _dio.get( path, queryParameters: queryParameters, options: options, cancelToken: cancelToken, onReceiveProgress: onReceiveProgress, ); } /// POST 요청 Future> post( String path, { dynamic data, Map? queryParameters, Options? options, CancelToken? cancelToken, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) { if (Environment.enableLogging && kDebugMode) { debugPrint('[ApiClient] POST 요청 시작: $path'); debugPrint('[ApiClient] 요청 데이터: $data'); } return _dio.post( path, data: data, queryParameters: queryParameters, options: options, cancelToken: cancelToken, onSendProgress: onSendProgress, onReceiveProgress: onReceiveProgress, ).then((response) { if (Environment.enableLogging && kDebugMode) { debugPrint('[ApiClient] POST 응답 수신: ${response.statusCode}'); } return response; }).catchError((error) { if (Environment.enableLogging && kDebugMode) { debugPrint('[ApiClient] POST 에러 발생: $error'); if (error is DioException) { debugPrint('[ApiClient] DioException 타입: ${error.type}'); debugPrint('[ApiClient] DioException 메시지: ${error.message}'); debugPrint('[ApiClient] DioException 에러: ${error.error}'); } } throw error; }); } /// PUT 요청 Future> put( String path, { dynamic data, Map? queryParameters, Options? options, CancelToken? cancelToken, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) { return _dio.put( path, data: data, queryParameters: queryParameters, options: options, cancelToken: cancelToken, onSendProgress: onSendProgress, onReceiveProgress: onReceiveProgress, ); } /// PATCH 요청 Future> patch( String path, { dynamic data, Map? queryParameters, Options? options, CancelToken? cancelToken, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) { return _dio.patch( path, data: data, queryParameters: queryParameters, options: options, cancelToken: cancelToken, onSendProgress: onSendProgress, onReceiveProgress: onReceiveProgress, ); } /// DELETE 요청 Future> delete( String path, { dynamic data, Map? queryParameters, Options? options, CancelToken? cancelToken, }) { return _dio.delete( path, data: data, queryParameters: queryParameters, options: options, cancelToken: cancelToken, ); } /// 파일 업로드 Future> uploadFile( String path, { required String filePath, required String fileFieldName, Map? additionalData, ProgressCallback? onSendProgress, CancelToken? cancelToken, }) async { final fileName = filePath.split('/').last; final formData = FormData.fromMap({ fileFieldName: await MultipartFile.fromFile( filePath, filename: fileName, ), ...?additionalData, }); return _dio.post( path, data: formData, options: Options( headers: { 'Content-Type': 'multipart/form-data', }, ), onSendProgress: onSendProgress, cancelToken: cancelToken, ); } /// 파일 다운로드 Future downloadFile( String path, { required String savePath, ProgressCallback? onReceiveProgress, CancelToken? cancelToken, Map? queryParameters, }) { return _dio.download( path, savePath, queryParameters: queryParameters, onReceiveProgress: onReceiveProgress, cancelToken: cancelToken, options: Options( responseType: ResponseType.bytes, followRedirects: false, ), ); } }