feat: 초기 프로젝트 설정 및 LunchPick 앱 구현

LunchPick(오늘 뭐 먹Z?) Flutter 앱의 초기 구현입니다.

주요 기능:
- 네이버 지도 연동 맛집 추가
- 랜덤 메뉴 추천 시스템
- 날씨 기반 거리 조정
- 방문 기록 관리
- Bluetooth 맛집 공유
- 다크모드 지원

기술 스택:
- Flutter 3.8.1+
- Riverpod 상태 관리
- Hive 로컬 DB
- Clean Architecture

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
JiWoong Sul
2025-07-30 19:03:28 +09:00
commit 85fde36157
237 changed files with 30953 additions and 0 deletions

View File

@@ -0,0 +1,247 @@
# 네이버 단축 URL 처리 아키텍처 개요
## 1. 프로젝트 요약
### 1.1 목표
네이버 단축 URL(naver.me)을 처리하여 식당 정보를 추출하고, 네이버 로컬 API를 통해 상세 정보를 보강하는 시스템 구축
### 1.2 핵심 가치
- **사이드이펙트 방지**: 명확한 책임 분리와 순수 함수 활용
- **책임 분리**: 각 컴포넌트가 단일 책임 원칙 준수
- **테스트 가능성**: 의존성 주입과 모킹을 통한 단위 테스트
- **확장성**: 새로운 데이터 소스 추가 용이
## 2. 아키텍처 선택
### 2.1 Clean Architecture + MVVM
- **이유**: 비즈니스 로직과 UI의 명확한 분리
- **장점**: 테스트 용이성, 유지보수성, 확장성
- **구현**: 3계층 구조 (Presentation → Domain → Data)
### 2.2 상태 관리: Riverpod
- **이유**:
- 컴파일 타임 안전성
- 의존성 주입 내장
- 기존 프로젝트와 일관성
- **장점**:
- Provider 범위 제어
- 자동 리소스 관리
- 테스트 모킹 용이
### 2.3 로컬 저장소: Hive
- **이유**:
- 빠른 성능
- 간단한 API
- 기존 인프라 활용
- **장점**:
- NoSQL 기반 유연성
- 타입 안전성
- 오프라인 우선 설계
## 3. 주요 컴포넌트 설계
### 3.1 NaverUrlProcessor (신규)
```dart
class NaverUrlProcessor {
// 책임: URL 처리 파이프라인 관리
// 의존성: NaverMapParser, NaverLocalApiClient
// 출력: Restaurant 엔티티
}
```
**주요 기능:**
- URL 유효성 검증
- 단축 URL 리다이렉션
- 정보 추출 조정
- 데이터 병합
### 3.2 NaverLocalApiClient (신규)
```dart
class NaverLocalApiClient {
// 책임: 네이버 로컬 API 통신
// 의존성: Dio, ApiKeys
// 출력: API 응답 모델
}
```
**주요 기능:**
- API 호출 및 에러 처리
- Rate limiting
- 응답 파싱
### 3.3 NaverMapParser (확장)
- 기존 HTML 스크래핑 기능 유지
- 검색 키워드 추출 기능 추가
- 메타데이터 추출 강화
## 4. 데이터 플로우
```
1. 사용자 입력 (네이버 단축 URL)
2. RestaurantRepository.addRestaurantFromUrl()
3. NaverUrlProcessor.processNaverUrl()
4. URL 검증 및 리다이렉션
5. 병렬 처리:
- NaverMapParser: HTML 스크래핑
- NaverLocalApiClient: API 검색
6. 데이터 매칭 및 병합
7. Restaurant 엔티티 생성
8. Hive 저장
```
## 5. 에러 처리 전략
### 5.1 계층별 예외
```dart
DataLayer: NetworkException, ParseException
DomainLayer: BusinessException, ValidationException
PresentationLayer: UIException
```
### 5.2 복구 전략
- **네트워크 실패**: 3회 재시도 후 캐시 데이터 사용
- **파싱 실패**: 기본값 사용 및 부분 데이터 반환
- **API 제한**: Rate limiting 및 백오프
## 6. 성능 최적화
### 6.1 캐싱 전략
- **메모리 캐시**: URL 리다이렉션, API 응답 (LRU)
- **디스크 캐시**: Restaurant 데이터 (Hive)
- **TTL**: URL 1시간, API 30분
### 6.2 동시성 제어
- 최대 동시 요청: 3개
- API Rate limit: 초당 10회
- 타임아웃: 10초
## 7. 테스트 전략
### 7.1 단위 테스트
- **목표 커버리지**: 80% 이상
- **핵심 테스트**: URL 파서, API 클라이언트, 매칭 알고리즘
### 7.2 통합 테스트
- E2E 시나리오 테스트
- 실제 네이버 URL 테스트 (CI 제외)
### 7.3 모킹
- Mockito/Mocktail 사용
- HTTP 응답 모킹
- Provider 오버라이드
## 8. 보안 고려사항
### 8.1 API 키 관리
- 환경 변수 사용
- 컴파일 타임 주입
- ProGuard 난독화
### 8.2 입력 검증
- URL 화이트리스트
- XSS 방지
- 인젝션 방지
## 9. 모니터링 및 로깅
### 9.1 구조화된 로깅
```dart
logger.info('URL 처리 시작', {
'url': url,
'timestamp': DateTime.now(),
'userId': userId,
});
```
### 9.2 성능 모니터링
- 응답 시간 측정
- 성공률 추적
- 에러율 모니터링
## 10. 향후 확장 계획
### 10.1 단기 (1-3개월)
- 카카오맵 URL 지원
- 메뉴 정보 수집
- 오프라인 모드 강화
### 10.2 중기 (3-6개월)
- 자체 백엔드 구축
- 리뷰 데이터 수집
- ML 기반 매칭
### 10.3 장기 (6개월+)
- 다중 플랫폼 통합
- 실시간 업데이트
- 개인화 추천
## 11. 개발 가이드라인
### 11.1 코딩 원칙
- DRY (Don't Repeat Yourself)
- KISS (Keep It Simple)
- SOLID 원칙 준수
### 11.2 PR 체크리스트
- [ ] 단위 테스트 작성
- [ ] 문서 업데이트
- [ ] 코드 리뷰 통과
- [ ] CI/CD 통과
### 11.3 배포 전략
- Feature flag 사용
- 점진적 롤아웃
- 롤백 계획 수립
## 12. 팀 협업
### 12.1 개발 프로세스
1. 이슈 생성 및 할당
2. 기능 브랜치 생성
3. 구현 및 테스트
4. PR 생성 및 리뷰
5. 머지 및 배포
### 12.2 문서화
- 코드 내 주석 (한국어)
- API 문서 자동 생성
- 아키텍처 결정 기록 (ADR)
## 13. 리스크 관리
| 리스크 | 가능성 | 영향도 | 대응 방안 |
|--------|--------|--------|-----------|
| 네이버 구조 변경 | 높음 | 높음 | 파서 모듈화, 빠른 업데이트 |
| API 제한 강화 | 중간 | 중간 | 캐싱 강화, 대체 API |
| CORS 프록시 장애 | 중간 | 높음 | 다중 프록시, 자체 구축 |
## 14. 성공 지표
### 14.1 기술적 지표
- URL 처리 성공률 > 95%
- 평균 응답 시간 < 3초
- 크래시율 < 0.1%
### 14.2 비즈니스 지표
- 사용자 만족도 향상
- 식당 등록 시간 단축
- 데이터 정확도 향상
## 15. 결론
본 아키텍처는 네이버 단축 URL 처리를 위한 확장 가능하고 유지보수가 용이한 시스템을 제공합니다. Clean Architecture 원칙을 따르며, 기존 시스템과의 원활한 통합을 보장합니다.
주요 이점:
- ✅ 명확한 책임 분리
- ✅ 높은 테스트 가능성
- ✅ 유연한 확장성
- ✅ 안정적인 에러 처리
이 설계를 통해 사용자는 더 쉽고 빠르게 네이버 지도의 식당 정보를 앱에 추가할 수 있으며, 개발팀은 안정적이고 유지보수가 용이한 코드베이스를 유지할 수 있습니다.

View File

@@ -0,0 +1,589 @@
# 코드 컨벤션 문서
## 1. 개요
이 문서는 "오늘 뭐 먹Z?" 프로젝트의 코드 작성 규칙과 스타일 가이드를 정의합니다. 일관된 코드 스타일은 가독성을 높이고 유지보수를 용이하게 합니다.
## 2. 일반 원칙
### 2.1 기본 규칙
- **DRY (Don't Repeat Yourself)**: 코드 중복 최소화
- **KISS (Keep It Simple, Stupid)**: 단순하고 명확한 코드 작성
- **YAGNI (You Aren't Gonna Need It)**: 필요하지 않은 기능 미리 구현 금지
- **단일 책임 원칙**: 하나의 클래스/함수는 하나의 책임만
### 2.2 언어별 규칙
- **코드**: 영어 (변수명, 함수명, 클래스명)
- **주석**: 한국어
- **커밋 메시지**: 한국어
- **문서**: 한국어
## 3. 네이밍 컨벤션
### 3.1 기본 네이밍 규칙
| 요소 | 스타일 | 예시 |
|------|--------|------|
| 클래스 | PascalCase | `NaverUrlProcessor`, `RestaurantRepository` |
| 인터페이스 | PascalCase | `IRestaurantRepository` (선택적 I 접두사) |
| 함수/메서드 | camelCase | `processNaverUrl`, `searchRestaurants` |
| 변수 | camelCase | `restaurantName`, `maxDistance` |
| 상수 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT`, `API_TIMEOUT` |
| 파일명 | snake_case | `naver_url_processor.dart` |
| 폴더명 | snake_case | `data_sources`, `use_cases` |
### 3.2 의미있는 이름 작성
```dart
// ❌ 나쁜 예
String n = "김밥천국";
int d = 500;
List<Restaurant> r = [];
// ✅ 좋은 예
String restaurantName = "김밥천국";
int maxDistanceInMeters = 500;
List<Restaurant> nearbyRestaurants = [];
```
### 3.3 Boolean 변수 네이밍
```dart
// ❌ 나쁜 예
bool loading = true;
bool error = false;
// ✅ 좋은 예
bool isLoading = true;
bool hasError = false;
bool canDelete = true;
bool shouldRefresh = false;
```
### 3.4 함수/메서드 네이밍
```dart
// 동사로 시작
Future<void> saveRestaurant(Restaurant restaurant);
Future<List<Restaurant>> fetchNearbyRestaurants();
bool validateUrl(String url);
void clearCache();
// 상태 확인 메서드는 is/has/can으로 시작
bool isValidNaverUrl(String url);
bool hasLocationPermission();
bool canProcessUrl(String url);
```
## 4. 파일 구조 및 조직
### 4.1 파일 구조 템플릿
```dart
// 1. 라이브러리 선언 (필요한 경우)
library restaurant_repository;
// 2. Dart 임포트
import 'dart:async';
import 'dart:convert';
// 3. 패키지 임포트 (알파벳 순)
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 4. 프로젝트 임포트 (알파벳 순)
import 'package:lunchpick/core/constants/api_keys.dart';
import 'package:lunchpick/domain/entities/restaurant.dart';
// 5. Part 파일 (있는 경우)
part 'restaurant_repository.g.dart';
// 6. 전역 상수
const int kDefaultTimeout = 30;
// 7. 클래스 정의
class RestaurantRepository {
// 구현
}
```
### 4.2 클래스 내부 구조
```dart
class NaverUrlProcessor {
// 1. 정적 상수
static const int maxRetryCount = 3;
// 2. 정적 변수
static String? _cachedUrl;
// 3. 인스턴스 변수 (private 먼저)
final NaverMapParser _mapParser;
final NaverLocalApiClient _apiClient;
late final Logger _logger;
// 4. 생성자
NaverUrlProcessor({
required NaverMapParser mapParser,
required NaverLocalApiClient apiClient,
}) : _mapParser = mapParser,
_apiClient = apiClient {
_logger = Logger('NaverUrlProcessor');
}
// 5. 팩토리 생성자
factory NaverUrlProcessor.create() {
return NaverUrlProcessor(
mapParser: NaverMapParser(),
apiClient: NaverLocalApiClient(),
);
}
// 6. getter/setter
bool get isReady => _mapParser != null && _apiClient != null;
// 7. public 메서드
Future<Restaurant> processUrl(String url) async {
// 구현
}
// 8. private 메서드
Future<String> _resolveUrl(String url) async {
// 구현
}
// 9. 정적 메서드
static bool isValidUrl(String url) {
// 구현
}
}
```
## 5. 코딩 스타일
### 5.1 들여쓰기 및 정렬
```dart
// 2 스페이스 들여쓰기 사용
class Restaurant {
final String id;
final String name;
Restaurant({
required this.id,
required this.name,
});
}
// 긴 파라미터는 세로 정렬
final restaurant = Restaurant(
id: generateId(),
name: '김밥천국',
category: '분식',
roadAddress: '서울특별시 강남구 테헤란로 123',
latitude: 37.123456,
longitude: 127.123456,
);
```
### 5.2 줄 길이
- 최대 80자 권장 (하드 리밋: 120자)
- 긴 문자열은 여러 줄로 분할
```dart
// ❌ 나쁜 예
String errorMessage = '네이버 지도 URL 파싱 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
// ✅ 좋은 예
String errorMessage = '네이버 지도 URL 파싱 중 오류가 발생했습니다. '
'잠시 후 다시 시도해주세요.';
```
### 5.3 중괄호 사용
```dart
// 한 줄이어도 중괄호 사용
if (isValid) {
return true;
}
// 여러 조건은 읽기 쉽게 정렬
if (restaurant.name.isNotEmpty &&
restaurant.category.isNotEmpty &&
restaurant.latitude != 0) {
// 처리
}
```
### 5.4 빈 줄 사용
```dart
class RestaurantService {
final RestaurantRepository _repository;
final CacheManager _cache;
RestaurantService(this._repository, this._cache);
// 메서드 사이에 빈 줄
Future<List<Restaurant>> getNearbyRestaurants() async {
// 구현
}
Future<void> addRestaurant(Restaurant restaurant) async {
// 구현
}
}
```
## 6. 주석 작성 규칙
### 6.1 문서 주석
```dart
/// 네이버 단축 URL을 처리하여 식당 정보를 추출합니다.
///
/// [url]은 네이버 지도 또는 naver.me 단축 URL이어야 합니다.
///
/// 처리 과정:
/// 1. URL 유효성 검증
/// 2. 단축 URL 리다이렉션
/// 3. HTML 스크래핑
/// 4. 네이버 로컬 API 검색
/// 5. 정보 병합
///
/// 실패 시 [NaverUrlException]을 던집니다.
Future<Restaurant> processNaverUrl(String url) async {
// 구현
}
```
### 6.2 인라인 주석
```dart
// 단축 URL인 경우 리다이렉션 처리
if (url.contains('naver.me')) {
url = await _resolveShortUrl(url);
}
// TODO: 캐싱 로직 추가 필요
// FIXME: 타임아웃 처리 개선 필요
// HACK: CORS 우회를 위한 임시 방법
```
### 6.3 주석 작성 원칙
- **Why, not What**: 코드가 무엇을 하는지가 아닌 왜 그렇게 하는지 설명
- **한국어 사용**: 모든 주석은 한국어로 작성
- **최신 상태 유지**: 코드 변경 시 주석도 함께 업데이트
## 7. 에러 처리
### 7.1 예외 정의
```dart
// 계층별 예외 클래스 정의
abstract class AppException implements Exception {
final String message;
final String? code;
final dynamic originalError;
AppException(this.message, {this.code, this.originalError});
}
class NetworkException extends AppException {
NetworkException(String message, {String? code})
: super(message, code: code);
}
class ParseException extends AppException {
ParseException(String message, {dynamic originalError})
: super(message, originalError: originalError);
}
```
### 7.2 에러 처리 패턴
```dart
Future<Restaurant?> fetchRestaurant(String id) async {
try {
final response = await _api.getRestaurant(id);
return Restaurant.fromJson(response);
} on DioException catch (e) {
// 네트워크 에러 처리
_logger.error('네트워크 에러 발생', error: e);
throw NetworkException('식당 정보를 가져올 수 없습니다');
} on FormatException catch (e) {
// 파싱 에러 처리
_logger.error('데이터 파싱 실패', error: e);
throw ParseException('잘못된 데이터 형식입니다');
} catch (e) {
// 예상치 못한 에러
_logger.error('알 수 없는 에러', error: e);
rethrow;
}
}
```
## 8. 비동기 프로그래밍
### 8.1 async/await 사용
```dart
// ✅ 좋은 예
Future<void> processRestaurants() async {
final restaurants = await fetchRestaurants();
for (final restaurant in restaurants) {
await processRestaurant(restaurant);
}
}
// ❌ 나쁜 예 (불필요한 then 체이닝)
void processRestaurants() {
fetchRestaurants().then((restaurants) {
restaurants.forEach((restaurant) {
processRestaurant(restaurant);
});
});
}
```
### 8.2 동시 실행
```dart
// 병렬 실행이 가능한 경우
Future<void> initializeApp() async {
final results = await Future.wait([
_loadUserSettings(),
_fetchRestaurants(),
_checkLocationPermission(),
]);
}
// 순차 실행이 필요한 경우
Future<void> processInOrder() async {
final user = await _loadUser();
final settings = await _loadUserSettings(user.id);
final restaurants = await _fetchUserRestaurants(user.id);
}
```
## 9. 상태 관리 (Riverpod)
### 9.1 Provider 네이밍
```dart
// Provider 이름은 제공하는 값 + Provider
final restaurantListProvider = FutureProvider<List<Restaurant>>((ref) {
return ref.watch(restaurantRepositoryProvider).getAllRestaurants();
});
// StateNotifierProvider는 notifier 추가
final restaurantFilterNotifierProvider =
StateNotifierProvider<RestaurantFilterNotifier, RestaurantFilter>((ref) {
return RestaurantFilterNotifier();
});
```
### 9.2 Provider 구조화
```dart
// providers/restaurant_provider.dart
final restaurantRepositoryProvider = Provider<RestaurantRepository>((ref) {
return RestaurantRepositoryImpl();
});
final nearbyRestaurantsProvider = FutureProvider<List<Restaurant>>((ref) {
final location = ref.watch(locationProvider);
final repository = ref.watch(restaurantRepositoryProvider);
return repository.getNearbyRestaurants(
latitude: location.latitude,
longitude: location.longitude,
radius: 500,
);
});
```
## 10. 테스트 코드 작성
### 10.1 테스트 파일 구조
```dart
// test/data/datasources/remote/naver_url_processor_test.dart
void main() {
// 테스트 대상 선언
late NaverUrlProcessor processor;
late MockNaverMapParser mockMapParser;
late MockNaverLocalApiClient mockApiClient;
// 셋업
setUp(() {
mockMapParser = MockNaverMapParser();
mockApiClient = MockNaverLocalApiClient();
processor = NaverUrlProcessor(
mapParser: mockMapParser,
apiClient: mockApiClient,
);
});
// 테스트 그룹화
group('processNaverUrl', () {
test('유효한 단축 URL을 처리해야 함', () async {
// Given
const url = 'https://naver.me/abc123';
when(() => mockMapParser.parseUrl(any())).thenAnswer(
(_) async => testRestaurant,
);
// When
final result = await processor.processNaverUrl(url);
// Then
expect(result.name, equals('테스트 식당'));
verify(() => mockMapParser.parseUrl(url)).called(1);
});
test('유효하지 않은 URL은 예외를 던져야 함', () async {
// Given
const invalidUrl = 'https://google.com';
// When & Then
expect(
() => processor.processNaverUrl(invalidUrl),
throwsA(isA<NaverUrlException>()),
);
});
});
}
```
### 10.2 테스트 네이밍
```dart
// 테스트 이름은 한국어로 명확하게
test('레스토랑 이름이 비어있으면 예외를 던져야 함', () {});
test('중복된 레스토랑은 추가되지 않아야 함', () {});
test('거리 계산이 정확해야 함', () {});
```
## 11. 성능 최적화 규칙
### 11.1 위젯 최적화
```dart
// const 생성자 사용
class RestaurantCard extends StatelessWidget {
const RestaurantCard({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Text('Restaurant'),
),
);
}
}
// 무거운 빌드 메서드 분리
class RestaurantList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ListView.builder(
itemBuilder: (context, index) => RestaurantItem(index: index),
);
}
}
```
### 11.2 메모리 관리
```dart
class RestaurantService {
// 캐시 크기 제한
final _cache = LruMap<String, Restaurant>(maximumSize: 100);
// 리소스 정리
void dispose() {
_cache.clear();
_subscription?.cancel();
}
}
```
## 12. Git 커밋 규칙
### 12.1 커밋 메시지 형식
```
타입(범위): 제목
본문 (선택사항)
이슈: #123
```
### 12.2 커밋 타입
- `feat`: 새로운 기능
- `fix`: 버그 수정
- `refactor`: 리팩토링
- `style`: 코드 스타일 변경
- `test`: 테스트 추가/수정
- `docs`: 문서 수정
- `chore`: 빌드, 설정 변경
### 12.3 커밋 예시
```
feat(restaurant): 네이버 단축 URL 처리 기능 추가
- NaverUrlProcessor 클래스 구현
- 네이버 로컬 API 클라이언트 추가
- URL 매칭 알고리즘 구현
이슈: #42
```
## 13. 코드 리뷰 체크리스트
### 13.1 기능 확인
- [ ] 요구사항을 모두 충족하는가?
- [ ] 엣지 케이스를 처리하는가?
- [ ] 에러 처리가 적절한가?
### 13.2 코드 품질
- [ ] 네이밍 컨벤션을 따르는가?
- [ ] 주석이 적절히 작성되었는가?
- [ ] 중복 코드가 없는가?
- [ ] 함수/클래스가 단일 책임을 가지는가?
### 13.3 테스트
- [ ] 테스트가 작성되었는가?
- [ ] 테스트 커버리지가 충분한가?
- [ ] 테스트가 의미있는가?
### 13.4 성능
- [ ] 불필요한 리빌드가 없는가?
- [ ] 메모리 누수 가능성이 없는가?
- [ ] 비동기 처리가 적절한가?
## 14. 프로젝트별 특별 규칙
### 14.1 네이버 관련 코드
- 모든 네이버 관련 클래스는 `Naver` 접두사 사용
- API 응답은 항상 null 체크
- 스크래핑 선택자는 상수로 정의
### 14.2 Restaurant 엔티티
- 불변 객체로 유지
- copyWith 메서드 제공
- 좌표는 항상 유효성 검증
### 14.3 로컬 저장소
- Hive 박스 이름은 상수로 정의
- 마이그레이션 전략 문서화
- 트랜잭션 단위로 처리

View File

@@ -0,0 +1,400 @@
# 네이버 단축 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 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 환경별 설정
```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는 그대로 유지
- 새로운 기능은 옵트인 방식으로 제공
- 점진적 마이그레이션 지원

