# 네이버 단축 URL 처리 아키텍처 설계 ## 1. 개요 ### 1.1 목적 네이버 단축 URL(naver.me)을 처리하여 식당 정보를 추출하고, 네이버 로컬 API를 통해 상세 정보를 보강하는 시스템을 설계합니다. ### 1.2 핵심 요구사항 - 네이버 단축 URL 리다이렉션 처리 - HTML 스크래핑을 통한 기본 정보 추출 - 네이버 로컬 API를 통한 상세 정보 검색 - 기존 Clean Architecture 패턴 유지 - 사이드이펙트 방지 및 테스트 가능성 확보 ## 2. 아키텍처 구조 ### 2.1 계층 구조 ``` Presentation Layer ↓ Domain Layer (Use Cases) ↓ Data Layer ├── Repository Implementation ├── Data Sources │ ├── Remote │ │ ├── NaverMapParser (기존) │ │ ├── NaverLocalApiClient (신규) │ │ └── NaverUrlProcessor (신규) │ └── Local │ └── Hive Database └── Models/DTOs ``` ### 2.2 주요 컴포넌트 #### 2.2.1 Data Layer - Remote Data Sources **NaverUrlProcessor (신규)** ```dart // lib/data/datasources/remote/naver_url_processor.dart class NaverUrlProcessor { final NaverMapParser _mapParser; final NaverLocalApiClient _apiClient; // 단축 URL 처리 파이프라인 Future processNaverUrl(String url); // URL 유효성 검증 bool isValidNaverUrl(String url); // 단축 URL → 실제 URL 변환 Future resolveShortUrl(String shortUrl); } ``` **NaverLocalApiClient (신규)** ```dart // lib/data/datasources/remote/naver_local_api_client.dart class NaverLocalApiClient { final Dio _dio; // 네이버 로컬 API 검색 Future> searchLocal({ required String query, String? category, int display = 5, }); // 상세 정보 조회 (Place ID 기반) Future getPlaceDetail(String placeId); } ``` **NaverMapParser (기존 확장)** - 기존 HTML 스크래핑 기능 유지 - 새로운 메서드 추가: - `extractSearchQuery()`: HTML에서 검색 가능한 키워드 추출 - `extractPlaceMetadata()`: 메타 정보 추출 강화 #### 2.2.2 Data Layer - Models **NaverLocalSearchResult (신규)** ```dart // lib/data/models/naver_local_search_result.dart class NaverLocalSearchResult { final String title; final String link; final String category; final String description; final String telephone; final String address; final String roadAddress; final int mapx; // 경도 * 10,000,000 final int mapy; // 위도 * 10,000,000 } ``` **NaverPlaceDetail (신규)** ```dart // lib/data/models/naver_place_detail.dart class NaverPlaceDetail { final String id; final String name; final String category; final Map businessHours; final List menuItems; final String? homePage; final List images; } ``` #### 2.2.3 Repository Layer **RestaurantRepositoryImpl (확장)** ```dart // 기존 메서드 확장 @override Future addRestaurantFromUrl(String url) async { try { // NaverUrlProcessor 사용 final processor = NaverUrlProcessor( mapParser: _naverMapParser, apiClient: _naverLocalApiClient, ); final restaurant = await processor.processNaverUrl(url); // 중복 체크 및 저장 로직 (기존 유지) // ... } catch (e) { // 에러 처리 } } ``` ### 2.3 처리 흐름 ```mermaid sequenceDiagram participant User participant UI participant Repository participant UrlProcessor participant MapParser participant ApiClient participant Hive User->>UI: 네이버 단축 URL 입력 UI->>Repository: addRestaurantFromUrl(url) Repository->>UrlProcessor: processNaverUrl(url) UrlProcessor->>UrlProcessor: isValidNaverUrl(url) UrlProcessor->>MapParser: resolveShortUrl(url) MapParser-->>UrlProcessor: 실제 URL UrlProcessor->>MapParser: parseRestaurantFromUrl(url) MapParser-->>UrlProcessor: 기본 정보 (HTML 스크래핑) UrlProcessor->>MapParser: extractSearchQuery() MapParser-->>UrlProcessor: 검색 키워드 UrlProcessor->>ApiClient: searchLocal(query) ApiClient-->>UrlProcessor: 검색 결과 리스트 UrlProcessor->>UrlProcessor: 매칭 및 병합 UrlProcessor-->>Repository: 완성된 Restaurant 객체 Repository->>Hive: 중복 체크 Repository->>Hive: 저장 Repository-->>UI: 결과 반환 UI-->>User: 성공/실패 표시 ``` ## 3. 상세 설계 ### 3.1 URL 처리 파이프라인 1. **URL 유효성 검증** - 네이버 도메인 확인 (naver.com, naver.me) - URL 형식 검증 2. **단축 URL 리다이렉션** - HTTP HEAD/GET 요청으로 실제 URL 획득 - 웹 환경에서는 CORS 프록시 사용 3. **HTML 스크래핑 (기존 NaverMapParser)** - 기본 정보 추출: 이름, 주소, 카테고리 - Place ID 추출 시도 4. **네이버 로컬 API 검색** - 추출된 이름과 주소로 검색 - 결과 매칭 알고리즘 적용 5. **정보 병합** - HTML 스크래핑 데이터 + API 데이터 병합 - 우선순위: API 데이터 > 스크래핑 데이터 ### 3.2 에러 처리 전략 ```dart // 계층별 예외 정의 abstract class NaverException implements Exception { final String message; NaverException(this.message); } class NaverUrlException extends NaverException { NaverUrlException(String message) : super(message); } class NaverApiException extends NaverException { final int? statusCode; NaverApiException(String message, {this.statusCode}) : super(message); } class NaverParseException extends NaverException { NaverParseException(String message) : super(message); } ``` ### 3.3 매칭 알고리즘 ```dart class RestaurantMatcher { // 스크래핑 데이터와 API 결과 매칭 static NaverLocalSearchResult? findBestMatch( Restaurant scrapedData, List apiResults, ) { // 1. 이름 유사도 계산 (Levenshtein distance) // 2. 주소 유사도 계산 // 3. 카테고리 일치 여부 // 4. 거리 계산 (좌표 기반) // 5. 종합 점수로 최적 매칭 선택 } } ``` ## 4. 테스트 전략 ### 4.1 단위 테스트 ```dart // test/data/datasources/remote/naver_url_processor_test.dart - URL 유효성 검증 테스트 - 단축 URL 리다이렉션 테스트 - 정보 병합 로직 테스트 // test/data/datasources/remote/naver_local_api_client_test.dart - API 호출 성공/실패 테스트 - 응답 파싱 테스트 - 에러 처리 테스트 ``` ### 4.2 통합 테스트 ```dart // test/integration/naver_url_processing_test.dart - 전체 파이프라인 테스트 - 실제 URL로 E2E 테스트 - 에러 시나리오 테스트 ``` ### 4.3 모킹 전략 ```dart // Mock 객체 사용 class MockNaverMapParser extends Mock implements NaverMapParser {} class MockNaverLocalApiClient extends Mock implements NaverLocalApiClient {} class MockHttpClient extends Mock implements Client {} ``` ## 5. 설정 및 환경 변수 ### 5.1 API 키 관리 ```dart // lib/core/constants/api_keys.dart class ApiKeys { static const String _encodedClientId = String.fromEnvironment('NAVER_CLIENT_ID', defaultValue: ''); static const String _encodedClientSecret = String.fromEnvironment('NAVER_CLIENT_SECRET', defaultValue: ''); static String get naverClientId => _decode(_encodedClientId); static String get naverClientSecret => _decode(_encodedClientSecret); static bool areKeysConfigured() { return naverClientId.isNotEmpty && naverClientSecret.isNotEmpty; } static String _decode(String value) { if (value.isEmpty) return ''; try { return utf8.decode(base64.decode(value)); } on FormatException { return value; } } } ``` ### 5.2 환경별 설정 ```dart // lib/core/config/environment.dart abstract class Environment { static const bool isProduction = bool.fromEnvironment('dart.vm.product'); static String get corsProxyUrl { return isProduction ? 'https://api.allorigins.win/get?url=' : 'http://localhost:8080/proxy?url='; } } ``` ## 6. 성능 최적화 ### 6.1 캐싱 전략 ```dart class CacheManager { // URL 리다이렉션 캐시 (TTL: 1시간) final Map _urlCache = {}; // API 검색 결과 캐시 (TTL: 30분) final Map _searchCache = {}; } ``` ### 6.2 동시성 제어 ```dart class RateLimiter { // 네이버 API 호출 제한 (초당 10회) static const int maxRequestsPerSecond = 10; // 동시 요청 수 제한 static const int maxConcurrentRequests = 3; } ``` ## 7. 보안 고려사항 ### 7.1 API 키 보호 - 환경 변수 사용 - 클라이언트 사이드에서 직접 노출 방지 - ProGuard/R8 난독화 적용 ### 7.2 입력 검증 - URL 인젝션 방지 - XSS 방지를 위한 HTML 이스케이핑 - SQL 인젝션 방지 (Hive는 NoSQL이므로 해당 없음) ## 8. 모니터링 및 로깅 ### 8.1 로깅 전략 ```dart class NaverUrlLogger { static void logUrlProcessing(String url, ProcessingStep step, {dynamic data}) { // 구조화된 로그 기록 // - 타임스탬프 // - 처리 단계 // - 성공/실패 여부 // - 소요 시간 } } ``` ### 8.2 에러 추적 ```dart class ErrorReporter { static void reportError(Exception error, StackTrace stackTrace, { Map? extra, }) { // Crashlytics 또는 Sentry로 에러 전송 } } ``` ## 9. 향후 확장 고려사항 ### 9.1 다른 플랫폼 지원 - 카카오맵 URL 처리 - 구글맵 URL 처리 - 배달앱 공유 링크 처리 ### 9.2 기능 확장 - 메뉴 정보 수집 - 리뷰 데이터 수집 - 영업시간 실시간 업데이트 ### 9.3 성능 개선 - 백그라운드 프리페칭 - 예측 기반 캐싱 - CDN 활용 ## 10. 마이그레이션 계획 ### 10.1 단계별 적용 1. NaverLocalApiClient 구현 및 테스트 2. NaverUrlProcessor 구현 3. 기존 addRestaurantFromUrl 메서드 리팩토링 4. UI 업데이트 (로딩 상태, 에러 처리) 5. 프로덕션 배포 ### 10.2 하위 호환성 - 기존 NaverMapParser는 그대로 유지 - 새로운 기능은 옵트인 방식으로 제공 - 점진적 마이그레이션 지원