feat(app): add vworld geocoding and native ads placeholders

This commit is contained in:
JiWoong Sul
2025-12-03 14:30:20 +09:00
parent d101f7d0dc
commit 3ff9e5f837
23 changed files with 1108 additions and 540 deletions

View File

@@ -98,9 +98,34 @@ class RecommendationEngine {
.toSet();
// 최근 방문하지 않은 식당만 필터링
return restaurants.where((restaurant) {
final filtered = restaurants.where((restaurant) {
return !recentlyVisitedIds.contains(restaurant.id);
}).toList();
if (filtered.isNotEmpty) return filtered;
// 모든 식당이 제외되면 가장 오래전에 방문한 식당을 반환
final lastVisitByRestaurant = <String, DateTime>{};
for (final visit in recentVisits) {
final current = lastVisitByRestaurant[visit.restaurantId];
if (current == null || visit.visitDate.isAfter(current)) {
lastVisitByRestaurant[visit.restaurantId] = visit.visitDate;
}
}
Restaurant? oldestRestaurant;
DateTime? oldestVisitDate;
for (final restaurant in restaurants) {
final lastVisit = lastVisitByRestaurant[restaurant.id];
if (lastVisit == null) continue;
if (oldestVisitDate == null || lastVisit.isBefore(oldestVisitDate)) {
oldestVisitDate = lastVisit;
oldestRestaurant = restaurant;
}
}
return oldestRestaurant != null ? [oldestRestaurant] : restaurants;
}
/// 카테고리 필터링
@@ -123,142 +148,7 @@ class RecommendationEngine {
) {
if (restaurants.isEmpty) return null;
// 각 식당에 대한 가중치 계산
final weightedRestaurants = restaurants.map((restaurant) {
double weight = 1.0;
// 카테고리 가중치 적용
final categoryWeight =
config.userSettings.categoryWeights[restaurant.category];
if (categoryWeight != null) {
weight *= categoryWeight;
}
// 거리 가중치 적용 (가까울수록 높은 가중치)
final distance = DistanceCalculator.calculateDistance(
lat1: config.userLatitude,
lon1: config.userLongitude,
lat2: restaurant.latitude,
lon2: restaurant.longitude,
);
final distanceWeight = 1.0 - (distance / config.maxDistance);
weight *= (0.5 + distanceWeight * 0.5); // 50% ~ 100% 범위
// 시간대별 가중치 적용
weight *= _getTimeBasedWeight(restaurant, config.currentTime);
// 날씨 기반 가중치 적용
if (config.weather != null) {
weight *= _getWeatherBasedWeight(restaurant, config.weather!);
}
return _WeightedRestaurant(restaurant, weight);
}).toList();
// 가중치 기반 랜덤 선택
return _weightedRandomSelection(weightedRestaurants);
}
/// 시간대별 가중치 계산
double _getTimeBasedWeight(Restaurant restaurant, DateTime currentTime) {
final hour = currentTime.hour;
// 아침 시간대 (7-10시)
if (hour >= 7 && hour < 10) {
if (restaurant.category == 'cafe' || restaurant.category == 'korean') {
return 1.2;
}
if (restaurant.category == 'bar') {
return 0.3;
}
}
// 점심 시간대 (11-14시)
else if (hour >= 11 && hour < 14) {
if (restaurant.category == 'korean' ||
restaurant.category == 'chinese' ||
restaurant.category == 'japanese') {
return 1.3;
}
}
// 저녁 시간대 (17-21시)
else if (hour >= 17 && hour < 21) {
if (restaurant.category == 'bar' || restaurant.category == 'western') {
return 1.2;
}
}
// 늦은 저녁 (21시 이후)
else if (hour >= 21) {
if (restaurant.category == 'bar' || restaurant.category == 'fastfood') {
return 1.3;
}
if (restaurant.category == 'cafe') {
return 0.5;
}
}
return 1.0;
}
/// 날씨 기반 가중치 계산
double _getWeatherBasedWeight(Restaurant restaurant, WeatherInfo weather) {
if (weather.current.isRainy) {
// 비가 올 때는 가까운 식당 선호
// 이미 거리 가중치에서 처리했으므로 여기서는 실내 카테고리 선호
if (restaurant.category == 'cafe' || restaurant.category == 'fastfood') {
return 1.2;
}
}
// 더운 날씨 (25도 이상)
if (weather.current.temperature >= 25) {
if (restaurant.category == 'cafe' || restaurant.category == 'japanese') {
return 1.1;
}
}
// 추운 날씨 (10도 이하)
if (weather.current.temperature <= 10) {
if (restaurant.category == 'korean' || restaurant.category == 'chinese') {
return 1.2;
}
}
return 1.0;
}
/// 가중치 기반 랜덤 선택
Restaurant? _weightedRandomSelection(
List<_WeightedRestaurant> weightedRestaurants,
) {
if (weightedRestaurants.isEmpty) return null;
// 전체 가중치 합계 계산
final totalWeight = weightedRestaurants.fold<double>(
0,
(sum, item) => sum + item.weight,
);
// 랜덤 값 생성
final randomValue = _random.nextDouble() * totalWeight;
// 누적 가중치로 선택
double cumulativeWeight = 0;
for (final weightedRestaurant in weightedRestaurants) {
cumulativeWeight += weightedRestaurant.weight;
if (randomValue <= cumulativeWeight) {
return weightedRestaurant.restaurant;
}
}
// 예외 처리 (여기에 도달하면 안됨)
return weightedRestaurants.last.restaurant;
// 가중치 미적용: 거리/방문 필터를 통과한 식당 중 균등 무작위 선택
return restaurants[_random.nextInt(restaurants.length)];
}
}
/// 가중치가 적용된 식당 모델
class _WeightedRestaurant {
final Restaurant restaurant;
final double weight;
_WeightedRestaurant(this.restaurant, this.weight);
}