View File

@@ -0,0 +1,297 @@
# 프로젝트 구조도
## 1. 전체 구조 개요
```
lunchpick/
├── lib/
│ ├── core/ # 핵심 공통 모듈
│ │ ├── constants/ # 상수 정의
│ │ ├── errors/ # 에러 및 예외 처리
│ │ ├── services/ # 공통 서비스
│ │ ├── utils/ # 유틸리티 함수
│ │ └── widgets/ # 공통 위젯
│ │
│ ├── data/ # 데이터 계층
│ │ ├── datasources/ # 데이터 소스
│ │ │ ├── local/ # 로컬 데이터 소스
│ │ │ └── remote/ # 원격 데이터 소스
│ │ ├── models/ # 데이터 모델 (DTO)
│ │ └── repositories/ # 레포지토리 구현체
│ │
│ ├── domain/ # 도메인 계층
│ │ ├── entities/ # 도메인 엔티티
│ │ ├── repositories/ # 레포지토리 인터페이스
│ │ └── usecases/ # 유스케이스 (비즈니스 로직)
│ │
│ ├── presentation/ # 프레젠테이션 계층
│ │ ├── pages/ # 화면 단위 구성
│ │ ├── providers/ # 상태 관리 (Riverpod)
│ │ └── widgets/ # 프레젠테이션 공통 위젯
│ │
│ └── main.dart # 앱 진입점
├── test/ # 테스트 코드
├── doc/ # 문서
└── pubspec.yaml # 의존성 관리
```
## 2. 계층별 상세 구조
### 2.1 Core Layer (핵심 모듈)
```
core/
├── constants/
│ ├── api_keys.dart # API 키 관리
│ ├── app_colors.dart # 색상 테마
│ ├── app_constants.dart # 앱 전역 상수
│ ├── app_typography.dart # 텍스트 스타일
│ └── categories.dart # 카테고리 정의
├── errors/
│ ├── exceptions.dart # 커스텀 예외
│ └── failures.dart # 실패 타입 정의
├── services/
│ └── notification_service.dart # 알림 서비스
├── utils/
│ ├── distance_calculator.dart # 거리 계산 유틸
│ └── validators.dart # 입력 검증 유틸
└── widgets/
├── empty_state_widget.dart # 빈 상태 위젯
├── error_widget.dart # 에러 표시 위젯
└── loading_indicator.dart # 로딩 인디케이터
```
### 2.2 Data Layer (데이터 계층)
```
data/
├── datasources/
│ ├── local/
│ │ └── (Hive 데이터소스)
│ │
│ └── remote/
│ ├── naver_map_parser.dart # 네이버 지도 파서 (기존)
│ ├── naver_local_api_client.dart # 네이버 로컬 API (신규)
│ └── naver_url_processor.dart # URL 처리기 (신규)
├── models/
│ ├── naver_local_search_result.dart # API 검색 결과 모델 (신규)
│ └── naver_place_detail.dart # 장소 상세 모델 (신규)
└── repositories/
├── recommendation_repository_impl.dart
├── restaurant_repository_impl.dart
├── settings_repository_impl.dart
├── visit_repository_impl.dart
└── weather_repository_impl.dart
```
### 2.3 Domain Layer (도메인 계층)
```
domain/
├── entities/
│ ├── recommendation_record.dart # 추천 기록
│ ├── restaurant.dart # 식당 엔티티
│ ├── share_device.dart # 공유 디바이스
│ ├── user_settings.dart # 사용자 설정
│ ├── visit_record.dart # 방문 기록
│ └── weather_info.dart # 날씨 정보
├── repositories/
│ ├── recommendation_repository.dart
│ ├── restaurant_repository.dart
│ ├── settings_repository.dart
│ ├── visit_repository.dart
│ └── weather_repository.dart
└── usecases/
└── recommendation_engine.dart # 추천 엔진
```
### 2.4 Presentation Layer (프레젠테이션 계층)
```
presentation/
├── pages/
│ ├── calendar/ # 캘린더 화면
│ │ ├── calendar_screen.dart
│ │ └── widgets/
│ │ ├── visit_confirmation_dialog.dart
│ │ ├── visit_record_card.dart
│ │ └── visit_statistics.dart
│ │
│ ├── main/ # 메인 화면
│ │ └── main_screen.dart
│ │
│ ├── random_selection/ # 랜덤 선택 화면
│ │ ├── random_selection_screen.dart
│ │ └── widgets/
│ │ └── recommendation_result_dialog.dart
│ │
│ ├── restaurant_list/ # 식당 목록 화면
│ │ ├── restaurant_list_screen.dart
│ │ └── widgets/
│ │ ├── add_restaurant_dialog.dart
│ │ └── restaurant_card.dart
│ │
│ ├── settings/ # 설정 화면
│ │ ├── settings_screen.dart
│ │ └── widgets/
│ │
│ ├── share/ # 공유 화면
│ │ ├── share_screen.dart
│ │ └── widgets/
│ │
│ └── splash/ # 스플래시 화면
│ └── splash_screen.dart
├── providers/
│ ├── di_providers.dart # 의존성 주입
│ ├── location_provider.dart # 위치 상태 관리
│ ├── notification_handler_provider.dart
│ ├── notification_provider.dart
│ ├── recommendation_provider.dart
│ ├── restaurant_provider.dart
│ ├── settings_provider.dart
│ ├── visit_provider.dart
│ └── weather_provider.dart
└── widgets/
└── category_selector.dart # 카테고리 선택기
```
## 3. 데이터 흐름도
```mermaid
graph TD
A[사용자 인터페이스] -->|사용자 액션| B[Presentation Layer]
B -->|Provider/State| C[Domain Layer]
C -->|Use Case 실행| D[Data Layer]
D -->|Repository 구현| E[Data Sources]
E --> F[Local Data Source<br/>Hive]
E --> G[Remote Data Source<br/>APIs]
G --> H[네이버 지도 파서]
G --> I[네이버 로컬 API]
G --> J[날씨 API]
F -->|데이터 반환| D
G -->|데이터 반환| D
D -->|엔티티 반환| C
C -->|결과 반환| B
B -->|UI 업데이트| A
```
## 4. 네이버 URL 처리 플로우
```mermaid
graph LR
A[네이버 단축 URL] --> B[NaverUrlProcessor]
B --> C{URL 유효성<br/>검증}
C -->|유효| D[NaverMapParser]
C -->|무효| E[에러 반환]
D --> F[단축 URL<br/>리다이렉션]
F --> G[HTML<br/>스크래핑]
G --> H[기본 정보<br/>추출]
H --> I[NaverLocalApiClient]
I --> J[로컬 API<br/>검색]
J --> K[결과 매칭]
K --> L[정보 병합]
L --> M[Restaurant<br/>엔티티 생성]
M --> N[Repository]
N --> O[Hive 저장]
```
## 5. 의존성 관계도
```mermaid
graph BT
A[main.dart] --> B[Presentation Layer]
B --> C[Domain Layer]
C --> D[Data Layer]
D --> E[Core Layer]
B --> E
D --> F[External Packages]
F --> G[flutter_riverpod]
F --> H[hive]
F --> I[dio]
F --> J[http]
F --> K[html]
E --> L[Flutter SDK]
```
## 6. 모듈별 책임
### 6.1 Core 모듈
- **역할**: 앱 전체에서 공통으로 사용되는 기능 제공
- **책임**:
- 상수 및 설정 관리
- 공통 유틸리티 제공
- 전역 위젯 제공
- 에러 처리 표준화
### 6.2 Data 모듈
- **역할**: 데이터 접근 및 변환
- **책임**:
- 외부 API 통신
- 로컬 데이터베이스 관리
- DTO ↔ Entity 변환
- 캐싱 처리
### 6.3 Domain 모듈
- **역할**: 비즈니스 로직 정의
- **책임**:
- 엔티티 정의
- 비즈니스 규칙 구현
- 유스케이스 정의
- 레포지토리 인터페이스 정의
### 6.4 Presentation 모듈
- **역할**: 사용자 인터페이스 및 상태 관리
- **책임**:
- UI 렌더링
- 사용자 입력 처리
- 상태 관리 (Riverpod)
- 네비게이션 처리
## 7. 확장 포인트
### 7.1 새로운 데이터 소스 추가
```
data/datasources/remote/
├── kakao_map_parser.dart # 카카오맵 지원
├── google_maps_parser.dart # 구글맵 지원
└── delivery_app_parser.dart # 배달앱 지원
```
### 7.2 새로운 기능 모듈 추가
```
presentation/pages/
├── reviews/ # 리뷰 기능
├── social/ # 소셜 기능
└── analytics/ # 통계 기능
```
### 7.3 새로운 저장소 추가
```
domain/repositories/
├── review_repository.dart # 리뷰 저장소
├── user_repository.dart # 사용자 저장소
└── analytics_repository.dart # 분석 저장소
```

