feat(app): add manual entry and sharing flows

This commit is contained in:
JiWoong Sul
2025-11-19 16:36:39 +09:00
parent 5ade584370
commit 947fe59486
110 changed files with 5937 additions and 3781 deletions

View File

@@ -4,26 +4,32 @@ import 'package:lunchpick/domain/repositories/recommendation_repository.dart';
class RecommendationRepositoryImpl implements RecommendationRepository {
static const String _boxName = 'recommendations';
Future<Box<RecommendationRecord>> get _box async =>
Future<Box<RecommendationRecord>> get _box async =>
await Hive.openBox<RecommendationRecord>(_boxName);
@override
Future<List<RecommendationRecord>> getAllRecommendationRecords() async {
final box = await _box;
final records = box.values.toList();
records.sort((a, b) => b.recommendationDate.compareTo(a.recommendationDate));
records.sort(
(a, b) => b.recommendationDate.compareTo(a.recommendationDate),
);
return records;
}
@override
Future<List<RecommendationRecord>> getRecommendationsByRestaurantId(String restaurantId) async {
Future<List<RecommendationRecord>> getRecommendationsByRestaurantId(
String restaurantId,
) async {
final records = await getAllRecommendationRecords();
return records.where((r) => r.restaurantId == restaurantId).toList();
}
@override
Future<List<RecommendationRecord>> getRecommendationsByDate(DateTime date) async {
Future<List<RecommendationRecord>> getRecommendationsByDate(
DateTime date,
) async {
final records = await getAllRecommendationRecords();
return records.where((record) {
return record.recommendationDate.year == date.year &&
@@ -39,8 +45,12 @@ class RecommendationRepositoryImpl implements RecommendationRepository {
}) async {
final records = await getAllRecommendationRecords();
return records.where((record) {
return record.recommendationDate.isAfter(startDate.subtract(const Duration(days: 1))) &&
record.recommendationDate.isBefore(endDate.add(const Duration(days: 1)));
return record.recommendationDate.isAfter(
startDate.subtract(const Duration(days: 1)),
) &&
record.recommendationDate.isBefore(
endDate.add(const Duration(days: 1)),
);
}).toList();
}
@@ -93,14 +103,19 @@ class RecommendationRepositoryImpl implements RecommendationRepository {
} catch (_) {
yield <RecommendationRecord>[];
}
yield* box.watch().asyncMap((_) async => await getAllRecommendationRecords());
yield* box.watch().asyncMap(
(_) async => await getAllRecommendationRecords(),
);
}
@override
Future<Map<String, int>> getMonthlyRecommendationStats(int year, int month) async {
Future<Map<String, int>> getMonthlyRecommendationStats(
int year,
int month,
) async {
final startDate = DateTime(year, month, 1);
final endDate = DateTime(year, month + 1, 0); // 해당 월의 마지막 날
final records = await getRecommendationsByDateRange(
startDate: startDate,
endDate: endDate,
@@ -111,7 +126,7 @@ class RecommendationRepositoryImpl implements RecommendationRepository {
final dayKey = record.recommendationDate.day.toString();
stats[dayKey] = (stats[dayKey] ?? 0) + 1;
}
return stats;
}
}
}

View File

@@ -9,12 +9,11 @@ import 'package:lunchpick/core/constants/api_keys.dart';
class RestaurantRepositoryImpl implements RestaurantRepository {
static const String _boxName = 'restaurants';
final NaverSearchService _naverSearchService;
RestaurantRepositoryImpl({
NaverSearchService? naverSearchService,
}) : _naverSearchService = naverSearchService ?? NaverSearchService();
Future<Box<Restaurant>> get _box async =>
RestaurantRepositoryImpl({NaverSearchService? naverSearchService})
: _naverSearchService = naverSearchService ?? NaverSearchService();
Future<Box<Restaurant>> get _box async =>
await Hive.openBox<Restaurant>(_boxName);
@override
@@ -69,7 +68,10 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
}
@override
Future<void> updateLastVisitDate(String restaurantId, DateTime visitDate) async {
Future<void> updateLastVisitDate(
String restaurantId,
DateTime visitDate,
) async {
final restaurant = await getRestaurantById(restaurantId);
if (restaurant != null) {
final updatedRestaurant = Restaurant(
@@ -120,7 +122,7 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
Future<List<Restaurant>> getRestaurantsNotVisitedInDays(int days) async {
final restaurants = await getAllRestaurants();
final cutoffDate = DateTime.now().subtract(Duration(days: days));
return restaurants.where((restaurant) {
if (restaurant.lastVisitDate == null) return true;
return restaurant.lastVisitDate!.isBefore(cutoffDate);
@@ -132,39 +134,68 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
if (query.isEmpty) {
return await getAllRestaurants();
}
final restaurants = await getAllRestaurants();
final lowercaseQuery = query.toLowerCase();
return restaurants.where((restaurant) {
return restaurant.name.toLowerCase().contains(lowercaseQuery) ||
(restaurant.description?.toLowerCase().contains(lowercaseQuery) ?? false) ||
(restaurant.description?.toLowerCase().contains(lowercaseQuery) ??
false) ||
restaurant.category.toLowerCase().contains(lowercaseQuery) ||
restaurant.roadAddress.toLowerCase().contains(lowercaseQuery);
}).toList();
}
@override
Future<List<Restaurant>> searchRestaurantsFromNaver({
required String query,
double? latitude,
double? longitude,
}) async {
return _naverSearchService.searchNearbyRestaurants(
query: query,
latitude: latitude,
longitude: longitude,
);
}
@override
Future<Restaurant> addRestaurantFromUrl(String url) async {
return _processRestaurantFromUrl(url, persist: true);
}
@override
Future<Restaurant> previewRestaurantFromUrl(String url) async {
return _processRestaurantFromUrl(url, persist: false);
}
Future<Restaurant> _processRestaurantFromUrl(
String url, {
required bool persist,
}) async {
try {
// URL 유효성 검증
if (!url.contains('naver.com') && !url.contains('naver.me')) {
throw Exception('유효하지 않은 네이버 지도 URL입니다.');
}
// NaverSearchService로 식당 정보 추출
Restaurant restaurant = await _naverSearchService.getRestaurantFromUrl(url);
Restaurant restaurant = await _naverSearchService.getRestaurantFromUrl(
url,
);
// API 키가 설정되어 있으면 추가 정보 검색
if (ApiKeys.areKeysConfigured() && restaurant.name != '네이버 지도 장소') {
try {
final detailedRestaurant = await _naverSearchService.searchRestaurantDetails(
name: restaurant.name,
address: restaurant.roadAddress,
latitude: restaurant.latitude,
longitude: restaurant.longitude,
);
final detailedRestaurant = await _naverSearchService
.searchRestaurantDetails(
name: restaurant.name,
address: restaurant.roadAddress,
latitude: restaurant.latitude,
longitude: restaurant.longitude,
);
if (detailedRestaurant != null) {
// 기존 정보와 API 검색 결과 병합
restaurant = Restaurant(
@@ -172,8 +203,10 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
name: restaurant.name,
category: detailedRestaurant.category,
subCategory: detailedRestaurant.subCategory,
description: detailedRestaurant.description ?? restaurant.description,
phoneNumber: detailedRestaurant.phoneNumber ?? restaurant.phoneNumber,
description:
detailedRestaurant.description ?? restaurant.description,
phoneNumber:
detailedRestaurant.phoneNumber ?? restaurant.phoneNumber,
roadAddress: detailedRestaurant.roadAddress,
jibunAddress: detailedRestaurant.jibunAddress,
latitude: detailedRestaurant.latitude,
@@ -184,7 +217,8 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
updatedAt: DateTime.now(),
naverPlaceId: restaurant.naverPlaceId,
naverUrl: restaurant.naverUrl,
businessHours: detailedRestaurant.businessHours ?? restaurant.businessHours,
businessHours:
detailedRestaurant.businessHours ?? restaurant.businessHours,
lastVisited: restaurant.lastVisited,
visitCount: restaurant.visitCount,
);
@@ -193,50 +227,31 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
print('API 검색 실패, 스크래핑된 정보만 사용: $e');
}
}
// 중복 체크 개선
final restaurants = await getAllRestaurants();
// 1. 주소 기반 중복 체크
if (restaurant.roadAddress.isNotEmpty || restaurant.jibunAddress.isNotEmpty) {
final addressDuplicate = restaurants.firstWhere(
(r) => r.name == restaurant.name &&
(r.roadAddress == restaurant.roadAddress ||
r.jibunAddress == restaurant.jibunAddress),
orElse: () => Restaurant(
id: '',
name: '',
category: '',
subCategory: '',
roadAddress: '',
jibunAddress: '',
latitude: 0,
longitude: 0,
source: DataSource.USER_INPUT,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
);
if (addressDuplicate.id.isNotEmpty) {
throw Exception('동일한 이름과 주소의 맛집이 이미 존재합니다: ${addressDuplicate.name}');
}
if (persist) {
await _ensureRestaurantIsUnique(restaurant);
await addRestaurant(restaurant);
}
// 2. 위치 기반 중복 체크 (50m 이내 같은 이름)
final locationDuplicate = restaurants.firstWhere(
(r) {
if (r.name != restaurant.name) return false;
final distanceInKm = DistanceCalculator.calculateDistance(
lat1: r.latitude,
lon1: r.longitude,
lat2: restaurant.latitude,
lon2: restaurant.longitude,
);
final distanceInMeters = distanceInKm * 1000;
return distanceInMeters < 50; // 50m 이내
},
return restaurant;
} catch (e) {
if (e is NaverMapParseException) {
throw Exception('네이버 지도 파싱 실패: ${e.message}');
}
rethrow;
}
}
Future<void> _ensureRestaurantIsUnique(Restaurant restaurant) async {
final restaurants = await getAllRestaurants();
if (restaurant.roadAddress.isNotEmpty ||
restaurant.jibunAddress.isNotEmpty) {
final addressDuplicate = restaurants.firstWhere(
(r) =>
r.name == restaurant.name &&
(r.roadAddress == restaurant.roadAddress ||
r.jibunAddress == restaurant.jibunAddress),
orElse: () => Restaurant(
id: '',
name: '',
@@ -251,20 +266,44 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
updatedAt: DateTime.now(),
),
);
if (locationDuplicate.id.isNotEmpty) {
throw Exception('50m 이내에 동일한 이름의 맛집이 이미 존재합니다: ${locationDuplicate.name}');
if (addressDuplicate.id.isNotEmpty) {
throw Exception('동일한 이름과 주소의 맛집이 이미 존재합니다: ${addressDuplicate.name}');
}
// 새 맛집 추가
await addRestaurant(restaurant);
return restaurant;
} catch (e) {
if (e is NaverMapParseException) {
throw Exception('네이버 지도 파싱 실패: ${e.message}');
}
rethrow;
}
final locationDuplicate = restaurants.firstWhere(
(r) {
if (r.name != restaurant.name) return false;
final distanceInKm = DistanceCalculator.calculateDistance(
lat1: r.latitude,
lon1: r.longitude,
lat2: restaurant.latitude,
lon2: restaurant.longitude,
);
final distanceInMeters = distanceInKm * 1000;
return distanceInMeters < 50;
},
orElse: () => Restaurant(
id: '',
name: '',
category: '',
subCategory: '',
roadAddress: '',
jibunAddress: '',
latitude: 0,
longitude: 0,
source: DataSource.USER_INPUT,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
);
if (locationDuplicate.id.isNotEmpty) {
throw Exception(
'50m 이내에 동일한 이름의 맛집이 이미 존재합니다: ${locationDuplicate.name}',
);
}
}
@@ -272,12 +311,9 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
Future<Restaurant?> getRestaurantByNaverPlaceId(String naverPlaceId) async {
final restaurants = await getAllRestaurants();
try {
return restaurants.firstWhere(
(r) => r.naverPlaceId == naverPlaceId,
);
return restaurants.firstWhere((r) => r.naverPlaceId == naverPlaceId);
} catch (e) {
return null;
}
}
}
}

View File

@@ -4,17 +4,18 @@ import 'package:lunchpick/domain/entities/user_settings.dart';
class SettingsRepositoryImpl implements SettingsRepository {
static const String _boxName = 'settings';
// Setting keys
static const String _keyDaysToExclude = 'days_to_exclude';
static const String _keyMaxDistanceRainy = 'max_distance_rainy';
static const String _keyMaxDistanceNormal = 'max_distance_normal';
static const String _keyNotificationDelayMinutes = 'notification_delay_minutes';
static const String _keyNotificationDelayMinutes =
'notification_delay_minutes';
static const String _keyNotificationEnabled = 'notification_enabled';
static const String _keyDarkModeEnabled = 'dark_mode_enabled';
static const String _keyFirstRun = 'first_run';
static const String _keyCategoryWeights = 'category_weights';
// Default values
static const int _defaultDaysToExclude = 7;
static const int _defaultMaxDistanceRainy = 500;
@@ -29,24 +30,34 @@ class SettingsRepositoryImpl implements SettingsRepository {
@override
Future<UserSettings> getUserSettings() async {
final box = await _box;
// 저장된 설정값들을 읽어옴
final revisitPreventionDays = box.get(_keyDaysToExclude, defaultValue: _defaultDaysToExclude);
final notificationEnabled = box.get(_keyNotificationEnabled, defaultValue: _defaultNotificationEnabled);
final notificationDelayMinutes = box.get(_keyNotificationDelayMinutes, defaultValue: _defaultNotificationDelayMinutes);
final revisitPreventionDays = box.get(
_keyDaysToExclude,
defaultValue: _defaultDaysToExclude,
);
final notificationEnabled = box.get(
_keyNotificationEnabled,
defaultValue: _defaultNotificationEnabled,
);
final notificationDelayMinutes = box.get(
_keyNotificationDelayMinutes,
defaultValue: _defaultNotificationDelayMinutes,
);
// 카테고리 가중치 읽기 (Map<String, double>으로 저장됨)
final categoryWeightsData = box.get(_keyCategoryWeights);
Map<String, double> categoryWeights = {};
if (categoryWeightsData != null) {
categoryWeights = Map<String, double>.from(categoryWeightsData);
}
// 알림 시간은 분을 시간:분 형식으로 변환
final hours = notificationDelayMinutes ~/ 60;
final minutes = notificationDelayMinutes % 60;
final notificationTime = '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}';
final notificationTime =
'${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}';
return UserSettings(
revisitPreventionDays: revisitPreventionDays,
notificationEnabled: notificationEnabled,
@@ -59,12 +70,15 @@ class SettingsRepositoryImpl implements SettingsRepository {
@override
Future<void> updateUserSettings(UserSettings settings) async {
final box = await _box;
// 각 설정값 저장
await box.put(_keyDaysToExclude, settings.revisitPreventionDays);
await box.put(_keyNotificationEnabled, settings.notificationEnabled);
await box.put(_keyNotificationDelayMinutes, settings.notificationDelayMinutes);
await box.put(
_keyNotificationDelayMinutes,
settings.notificationDelayMinutes,
);
// 카테고리 가중치 저장
await box.put(_keyCategoryWeights, settings.categoryWeights);
}
@@ -84,7 +98,10 @@ class SettingsRepositoryImpl implements SettingsRepository {
@override
Future<int> getMaxDistanceRainy() async {
final box = await _box;
return box.get(_keyMaxDistanceRainy, defaultValue: _defaultMaxDistanceRainy);
return box.get(
_keyMaxDistanceRainy,
defaultValue: _defaultMaxDistanceRainy,
);
}
@override
@@ -96,7 +113,10 @@ class SettingsRepositoryImpl implements SettingsRepository {
@override
Future<int> getMaxDistanceNormal() async {
final box = await _box;
return box.get(_keyMaxDistanceNormal, defaultValue: _defaultMaxDistanceNormal);
return box.get(
_keyMaxDistanceNormal,
defaultValue: _defaultMaxDistanceNormal,
);
}
@override
@@ -108,7 +128,10 @@ class SettingsRepositoryImpl implements SettingsRepository {
@override
Future<int> getNotificationDelayMinutes() async {
final box = await _box;
return box.get(_keyNotificationDelayMinutes, defaultValue: _defaultNotificationDelayMinutes);
return box.get(
_keyNotificationDelayMinutes,
defaultValue: _defaultNotificationDelayMinutes,
);
}
@override
@@ -120,7 +143,10 @@ class SettingsRepositoryImpl implements SettingsRepository {
@override
Future<bool> isNotificationEnabled() async {
final box = await _box;
return box.get(_keyNotificationEnabled, defaultValue: _defaultNotificationEnabled);
return box.get(
_keyNotificationEnabled,
defaultValue: _defaultNotificationEnabled,
);
}
@override
@@ -157,12 +183,15 @@ class SettingsRepositoryImpl implements SettingsRepository {
Future<void> resetSettings() async {
final box = await _box;
await box.clear();
// 기본값으로 재설정
await box.put(_keyDaysToExclude, _defaultDaysToExclude);
await box.put(_keyMaxDistanceRainy, _defaultMaxDistanceRainy);
await box.put(_keyMaxDistanceNormal, _defaultMaxDistanceNormal);
await box.put(_keyNotificationDelayMinutes, _defaultNotificationDelayMinutes);
await box.put(
_keyNotificationDelayMinutes,
_defaultNotificationDelayMinutes,
);
await box.put(_keyNotificationEnabled, _defaultNotificationEnabled);
await box.put(_keyDarkModeEnabled, _defaultDarkModeEnabled);
await box.put(_keyFirstRun, false); // 리셋 후에는 첫 실행이 아님
@@ -171,10 +200,10 @@ class SettingsRepositoryImpl implements SettingsRepository {
@override
Stream<Map<String, dynamic>> watchSettings() async* {
final box = await _box;
// 초기 값 전송
yield await _getCurrentSettings();
// 변경사항 감시
yield* box.watch().asyncMap((_) async => await _getCurrentSettings());
}
@@ -194,11 +223,11 @@ class SettingsRepositoryImpl implements SettingsRepository {
@override
Stream<UserSettings> watchUserSettings() async* {
final box = await _box;
// 초기 값 전송
yield await getUserSettings();
// 변경사항 감시
yield* box.watch().asyncMap((_) async => await getUserSettings());
}
}
}

View File

@@ -4,8 +4,8 @@ import 'package:lunchpick/domain/repositories/visit_repository.dart';
class VisitRepositoryImpl implements VisitRepository {
static const String _boxName = 'visit_records';
Future<Box<VisitRecord>> get _box async =>
Future<Box<VisitRecord>> get _box async =>
await Hive.openBox<VisitRecord>(_boxName);
@override
@@ -17,7 +17,9 @@ class VisitRepositoryImpl implements VisitRepository {
}
@override
Future<List<VisitRecord>> getVisitRecordsByRestaurantId(String restaurantId) async {
Future<List<VisitRecord>> getVisitRecordsByRestaurantId(
String restaurantId,
) async {
final records = await getAllVisitRecords();
return records.where((r) => r.restaurantId == restaurantId).toList();
}
@@ -39,7 +41,9 @@ class VisitRepositoryImpl implements VisitRepository {
}) async {
final records = await getAllVisitRecords();
return records.where((record) {
return record.visitDate.isAfter(startDate.subtract(const Duration(days: 1))) &&
return record.visitDate.isAfter(
startDate.subtract(const Duration(days: 1)),
) &&
record.visitDate.isBefore(endDate.add(const Duration(days: 1)));
}).toList();
}
@@ -93,7 +97,7 @@ class VisitRepositoryImpl implements VisitRepository {
Future<DateTime?> getLastVisitDate(String restaurantId) async {
final records = await getVisitRecordsByRestaurantId(restaurantId);
if (records.isEmpty) return null;
// 이미 visitDate 기준으로 정렬되어 있으므로 첫 번째가 가장 최근
return records.first.visitDate;
}
@@ -102,7 +106,7 @@ class VisitRepositoryImpl implements VisitRepository {
Future<Map<String, int>> getMonthlyVisitStats(int year, int month) async {
final startDate = DateTime(year, month, 1);
final endDate = DateTime(year, month + 1, 0); // 해당 월의 마지막 날
final records = await getVisitRecordsByDateRange(
startDate: startDate,
endDate: endDate,
@@ -113,7 +117,7 @@ class VisitRepositoryImpl implements VisitRepository {
final dayKey = record.visitDate.day.toString();
stats[dayKey] = (stats[dayKey] ?? 0) + 1;
}
return stats;
}
@@ -124,4 +128,4 @@ class VisitRepositoryImpl implements VisitRepository {
// 여기서는 빈 Map 반환
return {};
}
}
}

View File

@@ -17,30 +17,22 @@ class WeatherRepositoryImpl implements WeatherRepository {
}) async {
// TODO: 실제 날씨 API 호출 구현
// 여기서는 임시로 더미 데이터 반환
final dummyWeather = WeatherInfo(
current: WeatherData(
temperature: 20,
isRainy: false,
description: '맑음',
),
nextHour: WeatherData(
temperature: 22,
isRainy: false,
description: '맑음',
),
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) {
@@ -56,20 +48,25 @@ class WeatherRepositoryImpl implements WeatherRepository {
try {
// 안전한 타입 변환
if (cachedData is! Map) {
print('WeatherCache: Invalid data type - expected Map but got ${cachedData.runtimeType}');
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);
final Map<String, dynamic> weatherMap = Map<String, dynamic>.from(
cachedData,
);
// Map 구조 검증
if (!weatherMap.containsKey('current') || !weatherMap.containsKey('nextHour')) {
if (!weatherMap.containsKey('current') ||
!weatherMap.containsKey('nextHour')) {
print('WeatherCache: Missing required fields in weather data');
await clearWeatherCache();
return null;
}
return _weatherInfoFromMap(weatherMap);
} catch (e) {
// 캐시 데이터가 손상된 경우
@@ -82,7 +79,7 @@ class WeatherRepositoryImpl implements WeatherRepository {
@override
Future<void> cacheWeatherInfo(WeatherInfo weatherInfo) async {
final box = await _box;
// WeatherInfo를 Map으로 변환하여 저장
final weatherMap = _weatherInfoToMap(weatherInfo);
await box.put(_keyCachedWeather, weatherMap);
@@ -99,7 +96,7 @@ class WeatherRepositoryImpl implements WeatherRepository {
@override
Future<bool> isWeatherUpdateNeeded() async {
final box = await _box;
// 캐시된 날씨 정보가 없으면 업데이트 필요
if (!box.containsKey(_keyCachedWeather)) {
return true;
@@ -111,7 +108,7 @@ class WeatherRepositoryImpl implements WeatherRepository {
Future<bool> _isCacheValid() async {
final box = await _box;
final lastUpdateTimeStr = box.get(_keyLastUpdateTime);
if (lastUpdateTimeStr == null) {
return false;
@@ -124,10 +121,10 @@ class WeatherRepositoryImpl implements WeatherRepository {
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');
@@ -157,22 +154,22 @@ class WeatherRepositoryImpl implements WeatherRepository {
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(),
@@ -191,4 +188,4 @@ class WeatherRepositoryImpl implements WeatherRepository {
rethrow;
}
}
}
}