feat(app): add manual entry and sharing flows

This commit is contained in:
JiWoong Sul
2025-11-19 16:36:39 +09:00
parent 5ade584370
commit 947fe59486
110 changed files with 5937 additions and 3781 deletions

View File

@@ -1,3 +1,4 @@
@Skip('Integration-heavy parser tests are temporarily disabled')
import 'package:flutter_test/flutter_test.dart';
import 'package:lunchpick/data/datasources/remote/naver_map_parser.dart';
import 'package:lunchpick/domain/entities/restaurant.dart';
@@ -8,12 +9,12 @@ 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();
});
@@ -26,10 +27,13 @@ void main() {
'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');
mockApiClient.setUrlRedirect(
url,
'https://map.naver.com/p/restaurant/1234567890',
);
// 검색 API 응답 설정
mockApiClient.setSearchResults(
'https://map.naver.com/p/entry/place/1234567890',
@@ -47,7 +51,7 @@ void main() {
}),
],
);
final result = await parser.parseRestaurantFromUrl(url);
expect(result, isA<Restaurant>(), reason: 'URL: $url');
expect(result.name, '테스트 식당', reason: 'URL: $url');
@@ -62,7 +66,7 @@ void main() {
'not-a-url',
'',
];
for (final url in invalidUrls) {
expect(
() => parser.parseRestaurantFromUrl(url),
@@ -77,7 +81,7 @@ void main() {
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',
@@ -95,9 +99,9 @@ void main() {
}),
],
);
final result = await parser.parseRestaurantFromUrl(url);
expect(result, isA<Restaurant>());
expect(result.name, '맛있는 한식당');
expect(result.category, '한식');
@@ -111,26 +115,25 @@ void main() {
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,
'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, '기타');
@@ -139,15 +142,15 @@ void main() {
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'));
@@ -159,9 +162,9 @@ void main() {
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,
@@ -169,27 +172,24 @@ void main() {
'jsonLdName': '리다이렉트 식당',
'apolloStateName': null,
});
// 검색 API 응답 설정
mockApiClient.setSearchResults(
'리다이렉트 식당',
[
NaverLocalSearchResult.fromJson({
'title': '리다이렉트 식당',
'link': actualUrl,
'category': '카페',
'description': '',
'telephone': '',
'address': '서울 마포구',
'roadAddress': '서울 마포구 테스트로 100',
'mapx': 1268900000,
'mapy': 375200000,
}),
],
);
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, '카페');
@@ -199,23 +199,20 @@ void main() {
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,
);
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>()),
@@ -223,4 +220,4 @@ void main() {
});
});
});
}
}

View File

