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,126 @@
import 'package:uuid/uuid.dart';
import '../../../domain/entities/restaurant.dart';
import '../naver/naver_local_search_api.dart';
import '../../../core/utils/category_mapper.dart';
/// 네이버 데이터 변환기
///
/// 네이버 API 응답을 도메인 엔티티로 변환합니다.
class NaverDataConverter {
static const _uuid = Uuid();
/// NaverLocalSearchResult를 Restaurant 엔티티로 변환
static Restaurant fromLocalSearchResult(
NaverLocalSearchResult result, {
String? id,
}) {
// 좌표 변환 (네이버 지도 좌표계 -> WGS84)
final convertedCoords = _convertNaverMapCoordinates(
result.mapx,
result.mapy,
);
// 카테고리 파싱 및 정규화
final categoryParts = result.category.split('>').map((s) => s.trim()).toList();
final mainCategory = categoryParts.isNotEmpty ? categoryParts.first : '음식점';
final subCategory = categoryParts.length > 1 ? categoryParts.last : mainCategory;
// CategoryMapper를 사용한 정규화
final normalizedCategory = CategoryMapper.normalizeNaverCategory(mainCategory, subCategory);
return Restaurant(
id: id ?? _uuid.v4(),
name: result.title,
category: normalizedCategory,
subCategory: subCategory,
description: result.description.isNotEmpty ? result.description : null,
phoneNumber: result.telephone.isNotEmpty ? result.telephone : null,
roadAddress: result.roadAddress.isNotEmpty
? result.roadAddress
: result.address,
jibunAddress: result.address,
latitude: convertedCoords['latitude'] ?? 37.5665,
longitude: convertedCoords['longitude'] ?? 126.9780,
naverUrl: result.link.isNotEmpty ? result.link : null,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
source: DataSource.NAVER,
);
}
/// GraphQL 응답을 Restaurant 엔티티로 변환
static Restaurant fromGraphQLResponse(
Map<String, dynamic> placeData, {
String? id,
String? naverUrl,
}) {
// 영업시간 파싱
String? businessHours;
if (placeData['businessHours'] != null) {
final hours = placeData['businessHours'] as List;
businessHours = hours
.where((h) => h['businessHours'] != null)
.map((h) => h['businessHours'])
.join('\n');
}
// 좌표 추출
double? latitude;
double? longitude;
if (placeData['location'] != null) {
latitude = placeData['location']['latitude']?.toDouble();
longitude = placeData['location']['longitude']?.toDouble();
}
// 카테고리 파싱 및 정규화
final rawCategory = placeData['category'] ?? '음식점';
final categoryParts = rawCategory.split('>').map((s) => s.trim()).toList();
final mainCategory = categoryParts.isNotEmpty ? categoryParts.first : '음식점';
final subCategory = categoryParts.length > 1 ? categoryParts.last : mainCategory;
// CategoryMapper를 사용한 정규화
final normalizedCategory = CategoryMapper.normalizeNaverCategory(mainCategory, subCategory);
return Restaurant(
id: id ?? _uuid.v4(),
name: placeData['name'] ?? '이름 없음',
category: normalizedCategory,
subCategory: subCategory,
description: placeData['description'],
phoneNumber: placeData['phone'],
roadAddress: placeData['address']?['roadAddress'] ?? '',
jibunAddress: placeData['address']?['jibunAddress'] ?? '',
latitude: latitude ?? 37.5665,
longitude: longitude ?? 126.9780,
businessHours: businessHours,
naverUrl: naverUrl,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
source: DataSource.NAVER,
);
}
/// 네이버 지도 좌표를 WGS84로 변환
static Map<String, double?> _convertNaverMapCoordinates(
double? mapx,
double? mapy,
) {
if (mapx == null || mapy == null) {
return {'latitude': null, 'longitude': null};
}
// 네이버 지도 좌표계는 KATEC을 사용
// 간단한 변환 공식 (정확도는 떨어지지만 실용적)
// 실제로는 더 정교한 변환이 필요할 수 있음
final longitude = mapx / 10000000.0;
final latitude = mapy / 10000000.0;
return {
'latitude': latitude,
'longitude': longitude,
};
}
}