152 lines
4.2 KiB
Dart
152 lines
4.2 KiB
Dart
import 'package:dio/dio.dart';
|
|
import 'package:flutter/foundation.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;
|
|
}
|
|
|
|
// 리다이렉트가 없으면 원본 URL 반환
|
|
return shortUrl;
|
|
} on DioException catch (e) {
|
|
debugPrint('resolveShortUrl error: $e');
|
|
|
|
// 리다이렉트 응답인 경우 Location 헤더 확인
|
|
if (e.response?.statusCode == 301 || e.response?.statusCode == 302) {
|
|
final location = e.response?.headers.value('location');
|
|
if (location != null) {
|
|
return location;
|
|
}
|
|
}
|
|
|
|
// 오류 발생 시 원본 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) {
|
|
debugPrint('_resolveShortUrlViaProxy error: $e');
|
|
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) {
|
|
debugPrint('getFinalRedirectUrl error: $e');
|
|
return url;
|
|
}
|
|
}
|
|
|
|
void dispose() {
|
|
// 필요시 리소스 정리
|
|
}
|
|
}
|