import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import '../../../core/constants/api_keys.dart'; import '../../../core/network/network_client.dart'; import '../../../core/errors/network_exceptions.dart'; /// 네이버 로컬 검색 API 결과 모델 class NaverLocalSearchResult { final String title; final String link; final String category; final String description; final String telephone; final String address; final String roadAddress; final double? mapx; final double? mapy; NaverLocalSearchResult({ required this.title, required this.link, required this.category, required this.description, required this.telephone, required this.address, required this.roadAddress, this.mapx, this.mapy, }); factory NaverLocalSearchResult.fromJson(Map json) { // HTML 태그 제거 헬퍼 함수 String removeHtmlTags(String text) { return text .replaceAll(RegExp(r'<[^>]*>'), '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll(''', "'") .replaceAll(' ', ' '); } return NaverLocalSearchResult( title: removeHtmlTags(json['title'] ?? ''), link: json['link'] ?? '', category: json['category'] ?? '', description: removeHtmlTags(json['description'] ?? ''), telephone: json['telephone'] ?? '', address: json['address'] ?? '', roadAddress: json['roadAddress'] ?? '', mapx: json['mapx'] != null ? double.tryParse(json['mapx'].toString()) : null, mapy: json['mapy'] != null ? double.tryParse(json['mapy'].toString()) : null, ); } /// link 필드에서 Place ID 추출 /// /// link가 비어있거나 Place ID가 없으면 null 반환 String? extractPlaceId() { if (link.isEmpty) return null; // 네이버 지도 URL 패턴에서 Place ID 추출 // 예: https://map.naver.com/p/entry/place/1638379069 final placeIdMatch = RegExp(r'/place/(\d+)').firstMatch(link); if (placeIdMatch != null) { return placeIdMatch.group(1); } // 다른 패턴 시도: restaurant/1638379069 final restaurantIdMatch = RegExp(r'/restaurant/(\d+)').firstMatch(link); if (restaurantIdMatch != null) { return restaurantIdMatch.group(1); } // ID만 있는 경우 (10자리 숫자) final idOnlyMatch = RegExp(r'(\d{10})').firstMatch(link); if (idOnlyMatch != null) { return idOnlyMatch.group(1); } return null; } } /// 네이버 로컬 검색 API 클라이언트 /// /// 네이버 검색 API를 통해 장소 정보를 검색합니다. class NaverLocalSearchApi { final NetworkClient _networkClient; NaverLocalSearchApi({NetworkClient? networkClient}) : _networkClient = networkClient ?? NetworkClient(); /// 로컬 검색 API 호출 /// /// 검색어와 좌표를 기반으로 주변 식당을 검색합니다. Future> searchLocal({ required String query, double? latitude, double? longitude, int display = 20, int start = 1, String sort = 'random', // random, comment }) async { // API 키 확인 if (!ApiKeys.areKeysConfigured()) { throw ApiKeyException(); } try { final response = await _networkClient.get>( ApiKeys.naverLocalSearchEndpoint, queryParameters: { 'query': query, 'display': display, 'start': start, 'sort': sort, if (latitude != null && longitude != null) ...{ 'coordinate': '$longitude,$latitude', // 경도,위도 순서 }, }, options: Options( headers: { 'X-Naver-Client-Id': ApiKeys.naverClientId, 'X-Naver-Client-Secret': ApiKeys.naverClientSecret, }, ), ); final data = response.data; if (data == null || data['items'] == null) { return []; } final items = data['items'] as List; return items .map((item) => NaverLocalSearchResult.fromJson(item)) .toList(); } on DioException catch (e) { debugPrint('NaverLocalSearchApi Error: ${e.message}'); debugPrint('Error type: ${e.type}'); debugPrint('Error response: ${e.response?.data}'); if (e.error is NetworkException) { throw e.error!; } throw ServerException( message: '네이버 검색 중 오류가 발생했습니다', statusCode: e.response?.statusCode ?? 500, originalError: e, ); } } /// 특정 식당 상세 정보 검색 Future searchRestaurantDetails({ required String name, required String address, double? latitude, double? longitude, }) async { try { // 주소와 이름을 조합한 검색어 final query = '$name $address'; final results = await searchLocal( query: query, latitude: latitude, longitude: longitude, display: 5, sort: 'comment', // 정확도순 ); if (results.isEmpty) { return null; } // 가장 정확한 결과 찾기 for (final result in results) { if (result.title.contains(name) || name.contains(result.title)) { return result; } } // 정확한 매칭이 없으면 첫 번째 결과 반환 return results.first; } catch (e) { debugPrint('searchRestaurantDetails error: $e'); return null; } } void dispose() { // 필요시 리소스 정리 } }