feat: 초기 프로젝트 설정 및 LunchPick 앱 구현
LunchPick(오늘 뭐 먹Z?) Flutter 앱의 초기 구현입니다. 주요 기능: - 네이버 지도 연동 맛집 추가 - 랜덤 메뉴 추천 시스템 - 날씨 기반 거리 조정 - 방문 기록 관리 - Bluetooth 맛집 공유 - 다크모드 지원 기술 스택: - Flutter 3.8.1+ - Riverpod 상태 관리 - Hive 로컬 DB - Clean Architecture 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
172
lib/core/network/README.md
Normal file
172
lib/core/network/README.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# 네트워크 모듈 사용 가이드
|
||||
|
||||
## 개요
|
||||
|
||||
이 네트워크 모듈은 네이버 단축 URL 처리와 로컬 API 검색을 위한 통합 솔루션을 제공합니다. Dio 기반으로 구축되어 재시도, 캐싱, 로깅 등의 기능을 제공합니다.
|
||||
|
||||
## 주요 기능
|
||||
|
||||
1. **네이버 단축 URL 리다이렉션 처리**
|
||||
2. **HTML 스크래핑으로 식당 정보 추출**
|
||||
3. **네이버 로컬 검색 API 통합**
|
||||
4. **자동 재시도 및 에러 처리**
|
||||
5. **응답 캐싱으로 성능 최적화**
|
||||
6. **네트워크 불안정 상황 대응**
|
||||
|
||||
## 사용 방법
|
||||
|
||||
### 1. 네이버 지도 URL에서 식당 정보 추출
|
||||
|
||||
```dart
|
||||
import 'package:lunchpick/data/datasources/remote/naver_search_service.dart';
|
||||
|
||||
final searchService = NaverSearchService();
|
||||
|
||||
try {
|
||||
// 일반 네이버 지도 URL
|
||||
final restaurant = await searchService.getRestaurantFromUrl(
|
||||
'https://map.naver.com/p/restaurant/1234567890',
|
||||
);
|
||||
|
||||
// 단축 URL도 자동 처리
|
||||
final restaurant2 = await searchService.getRestaurantFromUrl(
|
||||
'https://naver.me/abc123',
|
||||
);
|
||||
|
||||
print('식당명: ${restaurant.name}');
|
||||
print('카테고리: ${restaurant.category}');
|
||||
print('주소: ${restaurant.roadAddress}');
|
||||
} catch (e) {
|
||||
print('오류 발생: $e');
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 키워드로 주변 식당 검색
|
||||
|
||||
```dart
|
||||
// 현재 위치 기반 검색
|
||||
final restaurants = await searchService.searchNearbyRestaurants(
|
||||
query: '파스타',
|
||||
latitude: 37.5666805,
|
||||
longitude: 126.9784147,
|
||||
maxResults: 20,
|
||||
sort: 'random', // 정확도순 정렬 (기본값)
|
||||
);
|
||||
|
||||
for (final restaurant in restaurants) {
|
||||
print('${restaurant.name} - ${restaurant.roadAddress}');
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 식당 상세 정보 검색
|
||||
|
||||
```dart
|
||||
// 식당 이름과 주소로 상세 정보 검색
|
||||
final details = await searchService.searchRestaurantDetails(
|
||||
name: '맛있는 한식당',
|
||||
address: '서울 중구 세종대로',
|
||||
latitude: 37.5666805,
|
||||
longitude: 126.9784147,
|
||||
);
|
||||
|
||||
if (details != null) {
|
||||
print('영업시간: ${details.businessHours}');
|
||||
print('전화번호: ${details.phoneNumber}');
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 네트워크 에러 처리
|
||||
|
||||
```dart
|
||||
import 'package:lunchpick/core/errors/network_exceptions.dart';
|
||||
|
||||
try {
|
||||
final restaurant = await searchService.getRestaurantFromUrl(url);
|
||||
} on ConnectionTimeoutException {
|
||||
// 연결 타임아웃
|
||||
showSnackBar('네트워크 연결이 느립니다. 다시 시도해주세요.');
|
||||
} on NoInternetException {
|
||||
// 인터넷 연결 없음
|
||||
showSnackBar('인터넷 연결을 확인해주세요.');
|
||||
} on ApiKeyException {
|
||||
// API 키 설정 필요
|
||||
showSnackBar('네이버 API 키를 설정해주세요.');
|
||||
} on NaverMapParseException catch (e) {
|
||||
// 파싱 오류
|
||||
showSnackBar('식당 정보를 가져올 수 없습니다: ${e.message}');
|
||||
} catch (e) {
|
||||
// 기타 오류
|
||||
showSnackBar('알 수 없는 오류가 발생했습니다.');
|
||||
}
|
||||
```
|
||||
|
||||
## 설정
|
||||
|
||||
### API 키 설정
|
||||
|
||||
네이버 로컬 검색 API를 사용하려면 API 키가 필요합니다:
|
||||
|
||||
1. [네이버 개발자 센터](https://developers.naver.com)에서 애플리케이션 등록
|
||||
2. Client ID와 Client Secret 발급
|
||||
3. `lib/core/constants/api_keys.dart` 파일에 키 입력:
|
||||
|
||||
```dart
|
||||
class ApiKeys {
|
||||
static const String naverClientId = 'YOUR_CLIENT_ID';
|
||||
static const String naverClientSecret = 'YOUR_CLIENT_SECRET';
|
||||
}
|
||||
```
|
||||
|
||||
### 네트워크 설정 커스터마이징
|
||||
|
||||
`lib/core/network/network_config.dart`에서 타임아웃, 재시도 횟수 등을 조정할 수 있습니다:
|
||||
|
||||
```dart
|
||||
class NetworkConfig {
|
||||
static const int connectTimeout = 15000; // 15초
|
||||
static const int maxRetries = 3; // 최대 3회 재시도
|
||||
static const Duration cacheMaxAge = Duration(minutes: 15); // 15분 캐싱
|
||||
}
|
||||
```
|
||||
|
||||
## 아키텍처
|
||||
|
||||
```
|
||||
lib/
|
||||
├── core/
|
||||
│ ├── errors/
|
||||
│ │ ├── app_exceptions.dart # 앱 전체 예외 클래스들
|
||||
│ │ ├── data_exceptions.dart # 데이터 레이어 예외
|
||||
│ │ └── network_exceptions.dart # 네트워크 예외
|
||||
│ └── network/
|
||||
│ ├── network_client.dart # Dio 기반 HTTP 클라이언트
|
||||
│ ├── network_config.dart # 네트워크 설정
|
||||
│ └── interceptors/
|
||||
│ ├── retry_interceptor.dart # 재시도 로직
|
||||
│ └── logging_interceptor.dart # 로깅
|
||||
├── data/
|
||||
│ ├── api/
|
||||
│ │ └── naver_api_client.dart # 네이버 API 클라이언트
|
||||
│ └── datasources/
|
||||
│ └── remote/
|
||||
│ ├── naver_map_parser.dart # HTML 파싱
|
||||
│ └── naver_search_service.dart # 통합 검색 서비스
|
||||
```
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **API 키 보안**: API 키는 절대 Git에 커밋하지 마세요. `.gitignore`에 추가하세요.
|
||||
2. **요청 제한**: 네이버 API는 일일 요청 제한이 있습니다. 과도한 요청을 피하세요.
|
||||
3. **캐싱**: 동일한 요청은 15분간 캐싱됩니다. 실시간 정보가 필요한 경우 `useCache: false` 옵션을 사용하세요.
|
||||
4. **웹 환경**: 웹에서는 CORS 제한으로 인해 프록시 서버를 통해 요청합니다.
|
||||
|
||||
## 문제 해결
|
||||
|
||||
### CORS 에러 (웹)
|
||||
웹 환경에서 CORS 에러가 발생하면 프록시 서버가 일시적으로 사용 불가능한 상태일 수 있습니다. 잠시 후 다시 시도하거나 직접 입력 기능을 사용하세요.
|
||||
|
||||
### 타임아웃 에러
|
||||
네트워크가 느린 환경에서는 `NetworkConfig`의 타임아웃 값을 늘려보세요.
|
||||
|
||||
### API 키 에러
|
||||
API 키가 올바르게 설정되었는지 확인하고, 네이버 개발자 센터에서 API 사용 권한이 활성화되어 있는지 확인하세요.
|
||||
79
lib/core/network/interceptors/logging_interceptor.dart
Normal file
79
lib/core/network/interceptors/logging_interceptor.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// 로깅 인터셉터
|
||||
///
|
||||
/// 네트워크 요청과 응답을 로그로 기록합니다.
|
||||
/// 디버그 모드에서만 활성화됩니다.
|
||||
class LoggingInterceptor extends Interceptor {
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
if (kDebugMode) {
|
||||
final uri = options.uri;
|
||||
final method = options.method;
|
||||
final headers = options.headers;
|
||||
|
||||
print('═══════════════════════════════════════════════════════════════');
|
||||
print('>>> REQUEST [$method] $uri');
|
||||
print('>>> Headers: $headers');
|
||||
|
||||
if (options.data != null) {
|
||||
print('>>> Body: ${options.data}');
|
||||
}
|
||||
|
||||
if (options.queryParameters.isNotEmpty) {
|
||||
print('>>> Query Parameters: ${options.queryParameters}');
|
||||
}
|
||||
}
|
||||
|
||||
return handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
if (kDebugMode) {
|
||||
final statusCode = response.statusCode;
|
||||
final uri = response.requestOptions.uri;
|
||||
|
||||
print('<<< RESPONSE [$statusCode] $uri');
|
||||
|
||||
if (response.headers.map.isNotEmpty) {
|
||||
print('<<< Headers: ${response.headers.map}');
|
||||
}
|
||||
|
||||
// 응답 본문은 너무 길 수 있으므로 처음 500자만 출력
|
||||
final responseData = response.data.toString();
|
||||
if (responseData.length > 500) {
|
||||
print('<<< Body: ${responseData.substring(0, 500)}...(truncated)');
|
||||
} else {
|
||||
print('<<< Body: $responseData');
|
||||
}
|
||||
|
||||
print('═══════════════════════════════════════════════════════════════');
|
||||
}
|
||||
|
||||
return handler.next(response);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
if (kDebugMode) {
|
||||
final uri = err.requestOptions.uri;
|
||||
final message = err.message;
|
||||
|
||||
print('═══════════════════════════════════════════════════════════════');
|
||||
print('!!! ERROR $uri');
|
||||
print('!!! Message: $message');
|
||||
|
||||
if (err.response != null) {
|
||||
print('!!! Status Code: ${err.response!.statusCode}');
|
||||
print('!!! Response: ${err.response!.data}');
|
||||
}
|
||||
|
||||
print('!!! Error Type: ${err.type}');
|
||||
print('═══════════════════════════════════════════════════════════════');
|
||||
}
|
||||
|
||||
return handler.next(err);
|
||||
}
|
||||
}
|
||||
97
lib/core/network/interceptors/retry_interceptor.dart
Normal file
97
lib/core/network/interceptors/retry_interceptor.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../network_config.dart';
|
||||
import '../../errors/network_exceptions.dart';
|
||||
|
||||
/// 재시도 인터셉터
|
||||
///
|
||||
/// 네트워크 오류 발생 시 자동으로 재시도합니다.
|
||||
/// 지수 백오프(exponential backoff) 알고리즘을 사용합니다.
|
||||
class RetryInterceptor extends Interceptor {
|
||||
final Dio dio;
|
||||
|
||||
RetryInterceptor({required this.dio});
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
// 재시도 카운트 확인
|
||||
final retryCount = err.requestOptions.extra['retryCount'] ?? 0;
|
||||
|
||||
// 재시도 가능한 오류인지 확인
|
||||
if (_shouldRetry(err) && retryCount < NetworkConfig.maxRetries) {
|
||||
try {
|
||||
// 지수 백오프 계산
|
||||
final delay = _calculateBackoffDelay(retryCount);
|
||||
|
||||
print('RetryInterceptor: 재시도 ${retryCount + 1}/${NetworkConfig.maxRetries} - ${delay}ms 대기');
|
||||
|
||||
// 대기
|
||||
await Future.delayed(Duration(milliseconds: delay));
|
||||
|
||||
// 재시도 카운트 증가
|
||||
err.requestOptions.extra['retryCount'] = retryCount + 1;
|
||||
|
||||
// 재시도 실행
|
||||
final response = await dio.fetch(err.requestOptions);
|
||||
|
||||
return handler.resolve(response);
|
||||
} catch (e) {
|
||||
// 재시도도 실패한 경우
|
||||
if (retryCount + 1 >= NetworkConfig.maxRetries) {
|
||||
return handler.reject(
|
||||
DioException(
|
||||
requestOptions: err.requestOptions,
|
||||
error: MaxRetriesExceededException(originalError: e),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return handler.next(err);
|
||||
}
|
||||
|
||||
/// 재시도 가능한 오류인지 판단
|
||||
bool _shouldRetry(DioException err) {
|
||||
// 네이버 관련 요청은 재시도하지 않음
|
||||
final url = err.requestOptions.uri.toString();
|
||||
if (url.contains('naver.com') || url.contains('naver.me')) {
|
||||
print('RetryInterceptor: 네이버 API 요청은 재시도하지 않음 - $url');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 네트워크 연결 오류
|
||||
if (err.type == DioExceptionType.connectionTimeout ||
|
||||
err.type == DioExceptionType.sendTimeout ||
|
||||
err.type == DioExceptionType.receiveTimeout ||
|
||||
err.type == DioExceptionType.connectionError) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 서버 오류 (5xx)
|
||||
final statusCode = err.response?.statusCode;
|
||||
if (statusCode != null && statusCode >= 500 && statusCode < 600) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 429 Too Many Requests는 재시도하지 않음
|
||||
// 재시도하면 더 많은 요청이 발생하여 문제가 악화됨
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 지수 백오프 지연 시간 계산
|
||||
int _calculateBackoffDelay(int retryCount) {
|
||||
final baseDelay = NetworkConfig.retryDelayMillis;
|
||||
final multiplier = NetworkConfig.retryDelayMultiplier;
|
||||
|
||||
// 지수 백오프: delay = baseDelay * (multiplier ^ retryCount)
|
||||
final exponentialDelay = baseDelay * pow(multiplier, retryCount);
|
||||
|
||||
// 지터(jitter) 추가로 동시 재시도 방지
|
||||
final jitter = Random().nextInt(1000);
|
||||
|
||||
return exponentialDelay.toInt() + jitter;
|
||||
}
|
||||
}
|
||||
260
lib/core/network/network_client.dart
Normal file
260
lib/core/network/network_client.dart
Normal file
@@ -0,0 +1,260 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
|
||||
import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'network_config.dart';
|
||||
import '../errors/network_exceptions.dart';
|
||||
import 'interceptors/retry_interceptor.dart';
|
||||
import 'interceptors/logging_interceptor.dart';
|
||||
|
||||
/// 네트워크 클라이언트
|
||||
///
|
||||
/// Dio를 기반으로 한 중앙화된 HTTP 클라이언트입니다.
|
||||
/// 재시도, 캐싱, 로깅 등의 기능을 제공합니다.
|
||||
class NetworkClient {
|
||||
late final Dio _dio;
|
||||
CacheStore? _cacheStore;
|
||||
|
||||
NetworkClient() {
|
||||
_dio = Dio(_createBaseOptions());
|
||||
_setupInterceptors();
|
||||
}
|
||||
|
||||
/// 기본 옵션 생성
|
||||
BaseOptions _createBaseOptions() {
|
||||
return BaseOptions(
|
||||
connectTimeout: Duration(milliseconds: NetworkConfig.connectTimeout),
|
||||
receiveTimeout: Duration(milliseconds: NetworkConfig.receiveTimeout),
|
||||
sendTimeout: Duration(milliseconds: NetworkConfig.sendTimeout),
|
||||
headers: {
|
||||
'User-Agent': NetworkConfig.userAgent,
|
||||
'Accept': 'application/json, text/html, */*',
|
||||
'Accept-Language': 'ko-KR,ko;q=0.9,en;q=0.8',
|
||||
},
|
||||
validateStatus: (status) => status != null && status < 500,
|
||||
);
|
||||
}
|
||||
|
||||
/// 인터셉터 설정
|
||||
Future<void> _setupInterceptors() async {
|
||||
// 로깅 인터셉터 (디버그 모드에서만)
|
||||
if (kDebugMode) {
|
||||
_dio.interceptors.add(LoggingInterceptor());
|
||||
}
|
||||
|
||||
// 재시도 인터셉터
|
||||
_dio.interceptors.add(RetryInterceptor(dio: _dio));
|
||||
|
||||
// 캐시 인터셉터 설정
|
||||
await _setupCacheInterceptor();
|
||||
|
||||
// 에러 변환 인터셉터
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onError: (error, handler) {
|
||||
handler.next(_transformError(error));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 캐시 인터셉터 설정
|
||||
Future<void> _setupCacheInterceptor() async {
|
||||
try {
|
||||
if (!kIsWeb) {
|
||||
final dir = await getTemporaryDirectory();
|
||||
final cacheDir = Directory('${dir.path}/lunchpick_cache');
|
||||
|
||||
if (!await cacheDir.exists()) {
|
||||
await cacheDir.create(recursive: true);
|
||||
}
|
||||
|
||||
_cacheStore = HiveCacheStore(cacheDir.path);
|
||||
} else {
|
||||
// 웹 환경에서는 메모리 캐시 사용
|
||||
_cacheStore = MemCacheStore();
|
||||
}
|
||||
|
||||
final cacheOptions = CacheOptions(
|
||||
store: _cacheStore,
|
||||
policy: CachePolicy.forceCache,
|
||||
maxStale: NetworkConfig.cacheMaxAge,
|
||||
priority: CachePriority.normal,
|
||||
keyBuilder: CacheOptions.defaultCacheKeyBuilder,
|
||||
allowPostMethod: false,
|
||||
);
|
||||
|
||||
_dio.interceptors.add(DioCacheInterceptor(options: cacheOptions));
|
||||
} catch (e) {
|
||||
debugPrint('NetworkClient: 캐시 설정 실패 - $e');
|
||||
// 캐시 실패해도 계속 진행
|
||||
}
|
||||
}
|
||||
|
||||
/// 에러 변환
|
||||
DioException _transformError(DioException error) {
|
||||
NetworkException networkException;
|
||||
|
||||
switch (error.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
networkException = ConnectionTimeoutException(originalError: error);
|
||||
break;
|
||||
|
||||
case DioExceptionType.connectionError:
|
||||
networkException = NoInternetException(originalError: error);
|
||||
break;
|
||||
|
||||
case DioExceptionType.badResponse:
|
||||
final statusCode = error.response?.statusCode ?? 0;
|
||||
final message = _getErrorMessage(error.response);
|
||||
|
||||
if (statusCode >= 500) {
|
||||
networkException = ServerException(
|
||||
message: message,
|
||||
statusCode: statusCode,
|
||||
originalError: error,
|
||||
);
|
||||
} else if (statusCode >= 400) {
|
||||
networkException = ClientException(
|
||||
message: message,
|
||||
statusCode: statusCode,
|
||||
originalError: error,
|
||||
);
|
||||
} else {
|
||||
networkException = ClientException(
|
||||
message: message,
|
||||
statusCode: statusCode,
|
||||
originalError: error,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
networkException = NoInternetException(
|
||||
message: error.message ?? '알 수 없는 네트워크 오류가 발생했습니다',
|
||||
originalError: error,
|
||||
);
|
||||
}
|
||||
|
||||
return DioException(
|
||||
requestOptions: error.requestOptions,
|
||||
response: error.response,
|
||||
type: error.type,
|
||||
error: networkException,
|
||||
);
|
||||
}
|
||||
|
||||
/// 에러 메시지 추출
|
||||
String _getErrorMessage(Response? response) {
|
||||
if (response == null) {
|
||||
return '서버 응답을 받을 수 없습니다';
|
||||
}
|
||||
|
||||
final statusCode = response.statusCode ?? 0;
|
||||
|
||||
// 상태 코드별 기본 메시지
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
return '잘못된 요청입니다';
|
||||
case 401:
|
||||
return '인증이 필요합니다';
|
||||
case 403:
|
||||
return '접근 권한이 없습니다';
|
||||
case 404:
|
||||
return '요청한 리소스를 찾을 수 없습니다';
|
||||
case 429:
|
||||
return '너무 많은 요청을 보냈습니다. 잠시 후 다시 시도해주세요';
|
||||
case 500:
|
||||
return '서버 내부 오류가 발생했습니다';
|
||||
case 502:
|
||||
return '게이트웨이 오류가 발생했습니다';
|
||||
case 503:
|
||||
return '서비스를 일시적으로 사용할 수 없습니다';
|
||||
default:
|
||||
return '서버 오류가 발생했습니다 (HTTP $statusCode)';
|
||||
}
|
||||
}
|
||||
|
||||
/// GET 요청
|
||||
Future<Response<T>> get<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
bool useCache = true,
|
||||
}) {
|
||||
final requestOptions = options ?? Options();
|
||||
|
||||
// 캐시 사용 설정
|
||||
if (!useCache) {
|
||||
requestOptions.extra = {
|
||||
...?requestOptions.extra,
|
||||
'disableCache': true,
|
||||
};
|
||||
}
|
||||
|
||||
return _dio.get<T>(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
options: requestOptions,
|
||||
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,
|
||||
}) {
|
||||
return _dio.post<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
|
||||
/// HEAD 요청 (리다이렉션 확인용)
|
||||
Future<Response<T>> head<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) {
|
||||
return _dio.head<T>(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// 캐시 삭제
|
||||
Future<void> clearCache() async {
|
||||
await _cacheStore?.clean();
|
||||
}
|
||||
|
||||
/// 리소스 정리
|
||||
void dispose() {
|
||||
_dio.close();
|
||||
_cacheStore?.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// 기본 네트워크 클라이언트 인스턴스
|
||||
final networkClient = NetworkClient();
|
||||
34
lib/core/network/network_config.dart
Normal file
34
lib/core/network/network_config.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
/// 네트워크 설정 상수
|
||||
///
|
||||
/// 모든 네트워크 관련 설정을 중앙 관리합니다.
|
||||
class NetworkConfig {
|
||||
// 타임아웃 설정 (밀리초)
|
||||
static const int connectTimeout = 15000; // 15초
|
||||
static const int receiveTimeout = 30000; // 30초
|
||||
static const int sendTimeout = 15000; // 15초
|
||||
|
||||
// 재시도 설정
|
||||
static const int maxRetries = 3;
|
||||
static const int retryDelayMillis = 1000; // 1초
|
||||
static const double retryDelayMultiplier = 2.0; // 지수 백오프
|
||||
|
||||
// 캐시 설정
|
||||
static const Duration cacheMaxAge = Duration(minutes: 15);
|
||||
static const int cacheMaxSize = 50 * 1024 * 1024; // 50MB
|
||||
|
||||
// 네이버 API 설정
|
||||
static const String naverApiBaseUrl = 'https://openapi.naver.com';
|
||||
static const String naverMapBaseUrl = 'https://map.naver.com';
|
||||
static const String naverShortUrlBase = 'https://naver.me';
|
||||
|
||||
// CORS 프록시 (웹 환경용)
|
||||
static const String corsProxyUrl = 'https://api.allorigins.win/get';
|
||||
|
||||
// User Agent
|
||||
static const String userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
||||
|
||||
/// CORS 프록시 URL 생성
|
||||
static String getCorsProxyUrl(String originalUrl) {
|
||||
return '$corsProxyUrl?url=${Uri.encodeComponent(originalUrl)}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user