feat(app): add manual entry and sharing flows
This commit is contained in:
@@ -108,15 +108,19 @@ try {
|
||||
|
||||
1. [네이버 개발자 센터](https://developers.naver.com)에서 애플리케이션 등록
|
||||
2. Client ID와 Client Secret 발급
|
||||
3. `lib/core/constants/api_keys.dart` 파일에 키 입력:
|
||||
3. 값을 base64로 인코딩한 뒤 `flutter run --dart-define`으로 전달:
|
||||
|
||||
```dart
|
||||
class ApiKeys {
|
||||
static const String naverClientId = 'YOUR_CLIENT_ID';
|
||||
static const String naverClientSecret = 'YOUR_CLIENT_SECRET';
|
||||
}
|
||||
```bash
|
||||
NAVER_CLIENT_ID=$(printf 'YOUR_CLIENT_ID' | base64)
|
||||
NAVER_CLIENT_SECRET=$(printf 'YOUR_CLIENT_SECRET' | base64)
|
||||
|
||||
flutter run \
|
||||
--dart-define=NAVER_CLIENT_ID=$NAVER_CLIENT_ID \
|
||||
--dart-define=NAVER_CLIENT_SECRET=$NAVER_CLIENT_SECRET
|
||||
```
|
||||
|
||||
로컬에서 빠르게 확인할 때는 base64 인코딩을 생략할 수 있습니다.
|
||||
|
||||
### 네트워크 설정 커스터마이징
|
||||
|
||||
`lib/core/network/network_config.dart`에서 타임아웃, 재시도 횟수 등을 조정할 수 있습니다:
|
||||
@@ -169,4 +173,4 @@ lib/
|
||||
네트워크가 느린 환경에서는 `NetworkConfig`의 타임아웃 값을 늘려보세요.
|
||||
|
||||
### API 키 에러
|
||||
API 키가 올바르게 설정되었는지 확인하고, 네이버 개발자 센터에서 API 사용 권한이 활성화되어 있는지 확인하세요.
|
||||
API 키가 올바르게 설정되었는지 확인하고, 네이버 개발자 센터에서 API 사용 권한이 활성화되어 있는지 확인하세요.
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// 로깅 인터셉터
|
||||
///
|
||||
///
|
||||
/// 네트워크 요청과 응답을 로그로 기록합니다.
|
||||
/// 디버그 모드에서만 활성화됩니다.
|
||||
class LoggingInterceptor extends Interceptor {
|
||||
@@ -12,35 +12,35 @@ class LoggingInterceptor extends Interceptor {
|
||||
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) {
|
||||
@@ -48,32 +48,32 @@ class LoggingInterceptor extends Interceptor {
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,36 +5,38 @@ 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 대기');
|
||||
|
||||
|
||||
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) {
|
||||
// 재시도도 실패한 경우
|
||||
@@ -48,10 +50,10 @@ class RetryInterceptor extends Interceptor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return handler.next(err);
|
||||
}
|
||||
|
||||
|
||||
/// 재시도 가능한 오류인지 판단
|
||||
bool _shouldRetry(DioException err) {
|
||||
// 네이버 관련 요청은 재시도하지 않음
|
||||
@@ -60,7 +62,7 @@ class RetryInterceptor extends Interceptor {
|
||||
print('RetryInterceptor: 네이버 API 요청은 재시도하지 않음 - $url');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 네트워크 연결 오류
|
||||
if (err.type == DioExceptionType.connectionTimeout ||
|
||||
err.type == DioExceptionType.sendTimeout ||
|
||||
@@ -68,30 +70,30 @@ class RetryInterceptor extends Interceptor {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,18 +11,18 @@ 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(
|
||||
@@ -37,20 +37,20 @@ class NetworkClient {
|
||||
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(
|
||||
@@ -60,24 +60,24 @@ class NetworkClient {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// 캐시 인터셉터 설정
|
||||
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,
|
||||
@@ -86,33 +86,33 @@ class NetworkClient {
|
||||
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,
|
||||
@@ -133,14 +133,14 @@ class NetworkClient {
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
networkException = NoInternetException(
|
||||
message: error.message ?? '알 수 없는 네트워크 오류가 발생했습니다',
|
||||
originalError: error,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return DioException(
|
||||
requestOptions: error.requestOptions,
|
||||
response: error.response,
|
||||
@@ -148,15 +148,15 @@ class NetworkClient {
|
||||
error: networkException,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// 에러 메시지 추출
|
||||
String _getErrorMessage(Response? response) {
|
||||
if (response == null) {
|
||||
return '서버 응답을 받을 수 없습니다';
|
||||
}
|
||||
|
||||
|
||||
final statusCode = response.statusCode ?? 0;
|
||||
|
||||
|
||||
// 상태 코드별 기본 메시지
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
@@ -179,7 +179,7 @@ class NetworkClient {
|
||||
return '서버 오류가 발생했습니다 (HTTP $statusCode)';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// GET 요청
|
||||
Future<Response<T>> get<T>(
|
||||
String path, {
|
||||
@@ -190,15 +190,12 @@ class NetworkClient {
|
||||
bool useCache = true,
|
||||
}) {
|
||||
final requestOptions = options ?? Options();
|
||||
|
||||
|
||||
// 캐시 사용 설정
|
||||
if (!useCache) {
|
||||
requestOptions.extra = {
|
||||
...?requestOptions.extra,
|
||||
'disableCache': true,
|
||||
};
|
||||
requestOptions.extra = {...?requestOptions.extra, 'disableCache': true};
|
||||
}
|
||||
|
||||
|
||||
return _dio.get<T>(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
@@ -207,7 +204,7 @@ class NetworkClient {
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// POST 요청
|
||||
Future<Response<T>> post<T>(
|
||||
String path, {
|
||||
@@ -228,7 +225,7 @@ class NetworkClient {
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// HEAD 요청 (리다이렉션 확인용)
|
||||
Future<Response<T>> head<T>(
|
||||
String path, {
|
||||
@@ -243,12 +240,12 @@ class NetworkClient {
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// 캐시 삭제
|
||||
Future<void> clearCache() async {
|
||||
await _cacheStore?.clean();
|
||||
}
|
||||
|
||||
|
||||
/// 리소스 정리
|
||||
void dispose() {
|
||||
_dio.close();
|
||||
@@ -257,4 +254,4 @@ class NetworkClient {
|
||||
}
|
||||
|
||||
/// 기본 네트워크 클라이언트 인스턴스
|
||||
final networkClient = NetworkClient();
|
||||
final networkClient = NetworkClient();
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
/// 네트워크 설정 상수
|
||||
///
|
||||
///
|
||||
/// 모든 네트워크 관련 설정을 중앙 관리합니다.
|
||||
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';
|
||||
|
||||
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