Files
lunchpick/lib/data/api/naver/naver_url_resolver.dart

200 lines
5.7 KiB
Dart

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:lunchpick/core/utils/app_logger.dart';
import '../../../core/network/network_client.dart';
import '../../../core/network/network_config.dart';
/// 네이버 URL 리졸버
///
/// 네이버 단축 URL을 실제 URL로 변환하고 최종 리다이렉트 URL을 추적합니다.
class NaverUrlResolver {
final NetworkClient _networkClient;
NaverUrlResolver({NetworkClient? networkClient})
: _networkClient = networkClient ?? NetworkClient();
/// 단축 URL을 실제 URL로 변환
Future<String> resolveShortUrl(String shortUrl) async {
try {
// 웹 환경에서는 CORS 프록시 사용
if (kIsWeb) {
return await _resolveShortUrlViaProxy(shortUrl);
}
// 모바일 환경에서는 직접 HEAD 요청
final response = await _networkClient.head(
shortUrl,
options: Options(
followRedirects: false,
validateStatus: (status) => status! < 400,
),
);
// Location 헤더에서 리다이렉트 URL 추출
final location = response.headers.value('location');
if (location != null) {
return location;
}
// Location이 없는 경우, http.Client로 리다이렉트를 끝까지 따라가며 최종 URL 추출 (fallback)
final expanded = await _followRedirectsWithHttp(shortUrl);
if (expanded != null) {
return expanded;
}
// 리다이렉트가 없으면 원본 URL 반환
return shortUrl;
} on DioException catch (e) {
AppLogger.error(
'resolveShortUrl error: $e',
error: e,
stackTrace: e.stackTrace,
);
// 리다이렉트 응답인 경우 Location 헤더 확인
if (e.response?.statusCode == 301 || e.response?.statusCode == 302) {
final location = e.response?.headers.value('location');
if (location != null) {
return location;
}
}
// Dio 실패 시 fallback으로 http.Client 리다이렉트 추적 시도
final expanded = await _followRedirectsWithHttp(shortUrl);
if (expanded != null) {
return expanded;
}
// 오류 발생 시 원본 URL 반환
return shortUrl;
}
}
/// 프록시를 통한 단축 URL 해결 (웹 환경)
Future<String> _resolveShortUrlViaProxy(String shortUrl) async {
try {
final proxyUrl = NetworkConfig.getCorsProxyUrl(shortUrl);
final response = await _networkClient.get(
proxyUrl,
options: Options(
followRedirects: false,
validateStatus: (status) => true,
responseType: ResponseType.plain,
),
);
// 응답에서 URL 정보 추출
final responseData = response.data.toString();
// meta refresh 태그에서 URL 추출
final metaRefreshRegex = RegExp(
'<meta[^>]+http-equiv="refresh"[^>]+content="0;url=([^"]+)"[^>]*>',
caseSensitive: false,
);
final metaMatch = metaRefreshRegex.firstMatch(responseData);
if (metaMatch != null) {
return metaMatch.group(1) ?? shortUrl;
}
// og:url 메타 태그에서 URL 추출
final ogUrlRegex = RegExp(
'<meta[^>]+property="og:url"[^>]+content="([^"]+)"[^>]*>',
caseSensitive: false,
);
final ogMatch = ogUrlRegex.firstMatch(responseData);
if (ogMatch != null) {
return ogMatch.group(1) ?? shortUrl;
}
// Location 헤더 확인
final location = response.headers.value('location');
if (location != null) {
return location;
}
return shortUrl;
} catch (e, stackTrace) {
AppLogger.error(
'_resolveShortUrlViaProxy error: $e',
error: e,
stackTrace: stackTrace,
);
return shortUrl;
}
}
/// 최종 리다이렉트 URL 가져오기
///
/// 여러 단계의 리다이렉트를 거쳐 최종 URL을 반환합니다.
Future<String> getFinalRedirectUrl(String url) async {
try {
String currentUrl = url;
int redirectCount = 0;
const maxRedirects = 5;
while (redirectCount < maxRedirects) {
final response = await _networkClient.head(
currentUrl,
options: Options(
followRedirects: false,
validateStatus: (status) => status! < 400,
),
);
final location = response.headers.value('location');
if (location == null) {
break;
}
// 절대 URL로 변환
if (location.startsWith('/')) {
final uri = Uri.parse(currentUrl);
currentUrl = '${uri.scheme}://${uri.host}$location';
} else {
currentUrl = location;
}
redirectCount++;
}
return currentUrl;
} catch (e, stackTrace) {
AppLogger.error(
'getFinalRedirectUrl error: $e',
error: e,
stackTrace: stackTrace,
);
return url;
}
}
void dispose() {
// 필요시 리소스 정리
}
/// http.Client를 사용해 리다이렉트를 끝까지 따라가며 최종 URL을 반환한다.
/// 실패 시 null 반환.
Future<String?> _followRedirectsWithHttp(String shortUrl) async {
final client = http.Client();
try {
final request = http.Request('HEAD', Uri.parse(shortUrl))
..followRedirects = true
..maxRedirects = 5;
final response = await client.send(request);
return response.request?.url.toString();
} catch (e, stackTrace) {
AppLogger.error(
'_followRedirectsWithHttp error: $e',
error: e,
stackTrace: stackTrace,
);
return null;
} finally {
client.close();
}
}
}