import '../models/service_info.dart'; import '../data/service_data_repository.dart'; import '../data/legacy_service_data.dart'; import 'category_mapper_service.dart'; import '../../../utils/logger.dart'; /// URL 매칭 관련 기능을 제공하는 서비스 클래스 class UrlMatcherService { final ServiceDataRepository _dataRepository; final CategoryMapperService _categoryMapper; UrlMatcherService(this._dataRepository, this._categoryMapper); /// 도메인 추출 (www와 TLD 제외) String? extractDomain(String url) { try { final uri = Uri.parse(url); final host = uri.host.toLowerCase(); // 도메인 부분 추출 var parts = host.split('.'); // www 제거 if (parts.isNotEmpty && parts[0] == 'www') { parts = parts.sublist(1); } // 서브도메인 처리 (예: music.youtube.com) if (parts.length >= 3) { // 서브도메인 포함 전체 도메인 반환 return parts.sublist(0, parts.length - 1).join('.'); } else if (parts.length >= 2) { // 메인 도메인만 반환 return parts[0]; } return null; } catch (e) { Log.e('UrlMatcherService: 도메인 추출 실패', e); return null; } } /// URL로 서비스 찾기 Future findServiceByUrl(String url) async { final domain = extractDomain(url); if (domain == null) return null; // JSON 데이터가 있으면 JSON에서 찾기 final servicesData = _dataRepository.getServicesData(); if (servicesData != null) { final categories = servicesData['categories'] as Map; for (final categoryEntry in categories.entries) { final categoryId = categoryEntry.key; final categoryData = categoryEntry.value as Map; final services = categoryData['services'] as Map; for (final serviceEntry in services.entries) { final serviceId = serviceEntry.key; final serviceData = serviceEntry.value as Map; final domains = List.from(serviceData['domains'] ?? []); // 도메인이 일치하는지 확인 for (final serviceDomain in domains) { if (domain.contains(serviceDomain) || serviceDomain.contains(domain)) { final names = List.from(serviceData['names'] ?? []); final urls = serviceData['urls'] as Map?; return ServiceInfo( serviceId: serviceId, serviceName: names.isNotEmpty ? names[0] : serviceId, serviceUrl: urls?['kr'] ?? urls?['en'], cancellationUrl: null, categoryId: _categoryMapper.getCategoryIdByKey(categoryId), categoryNameKr: categoryData['nameKr'] ?? '', categoryNameEn: categoryData['nameEn'] ?? '', ); } } } } } // JSON에서 못 찾았으면 레거시 방식으로 찾기 for (final entry in LegacyServiceData.allServices.entries) { final serviceUrl = entry.value; final serviceDomain = extractDomain(serviceUrl); if (serviceDomain != null && (domain.contains(serviceDomain) || serviceDomain.contains(domain))) { return ServiceInfo( serviceId: entry.key, serviceName: entry.key, serviceUrl: serviceUrl, cancellationUrl: null, categoryId: _categoryMapper.getCategoryForLegacyService(entry.key), categoryNameKr: '', categoryNameEn: '', ); } } return null; } /// 서비스명으로 URL 찾기 String? suggestUrl(String serviceName) { if (serviceName.isEmpty) { Log.w('UrlMatcherService: 빈 serviceName'); return null; } // 소문자로 변환하여 비교 final lowerName = serviceName.toLowerCase().trim(); try { // 정확한 매칭을 먼저 시도 for (final entry in LegacyServiceData.allServices.entries) { if (lowerName.contains(entry.key.toLowerCase())) { Log.d('UrlMatcherService: 정확한 매칭 - $lowerName -> ${entry.key}'); return entry.value; } } // OTT 서비스 검사 for (final entry in LegacyServiceData.ottServices.entries) { if (lowerName.contains(entry.key.toLowerCase())) { Log.d('UrlMatcherService: OTT 서비스 매칭 - $lowerName -> ${entry.key}'); return entry.value; } } // 음악 서비스 검사 for (final entry in LegacyServiceData.musicServices.entries) { if (lowerName.contains(entry.key.toLowerCase())) { Log.d('UrlMatcherService: 음악 서비스 매칭 - $lowerName -> ${entry.key}'); return entry.value; } } // AI 서비스 검사 for (final entry in LegacyServiceData.aiServices.entries) { if (lowerName.contains(entry.key.toLowerCase())) { Log.d('UrlMatcherService: AI 서비스 매칭 - $lowerName -> ${entry.key}'); return entry.value; } } // 프로그래밍 서비스 검사 for (final entry in LegacyServiceData.programmingServices.entries) { if (lowerName.contains(entry.key.toLowerCase())) { Log.d('UrlMatcherService: 프로그래밍 서비스 매칭 - $lowerName -> ${entry.key}'); return entry.value; } } // 오피스 툴 검사 for (final entry in LegacyServiceData.officeTools.entries) { if (lowerName.contains(entry.key.toLowerCase())) { Log.d('UrlMatcherService: 오피스 툴 매칭 - $lowerName -> ${entry.key}'); return entry.value; } } // 기타 서비스 검사 for (final entry in LegacyServiceData.otherServices.entries) { if (lowerName.contains(entry.key.toLowerCase())) { Log.d('UrlMatcherService: 기타 서비스 매칭 - $lowerName -> ${entry.key}'); return entry.value; } } // 전체 서비스에서 부분 매칭 재시도 for (final entry in LegacyServiceData.allServices.entries) { final key = entry.key.toLowerCase(); if (key.contains(lowerName) || lowerName.contains(key)) { Log.d('UrlMatcherService: 부분 매칭 - $lowerName -> ${entry.key}'); return entry.value; } } Log.d('UrlMatcherService: 매칭 실패 - $lowerName'); return null; } catch (e) { Log.e('UrlMatcherService: suggestUrl 에러', e); return null; } } /// URL이 알려진 서비스 URL인지 확인 Future isKnownServiceUrl(String url) async { final serviceInfo = await findServiceByUrl(url); return serviceInfo != null; } /// 입력된 서비스 이름이나 문자열에서 매칭되는 URL을 찾아 반환 String? findMatchingUrl(String text, {bool usePartialMatch = true}) { // 입력 텍스트가 비어있거나 null인 경우 if (text.isEmpty) { return null; } // 소문자로 변환하여 처리 final String lowerText = text.toLowerCase().trim(); // 정확히 일치하는 경우 if (LegacyServiceData.allServices.containsKey(lowerText)) { return LegacyServiceData.allServices[lowerText]; } // 부분 일치 검색이 활성화된 경우 if (usePartialMatch) { // 가장 긴 부분 매칭 찾기 String? bestMatch; int maxLength = 0; for (var entry in LegacyServiceData.allServices.entries) { final String key = entry.key; // 입력된 텍스트에 서비스 키워드가 포함되어 있거나, 서비스 키워드에 입력된 텍스트가 포함된 경우 if (lowerText.contains(key) || key.contains(lowerText)) { // 더 긴 매칭을 우선시 if (key.length > maxLength) { maxLength = key.length; bestMatch = entry.value; } } } return bestMatch; } return null; } }