View File

@@ -0,0 +1,110 @@
name: lunchpick
description: "오늘 뭐 먹Z? - 점심 메뉴 추천 앱"
publish_to: 'none'
version: 1.1.0+2
environment:
sdk: ^3.8.1
dependencies:
flutter:
sdk: flutter
# UI/UX
cupertino_icons: ^1.0.8
adaptive_theme: ^3.5.0
table_calendar: ^3.0.9
# 상태 관리
flutter_riverpod: ^2.4.0
riverpod_annotation: ^2.3.0
# 로컬 저장소
hive: ^2.2.3
hive_flutter: ^1.1.0
# 네비게이션
go_router: ^13.0.0
# 네트워킹
dio: ^5.4.0
http: ^1.1.0
connectivity_plus: ^5.0.0
# 데이터 처리
json_annotation: ^4.8.1
html: ^0.15.4
collection: ^1.18.0
# 권한 및 시스템
permission_handler: ^11.1.0
geolocator: ^10.1.0
flutter_local_notifications: ^17.2.3
workmanager: ^0.8.0
timezone: ^0.9.2
# 유틸리티
uuid: ^4.2.1
share_plus: ^7.2.1
url_launcher: ^6.2.0
flutter_blue_plus: ^1.31.0
intl: ^0.18.1
# 로깅 및 모니터링 (신규 추가)
logger: ^2.0.0
# 캐싱 (신규 추가)
lru_map: ^1.0.0
# 광고 (주석 처리됨 - 필요시 활성화)
# google_mobile_ads: ^4.0.0
dev_dependencies:
flutter_test:
sdk: flutter
# 린팅
flutter_lints: ^5.0.0
# 코드 생성
build_runner: ^2.4.6
json_serializable: ^6.7.1
hive_generator: ^2.0.1
riverpod_generator: ^2.3.0
# 테스트
mockito: ^5.4.0
mocktail: ^1.0.0
test: ^1.24.0
integration_test:
sdk: flutter
flutter:
uses-material-design: true
# 에셋 (필요시 추가)
# assets:
# - assets/images/
# - assets/icons/
# 폰트 (필요시 추가)
# fonts:
# - family: Pretendard
# fonts:
# - asset: fonts/Pretendard-Regular.ttf
# - asset: fonts/Pretendard-Bold.ttf
# weight: 700
# 스크립트 정의 (flutter pub run 사용)
scripts:
generate: flutter pub run build_runner build --delete-conflicting-outputs
watch: flutter pub run build_runner watch --delete-conflicting-outputs
clean: flutter clean && flutter pub get
test: flutter test
coverage: flutter test --coverage
analyze: flutter analyze
# 의존성 버전 고정 (필요시)
dependency_overrides:
# 예시: 특정 버전 고정이 필요한 경우
# collection: 1.17.0

