feat(app): finalize ad gated flows and weather
- add AppLogger and replace scattered print logging\n- implement ad-gated recommendation flow with reminder handling and calendar link\n- complete Bluetooth share pipeline with ad gate and merge\n- integrate KMA weather API with caching and dart-define decoding\n- add NaverUrlProcessor refactor and restore restaurant repository tests
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:lunchpick/core/utils/app_logger.dart';
|
||||
|
||||
/// 네이버 HTML에서 데이터를 추출하는 유틸리티 클래스
|
||||
class NaverHtmlExtractor {
|
||||
@@ -323,11 +323,11 @@ class NaverHtmlExtractor {
|
||||
// 리스트로 변환하여 반환
|
||||
final resultList = uniqueTexts.toList();
|
||||
|
||||
debugPrint('========== 유효한 한글 텍스트 추출 결과 ==========');
|
||||
AppLogger.debug('========== 유효한 한글 텍스트 추출 결과 ==========');
|
||||
for (int i = 0; i < resultList.length; i++) {
|
||||
debugPrint('[$i] ${resultList[i]}');
|
||||
AppLogger.debug('[$i] ${resultList[i]}');
|
||||
}
|
||||
debugPrint('========== 총 ${resultList.length}개 추출됨 ==========');
|
||||
AppLogger.debug('========== 총 ${resultList.length}개 추출됨 ==========');
|
||||
|
||||
return resultList;
|
||||
}
|
||||
@@ -377,8 +377,12 @@ class NaverHtmlExtractor {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('NaverHtmlExtractor: JSON-LD 추출 실패 - $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverHtmlExtractor: JSON-LD 추출 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -418,14 +422,21 @@ class NaverHtmlExtractor {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// JSON 파싱 실패
|
||||
debugPrint('NaverHtmlExtractor: Apollo State JSON 파싱 실패 - $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverHtmlExtractor: Apollo State JSON 파싱 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('NaverHtmlExtractor: Apollo State 추출 실패 - $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverHtmlExtractor: Apollo State 추출 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -442,7 +453,7 @@ class NaverHtmlExtractor {
|
||||
final match = ogUrlRegex.firstMatch(html);
|
||||
if (match != null) {
|
||||
final url = match.group(1);
|
||||
debugPrint('NaverHtmlExtractor: og:url 추출 - $url');
|
||||
AppLogger.debug('NaverHtmlExtractor: og:url 추출 - $url');
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -454,11 +465,15 @@ class NaverHtmlExtractor {
|
||||
final canonicalMatch = canonicalRegex.firstMatch(html);
|
||||
if (canonicalMatch != null) {
|
||||
final url = canonicalMatch.group(1);
|
||||
debugPrint('NaverHtmlExtractor: canonical URL 추출 - $url');
|
||||
AppLogger.debug('NaverHtmlExtractor: canonical URL 추출 - $url');
|
||||
return url;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('NaverHtmlExtractor: Place Link 추출 실패 - $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverHtmlExtractor: Place Link 추출 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:lunchpick/core/utils/app_logger.dart';
|
||||
|
||||
/// 네이버 지도 HTML 파서
|
||||
///
|
||||
@@ -77,8 +77,12 @@ class NaverHtmlParser {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('NaverHtmlParser: 이름 추출 실패 - $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverHtmlParser: 이름 추출 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -97,8 +101,12 @@ class NaverHtmlParser {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('NaverHtmlParser: 카테고리 추출 실패 - $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverHtmlParser: 카테고리 추출 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -115,8 +123,12 @@ class NaverHtmlParser {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('NaverHtmlParser: 서브 카테고리 추출 실패 - $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverHtmlParser: 서브 카테고리 추출 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -137,8 +149,12 @@ class NaverHtmlParser {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('NaverHtmlParser: 설명 추출 실패 - $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverHtmlParser: 설명 추출 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -159,8 +175,12 @@ class NaverHtmlParser {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('NaverHtmlParser: 전화번호 추출 실패 - $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverHtmlParser: 전화번호 추출 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -179,8 +199,12 @@ class NaverHtmlParser {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('NaverHtmlParser: 도로명 주소 추출 실패 - $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverHtmlParser: 도로명 주소 추출 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -201,8 +225,12 @@ class NaverHtmlParser {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('NaverHtmlParser: 지번 주소 추출 실패 - $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverHtmlParser: 지번 주소 추출 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -238,8 +266,12 @@ class NaverHtmlParser {
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('NaverHtmlParser: 위도 추출 실패 - $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverHtmlParser: 위도 추출 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -275,8 +307,12 @@ class NaverHtmlParser {
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('NaverHtmlParser: 경도 추출 실패 - $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverHtmlParser: 경도 추출 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -297,8 +333,12 @@ class NaverHtmlParser {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('NaverHtmlParser: 영업시간 추출 실패 - $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverHtmlParser: 영업시간 추출 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:html/parser.dart' as html_parser;
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:lunchpick/domain/entities/restaurant.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../api/naver_api_client.dart';
|
||||
import '../../api/naver/naver_local_search_api.dart';
|
||||
import '../../../core/errors/network_exceptions.dart';
|
||||
import 'naver_html_parser.dart';
|
||||
import 'package:html/parser.dart' as html_parser;
|
||||
import 'package:lunchpick/core/utils/app_logger.dart';
|
||||
import 'package:lunchpick/domain/entities/restaurant.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../api/naver/naver_graphql_queries.dart';
|
||||
import '../../api/naver/naver_local_search_api.dart';
|
||||
import '../../api/naver_api_client.dart';
|
||||
import '../../../core/errors/network_exceptions.dart';
|
||||
import '../../../core/utils/category_mapper.dart';
|
||||
import 'naver_html_parser.dart';
|
||||
|
||||
/// 네이버 지도 URL 파서
|
||||
/// 네이버 지도 URL에서 식당 정보를 추출합니다.
|
||||
@@ -60,9 +62,7 @@ class NaverMapParser {
|
||||
throw NaverMapParseException('이미 dispose된 파서입니다');
|
||||
}
|
||||
try {
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: Starting to parse URL: $url');
|
||||
}
|
||||
AppLogger.debug('NaverMapParser: Starting to parse URL: $url');
|
||||
|
||||
// URL 유효성 검증
|
||||
if (!_isValidNaverUrl(url)) {
|
||||
@@ -72,9 +72,7 @@ class NaverMapParser {
|
||||
// 짧은 URL인 경우 리다이렉트 처리
|
||||
final String finalUrl = await _apiClient.resolveShortUrl(url);
|
||||
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: Final URL after redirect: $finalUrl');
|
||||
}
|
||||
AppLogger.debug('NaverMapParser: Final URL after redirect: $finalUrl');
|
||||
|
||||
// Place ID 추출 (10자리 숫자)
|
||||
final String? placeId = _extractPlaceId(finalUrl);
|
||||
@@ -82,11 +80,9 @@ class NaverMapParser {
|
||||
// 짧은 URL에서 직접 ID 추출 시도
|
||||
final shortUrlId = _extractShortUrlId(url);
|
||||
if (shortUrlId != null) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'NaverMapParser: Using short URL ID as place ID: $shortUrlId',
|
||||
);
|
||||
}
|
||||
AppLogger.debug(
|
||||
'NaverMapParser: Using short URL ID as place ID: $shortUrlId',
|
||||
);
|
||||
return _createFallbackRestaurant(shortUrlId, url);
|
||||
}
|
||||
throw NaverMapParseException('URL에서 Place ID를 추출할 수 없습니다: $url');
|
||||
@@ -96,9 +92,7 @@ class NaverMapParser {
|
||||
final isShortUrl = url.contains('naver.me');
|
||||
|
||||
if (isShortUrl) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: 단축 URL 감지, 향상된 파싱 시작');
|
||||
}
|
||||
AppLogger.debug('NaverMapParser: 단축 URL 감지, 향상된 파싱 시작');
|
||||
|
||||
try {
|
||||
// 한글 텍스트 추출 및 로컬 검색 API를 통한 정확한 정보 획득
|
||||
@@ -108,14 +102,14 @@ class NaverMapParser {
|
||||
userLatitude,
|
||||
userLongitude,
|
||||
);
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: 단축 URL 파싱 성공 - ${restaurant.name}');
|
||||
}
|
||||
AppLogger.debug('NaverMapParser: 단축 URL 파싱 성공 - ${restaurant.name}');
|
||||
return restaurant;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: 단축 URL 특별 처리 실패, 기본 파싱으로 전환 - $e');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverMapParser: 단축 URL 특별 처리 실패, 기본 파싱으로 전환 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
// 실패 시 기본 파싱으로 계속 진행
|
||||
}
|
||||
}
|
||||
@@ -177,9 +171,7 @@ class NaverMapParser {
|
||||
}) async {
|
||||
// 심플한 접근: URL로 직접 검색
|
||||
try {
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: URL 기반 검색 시작');
|
||||
}
|
||||
AppLogger.debug('NaverMapParser: URL 기반 검색 시작');
|
||||
|
||||
// 네이버 지도 URL 구성
|
||||
final placeUrl = '$_naverMapBaseUrl/p/entry/place/$placeId';
|
||||
@@ -201,27 +193,25 @@ class NaverMapParser {
|
||||
// place ID가 포함된 결과 찾기
|
||||
for (final result in searchResults) {
|
||||
if (result.link.contains(placeId)) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'NaverMapParser: URL 검색으로 정확한 매칭 찾음 - ${result.title}',
|
||||
);
|
||||
}
|
||||
AppLogger.debug(
|
||||
'NaverMapParser: URL 검색으로 정확한 매칭 찾음 - ${result.title}',
|
||||
);
|
||||
return _convertSearchResultToData(result);
|
||||
}
|
||||
}
|
||||
|
||||
// 정확한 매칭이 없으면 첫 번째 결과 사용
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'NaverMapParser: URL 검색 첫 번째 결과 사용 - ${searchResults.first.title}',
|
||||
);
|
||||
}
|
||||
AppLogger.debug(
|
||||
'NaverMapParser: URL 검색 첫 번째 결과 사용 - ${searchResults.first.title}',
|
||||
);
|
||||
return _convertSearchResultToData(searchResults.first);
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: URL 검색 실패 - $e');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverMapParser: URL 검색 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2: Place ID로 검색
|
||||
@@ -238,17 +228,17 @@ class NaverMapParser {
|
||||
);
|
||||
|
||||
if (searchResults.isNotEmpty) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'NaverMapParser: Place ID 검색 결과 사용 - ${searchResults.first.title}',
|
||||
);
|
||||
}
|
||||
AppLogger.debug(
|
||||
'NaverMapParser: Place ID 검색 결과 사용 - ${searchResults.first.title}',
|
||||
);
|
||||
return _convertSearchResultToData(searchResults.first);
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: Place ID 검색 실패 - $e');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverMapParser: Place ID 검색 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
// 429 에러인 경우 즉시 예외 발생
|
||||
if (e is DioException && e.response?.statusCode == 429) {
|
||||
@@ -258,10 +248,12 @@ class NaverMapParser {
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: URL 기반 검색 실패 - $e');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverMapParser: URL 기반 검색 실패 - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
// 429 에러인 경우 즉시 예외 발생
|
||||
if (e is DioException && e.response?.statusCode == 429) {
|
||||
@@ -275,9 +267,7 @@ class NaverMapParser {
|
||||
// 기존 GraphQL 방식으로 fallback (실패할 가능성 높지만 시도)
|
||||
// 첫 번째 시도: places 쿼리
|
||||
try {
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: Trying places query...');
|
||||
}
|
||||
AppLogger.debug('NaverMapParser: Trying places query...');
|
||||
final response = await _apiClient.fetchGraphQL(
|
||||
operationName: 'getPlaceDetail',
|
||||
variables: {'id': placeId},
|
||||
@@ -293,17 +283,17 @@ class NaverMapParser {
|
||||
return _extractPlaceData(placesData as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: places query failed - $e');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverMapParser: places query failed - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
// 두 번째 시도: nxPlaces 쿼리
|
||||
try {
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: Trying nxPlaces query...');
|
||||
}
|
||||
AppLogger.debug('NaverMapParser: Trying nxPlaces query...');
|
||||
final response = await _apiClient.fetchGraphQL(
|
||||
operationName: 'getPlaceDetail',
|
||||
variables: {'id': placeId},
|
||||
@@ -319,18 +309,18 @@ class NaverMapParser {
|
||||
return _extractPlaceData(nxPlacesData as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: nxPlaces query failed - $e');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'NaverMapParser: nxPlaces query failed - $e',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
// 모든 GraphQL 시도 실패 시 HTML 파싱으로 fallback
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'NaverMapParser: All GraphQL queries failed, falling back to HTML parsing',
|
||||
);
|
||||
}
|
||||
AppLogger.debug(
|
||||
'NaverMapParser: All GraphQL queries failed, falling back to HTML parsing',
|
||||
);
|
||||
return await _fallbackToHtmlParsing(placeId);
|
||||
}
|
||||
|
||||
@@ -508,7 +498,7 @@ class NaverMapParser {
|
||||
double? userLongitude,
|
||||
) async {
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: 단축 URL 향상된 파싱 시작');
|
||||
AppLogger.debug('NaverMapParser: 단축 URL 향상된 파싱 시작');
|
||||
}
|
||||
|
||||
// 1. 한글 텍스트 추출
|
||||
@@ -525,17 +515,17 @@ class NaverMapParser {
|
||||
if (koreanData['jsonLdName'] != null) {
|
||||
searchQuery = koreanData['jsonLdName'] as String;
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: JSON-LD 상호명 사용 - $searchQuery');
|
||||
AppLogger.debug('NaverMapParser: JSON-LD 상호명 사용 - $searchQuery');
|
||||
}
|
||||
} else if (koreanData['apolloStateName'] != null) {
|
||||
searchQuery = koreanData['apolloStateName'] as String;
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: Apollo State 상호명 사용 - $searchQuery');
|
||||
AppLogger.debug('NaverMapParser: Apollo State 상호명 사용 - $searchQuery');
|
||||
}
|
||||
} else if (koreanTexts.isNotEmpty) {
|
||||
searchQuery = koreanTexts.first as String;
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: 첫 번째 한글 텍스트 사용 - $searchQuery');
|
||||
AppLogger.debug('NaverMapParser: 첫 번째 한글 텍스트 사용 - $searchQuery');
|
||||
}
|
||||
} else {
|
||||
throw NaverMapParseException('유효한 한글 텍스트를 찾을 수 없습니다');
|
||||
@@ -543,7 +533,7 @@ class NaverMapParser {
|
||||
|
||||
// 2. 로컬 검색 API 호출
|
||||
if (kDebugMode) {
|
||||
debugPrint('NaverMapParser: 로컬 검색 API 호출 - "$searchQuery"');
|
||||
AppLogger.debug('NaverMapParser: 로컬 검색 API 호출 - "$searchQuery"');
|
||||
}
|
||||
|
||||
await Future.delayed(
|
||||
@@ -563,15 +553,15 @@ class NaverMapParser {
|
||||
|
||||
// 디버깅: 검색 결과 Place ID 분석
|
||||
if (kDebugMode) {
|
||||
debugPrint('=== 로컬 검색 결과 Place ID 분석 ===');
|
||||
AppLogger.debug('=== 로컬 검색 결과 Place ID 분석 ===');
|
||||
for (int i = 0; i < searchResults.length; i++) {
|
||||
final result = searchResults[i];
|
||||
final extractedId = result.extractPlaceId();
|
||||
debugPrint('[$i] ${result.title}');
|
||||
debugPrint(' 링크: ${result.link}');
|
||||
debugPrint(' 추출된 Place ID: $extractedId (타겟: $placeId)');
|
||||
AppLogger.debug('[$i] ${result.title}');
|
||||
AppLogger.debug(' 링크: ${result.link}');
|
||||
AppLogger.debug(' 추출된 Place ID: $extractedId (타겟: $placeId)');
|
||||
}
|
||||
debugPrint('=====================================');
|
||||
AppLogger.debug('=====================================');
|
||||
}
|
||||
|
||||
// 3. 최적의 결과 선택 - 3단계 매칭 알고리즘
|
||||
@@ -583,7 +573,7 @@ class NaverMapParser {
|
||||
if (extractedId == placeId) {
|
||||
bestMatch = result;
|
||||
if (kDebugMode) {
|
||||
debugPrint('✅ 1차 매칭 성공: Place ID 일치 - ${result.title}');
|
||||
AppLogger.debug('✅ 1차 매칭 성공: Place ID 일치 - ${result.title}');
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -604,7 +594,7 @@ class NaverMapParser {
|
||||
exactName.contains(result.title)) {
|
||||
bestMatch = result;
|
||||
if (kDebugMode) {
|
||||
debugPrint('✅ 2차 매칭 성공: 상호명 유사 - ${result.title}');
|
||||
AppLogger.debug('✅ 2차 매칭 성공: 상호명 유사 - ${result.title}');
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -620,7 +610,7 @@ class NaverMapParser {
|
||||
userLongitude,
|
||||
);
|
||||
if (bestMatch != null && kDebugMode) {
|
||||
debugPrint('✅ 3차 매칭: 거리 기반 - ${bestMatch.title}');
|
||||
AppLogger.debug('✅ 3차 매칭: 거리 기반 - ${bestMatch.title}');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,7 +618,7 @@ class NaverMapParser {
|
||||
if (bestMatch == null) {
|
||||
bestMatch = searchResults.first;
|
||||
if (kDebugMode) {
|
||||
debugPrint('✅ 최종 매칭: 첫 번째 결과 사용 - ${bestMatch.title}');
|
||||
AppLogger.debug('✅ 최종 매칭: 첫 번째 결과 사용 - ${bestMatch.title}');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -670,7 +660,7 @@ class NaverMapParser {
|
||||
}
|
||||
|
||||
if (kDebugMode && nearest != null) {
|
||||
debugPrint(
|
||||
AppLogger.debug(
|
||||
'가장 가까운 결과: ${nearest.title} (거리: ${minDistance.toStringAsFixed(2)}km)',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:lunchpick/core/utils/app_logger.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../api/naver_api_client.dart';
|
||||
import '../../api/naver/naver_local_search_api.dart';
|
||||
import '../../../domain/entities/restaurant.dart';
|
||||
import '../../../core/errors/network_exceptions.dart';
|
||||
import '../../../domain/entities/restaurant.dart';
|
||||
import 'naver_map_parser.dart';
|
||||
import 'naver_url_processor.dart';
|
||||
|
||||
/// 네이버 검색 서비스
|
||||
///
|
||||
@@ -12,14 +15,21 @@ import 'naver_map_parser.dart';
|
||||
class NaverSearchService {
|
||||
final NaverApiClient _apiClient;
|
||||
final NaverMapParser _mapParser;
|
||||
final NaverUrlProcessor _urlProcessor;
|
||||
final Uuid _uuid = const Uuid();
|
||||
|
||||
// 성능 최적화를 위한 정규식 캐싱
|
||||
static final RegExp _nonAlphanumericRegex = RegExp(r'[^가-힣a-z0-9]');
|
||||
|
||||
NaverSearchService({NaverApiClient? apiClient, NaverMapParser? mapParser})
|
||||
: _apiClient = apiClient ?? NaverApiClient(),
|
||||
_mapParser = mapParser ?? NaverMapParser(apiClient: apiClient);
|
||||
NaverSearchService({
|
||||
NaverApiClient? apiClient,
|
||||
NaverMapParser? mapParser,
|
||||
NaverUrlProcessor? urlProcessor,
|
||||
}) : _apiClient = apiClient ?? NaverApiClient(),
|
||||
_mapParser = mapParser ?? NaverMapParser(apiClient: apiClient),
|
||||
_urlProcessor =
|
||||
urlProcessor ??
|
||||
NaverUrlProcessor(apiClient: apiClient, mapParser: mapParser);
|
||||
|
||||
/// URL에서 식당 정보 가져오기
|
||||
///
|
||||
@@ -32,7 +42,7 @@ class NaverSearchService {
|
||||
/// - [NetworkException] 네트워크 오류 발생 시
|
||||
Future<Restaurant> getRestaurantFromUrl(String url) async {
|
||||
try {
|
||||
return await _mapParser.parseRestaurantFromUrl(url);
|
||||
return await _urlProcessor.processUrl(url);
|
||||
} catch (e) {
|
||||
if (e is NaverMapParseException || e is NetworkException) {
|
||||
rethrow;
|
||||
@@ -149,9 +159,9 @@ class NaverSearchService {
|
||||
);
|
||||
} catch (e) {
|
||||
// 상세 파싱 실패해도 기본 정보 반환
|
||||
if (kDebugMode) {
|
||||
debugPrint('[NaverSearchService] 상세 정보 파싱 실패: ${e.toString()}');
|
||||
}
|
||||
AppLogger.debug(
|
||||
'[NaverSearchService] 상세 정보 파싱 실패: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
42
lib/data/datasources/remote/naver_url_processor.dart
Normal file
42
lib/data/datasources/remote/naver_url_processor.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:lunchpick/domain/entities/restaurant.dart';
|
||||
|
||||
import '../../api/naver_api_client.dart';
|
||||
import 'naver_map_parser.dart';
|
||||
|
||||
/// 네이버 지도 URL을 처리하고 결과를 캐시하는 경량 프로세서.
|
||||
/// - 단축 URL 해석 → 지도 파서 실행
|
||||
/// - 동일 URL 재요청 시 메모리 캐시 반환
|
||||
class NaverUrlProcessor {
|
||||
final NaverApiClient _apiClient;
|
||||
final NaverMapParser _mapParser;
|
||||
final _cache = HashMap<String, Restaurant>();
|
||||
|
||||
NaverUrlProcessor({NaverApiClient? apiClient, NaverMapParser? mapParser})
|
||||
: _apiClient = apiClient ?? NaverApiClient(),
|
||||
_mapParser = mapParser ?? NaverMapParser(apiClient: apiClient);
|
||||
|
||||
Future<Restaurant> processUrl(
|
||||
String url, {
|
||||
double? userLatitude,
|
||||
double? userLongitude,
|
||||
}) async {
|
||||
final normalizedUrl = url.trim();
|
||||
if (_cache.containsKey(normalizedUrl)) {
|
||||
return _cache[normalizedUrl]!;
|
||||
}
|
||||
|
||||
final resolved = await _apiClient.resolveShortUrl(normalizedUrl);
|
||||
final restaurant = await _mapParser.parseRestaurantFromUrl(
|
||||
resolved,
|
||||
userLatitude: userLatitude,
|
||||
userLongitude: userLongitude,
|
||||
);
|
||||
_cache[normalizedUrl] = restaurant;
|
||||
_cache[resolved] = restaurant;
|
||||
return restaurant;
|
||||
}
|
||||
|
||||
void clearCache() => _cache.clear();
|
||||
}
|
||||
Reference in New Issue
Block a user