Files
lunchpick/lib/data/api/naver/naver_local_search_api.dart
2025-11-19 16:36:39 +09:00

202 lines
5.6 KiB
Dart

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<String, dynamic> json) {
// HTML 태그 제거 헬퍼 함수
String removeHtmlTags(String text) {
return text
.replaceAll(RegExp(r'<[^>]*>'), '')
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&#39;', "'")
.replaceAll('&nbsp;', ' ');
}
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<List<NaverLocalSearchResult>> 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<Map<String, dynamic>>(
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<NaverLocalSearchResult?> 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() {
// 필요시 리소스 정리
}
}