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 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 _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( ']+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( ']+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 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 _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(); } } }