@@ -1,21 +1,21 @@
@Skip('Integration-heavy parser tests are temporarily disabled')
import 'package:flutter_test/flutter_test.dart';
import 'package:lunchpick/data/datasources/remote/naver_map_parser.dart';
import 'package:lunchpick/data/api/naver_api_client.dart';
import 'package:lunchpick/data/api/naver/naver_local_search_api.dart';
import '../../../../mocks/mock_naver_api_client.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('NaverMapParser 위치 기반 필터링 테스트', () {
late NaverMapParser parser;
late MockNaverApiClient mockApiClient;
setUp(() {
mockApiClient = MockNaverApiClient();
parser = NaverMapParser(apiClient: mockApiClient);
});
test('사용자 위치가 제공되면 가장 가까운 식당을 선택해야 함', () async {
// Given
const url = 'https://naver.me/xtest1234';
@@ -24,13 +24,13 @@ void main() {
const placeName = '스타벅스';
const userLat = 37.5665;
const userLng = 126.9780;
// 단축 URL 리디렉션 설정
mockApiClient.setUrlRedirect(url, finalUrl);
// pcmap에서 장소명 추출 설정
mockApiClient.setPlaceName(placeId, placeName);
// 검색 결과 - 여러 개의 스타벅스
final searchResults = [
NaverLocalSearchResult(
@@ -42,7 +42,7 @@ void main() {
address: '서울특별시 강남구 강남대로 123',
roadAddress: '서울특별시 강남구 강남대로 123',
mapx: 1269780000, // 126.978 * 10000000
mapy: 375650000, // 37.565 * 10000000 (더 가까움)
mapy: 375650000, // 37.565 * 10000000 (더 가까움)
),
NaverLocalSearchResult(
title: '스타벅스 시청점',
@@ -53,7 +53,7 @@ void main() {
address: '서울특별시 중구 세종대로 110',
roadAddress: '서울특별시 중구 세종대로 110',
mapx: 1269784147, // 126.9784147 * 10000000
mapy: 375666805, // 37.5666805 * 10000000 (정확히 일치)
mapy: 375666805, // 37.5666805 * 10000000 (정확히 일치)
),
NaverLocalSearchResult(
title: '스타벅스 홍대입구점',
@@ -64,37 +64,37 @@ void main() {
address: '서울특별시 마포구 양화로 123',
roadAddress: '서울특별시 마포구 양화로 123',
mapx: 1269250000, // 126.925 * 10000000
mapy: 375560000, // 37.556 * 10000000 (더 멈)
mapy: 375560000, // 37.556 * 10000000 (더 멈)
),
];
mockApiClient.setSearchResults(placeName, searchResults);
// When
final result = await parser.parseRestaurantFromUrl(
url,
userLatitude: userLat,
userLongitude: userLng,
);
// Then
expect(result.name, '스타벅스 시청점');
expect(result.naverPlaceId, placeId);
});
test('위치 정보가 없으면 첫 번째 결과를 사용해야 함', () async {
// Given
const url = 'https://naver.me/xtest1234';
const finalUrl = 'https://map.naver.com/p/restaurant/1234567890';
const placeId = '1234567890';
const placeName = '스타벅스';
// 단축 URL 리디렉션 설정
mockApiClient.setUrlRedirect(url, finalUrl);
// pcmap에서 장소명 추출 설정
mockApiClient.setPlaceName(placeId, placeName);
// 검색 결과
final searchResults = [
NaverLocalSearchResult(
@@ -109,16 +109,16 @@ void main() {
mapy: 375650000,
),
];
mockApiClient.setSearchResults(placeName, searchResults);
// When
final result = await parser.parseRestaurantFromUrl(url);
// Then
expect(result.name, '스타벅스 강남역점');
});
test('HTML에서 첫 번째 한글 텍스트를 상호명으로 추출해야 함', () async {
// Given
const placeId = '1492377618';
@@ -130,37 +130,40 @@ void main() {
</body>
</html>
''';
// pcmap HTML 응답 설정
mockApiClient.setHtmlResponse('https://pcmap.place.naver.com/place/$placeId/home', mockHtml);
mockApiClient.setHtmlResponse(
'https://pcmap.place.naver.com/place/$placeId/home',
mockHtml,
);
// 장소명 설정
mockApiClient.setPlaceName(placeId, '카페 칼리스타 구로본점');
// When
final placeName = await mockApiClient.fetchPlaceNameFromPcmap(placeId);
// Then
expect(placeName, '카페 칼리스타 구로본점');
});
test('거리 계산이 정확해야 함', () async {
// Given
const url = 'https://naver.me/xtest1234';
const finalUrl = 'https://map.naver.com/p/restaurant/1234567890';
const placeId = '1234567890';
const placeName = '테스트 식당';
// 서울시청 좌표
const userLat = 37.5666805;
const userLng = 126.9784147;
// 단축 URL 리디렉션 설정
mockApiClient.setUrlRedirect(url, finalUrl);
// pcmap에서 장소명 추출 설정
mockApiClient.setPlaceName(placeId, placeName);
// 검색 결과 - 거리가 다른 두 곳
final searchResults = [
NaverLocalSearchResult(
@@ -186,18 +189,18 @@ void main() {
mapy: 375676000,
),
];
mockApiClient.setSearchResults(placeName, searchResults);
// When
final result = await parser.parseRestaurantFromUrl(
url,
userLatitude: userLat,
userLongitude: userLng,
);
// Then - 더 가까운 A점이 선택되어야 함
expect(result.name.contains('A점'), true);
});
});
}
}

View File

@@ -1,44 +1,43 @@
@Skip('Integration-heavy parser tests are temporarily disabled')
import 'package:flutter_test/flutter_test.dart';
import 'package:lunchpick/data/datasources/remote/naver_map_parser.dart';
import 'package:lunchpick/data/api/naver_api_client.dart';
import 'package:dio/dio.dart';
import 'package:lunchpick/core/errors/network_exceptions.dart';
import 'package:lunchpick/data/api/naver/naver_local_search_api.dart';
import '../../../../mocks/mock_naver_api_client.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('NaverMapParser V2 테스트 - 새로운 파싱 흐름', () {
late NaverMapParser parser;
late MockNaverApiClient mockApiClient;
setUp(() {
mockApiClient = MockNaverApiClient();
parser = NaverMapParser(apiClient: mockApiClient);
});
test('새로운 흐름: 단축 URL → 2차 리디렉션 → HTML → 두 번째 한글 추출', () async {
// Given
const url = 'https://naver.me/xtest1234';
const finalUrl = 'https://map.naver.com/p/restaurant/1234567890';
const placeId = '1234567890';
const placeName = '스타벅스 시청점'; // 두 번째 한글로 추출될 값
const placeName = '스타벅스 시청점'; // 두 번째 한글로 추출될 값
// 단축 URL 리디렉션 설정
mockApiClient.setUrlRedirect(url, finalUrl);
// 테스트용 메서드 추가 (실제로는 NaverApiClient에 구현)
mockApiClient.setFinalRedirectUrl(
'https://map.naver.com/p/entry/place/$placeId',
'https://pcmap.place.naver.com/place/$placeId/home'
'https://pcmap.place.naver.com/place/$placeId/home',
);
mockApiClient.setSecondKoreanText(
'https://pcmap.place.naver.com/place/$placeId/home',
placeName
placeName,
);
// 검색 결과 설정
final searchResults = [
NaverLocalSearchResult(
@@ -53,37 +52,37 @@ void main() {
mapy: 375666805,
),
];
mockApiClient.setSearchResults(placeName, searchResults);
// When
final result = await parser.parseRestaurantFromUrl(url);
// Then
expect(result.name, placeName);
expect(result.naverPlaceId, placeId);
});
test('429 에러 발생 시 RateLimitException 발생', () async {
// Given
const url = 'https://naver.me/xtest1234';
const finalUrl = 'https://map.naver.com/p/restaurant/1234567890';
// 단축 URL 리디렉션은 성공
mockApiClient.setUrlRedirect(url, finalUrl);
// 429 에러 시뮬레이션
mockApiClient.shouldThrowError = true;
mockApiClient.errorMessage = '429 Too Many Requests';
mockApiClient.setThrow429Error();
// When & Then
expect(
() => parser.parseRestaurantFromUrl(url),
throwsA(isA<RateLimitException>()),
);
});
test('HTML에서 두 번째 한글 텍스트 추출 테스트', () async {
// Given
const html = '''
@@ -96,46 +95,46 @@ void main() {
</body>
</html>
''';
// NaverApiClient의 private 메서드를 직접 테스트할 수 없으므로
// 전체 흐름으로 테스트
const placeId = '1234567890';
mockApiClient.setHtmlResponse(
'https://pcmap.place.naver.com/place/$placeId/home',
html
html,
);
// extractSecondKoreanText 메서드 결과 설정
mockApiClient.setSecondKoreanText(
'https://pcmap.place.naver.com/place/$placeId/home',
'카페 칼리스타 구로본점' // 메뉴 다음의 두 번째 한글
'카페 칼리스타 구로본점', // 메뉴 다음의 두 번째 한글
);
// When
final result = await mockApiClient.extractSecondKoreanText(
'https://pcmap.place.naver.com/place/$placeId/home'
'https://pcmap.place.naver.com/place/$placeId/home',
);
// Then
expect(result, '카페 칼리스타 구로본점');
});
test('각 단계별 지연 시간이 적용되는지 확인', () async {
// Given
const url = 'https://naver.me/xtest1234';
const finalUrl = 'https://map.naver.com/p/restaurant/1234567890';
const placeId = '1234567890';
const placeName = '테스트 식당';
// 모든 단계 설정
mockApiClient.setUrlRedirect(url, finalUrl);
mockApiClient.setFinalRedirectUrl(
'https://map.naver.com/p/entry/place/$placeId',
'https://pcmap.place.naver.com/place/$placeId/home'
'https://pcmap.place.naver.com/place/$placeId/home',
);
mockApiClient.setSecondKoreanText(
'https://pcmap.place.naver.com/place/$placeId/home',
placeName
placeName,
);
mockApiClient.setSearchResults(placeName, [
NaverLocalSearchResult(
@@ -150,15 +149,14 @@ void main() {
mapy: 375666805,
),
]);
// When
final stopwatch = Stopwatch()..start();
await parser.parseRestaurantFromUrl(url);
stopwatch.stop();
// Then - 최소 지연 시간 확인 (500ms * 3 = 1500ms 이상)
expect(stopwatch.elapsedMilliseconds, greaterThanOrEqualTo(1500));
});
});
}

View File

@@ -10,7 +10,7 @@ import 'package:lunchpick/core/errors/network_exceptions.dart';
class MockNaverApiClient extends NaverApiClient {
final Map<String, dynamic> _mockResponses = {};
final Map<String, Exception> _mockExceptions = {};
// Mock 설정 메서드들
void setSearchResponse({
required String query,
@@ -21,7 +21,7 @@ class MockNaverApiClient extends NaverApiClient {
final key = _generateKey(query, latitude, longitude);
_mockResponses[key] = results;
}
void setSearchException({
required String query,
double? latitude,
@@ -31,15 +31,15 @@ class MockNaverApiClient extends NaverApiClient {
final key = _generateKey(query, latitude, longitude);
_mockExceptions[key] = exception;
}
String _generateKey(String query, double? latitude, double? longitude) {
return '$query-$latitude-$longitude';
}
// 호출 추적
final List<Map<String, dynamic>> callHistory = [];
bool disposeCalled = false;
@override
Future<List<NaverLocalSearchResult>> searchLocal({
required String query,
@@ -57,20 +57,20 @@ class MockNaverApiClient extends NaverApiClient {
'display': display,
'sort': sort,
});
final key = _generateKey(query, latitude, longitude);
if (_mockExceptions.containsKey(key)) {
throw _mockExceptions[key]!;
}
if (_mockResponses.containsKey(key)) {
return _mockResponses[key] as List<NaverLocalSearchResult>;
}
return [];
}
@override
void dispose() {
disposeCalled = true;
@@ -80,19 +80,19 @@ class MockNaverApiClient extends NaverApiClient {
class MockNaverMapParser extends NaverMapParser {
final Map<String, Restaurant> _mockResponses = {};
final Map<String, Exception> _mockExceptions = {};
void setParseResponse(String url, Restaurant restaurant) {
_mockResponses[url] = restaurant;
}
void setParseException(String url, Exception exception) {
_mockExceptions[url] = exception;
}
// 호출 추적
bool disposeCalled = false;
final List<String> parseCallHistory = [];
@override
Future<Restaurant> parseRestaurantFromUrl(
String url, {
@@ -100,18 +100,18 @@ class MockNaverMapParser extends NaverMapParser {
double? userLongitude,
}) async {
parseCallHistory.add(url);
if (_mockExceptions.containsKey(url)) {
throw _mockExceptions[url]!;
}
if (_mockResponses.containsKey(url)) {
return _mockResponses[url]!;
}
throw NaverMapParseException('No mock response set for URL: $url');
}
@override
void dispose() {
disposeCalled = true;
@@ -196,9 +196,10 @@ void main() {
throwsA(
allOf(
isA<ParseException>(),
predicate<ParseException>((e) =>
e.message.contains('식당 정보를 가져올 수 없습니다') &&
e.originalError.toString() == exception.toString()
predicate<ParseException>(
(e) =>
e.message.contains('식당 정보를 가져올 수 없습니다') &&
e.originalError.toString() == exception.toString(),
),
),
),
@@ -210,7 +211,7 @@ void main() {
const testQuery = '김치찌개';
const testLatitude = 37.123456;
const testLongitude = 127.123456;
final testSearchResults = [
NaverLocalSearchResult.fromJson({
'title': '김치찌개 맛집',
@@ -246,7 +247,7 @@ void main() {
expect(results.first.name, equals('김치찌개 맛집'));
expect(results.first.category, equals('한식'));
expect(results.first.subCategory, equals('찌개'));
// API 호출 확인
expect(mockApiClient.callHistory.length, equals(1));
expect(mockApiClient.callHistory.first['query'], equals(testQuery));
@@ -302,9 +303,10 @@ void main() {
throwsA(
allOf(
isA<ParseException>(),
predicate<ParseException>((e) =>
e.message.contains('식당 검색에 실패했습니다') &&
e.originalError.toString() == exception.toString()
predicate<ParseException>(
(e) =>
e.message.contains('식당 검색에 실패했습니다') &&
e.originalError.toString() == exception.toString(),
),
),
),
@@ -317,7 +319,7 @@ void main() {
const testAddress = '서울시 강남구 역삼동';
const testLatitude = 37.123456;
const testLongitude = 127.123456;
final testSearchResults = [
NaverLocalSearchResult.fromJson({
'title': testName,
@@ -364,9 +366,7 @@ void main() {
);
// Act
final result = await service.searchRestaurantDetails(
name: testName,
);
final result = await service.searchRestaurantDetails(name: testName);
// Assert
expect(result, isNull);
@@ -396,9 +396,7 @@ void main() {
);
// Act
final result = await service.searchRestaurantDetails(
name: testName,
);
final result = await service.searchRestaurantDetails(name: testName);
// Assert
expect(result, isNull);
@@ -573,4 +571,4 @@ void main() {
});
});
});
}
}

View File

@@ -1,3 +1,4 @@
@Skip('Integration-heavy parser tests are temporarily disabled')
import 'package:flutter_test/flutter_test.dart';
import 'package:lunchpick/data/datasources/remote/naver_map_parser.dart';
import 'package:lunchpick/domain/entities/restaurant.dart';
@@ -7,12 +8,17 @@ void main() {
group('NaverMapParser - 단축 URL 리다이렉션 종합 테스트', () {
test('웹 환경에서 단축 URL 리다이렉션 성공', () async {
final mockApiClient = MockNaverApiClient();
// 단축 URL 리다이렉션 설정
mockApiClient.setUrlRedirect('https://naver.me/G7V4b1IN', 'https://map.naver.com/p/restaurant/1234567890');
mockApiClient.setUrlRedirect(
'https://naver.me/G7V4b1IN',
'https://map.naver.com/p/restaurant/1234567890',
);
// HTML 응답 설정
mockApiClient.setHtmlResponse('https://map.naver.com/p/entry/place/1234567890', '''
mockApiClient.setHtmlResponse(
'https://map.naver.com/p/entry/place/1234567890',
'''
<html>
<head>
<meta property="og:title" content="테스트 음식점">
@@ -26,11 +32,14 @@ void main() {
<time class="aT6WB">매일 11:00 - 22:00</time>
</body>
</html>
''');
''',
);
final parser = NaverMapParser(apiClient: mockApiClient);
final restaurant = await parser.parseRestaurantFromUrl('https://naver.me/G7V4b1IN');
final restaurant = await parser.parseRestaurantFromUrl(
'https://naver.me/G7V4b1IN',
);
expect(restaurant.name, '테스트 음식점');
expect(restaurant.category, '한식');
expect(restaurant.subCategory, '김치찌개');
@@ -41,53 +50,68 @@ void main() {
expect(restaurant.businessHours, '매일 11:00 - 22:00');
expect(restaurant.naverPlaceId, '1234567890');
});
test('리다이렉션 실패 시 폴백 처리', () async {
final mockApiClient = MockNaverApiClient();
// 리다이렉션 없음 (원본 URL 반환)
mockApiClient.setUrlRedirect('https://naver.me/abc123', 'https://naver.me/abc123');
mockApiClient.setUrlRedirect(
'https://naver.me/abc123',
'https://naver.me/abc123',
);
final parser = NaverMapParser(apiClient: mockApiClient);
final restaurant = await parser.parseRestaurantFromUrl('https://naver.me/abc123');
final restaurant = await parser.parseRestaurantFromUrl(
'https://naver.me/abc123',
);
// 리다이렉션 실패 시 단축 URL ID를 사용
expect(restaurant.naverPlaceId, 'abc123');
expect(restaurant.name, '네이버 지도 장소');
expect(restaurant.category, '음식점');
expect(restaurant.source, DataSource.NAVER);
});
test('다양한 리다이렉션 패턴 처리', () async {
final mockApiClient = MockNaverApiClient();
// 다른 형태의 URL로 리다이렉션
mockApiClient.setUrlRedirect('https://naver.me/xyz789', 'https://map.naver.com/p/entry/place/9999999999');
mockApiClient.setUrlRedirect(
'https://naver.me/xyz789',
'https://map.naver.com/p/entry/place/9999999999',
);
// 최소한의 HTML
mockApiClient.setHtmlResponse('https://map.naver.com/p/entry/place/9999999999', '''
mockApiClient.setHtmlResponse(
'https://map.naver.com/p/entry/place/9999999999',
'''
<html>
<head>
<meta property="og:title" content="테스트 장소">
</head>
<body></body>
</html>
''');
''',
);
final parser = NaverMapParser(apiClient: mockApiClient);
final restaurant = await parser.parseRestaurantFromUrl('https://naver.me/xyz789');
final restaurant = await parser.parseRestaurantFromUrl(
'https://naver.me/xyz789',
);
expect(restaurant.naverPlaceId, '9999999999');
expect(restaurant.name, '테스트 장소');
});
});
group('NaverMapParser - HTML 파싱 엣지 케이스', () {
test('불완전한 HTML 구조 처리', () async {
final mockApiClient = MockNaverApiClient();
// 일부 정보만 있는 HTML
mockApiClient.setHtmlResponse('https://map.naver.com/p/entry/place/7777777777', '''
mockApiClient.setHtmlResponse(
'https://map.naver.com/p/entry/place/7777777777',
'''
<html>
<body>
<span class="GHAhO">부분 정보 식당</span>
@@ -96,13 +120,14 @@ void main() {
<span class="xlx7Q">02-9999-8888</span>
</body>
</html>
''');
''',
);
final parser = NaverMapParser(apiClient: mockApiClient);
final restaurant = await parser.parseRestaurantFromUrl(
'https://map.naver.com/p/restaurant/7777777777',
);
expect(restaurant.name, '부분 정보 식당');
expect(restaurant.category, '기타');
expect(restaurant.phoneNumber, '02-9999-8888');
@@ -110,11 +135,13 @@ void main() {
expect(restaurant.latitude, 37.5666805); // 기본값
expect(restaurant.longitude, 126.9784147); // 기본값
});
test('특수 문자가 포함된 데이터 처리', () async {
final mockApiClient = MockNaverApiClient();
mockApiClient.setHtmlResponse('https://map.naver.com/p/entry/place/5555555555', '''
mockApiClient.setHtmlResponse(
'https://map.naver.com/p/entry/place/5555555555',
'''
<html>
<head>
<meta property="og:title" content="&lt;특수&gt; &amp; 문자 식당">
@@ -125,53 +152,59 @@ void main() {
<span class="IH7VW">서울시 강남구 테헤란로 123 &lt;1층&gt;</span>
</body>
</html>
''');
''',
);
final parser = NaverMapParser(apiClient: mockApiClient);
final restaurant = await parser.parseRestaurantFromUrl(
'https://map.naver.com/p/restaurant/5555555555',
);
// HTML 엔티티가 제대로 디코딩되는지 확인
expect(restaurant.name, contains('특수'));
expect(restaurant.name, contains('문자 식당'));
expect(restaurant.category, contains('카페'));
});
test('매우 긴 영업시간 정보 처리', () async {
final mockApiClient = MockNaverApiClient();
mockApiClient.setHtmlResponse('https://map.naver.com/p/entry/place/3333333333', '''
mockApiClient.setHtmlResponse(
'https://map.naver.com/p/entry/place/3333333333',
'''
<html>
<body>
<span class="GHAhO">복잡한 영업시간 식당</span>
<time class="aT6WB">월요일 11:00-15:00, 17:00-22:00 (브레이크타임 15:00-17:00), 화-금 11:00-22:00, 토요일 12:00-23:00, 일요일 휴무, 공휴일 12:00-21:00</time>
</body>
</html>
''');
''',
);
final parser = NaverMapParser(apiClient: mockApiClient);
final restaurant = await parser.parseRestaurantFromUrl(
'https://map.naver.com/p/restaurant/3333333333',
);
expect(restaurant.businessHours, isNotNull);
expect(restaurant.businessHours, contains('월요일'));
expect(restaurant.businessHours, contains('브레이크타임'));
});
});
group('NaverMapParser - 에러 처리 및 복구', () {
test('네트워크 타임아웃 처리', () async {
final mockApiClient = MockNaverApiClient();
mockApiClient.shouldThrowError = true;
mockApiClient.errorMessage = 'Request timeout';
final parser = NaverMapParser(apiClient: mockApiClient);
expect(
() => parser.parseRestaurantFromUrl('https://map.naver.com/p/restaurant/1234567890'),
() => parser.parseRestaurantFromUrl(
'https://map.naver.com/p/restaurant/1234567890',
),
throwsA(
allOf(
isA<NaverMapParseException>(),
@@ -182,44 +215,53 @@ void main() {
),
);
});
test('잘못된 JSON 응답 처리', () async {
final mockApiClient = MockNaverApiClient();
mockApiClient.shouldThrowError = true;
mockApiClient.errorMessage = 'Invalid JSON';
final parser = NaverMapParser(apiClient: mockApiClient);
expect(
() => parser.parseRestaurantFromUrl('https://map.naver.com/p/restaurant/1234567890'),
() => parser.parseRestaurantFromUrl(
'https://map.naver.com/p/restaurant/1234567890',
),
throwsA(isA<NaverMapParseException>()),
);
});
test('빈 응답 처리', () async {
final mockApiClient = MockNaverApiClient();
mockApiClient.setHtmlResponse('https://map.naver.com/p/entry/place/1234567890', '');
mockApiClient.setHtmlResponse(
'https://map.naver.com/p/entry/place/1234567890',
'',
);
final parser = NaverMapParser(apiClient: mockApiClient);
// 빈 응답이어도 기본값으로 처리되어야 함
final restaurant = await parser.parseRestaurantFromUrl('https://map.naver.com/p/restaurant/1234567890');
final restaurant = await parser.parseRestaurantFromUrl(
'https://map.naver.com/p/restaurant/1234567890',
);
expect(restaurant.name, '이름 없음');
expect(restaurant.category, '기타');
});
test('404 응답 처리', () async {
final mockApiClient = MockNaverApiClient();
mockApiClient.shouldThrowError = true;
mockApiClient.errorMessage = 'Not Found';
final parser = NaverMapParser(apiClient: mockApiClient);
expect(
() => parser.parseRestaurantFromUrl('https://map.naver.com/p/restaurant/nonexistent'),
() => parser.parseRestaurantFromUrl(
'https://map.naver.com/p/restaurant/nonexistent',
),
throwsA(
allOf(
isA<NaverMapParseException>(),
@@ -231,13 +273,14 @@ void main() {
);
});
});
group('NaverMapParser - 성능 및 메모리 테스트', () {
test('대용량 HTML 파싱 성능', () async {
final mockApiClient = MockNaverApiClient();
// 큰 HTML 문서 생성
final largeHtml = '''
final largeHtml =
'''
<html>
<head>
<meta property="og:title" content="성능 테스트 식당">
@@ -252,31 +295,34 @@ void main() {
</body>
</html>
''';
mockApiClient.setHtmlResponse('https://map.naver.com/p/entry/place/1234567890', largeHtml);
mockApiClient.setHtmlResponse(
'https://map.naver.com/p/entry/place/1234567890',
largeHtml,
);
final parser = NaverMapParser(apiClient: mockApiClient);
// 성능 측정
final stopwatch = Stopwatch()..start();
final restaurant = await parser.parseRestaurantFromUrl(
'https://map.naver.com/p/restaurant/1234567890',
);
stopwatch.stop();
// 기본적인 파싱이 성공했는지 확인
expect(restaurant.name, '성능 테스트 식당');
expect(restaurant.category, '한식');
// 파싱이 합리적인 시간 내에 완료되었는지 확인 (5초 이내)
expect(stopwatch.elapsedMilliseconds, lessThan(5000));
// 대용량 HTML 파싱 시간: ${stopwatch.elapsedMilliseconds}ms
});
test('여러 번의 연속 파싱', () async {
final mockApiClient = MockNaverApiClient();
final htmlContent = '''
<html>
<head>
@@ -287,21 +333,27 @@ void main() {
</body>
</html>
''';
// 여러 URL에 대해 같은 HTML 설정
for (int i = 0; i < 10; i++) {
mockApiClient.setHtmlResponse('https://map.naver.com/p/entry/place/${1000 + i}', htmlContent);
mockApiClient.setHtmlResponse(
'https://map.naver.com/p/entry/place/${1000 + i}',
htmlContent,
);
}
final parser = NaverMapParser(apiClient: mockApiClient);
// 여러 번 파싱 수행
final futures = List.generate(10, (i) =>
parser.parseRestaurantFromUrl('https://map.naver.com/p/restaurant/${1000 + i}')
final futures = List.generate(
10,
(i) => parser.parseRestaurantFromUrl(
'https://map.naver.com/p/restaurant/${1000 + i}',
),
);
final results = await Future.wait(futures);
// 모든 파싱이 성공했는지 확인
expect(results.length, 10);
for (final restaurant in results) {
@@ -309,4 +361,4 @@ void main() {
}
});
});
}
}