View File

@@ -0,0 +1,319 @@
# 기술 스택 결정 문서
## 1. 개요
이 문서는 "오늘 뭐 먹Z?" 앱의 네이버 단축 URL 처리 기능 확장을 위한 기술 스택 선택 근거와 결정 사항을 설명합니다.
## 2. 현재 기술 스택 분석
### 2.1 기존 스택
- **상태 관리**: Riverpod 2.4.0
- **로컬 저장소**: Hive 2.2.3
- **네트워킹**: Dio 5.4.0, HTTP 1.1.0
- **HTML 파싱**: html 0.15.4
- **아키텍처**: Clean Architecture
### 2.2 강점 분석
- Riverpod의 강력한 의존성 주입과 상태 관리
- Hive의 빠른 성능과 간단한 사용법
- Clean Architecture로 인한 명확한 책임 분리
## 3. 네이버 URL 처리를 위한 기술 선택
### 3.1 HTTP 클라이언트
#### 선택: Dio + HTTP (하이브리드 접근)
**근거:**
- **Dio**: 인터셉터, 타임아웃, 재시도 등 고급 기능 필요한 API 호출용
- **HTTP**: 단순한 리다이렉션 처리와 기존 NaverMapParser 호환성
**구현 전략:**
```dart
// API 호출용 (Dio)
class NaverLocalApiClient {
final Dio _dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
))..interceptors.addAll([
LogInterceptor(),
RetryInterceptor(),
]);
}
// 단순 요청용 (HTTP)
class NaverUrlResolver {
final http.Client _client;
}
```
### 3.2 HTML 파싱
#### 선택: html 패키지 유지
**근거:**
- 이미 프로젝트에서 사용 중
- DOM 기반 파싱으로 안정적
- 네이버 지도 페이지 구조에 적합
**대안 검토:**
- ❌ BeautifulSoup (Python 전용)
- ❌ web_scraper (기능 중복, 추가 의존성)
- ✅ html (현재 선택)
### 3.3 상태 관리
#### 선택: Riverpod 유지
**근거:**
- 이미 프로젝트 전체에서 사용 중
- 컴파일 타임 안전성
- 의존성 주입 기능 내장
- 테스트 용이성
**Provider 구조:**
```dart
// 새로운 Provider 추가
final naverUrlProcessorProvider = Provider((ref) {
return NaverUrlProcessor(
mapParser: ref.watch(naverMapParserProvider),
apiClient: ref.watch(naverLocalApiClientProvider),
);
});
```
### 3.4 로컬 캐싱
#### 선택: Hive + 메모리 캐시 조합
**근거:**
- **Hive**: 영구 저장이 필요한 식당 데이터
- **메모리 캐시**: URL 리다이렉션, API 결과 등 임시 데이터
**구현:**
```dart
class CacheManager {
// 메모리 캐시 (LRU)
final _urlCache = LruMap<String, String>(maximumSize: 100);
final _apiCache = LruMap<String, dynamic>(maximumSize: 50);
// Hive 박스
late Box<Restaurant> _restaurantBox;
}
```
### 3.5 에러 처리 및 로깅
#### 선택: 계층별 예외 + 구조화된 로깅
**근거:**
- Clean Architecture의 계층 분리 원칙 준수
- 디버깅 용이성
- 프로덕션 모니터링 준비
**구현:**
```dart
// 계층별 예외
sealed class AppException implements Exception {
final String message;
final StackTrace? stackTrace;
}
class DataException extends AppException {}
class DomainException extends AppException {}
class PresentationException extends AppException {}
// 구조화된 로깅
class StructuredLogger {
void log(LogLevel level, String message, {
Map<String, dynamic>? data,
Exception? error,
StackTrace? stackTrace,
});
}
```
## 4. 아키텍처 패턴 결정
### 4.1 Repository Pattern 확장
**결정**: Repository Pattern + Facade Pattern
**근거:**
- 복잡한 네이버 URL 처리 로직을 단순한 인터페이스로 제공
- 기존 Repository 구조와 일관성 유지
```dart
// Facade 패턴 적용
class NaverUrlProcessor {
// 복잡한 내부 로직을 숨김
Future<Restaurant> processNaverUrl(String url) {
// 1. URL 검증
// 2. 리다이렉션
// 3. 스크래핑
// 4. API 호출
// 5. 매칭 및 병합
}
}
```
### 4.2 의존성 주입
**결정**: Riverpod Provider 기반 DI
**근거:**
- 기존 프로젝트 구조와 일치
- 런타임 오버헤드 최소화
- 테스트 시 모킹 용이
## 5. 외부 서비스 통합
### 5.1 네이버 로컬 API
**결정**: 직접 통합
**근거:**
- 공식 SDK 없음
- REST API로 간단한 구조
- 필요한 기능만 선택적 구현 가능
**API 엔드포인트:**
```dart
class NaverApiEndpoints {
static const String localSearch = '/v1/search/local.json';
static const String placeDetail = '/v1/search/place/detail';
}
```
### 5.2 CORS 프록시 (웹 환경)
**결정**: allorigins.win 사용
**근거:**
- 무료 서비스
- 안정적인 가동률
- JSON 응답 지원
**대안:**
- ❌ 자체 프록시 서버 (유지보수 부담)
- ❌ cors-anywhere (제한적)
- ✅ allorigins.win (선택)
## 6. 테스트 전략
### 6.1 테스트 프레임워크
**결정**: Flutter Test + Mockito
**근거:**
- Flutter 기본 제공
- Riverpod과 호환
- 풍부한 매칭 기능
### 6.2 테스트 범위
```yaml
단위 테스트:
- URL 파서: 90% 이상
- API 클라이언트: 85% 이상
- 매칭 알고리즘: 95% 이상
통합 테스트:
- URL 처리 파이프라인: 핵심 시나리오
- Repository 통합: 주요 플로우
E2E 테스트:
- 실제 네이버 URL로 테스트 (CI 제외)
```
## 7. 성능 고려사항
### 7.1 네트워크 최적화
**결정:**
- Connection pooling (Dio 기본 제공)
- Request 타임아웃: 10초
- 재시도: 최대 3회
### 7.2 메모리 최적화
**결정:**
- LRU 캐시 크기 제한
- 이미지 데이터 제외
- 주기적 캐시 정리
## 8. 보안 고려사항
### 8.1 API 키 관리
**결정**: 환경 변수 + 난독화
```dart
// 컴파일 타임 주입
const String apiKey = String.fromEnvironment('NAVER_API_KEY');
// 런타임 난독화
final obfuscatedKey = base64.encode(utf8.encode(apiKey));
```
### 8.2 네트워크 보안
**결정:**
- HTTPS 전용
- Certificate pinning (선택적)
- Request 서명 검증
## 9. 마이그레이션 계획
### 9.1 단계별 적용
1. **Phase 1**: 기본 구조 구현
- NaverLocalApiClient
- 기본 에러 처리
2. **Phase 2**: 고급 기능
- 캐싱 레이어
- 매칭 알고리즘
3. **Phase 3**: 최적화
- 성능 튜닝
- 모니터링 추가
### 9.2 롤백 계획
- Feature flag로 새 기능 제어
- 기존 NaverMapParser 유지
- 점진적 트래픽 전환
## 10. 결론
### 10.1 핵심 결정 사항
| 영역 | 선택 | 이유 |
|------|------|------|
| HTTP 클라이언트 | Dio + HTTP | 용도별 최적화 |
| 상태 관리 | Riverpod | 프로젝트 일관성 |
| 로컬 저장소 | Hive | 기존 인프라 활용 |
| 캐싱 | Hive + Memory | 성능과 영속성 균형 |
| 아키텍처 | Clean + Facade | 복잡도 관리 |
### 10.2 예상 효과
- **개발 속도**: 기존 스택 활용으로 빠른 구현
- **유지보수성**: 명확한 책임 분리로 관리 용이
- **확장성**: 다른 플랫폼 추가 시 쉬운 확장
- **성능**: 캐싱과 최적화로 빠른 응답
### 10.3 리스크 및 대응
| 리스크 | 영향도 | 대응 방안 |
|--------|--------|-----------|
| API 제한 | 중 | 캐싱 강화, Rate limiting |
| 네이버 구조 변경 | 높 | 파서 업데이트 자동화 |
| CORS 프록시 장애 | 중 | 대체 프록시 준비 |
### 10.4 향후 고려사항
- GraphQL 도입 검토 (복잡한 쿼리 증가 시)
- 자체 백엔드 구축 (사용자 증가 시)
- ML 기반 매칭 알고리즘 (정확도 개선)