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,194 @@
import 'package:hive_flutter/hive_flutter.dart';
import 'package:lunchpick/domain/entities/weather_info.dart';
import 'package:lunchpick/domain/repositories/weather_repository.dart';
class WeatherRepositoryImpl implements WeatherRepository {
static const String _boxName = 'weather_cache';
static const String _keyCachedWeather = 'cached_weather';
static const String _keyLastUpdateTime = 'last_update_time';
static const Duration _cacheValidDuration = Duration(hours: 1);
Future<Box> get _box async => await Hive.openBox(_boxName);
@override
Future<WeatherInfo> getCurrentWeather({
required double latitude,
required double longitude,
}) async {
// TODO: 실제 날씨 API 호출 구현
// 여기서는 임시로 더미 데이터 반환
final dummyWeather = WeatherInfo(
current: WeatherData(
temperature: 20,
isRainy: false,
description: '맑음',
),
nextHour: WeatherData(
temperature: 22,
isRainy: false,
description: '맑음',
),
);
// 캐시에 저장
await cacheWeatherInfo(dummyWeather);
return dummyWeather;
}
@override
Future<WeatherInfo?> getCachedWeather() async {
final box = await _box;
// 캐시가 유효한지 확인
final isValid = await _isCacheValid();
if (!isValid) {
return null;
}
// 캐시된 데이터 가져오기
final cachedData = box.get(_keyCachedWeather);
if (cachedData == null) {
return null;
}
try {
// 안전한 타입 변환
if (cachedData is! Map) {
print('WeatherCache: Invalid data type - expected Map but got ${cachedData.runtimeType}');
await clearWeatherCache();
return null;
}
final Map<String, dynamic> weatherMap = Map<String, dynamic>.from(cachedData);
// Map 구조 검증
if (!weatherMap.containsKey('current') || !weatherMap.containsKey('nextHour')) {
print('WeatherCache: Missing required fields in weather data');
await clearWeatherCache();
return null;
}
return _weatherInfoFromMap(weatherMap);
} catch (e) {
// 캐시 데이터가 손상된 경우
print('WeatherCache: Error parsing cached weather data: $e');
await clearWeatherCache();
return null;
}
}
@override
Future<void> cacheWeatherInfo(WeatherInfo weatherInfo) async {
final box = await _box;
// WeatherInfo를 Map으로 변환하여 저장
final weatherMap = _weatherInfoToMap(weatherInfo);
await box.put(_keyCachedWeather, weatherMap);
await box.put(_keyLastUpdateTime, DateTime.now().toIso8601String());
}
@override
Future<void> clearWeatherCache() async {
final box = await _box;
await box.delete(_keyCachedWeather);
await box.delete(_keyLastUpdateTime);
}
@override
Future<bool> isWeatherUpdateNeeded() async {
final box = await _box;
// 캐시된 날씨 정보가 없으면 업데이트 필요
if (!box.containsKey(_keyCachedWeather)) {
return true;
}
// 캐시가 유효한지 확인
return !(await _isCacheValid());
}
Future<bool> _isCacheValid() async {
final box = await _box;
final lastUpdateTimeStr = box.get(_keyLastUpdateTime);
if (lastUpdateTimeStr == null) {
return false;
}
try {
// 날짜 파싱 시도
final lastUpdateTime = DateTime.tryParse(lastUpdateTimeStr);
if (lastUpdateTime == null) {
print('WeatherCache: Invalid date format in cache: $lastUpdateTimeStr');
return false;
}
final now = DateTime.now();
final difference = now.difference(lastUpdateTime);
return difference < _cacheValidDuration;
} catch (e) {
print('WeatherCache: Error checking cache validity: $e');
return false;
}
}
Map<String, dynamic> _weatherInfoToMap(WeatherInfo weatherInfo) {
return {
'current': {
'temperature': weatherInfo.current.temperature,
'isRainy': weatherInfo.current.isRainy,
'description': weatherInfo.current.description,
},
'nextHour': {
'temperature': weatherInfo.nextHour.temperature,
'isRainy': weatherInfo.nextHour.isRainy,
'description': weatherInfo.nextHour.description,
},
};
}
WeatherInfo _weatherInfoFromMap(Map<String, dynamic> map) {
try {
// current 필드 검증
final currentMap = map['current'] as Map<String, dynamic>?;
if (currentMap == null) {
throw FormatException('Missing current weather data');
}
// nextHour 필드 검증
final nextHourMap = map['nextHour'] as Map<String, dynamic>?;
if (nextHourMap == null) {
throw FormatException('Missing nextHour weather data');
}
// 필수 필드 검증 및 기본값 제공
final currentTemp = currentMap['temperature'] as num? ?? 20;
final currentRainy = currentMap['isRainy'] as bool? ?? false;
final currentDesc = currentMap['description'] as String? ?? '알 수 없음';
final nextTemp = nextHourMap['temperature'] as num? ?? 20;
final nextRainy = nextHourMap['isRainy'] as bool? ?? false;
final nextDesc = nextHourMap['description'] as String? ?? '알 수 없음';
return WeatherInfo(
current: WeatherData(
temperature: currentTemp.round(),
isRainy: currentRainy,
description: currentDesc,
),
nextHour: WeatherData(
temperature: nextTemp.round(),
isRainy: nextRainy,
description: nextDesc,
),
);
} catch (e) {
print('WeatherCache: Error converting map to WeatherInfo: $e');
print('WeatherCache: Map data: $map');
rethrow;
}
}
}