LunchPick(오늘 뭐 먹Z?) Flutter 앱의 초기 구현입니다. 주요 기능: - 네이버 지도 연동 맛집 추가 - 랜덤 메뉴 추천 시스템 - 날씨 기반 거리 조정 - 방문 기록 관리 - Bluetooth 맛집 공유 - 다크모드 지원 기술 스택: - Flutter 3.8.1+ - Riverpod 상태 관리 - Hive 로컬 DB - Clean Architecture 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
360 lines
12 KiB
Dart
360 lines
12 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:lunchpick/data/datasources/remote/naver_map_parser.dart';
|
|
import '../mocks/mock_naver_api_client.dart';
|
|
|
|
void main() {
|
|
group('NaverMapParser - 네이버 API 통합 테스트', () {
|
|
test('네이버 로컬 API 응답 시뮬레이션', () async {
|
|
// 실제 네이버 로컬 API 응답 형식을 모방
|
|
final mockApiClient = MockNaverApiClient();
|
|
|
|
// HTML 응답 설정
|
|
final htmlContent = '''
|
|
<html>
|
|
<head>
|
|
<meta property="og:title" content="맛있는 김치찌개">
|
|
<meta property="og:url" content="https://map.naver.com/p/restaurant/1234567890?y=37.5666805&x=126.9784147">
|
|
</head>
|
|
<body>
|
|
<span class="GHAhO">맛있는 김치찌개</span>
|
|
<span class="DJJvD">한식 > 김치찌개</span>
|
|
<span class="IH7VW">서울특별시 종로구 세종대로 110</span>
|
|
<span class="xlx7Q">02-1234-5678</span>
|
|
<time class="aT6WB">매일 10:30 - 21:00</time>
|
|
<div class="description">35년 전통의 김치찌개 전문점입니다.</div>
|
|
</body>
|
|
</html>
|
|
''';
|
|
|
|
mockApiClient.setHtmlResponse(
|
|
'https://map.naver.com/p/restaurant/1234567890',
|
|
htmlContent,
|
|
);
|
|
|
|
// GraphQL 응답 설정
|
|
mockApiClient.setGraphQLResponse({
|
|
'place': {
|
|
'id': '1234567890',
|
|
'name': '맛있는 김치찌개',
|
|
'category': '한식>김치찌개',
|
|
'address': '서울특별시 종로구 세종대로 110',
|
|
'roadAddress': '서울특별시 종로구 세종대로 110',
|
|
'phone': '02-1234-5678',
|
|
'businessHours': {
|
|
'description': '매일 10:30 - 21:00',
|
|
},
|
|
'location': {
|
|
'lat': 37.5666805,
|
|
'lng': 126.9784147,
|
|
},
|
|
},
|
|
});
|
|
|
|
final parser = NaverMapParser(apiClient: mockApiClient);
|
|
|
|
// 네이버 지도 URL로 파싱
|
|
final restaurant = await parser.parseRestaurantFromUrl(
|
|
'https://map.naver.com/p/restaurant/1234567890',
|
|
);
|
|
|
|
// API 응답과 HTML 파싱 결과가 일치하는지 확인
|
|
expect(restaurant.name, '맛있는 김치찌개');
|
|
expect(restaurant.category, '한식');
|
|
expect(restaurant.subCategory, '김치찌개');
|
|
expect(restaurant.phoneNumber, '02-1234-5678');
|
|
expect(restaurant.roadAddress, '서울특별시 종로구 세종대로 110');
|
|
expect(restaurant.businessHours, '매일 10:30 - 21:00');
|
|
|
|
// 좌표 변환이 올바른지 확인
|
|
expect(restaurant.latitude, closeTo(37.5666805, 0.0000001));
|
|
expect(restaurant.longitude, closeTo(126.9784147, 0.0000001));
|
|
});
|
|
|
|
test('좌표 변환 정확성 테스트', () async {
|
|
final testCases = [
|
|
{
|
|
'mapx': '1269784147',
|
|
'mapy': '375666805',
|
|
'expectedLat': 37.5666805,
|
|
'expectedLng': 126.9784147,
|
|
},
|
|
{
|
|
'mapx': '1270333333',
|
|
'mapy': '374999999',
|
|
'expectedLat': 37.4999999,
|
|
'expectedLng': 127.0333333,
|
|
},
|
|
];
|
|
|
|
for (final testCase in testCases) {
|
|
final mockApiClient = MockNaverApiClient();
|
|
|
|
final htmlContent = '''
|
|
<html>
|
|
<head>
|
|
<meta property="og:url" content="https://map.naver.com/p/restaurant/1234567890?y=${testCase['expectedLat']}&x=${testCase['expectedLng']}">
|
|
</head>
|
|
<body>
|
|
<span class="GHAhO">좌표 테스트 식당</span>
|
|
</body>
|
|
</html>
|
|
''';
|
|
|
|
mockApiClient.setHtmlResponse(
|
|
'https://map.naver.com/p/restaurant/1234567890',
|
|
htmlContent,
|
|
);
|
|
|
|
// GraphQL 응답도 설정
|
|
mockApiClient.setGraphQLResponse({
|
|
'place': {
|
|
'id': '1234567890',
|
|
'name': '좌표 테스트 식당',
|
|
'location': {
|
|
'lat': testCase['expectedLat'],
|
|
'lng': testCase['expectedLng'],
|
|
},
|
|
},
|
|
});
|
|
|
|
final parser = NaverMapParser(apiClient: mockApiClient);
|
|
final restaurant = await parser.parseRestaurantFromUrl(
|
|
'https://map.naver.com/p/restaurant/1234567890',
|
|
);
|
|
|
|
expect(
|
|
restaurant.latitude,
|
|
closeTo(testCase['expectedLat'] as double, 0.0000001),
|
|
reason: '위도 변환이 정확해야 함',
|
|
);
|
|
expect(
|
|
restaurant.longitude,
|
|
closeTo(testCase['expectedLng'] as double, 0.0000001),
|
|
reason: '경도 변환이 정확해야 함',
|
|
);
|
|
}
|
|
});
|
|
|
|
test('카테고리 정규화 테스트', () async {
|
|
final categoryTests = [
|
|
{'input': '한식>김치찌개', 'expectedMain': '한식', 'expectedSub': '김치찌개'},
|
|
{'input': '카페,디저트', 'expectedMain': '카페', 'expectedSub': '카페'},
|
|
{'input': '양식 > 파스타', 'expectedMain': '양식', 'expectedSub': '파스타'},
|
|
{'input': '중식', 'expectedMain': '중식', 'expectedSub': '중식'},
|
|
];
|
|
|
|
for (final test in categoryTests) {
|
|
final mockApiClient = MockNaverApiClient();
|
|
|
|
final htmlContent = '''
|
|
<html>
|
|
<body>
|
|
<span class="GHAhO">카테고리 테스트</span>
|
|
<span class="DJJvD">${test['input']}</span>
|
|
</body>
|
|
</html>
|
|
''';
|
|
|
|
mockApiClient.setHtmlResponse(
|
|
'https://map.naver.com/p/restaurant/1234567890',
|
|
htmlContent,
|
|
);
|
|
|
|
final parser = NaverMapParser(apiClient: mockApiClient);
|
|
final restaurant = await parser.parseRestaurantFromUrl(
|
|
'https://map.naver.com/p/restaurant/1234567890',
|
|
);
|
|
|
|
expect(
|
|
restaurant.category,
|
|
test['expectedMain'],
|
|
reason: '메인 카테고리가 올바르게 추출되어야 함: ${test['input']}',
|
|
);
|
|
expect(
|
|
restaurant.subCategory,
|
|
test['expectedSub'],
|
|
reason: '서브 카테고리가 올바르게 추출되어야 함: ${test['input']}',
|
|
);
|
|
}
|
|
});
|
|
|
|
test('HTML 엔티티 디코딩 테스트', () async {
|
|
final mockApiClient = MockNaverApiClient();
|
|
|
|
final htmlContent = '''
|
|
<html>
|
|
<body>
|
|
<span class="GHAhO">Tom & Jerry's Restaurant</span>
|
|
<span class="IH7VW">서울시 강남구 <테헤란로> 123번지</span>
|
|
<span class="description">"최고의 맛"을 자랑하는 레스토랑</span>
|
|
</body>
|
|
</html>
|
|
''';
|
|
|
|
mockApiClient.setHtmlResponse(
|
|
'https://map.naver.com/p/restaurant/1234567890',
|
|
htmlContent,
|
|
);
|
|
|
|
final parser = NaverMapParser(apiClient: mockApiClient);
|
|
final restaurant = await parser.parseRestaurantFromUrl(
|
|
'https://map.naver.com/p/restaurant/1234567890',
|
|
);
|
|
|
|
expect(restaurant.name, contains('&'));
|
|
expect(restaurant.name, contains("'"));
|
|
expect(restaurant.roadAddress, contains('<'));
|
|
expect(restaurant.roadAddress, contains('>'));
|
|
});
|
|
|
|
test('영업시간 파싱 다양성 테스트', () async {
|
|
final businessHourTests = [
|
|
'매일 11:00 - 22:00',
|
|
'월-금 11:30 - 21:00, 주말 12:00 - 22:00',
|
|
'연중무휴 24시간',
|
|
'화요일 휴무, 그 외 10:00 - 20:00',
|
|
'평일 11:00~14:00, 17:00~22:00 (브레이크타임 14:00~17:00)',
|
|
];
|
|
|
|
for (final hours in businessHourTests) {
|
|
final mockApiClient = MockNaverApiClient();
|
|
|
|
final htmlContent = '''
|
|
<html>
|
|
<body>
|
|
<span class="GHAhO">영업시간 테스트</span>
|
|
<time class="aT6WB">$hours</time>
|
|
</body>
|
|
</html>
|
|
''';
|
|
|
|
mockApiClient.setHtmlResponse(
|
|
'https://map.naver.com/p/restaurant/1234567890',
|
|
htmlContent,
|
|
);
|
|
|
|
final parser = NaverMapParser(apiClient: mockApiClient);
|
|
final restaurant = await parser.parseRestaurantFromUrl(
|
|
'https://map.naver.com/p/restaurant/1234567890',
|
|
);
|
|
|
|
expect(
|
|
restaurant.businessHours,
|
|
hours,
|
|
reason: '영업시간이 정확히 파싱되어야 함',
|
|
);
|
|
}
|
|
});
|
|
|
|
test('Place ID 추출 패턴 테스트', () async {
|
|
final urlPatterns = [
|
|
{
|
|
'url': 'https://map.naver.com/p/restaurant/1234567890',
|
|
'expectedId': '1234567890',
|
|
},
|
|
{
|
|
'url': 'https://map.naver.com/p/entry/place/9876543210',
|
|
'expectedId': '9876543210',
|
|
},
|
|
{
|
|
'url': 'https://map.naver.com/p/restaurant/1234567890?entry=pll',
|
|
'expectedId': '1234567890',
|
|
},
|
|
];
|
|
|
|
for (final pattern in urlPatterns) {
|
|
final mockApiClient = MockNaverApiClient();
|
|
|
|
final htmlContent = '''
|
|
<html>
|
|
<body>
|
|
<span class="GHAhO">Place ID 테스트</span>
|
|
</body>
|
|
</html>
|
|
''';
|
|
|
|
mockApiClient.setHtmlResponse(
|
|
pattern['url']!,
|
|
htmlContent,
|
|
);
|
|
|
|
final parser = NaverMapParser(apiClient: mockApiClient);
|
|
final restaurant = await parser.parseRestaurantFromUrl(pattern['url']!);
|
|
|
|
expect(
|
|
restaurant.naverPlaceId,
|
|
pattern['expectedId'],
|
|
reason: 'Place ID가 올바르게 추출되어야 함: ${pattern['url']}',
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
group('NaverMapParser - 동시성 및 리소스 관리', () {
|
|
test('동시 다중 요청 처리', () async {
|
|
final mockApiClient = MockNaverApiClient();
|
|
|
|
final parser = NaverMapParser(apiClient: mockApiClient);
|
|
|
|
// 동시에 여러 요청 실행
|
|
final futures = List.generate(5, (i) {
|
|
final url = 'https://map.naver.com/p/restaurant/${1000 + i}';
|
|
|
|
// 각 URL에 대한 HTML 응답 설정
|
|
mockApiClient.setHtmlResponse(
|
|
url,
|
|
'''
|
|
<html>
|
|
<body>
|
|
<span class="GHAhO">동시성 테스트 식당 ${i + 1}</span>
|
|
<span class="DJJvD">한식</span>
|
|
</body>
|
|
</html>
|
|
''',
|
|
);
|
|
|
|
return parser.parseRestaurantFromUrl(url);
|
|
});
|
|
|
|
final results = await Future.wait(futures);
|
|
|
|
// 모든 요청이 성공했는지 확인
|
|
expect(results.length, 5);
|
|
|
|
// 각 결과가 고유한지 확인
|
|
final names = results.map((r) => r.name).toSet();
|
|
expect(names.length, 5);
|
|
});
|
|
|
|
test('리소스 정리 확인', () async {
|
|
final mockApiClient = MockNaverApiClient();
|
|
|
|
mockApiClient.setHtmlResponse(
|
|
'https://map.naver.com/p/restaurant/123456789',
|
|
'<html><body><span class="GHAhO">Test</span></body></html>',
|
|
);
|
|
|
|
final parser = NaverMapParser(apiClient: mockApiClient);
|
|
|
|
// 여러 번 사용
|
|
for (int i = 0; i < 3; i++) {
|
|
try {
|
|
await parser.parseRestaurantFromUrl(
|
|
'https://map.naver.com/p/restaurant/123456789'
|
|
);
|
|
} catch (_) {
|
|
// 에러 무시
|
|
}
|
|
}
|
|
|
|
// dispose 호출
|
|
parser.dispose();
|
|
|
|
// dispose 후에는 사용할 수 없어야 함
|
|
expect(
|
|
() => parser.parseRestaurantFromUrl('https://map.naver.com/p/restaurant/999'),
|
|
throwsA(anything),
|
|
);
|
|
});
|
|
});
|
|
} |