Files
lunchpick/lib/data/api/naver/naver_graphql_api.dart
JiWoong Sul 2a01fa50c6 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
2025-11-22 00:10:51 +09:00

175 lines
4.2 KiB
Dart

import 'package:dio/dio.dart';
import 'package:lunchpick/core/utils/app_logger.dart';
import '../../../core/network/network_client.dart';
import '../../../core/errors/network_exceptions.dart';
/// 네이버 GraphQL API 클라이언트
///
/// 네이버 지도의 GraphQL API를 호출하여 상세 정보를 가져옵니다.
class NaverGraphQLApi {
final NetworkClient _networkClient;
static const String _graphqlEndpoint =
'https://pcmap-api.place.naver.com/graphql';
NaverGraphQLApi({NetworkClient? networkClient})
: _networkClient = networkClient ?? NetworkClient();
/// GraphQL 쿼리 실행
Future<Map<String, dynamic>> fetchGraphQL({
required String operationName,
required String query,
Map<String, dynamic>? variables,
}) async {
try {
final response = await _networkClient.post<Map<String, dynamic>>(
_graphqlEndpoint,
data: {
'operationName': operationName,
'query': query,
'variables': variables ?? {},
},
options: Options(
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Referer': 'https://map.naver.com/',
'Origin': 'https://map.naver.com',
},
),
);
if (response.data == null) {
throw ParseException(message: 'GraphQL 응답이 비어있습니다');
}
return response.data!;
} on DioException catch (e) {
AppLogger.error(
'fetchGraphQL error: $e',
error: e,
stackTrace: e.stackTrace,
);
throw ServerException(
message: 'GraphQL 요청 중 오류가 발생했습니다',
statusCode: e.response?.statusCode ?? 500,
originalError: e,
);
}
}
/// 장소 상세 정보 가져오기 (한국어 텍스트)
Future<Map<String, dynamic>> fetchKoreanTextsFromPcmap(String placeId) async {
const query = '''
query getKoreanTexts(\$id: String!) {
place(input: { id: \$id }) {
id
name
category
businessHours {
description
isDayOff
openTime
closeTime
dayOfWeek
businessHours
}
phone
address {
roadAddress
jibunAddress
}
description
menuInfo {
menus {
name
price
description
images {
url
}
}
}
keywords
priceCategory
imageCount
visitorReviewCount
visitorReviewScore
}
}
''';
try {
final response = await fetchGraphQL(
operationName: 'getKoreanTexts',
query: query,
variables: {'id': placeId},
);
if (response['errors'] != null) {
AppLogger.error('GraphQL errors: ${response['errors']}');
throw ParseException(message: 'GraphQL 오류: ${response['errors']}');
}
return response['data']?['place'] ?? {};
} catch (e, stackTrace) {
AppLogger.error(
'fetchKoreanTextsFromPcmap error: $e',
error: e,
stackTrace: stackTrace,
);
rethrow;
}
}
/// 장소 기본 정보 가져오기
Future<Map<String, dynamic>> fetchPlaceBasicInfo(String placeId) async {
const query = '''
query getPlaceBasicInfo(\$id: String!) {
place(input: { id: \$id }) {
id
name
category
phone
address {
roadAddress
jibunAddress
}
location {
latitude
longitude
}
homepageUrl
bookingUrl
}
}
''';
try {
final response = await fetchGraphQL(
operationName: 'getPlaceBasicInfo',
query: query,
variables: {'id': placeId},
);
if (response['errors'] != null) {
throw ParseException(message: 'GraphQL 오류: ${response['errors']}');
}
return response['data']?['place'] ?? {};
} catch (e, stackTrace) {
AppLogger.error(
'fetchPlaceBasicInfo error: $e',
error: e,
stackTrace: stackTrace,
);
rethrow;
}
}
void dispose() {
// 필요시 리소스 정리
}
}