feat: 초기 프로젝트 설정 및 LunchPick 앱 구현
LunchPick(오늘 뭐 먹Z?) Flutter 앱의 초기 구현입니다. 주요 기능: - 네이버 지도 연동 맛집 추가 - 랜덤 메뉴 추천 시스템 - 날씨 기반 거리 조정 - 방문 기록 관리 - Bluetooth 맛집 공유 - 다크모드 지원 기술 스택: - Flutter 3.8.1+ - Riverpod 상태 관리 - Hive 로컬 DB - Clean Architecture 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
231
test/mocks/mock_naver_api_client.dart
Normal file
231
test/mocks/mock_naver_api_client.dart
Normal file
@@ -0,0 +1,231 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:lunchpick/data/api/naver_api_client.dart';
|
||||
import 'package:lunchpick/data/api/naver/naver_local_search_api.dart';
|
||||
import 'package:lunchpick/core/errors/network_exceptions.dart';
|
||||
|
||||
/// 테스트용 모의 네이버 API 클라이언트
|
||||
class MockNaverApiClient extends NaverApiClient {
|
||||
final Map<String, String> _urlMappings = {};
|
||||
final Map<String, String> _htmlResponses = {};
|
||||
final Map<String, dynamic> _searchResults = {};
|
||||
final Map<String, dynamic> _graphqlResponses = {};
|
||||
|
||||
/// URL 리다이렉션 매핑 설정
|
||||
void setUrlRedirect(String fromUrl, String toUrl) {
|
||||
_urlMappings[fromUrl] = toUrl;
|
||||
}
|
||||
|
||||
/// HTML 응답 설정
|
||||
void setHtmlResponse(String url, String html) {
|
||||
_htmlResponses[url] = html;
|
||||
}
|
||||
|
||||
/// 검색 결과 설정
|
||||
void setSearchResults(String query, List<NaverLocalSearchResult> results) {
|
||||
_searchResults[query] = results;
|
||||
}
|
||||
|
||||
/// GraphQL 응답 설정
|
||||
void setGraphQLResponse(Map<String, dynamic> response) {
|
||||
_graphqlResponses['default'] = response;
|
||||
}
|
||||
|
||||
/// 에러 시뮬레이션 설정
|
||||
bool shouldThrowError = false;
|
||||
String errorMessage = '테스트 에러';
|
||||
|
||||
@override
|
||||
Future<String> resolveShortUrl(String shortUrl) async {
|
||||
if (shouldThrowError && !_throw429) {
|
||||
throw Exception(errorMessage);
|
||||
}
|
||||
|
||||
// 설정된 매핑이 있으면 반환
|
||||
if (_urlMappings.containsKey(shortUrl)) {
|
||||
return _urlMappings[shortUrl]!;
|
||||
}
|
||||
|
||||
// 기본적으로 원본 URL 반환
|
||||
return shortUrl;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> fetchMapPageHtml(String url) async {
|
||||
if (shouldThrowError || _throw429) {
|
||||
throw Exception(errorMessage);
|
||||
}
|
||||
|
||||
// 설정된 HTML이 있으면 반환
|
||||
if (_htmlResponses.containsKey(url)) {
|
||||
return _htmlResponses[url]!;
|
||||
}
|
||||
|
||||
// 기본 HTML 반환
|
||||
return '''
|
||||
<html>
|
||||
<head>
|
||||
<meta property="og:title" content="기본 테스트 식당">
|
||||
</head>
|
||||
<body>
|
||||
<span class="GHAhO">기본 테스트 식당</span>
|
||||
</body>
|
||||
</html>
|
||||
''';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<NaverLocalSearchResult>> searchLocal({
|
||||
required String query,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
int display = 20,
|
||||
int start = 1,
|
||||
String sort = 'random',
|
||||
}) async {
|
||||
if (shouldThrowError) {
|
||||
throw Exception(errorMessage);
|
||||
}
|
||||
|
||||
// 설정된 검색 결과가 있으면 반환
|
||||
if (_searchResults.containsKey(query)) {
|
||||
return _searchResults[query] as List<NaverLocalSearchResult>;
|
||||
}
|
||||
|
||||
// 기본 검색 결과 반환
|
||||
return [
|
||||
NaverLocalSearchResult.fromJson({
|
||||
'title': '<b>$query</b> 테스트',
|
||||
'link': 'https://map.naver.com/p/restaurant/1234567890',
|
||||
'category': '한식>김치찌개',
|
||||
'description': '테스트 설명',
|
||||
'telephone': '02-1234-5678',
|
||||
'address': '서울시 종로구',
|
||||
'roadAddress': '서울시 종로구 세종대로 110',
|
||||
'mapx': 1269784147,
|
||||
'mapy': 375666805,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> fetchGraphQL({
|
||||
required String operationName,
|
||||
Map<String, dynamic>? variables,
|
||||
required String query,
|
||||
}) async {
|
||||
if (shouldThrowError || _throw429) {
|
||||
throw Exception(errorMessage);
|
||||
}
|
||||
|
||||
// 설정된 GraphQL 응답이 있으면 반환
|
||||
if (_graphqlResponses.containsKey('default')) {
|
||||
return {
|
||||
'data': _graphqlResponses['default'],
|
||||
};
|
||||
}
|
||||
|
||||
// 기본 응답 반환 (places 배열 형태로 반환)
|
||||
return {
|
||||
'data': {
|
||||
'places': [{
|
||||
'id': '1',
|
||||
'name': '기본 테스트 식당',
|
||||
'category': '한식',
|
||||
'address': '서울시 종로구',
|
||||
}],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> fetchPlaceNameFromPcmap(String placeId) async {
|
||||
if (shouldThrowError || _throw429) {
|
||||
throw Exception(errorMessage);
|
||||
}
|
||||
|
||||
// 테스트에서 설정한 값이 있으면 반환
|
||||
if (_placeNames.containsKey(placeId)) {
|
||||
return _placeNames[placeId];
|
||||
}
|
||||
|
||||
// 기본값 반환
|
||||
return '기본 테스트 식당';
|
||||
}
|
||||
|
||||
// fetchPlaceNameFromPcmap용 응답 저장소
|
||||
final Map<String, String> _placeNames = {};
|
||||
|
||||
/// 장소명 설정
|
||||
void setPlaceName(String placeId, String placeName) {
|
||||
_placeNames[placeId] = placeName;
|
||||
}
|
||||
|
||||
// V2 확장 메서드들
|
||||
final Map<String, String> _finalRedirectUrls = {};
|
||||
final Map<String, String> _secondKoreanTexts = {};
|
||||
bool _throw429 = false;
|
||||
|
||||
void setFinalRedirectUrl(String from, String to) {
|
||||
_finalRedirectUrls[from] = to;
|
||||
}
|
||||
|
||||
void setSecondKoreanText(String url, String text) {
|
||||
_secondKoreanTexts[url] = text;
|
||||
}
|
||||
|
||||
void setThrow429Error() {
|
||||
_throw429 = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getFinalRedirectUrl(String url) async {
|
||||
if (_throw429) {
|
||||
throw RateLimitException(
|
||||
retryAfter: '60',
|
||||
originalError: '429 Too Many Requests',
|
||||
);
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
return _finalRedirectUrls[url] ?? url;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> extractSecondKoreanText(String url) async {
|
||||
if (_throw429) {
|
||||
throw Exception('429 Too Many Requests');
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
return _secondKoreanTexts[url];
|
||||
}
|
||||
|
||||
// fetchKoreanTextsFromPcmap 구현
|
||||
final Map<String, Map<String, dynamic>> _koreanTextsData = {};
|
||||
|
||||
void setKoreanTextsData(String placeId, Map<String, dynamic> data) {
|
||||
_koreanTextsData[placeId] = data;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> fetchKoreanTextsFromPcmap(String placeId) async {
|
||||
if (shouldThrowError || _throw429) {
|
||||
throw Exception(errorMessage);
|
||||
}
|
||||
|
||||
// 설정된 데이터가 있으면 반환
|
||||
if (_koreanTextsData.containsKey(placeId)) {
|
||||
return _koreanTextsData[placeId]!;
|
||||
}
|
||||
|
||||
// 기본 데이터 반환
|
||||
return {
|
||||
'success': true,
|
||||
'koreanTexts': ['기본 테스트 식당'],
|
||||
'jsonLdName': '기본 테스트 식당',
|
||||
'apolloStateName': null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// NaverLocalSearchResult는 이미 naver_api_client.dart에 정의되어 있음
|
||||
Reference in New Issue
Block a user