LunchPick(오늘 뭐 먹Z?) Flutter 앱의 초기 구현입니다. 주요 기능: - 네이버 지도 연동 맛집 추가 - 랜덤 메뉴 추천 시스템 - 날씨 기반 거리 조정 - 방문 기록 관리 - Bluetooth 맛집 공유 - 다크모드 지원 기술 스택: - Flutter 3.8.1+ - Riverpod 상태 관리 - Hive 로컬 DB - Clean Architecture 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
9.7 KiB
9.7 KiB
네이버 단축 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 (신규)
// 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 (신규)
// 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 (신규)
// 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 (신규)
// 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 (확장)
// 기존 메서드 확장
@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 처리 흐름
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 처리 파이프라인
-
URL 유효성 검증
- 네이버 도메인 확인 (naver.com, naver.me)
- URL 형식 검증
-
단축 URL 리다이렉션
- HTTP HEAD/GET 요청으로 실제 URL 획득
- 웹 환경에서는 CORS 프록시 사용
-
HTML 스크래핑 (기존 NaverMapParser)
- 기본 정보 추출: 이름, 주소, 카테고리
- Place ID 추출 시도
-
네이버 로컬 API 검색
- 추출된 이름과 주소로 검색
- 결과 매칭 알고리즘 적용
-
정보 병합
- HTML 스크래핑 데이터 + API 데이터 병합
- 우선순위: API 데이터 > 스크래핑 데이터
3.2 에러 처리 전략
// 계층별 예외 정의
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 매칭 알고리즘
class RestaurantMatcher {
// 스크래핑 데이터와 API 결과 매칭
static NaverLocalSearchResult? findBestMatch(
Restaurant scrapedData,
List<NaverLocalSearchResult> apiResults,
) {
// 1. 이름 유사도 계산 (Levenshtein distance)
// 2. 주소 유사도 계산
// 3. 카테고리 일치 여부
// 4. 거리 계산 (좌표 기반)
// 5. 종합 점수로 최적 매칭 선택
}
}
4. 테스트 전략
4.1 단위 테스트
// test/data/datasources/remote/naver_url_processor_test.dart
- URL 유효성 검증 테스트
- 단축 URL 리다이렉션 테스트
- 정보 병합 로직 테스트
// test/data/datasources/remote/naver_local_api_client_test.dart
- API 호출 성공/실패 테스트
- 응답 파싱 테스트
- 에러 처리 테스트
4.2 통합 테스트
// test/integration/naver_url_processing_test.dart
- 전체 파이프라인 테스트
- 실제 URL로 E2E 테스트
- 에러 시나리오 테스트
4.3 모킹 전략
// Mock 객체 사용
class MockNaverMapParser extends Mock implements NaverMapParser {}
class MockNaverLocalApiClient extends Mock implements NaverLocalApiClient {}
class MockHttpClient extends Mock implements Client {}
5. 설정 및 환경 변수
5.1 API 키 관리
// lib/core/constants/api_keys.dart
class ApiKeys {
static const String naverClientId = String.fromEnvironment('NAVER_CLIENT_ID');
static const String naverClientSecret = String.fromEnvironment('NAVER_CLIENT_SECRET');
static bool areKeysConfigured() {
return naverClientId.isNotEmpty && naverClientSecret.isNotEmpty;
}
}
5.2 환경별 설정
// 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 캐싱 전략
class CacheManager {
// URL 리다이렉션 캐시 (TTL: 1시간)
final Map<String, CachedUrl> _urlCache = {};
// API 검색 결과 캐시 (TTL: 30분)
final Map<String, CachedSearchResult> _searchCache = {};
}
6.2 동시성 제어
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 로깅 전략
class NaverUrlLogger {
static void logUrlProcessing(String url, ProcessingStep step, {dynamic data}) {
// 구조화된 로그 기록
// - 타임스탬프
// - 처리 단계
// - 성공/실패 여부
// - 소요 시간
}
}
8.2 에러 추적
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 단계별 적용
- NaverLocalApiClient 구현 및 테스트
- NaverUrlProcessor 구현
- 기존 addRestaurantFromUrl 메서드 리팩토링
- UI 업데이트 (로딩 상태, 에러 처리)
- 프로덕션 배포
10.2 하위 호환성
- 기존 NaverMapParser는 그대로 유지
- 새로운 기능은 옵트인 방식으로 제공
- 점진적 마이그레이션 지원