## 주요 변경사항 ### 아키텍처 개선 - Clean Architecture 패턴 적용 (Domain, Data, Presentation 레이어 분리) - Use Case 패턴 도입으로 비즈니스 로직 캡슐화 - Repository 패턴으로 데이터 접근 추상화 - 의존성 주입 구조 개선 ### 상태 관리 최적화 - 모든 Controller에서 불필요한 상태 관리 로직 제거 - 페이지네이션 로직 통일 및 간소화 - 에러 처리 로직 개선 (에러 메시지 한글화) - 로딩 상태 관리 최적화 ### Mock 서비스 제거 - MockDataService 완전 제거 - 모든 화면을 실제 API 전용으로 전환 - 불필요한 Mock 관련 코드 정리 ### UI/UX 개선 - Overview 화면 대시보드 기능 강화 - 라이선스 만료 알림 위젯 추가 - 사이드바 네비게이션 개선 - 일관된 UI 컴포넌트 사용 ### 코드 품질 - 중복 코드 제거 및 함수 추출 - 파일별 책임 분리 명확화 - 테스트 코드 업데이트 ## 영향 범위 - 모든 화면의 Controller 리팩토링 - API 통신 레이어 구조 개선 - 에러 처리 및 로깅 시스템 개선 ## 향후 계획 - 단위 테스트 커버리지 확대 - 통합 테스트 시나리오 추가 - 성능 모니터링 도구 통합
298 lines
8.4 KiB
Dart
298 lines
8.4 KiB
Dart
import 'package:dio/dio.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import '../../../core/config/environment.dart';
|
|
import '../../../core/constants/app_constants.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: AppConstants.apiConnectTimeout,
|
|
receiveTimeout: AppConstants.apiReceiveTimeout,
|
|
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: AppConstants.apiConnectTimeout,
|
|
receiveTimeout: AppConstants.apiReceiveTimeout,
|
|
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<Response<T>> get<T>(
|
|
String path, {
|
|
Map<String, dynamic>? queryParameters,
|
|
Options? options,
|
|
CancelToken? cancelToken,
|
|
ProgressCallback? onReceiveProgress,
|
|
}) {
|
|
return _dio.get<T>(
|
|
path,
|
|
queryParameters: queryParameters,
|
|
options: options,
|
|
cancelToken: cancelToken,
|
|
onReceiveProgress: onReceiveProgress,
|
|
);
|
|
}
|
|
|
|
/// POST 요청
|
|
Future<Response<T>> post<T>(
|
|
String path, {
|
|
dynamic data,
|
|
Map<String, dynamic>? queryParameters,
|
|
Options? options,
|
|
CancelToken? cancelToken,
|
|
ProgressCallback? onSendProgress,
|
|
ProgressCallback? onReceiveProgress,
|
|
}) {
|
|
if (Environment.enableLogging && kDebugMode) {
|
|
debugPrint('[ApiClient] POST 요청 시작: $path');
|
|
debugPrint('[ApiClient] 요청 데이터: $data');
|
|
}
|
|
|
|
return _dio.post<T>(
|
|
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<Response<T>> put<T>(
|
|
String path, {
|
|
dynamic data,
|
|
Map<String, dynamic>? queryParameters,
|
|
Options? options,
|
|
CancelToken? cancelToken,
|
|
ProgressCallback? onSendProgress,
|
|
ProgressCallback? onReceiveProgress,
|
|
}) {
|
|
return _dio.put<T>(
|
|
path,
|
|
data: data,
|
|
queryParameters: queryParameters,
|
|
options: options,
|
|
cancelToken: cancelToken,
|
|
onSendProgress: onSendProgress,
|
|
onReceiveProgress: onReceiveProgress,
|
|
);
|
|
}
|
|
|
|
/// PATCH 요청
|
|
Future<Response<T>> patch<T>(
|
|
String path, {
|
|
dynamic data,
|
|
Map<String, dynamic>? queryParameters,
|
|
Options? options,
|
|
CancelToken? cancelToken,
|
|
ProgressCallback? onSendProgress,
|
|
ProgressCallback? onReceiveProgress,
|
|
}) {
|
|
return _dio.patch<T>(
|
|
path,
|
|
data: data,
|
|
queryParameters: queryParameters,
|
|
options: options,
|
|
cancelToken: cancelToken,
|
|
onSendProgress: onSendProgress,
|
|
onReceiveProgress: onReceiveProgress,
|
|
);
|
|
}
|
|
|
|
/// DELETE 요청
|
|
Future<Response<T>> delete<T>(
|
|
String path, {
|
|
dynamic data,
|
|
Map<String, dynamic>? queryParameters,
|
|
Options? options,
|
|
CancelToken? cancelToken,
|
|
}) {
|
|
return _dio.delete<T>(
|
|
path,
|
|
data: data,
|
|
queryParameters: queryParameters,
|
|
options: options,
|
|
cancelToken: cancelToken,
|
|
);
|
|
}
|
|
|
|
/// 파일 업로드
|
|
Future<Response<T>> uploadFile<T>(
|
|
String path, {
|
|
required String filePath,
|
|
required String fileFieldName,
|
|
Map<String, dynamic>? 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<T>(
|
|
path,
|
|
data: formData,
|
|
options: Options(
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
},
|
|
),
|
|
onSendProgress: onSendProgress,
|
|
cancelToken: cancelToken,
|
|
);
|
|
}
|
|
|
|
/// 파일 다운로드
|
|
Future<Response> downloadFile(
|
|
String path, {
|
|
required String savePath,
|
|
ProgressCallback? onReceiveProgress,
|
|
CancelToken? cancelToken,
|
|
Map<String, dynamic>? queryParameters,
|
|
}) {
|
|
return _dio.download(
|
|
path,
|
|
savePath,
|
|
queryParameters: queryParameters,
|
|
onReceiveProgress: onReceiveProgress,
|
|
cancelToken: cancelToken,
|
|
options: Options(
|
|
responseType: ResponseType.bytes,
|
|
followRedirects: false,
|
|
),
|
|
);
|
|
}
|
|
} |