415 lines
10 KiB
Markdown
415 lines
10 KiB
Markdown
# 네이버 단축 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<Restaurant> processNaverUrl(String url);
|
|
|
|
// URL 유효성 검증
|
|
bool isValidNaverUrl(String url);
|
|
|
|
// 단축 URL → 실제 URL 변환
|
|
Future<String> resolveShortUrl(String shortUrl);
|
|
}
|
|
```
|
|
|
|
**NaverLocalApiClient (신규)**
|
|
```dart
|
|
// lib/data/datasources/remote/naver_local_api_client.dart
|
|
class NaverLocalApiClient {
|
|
final Dio _dio;
|
|
|
|
// 네이버 로컬 API 검색
|
|
Future<List<NaverLocalSearchResult>> searchLocal({
|
|
required String query,
|
|
String? category,
|
|
int display = 5,
|
|
});
|
|
|
|
// 상세 정보 조회 (Place ID 기반)
|
|
Future<NaverPlaceDetail?> 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<String, dynamic> businessHours;
|
|
final List<String> menuItems;
|
|
final String? homePage;
|
|
final List<String> images;
|
|
}
|
|
```
|
|
|
|
#### 2.2.3 Repository Layer
|
|
|
|
**RestaurantRepositoryImpl (확장)**
|
|
```dart
|
|
// 기존 메서드 확장
|
|
@override
|
|
Future<Restaurant> 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<NaverLocalSearchResult> 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<String, CachedUrl> _urlCache = {};
|
|
|
|
// API 검색 결과 캐시 (TTL: 30분)
|
|
final Map<String, CachedSearchResult> _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<String, dynamic>? 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는 그대로 유지
|
|
- 새로운 기능은 옵트인 방식으로 제공
|
|
- 점진적 마이그레이션 지원
|