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:
226
test/unit/data/datasources/remote/naver_map_parser_test.dart
Normal file
226
test/unit/data/datasources/remote/naver_map_parser_test.dart
Normal file
@@ -0,0 +1,226 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:lunchpick/data/datasources/remote/naver_map_parser.dart';
|
||||
import 'package:lunchpick/domain/entities/restaurant.dart';
|
||||
import 'package:lunchpick/data/api/naver/naver_local_search_api.dart';
|
||||
import 'package:lunchpick/core/errors/network_exceptions.dart';
|
||||
import '../../../../mocks/mock_naver_api_client.dart';
|
||||
|
||||
void main() {
|
||||
late NaverMapParser parser;
|
||||
late MockNaverApiClient mockApiClient;
|
||||
|
||||
setUp(() {
|
||||
mockApiClient = MockNaverApiClient();
|
||||
parser = NaverMapParser(apiClient: mockApiClient);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
parser.dispose();
|
||||
});
|
||||
|
||||
group('NaverMapParser', () {
|
||||
group('URL 유효성 검증', () {
|
||||
test('유효한 네이버 지도 URL을 인식해야 함', () async {
|
||||
final validUrls = [
|
||||
'https://map.naver.com/p/restaurant/1234567890',
|
||||
'https://naver.me/abcdefgh',
|
||||
'https://map.naver.com/p/entry/place/1234567890',
|
||||
];
|
||||
|
||||
for (final url in validUrls) {
|
||||
mockApiClient.setUrlRedirect(url, 'https://map.naver.com/p/restaurant/1234567890');
|
||||
|
||||
// 검색 API 응답 설정
|
||||
mockApiClient.setSearchResults(
|
||||
'https://map.naver.com/p/entry/place/1234567890',
|
||||
[
|
||||
NaverLocalSearchResult.fromJson({
|
||||
'title': '테스트 식당',
|
||||
'link': 'https://map.naver.com/p/restaurant/1234567890',
|
||||
'category': '한식',
|
||||
'description': '테스트 설명',
|
||||
'telephone': '02-1234-5678',
|
||||
'address': '서울 강남구 테스트동 123-45',
|
||||
'roadAddress': '서울 강남구 테스트로 123',
|
||||
'mapx': 1269780000,
|
||||
'mapy': 375665000,
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
final result = await parser.parseRestaurantFromUrl(url);
|
||||
expect(result, isA<Restaurant>(), reason: 'URL: $url');
|
||||
expect(result.name, '테스트 식당', reason: 'URL: $url');
|
||||
}
|
||||
});
|
||||
|
||||
test('잘못된 URL은 예외를 던져야 함', () async {
|
||||
final invalidUrls = [
|
||||
'https://www.google.com',
|
||||
'https://map.kakao.com',
|
||||
'https://naver.com',
|
||||
'not-a-url',
|
||||
'',
|
||||
];
|
||||
|
||||
for (final url in invalidUrls) {
|
||||
expect(
|
||||
() => parser.parseRestaurantFromUrl(url),
|
||||
throwsA(isA<NaverMapParseException>()),
|
||||
reason: 'URL: $url should throw exception',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('API 응답 처리', () {
|
||||
test('검색 API로 식당 정보를 이지해야 함', () async {
|
||||
const url = 'https://map.naver.com/p/restaurant/1234567890';
|
||||
mockApiClient.setUrlRedirect(url, url);
|
||||
|
||||
// 검색 API 응답 설정
|
||||
mockApiClient.setSearchResults(
|
||||
'https://map.naver.com/p/entry/place/1234567890',
|
||||
[
|
||||
NaverLocalSearchResult.fromJson({
|
||||
'title': '맛있는 한식당',
|
||||
'link': 'https://map.naver.com/p/restaurant/1234567890',
|
||||
'category': '한식>고기요리',
|
||||
'description': '평일 11:00 - 22:00, 주말 10:00 - 23:00',
|
||||
'telephone': '02-123-4567',
|
||||
'address': '서울 강남구 역삼동 123-45',
|
||||
'roadAddress': '서울 강남구 테헤란로 123',
|
||||
'mapx': 1270396000,
|
||||
'mapy': 375012000,
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
final result = await parser.parseRestaurantFromUrl(url);
|
||||
|
||||
expect(result, isA<Restaurant>());
|
||||
expect(result.name, '맛있는 한식당');
|
||||
expect(result.category, '한식');
|
||||
expect(result.description, contains('평일 11:00 - 22:00'));
|
||||
expect(result.phoneNumber, '02-123-4567');
|
||||
expect(result.roadAddress, '서울 강남구 테헤란로 123');
|
||||
expect(result.latitude, closeTo(37.5012, 0.0001));
|
||||
expect(result.longitude, closeTo(127.0396, 0.0001));
|
||||
});
|
||||
|
||||
test('GraphQL API로 식당 정보를 가져와야 함', () async {
|
||||
const url = 'https://map.naver.com/p/restaurant/9876543210';
|
||||
mockApiClient.setUrlRedirect(url, url);
|
||||
|
||||
// GraphQL 응답 설정
|
||||
mockApiClient.setGraphQLResponse({
|
||||
'places': [{
|
||||
'id': '9876543210',
|
||||
'name': '메타태그 식당',
|
||||
'category': '기타',
|
||||
'description': '맛있는 음식점',
|
||||
'address': '서울시 강남구',
|
||||
'roadAddress': '서울시 강남구 테헤란로',
|
||||
'phone': '02-987-6543',
|
||||
'location': {
|
||||
'lat': 37.5,
|
||||
'lng': 127.0,
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
final result = await parser.parseRestaurantFromUrl(url);
|
||||
|
||||
expect(result, isA<Restaurant>());
|
||||
expect(result.name, '메타태그 식당');
|
||||
expect(result.category, '기타');
|
||||
});
|
||||
|
||||
test('필수 정보가 없으면 기본값을 사용해야 함', () async {
|
||||
const url = 'https://map.naver.com/p/restaurant/1234567890';
|
||||
mockApiClient.setUrlRedirect(url, url);
|
||||
|
||||
// 빈 GraphQL 응답
|
||||
mockApiClient.setGraphQLResponse({});
|
||||
|
||||
// HTML 파싱도 실패하도록 설정
|
||||
mockApiClient.setHtmlResponse(url, '<html></html>');
|
||||
|
||||
final result = await parser.parseRestaurantFromUrl(url);
|
||||
|
||||
// 기본값이 사용되어야 함
|
||||
expect(result, isA<Restaurant>());
|
||||
expect(result.name, contains('1234567890'));
|
||||
expect(result.category, '음식점');
|
||||
});
|
||||
});
|
||||
|
||||
group('단축 URL 처리', () {
|
||||
test('단축 URL을 실제 URL로 변환해야 함', () async {
|
||||
const shortUrl = 'https://naver.me/abc123';
|
||||
const actualUrl = 'https://map.naver.com/p/restaurant/1234567890';
|
||||
|
||||
mockApiClient.setUrlRedirect(shortUrl, actualUrl);
|
||||
|
||||
// 단축 URL용 한글 텍스트 추출 응답
|
||||
mockApiClient.setKoreanTextsData('1234567890', {
|
||||
'success': true,
|
||||
'koreanTexts': ['리다이렉트 식당'],
|
||||
'jsonLdName': '리다이렉트 식당',
|
||||
'apolloStateName': null,
|
||||
});
|
||||
|
||||
// 검색 API 응답 설정
|
||||
mockApiClient.setSearchResults(
|
||||
'리다이렉트 식당',
|
||||
[
|
||||
NaverLocalSearchResult.fromJson({
|
||||
'title': '리다이렉트 식당',
|
||||
'link': actualUrl,
|
||||
'category': '카페',
|
||||
'description': '',
|
||||
'telephone': '',
|
||||
'address': '서울 마포구',
|
||||
'roadAddress': '서울 마포구 테스트로 100',
|
||||
'mapx': 1268900000,
|
||||
'mapy': 375200000,
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
final result = await parser.parseRestaurantFromUrl(shortUrl);
|
||||
|
||||
expect(result, isA<Restaurant>());
|
||||
expect(result.name, '리다이렉트 식당');
|
||||
expect(result.category, '카페');
|
||||
});
|
||||
});
|
||||
|
||||
group('에러 처리', () {
|
||||
test('네트워크 오류 시 예외를 던져야 함', () async {
|
||||
const url = 'https://map.naver.com/p/restaurant/network-error';
|
||||
|
||||
mockApiClient.shouldThrowError = true;
|
||||
mockApiClient.errorMessage = 'Network error';
|
||||
|
||||
expect(
|
||||
() => parser.parseRestaurantFromUrl(url),
|
||||
throwsException,
|
||||
);
|
||||
});
|
||||
|
||||
test('429 에러 시 적절한 예외를 던져야 함', () async {
|
||||
const url = 'https://map.naver.com/p/restaurant/1234567890';
|
||||
mockApiClient.setUrlRedirect(url, url);
|
||||
|
||||
// 429 에러 설정
|
||||
mockApiClient.setThrow429Error();
|
||||
|
||||
expect(
|
||||
() => parser.parseRestaurantFromUrl(url),
|
||||
throwsA(isA<RateLimitException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user