feat(app): seed restaurants, geocode addresses, refresh sharing
This commit is contained in:
@@ -95,6 +95,7 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
|
||||
businessHours: restaurant.businessHours,
|
||||
lastVisited: visitDate,
|
||||
visitCount: restaurant.visitCount + 1,
|
||||
needsAddressVerification: restaurant.needsAddressVerification,
|
||||
);
|
||||
await updateRestaurant(updatedRestaurant);
|
||||
}
|
||||
|
||||
@@ -177,13 +177,19 @@ class WeatherRepositoryImpl implements WeatherRepository {
|
||||
WeatherInfo _weatherInfoFromMap(Map<String, dynamic> map) {
|
||||
try {
|
||||
// current 필드 검증
|
||||
final currentMap = map['current'] as Map<String, dynamic>?;
|
||||
final currentRaw = map['current'];
|
||||
final currentMap = currentRaw is Map
|
||||
? Map<String, dynamic>.from(currentRaw)
|
||||
: null;
|
||||
if (currentMap == null) {
|
||||
throw FormatException('Missing current weather data');
|
||||
}
|
||||
|
||||
// nextHour 필드 검증
|
||||
final nextHourMap = map['nextHour'] as Map<String, dynamic>?;
|
||||
final nextHourRaw = map['nextHour'];
|
||||
final nextHourMap = nextHourRaw is Map
|
||||
? Map<String, dynamic>.from(nextHourRaw)
|
||||
: null;
|
||||
if (nextHourMap == null) {
|
||||
throw FormatException('Missing nextHour weather data');
|
||||
}
|
||||
|
||||
@@ -3,10 +3,16 @@ import 'package:lunchpick/core/constants/app_constants.dart';
|
||||
import 'package:lunchpick/domain/entities/restaurant.dart';
|
||||
import 'package:lunchpick/domain/entities/visit_record.dart';
|
||||
|
||||
import 'store_dataset_seeder.dart';
|
||||
import 'manual_restaurant_samples.dart';
|
||||
|
||||
/// 초기 구동 시 샘플 데이터를 채워 넣는 도우미
|
||||
class SampleDataInitializer {
|
||||
static Future<void> seedInitialData() async {
|
||||
await StoreDatasetSeeder().seedIfNeeded();
|
||||
await seedManualRestaurantsIfNeeded();
|
||||
}
|
||||
|
||||
static Future<void> seedManualRestaurantsIfNeeded() async {
|
||||
final restaurantBox = Hive.box<Restaurant>(AppConstants.restaurantBox);
|
||||
final visitBox = Hive.box<VisitRecord>(AppConstants.visitRecordBox);
|
||||
|
||||
243
lib/data/sample/store_dataset_seeder.dart
Normal file
243
lib/data/sample/store_dataset_seeder.dart
Normal file
@@ -0,0 +1,243 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:lunchpick/core/constants/app_constants.dart';
|
||||
import 'package:lunchpick/core/utils/app_logger.dart';
|
||||
import 'package:lunchpick/domain/entities/restaurant.dart';
|
||||
|
||||
class StoreSeedMeta {
|
||||
final String version;
|
||||
final DateTime generatedAt;
|
||||
final int itemCount;
|
||||
final StoreSeedSourceSignature? sourceSignature;
|
||||
|
||||
StoreSeedMeta({
|
||||
required this.version,
|
||||
required this.generatedAt,
|
||||
required this.itemCount,
|
||||
this.sourceSignature,
|
||||
});
|
||||
|
||||
factory StoreSeedMeta.fromJson(Map<String, dynamic> json) {
|
||||
StoreSeedSourceSignature? signature;
|
||||
if (json['sourceSignature'] != null) {
|
||||
signature = StoreSeedSourceSignature.fromJson(
|
||||
json['sourceSignature'] as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
|
||||
return StoreSeedMeta(
|
||||
version: json['version'] as String,
|
||||
generatedAt: DateTime.parse(json['generatedAt'] as String),
|
||||
itemCount: json['itemCount'] as int,
|
||||
sourceSignature: signature,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StoreSeedSourceSignature {
|
||||
final String hash;
|
||||
final int? size;
|
||||
|
||||
StoreSeedSourceSignature({required this.hash, this.size});
|
||||
|
||||
factory StoreSeedSourceSignature.fromJson(Map<String, dynamic> json) {
|
||||
return StoreSeedSourceSignature(
|
||||
hash: json['hash'] as String,
|
||||
size: (json['size'] as num?)?.toInt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StoreSeedItem {
|
||||
final int storeId;
|
||||
final String name;
|
||||
final String title;
|
||||
final String address;
|
||||
final String roadAddress;
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
|
||||
StoreSeedItem({
|
||||
required this.storeId,
|
||||
required this.name,
|
||||
required this.title,
|
||||
required this.address,
|
||||
required this.roadAddress,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
});
|
||||
|
||||
factory StoreSeedItem.fromJson(Map<String, dynamic> json) {
|
||||
return StoreSeedItem(
|
||||
storeId: json['storeId'] as int,
|
||||
name: (json['name'] as String).trim(),
|
||||
title: (json['title'] as String).trim(),
|
||||
address: (json['address'] as String).trim(),
|
||||
roadAddress: (json['roadAddress'] as String).trim(),
|
||||
latitude: (json['latitude'] as num).toDouble(),
|
||||
longitude: (json['longitude'] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StoreDatasetSeeder {
|
||||
Future<void> seedIfNeeded() async {
|
||||
final restaurantBox = Hive.box<Restaurant>(AppConstants.restaurantBox);
|
||||
final settingsBox = Hive.box(AppConstants.settingsBox);
|
||||
|
||||
final meta = await _loadMeta();
|
||||
if (meta == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final currentVersion =
|
||||
settingsBox.get(AppConstants.storeSeedVersionKey) as String?;
|
||||
final shouldSeed = restaurantBox.isEmpty || currentVersion != meta.version;
|
||||
if (!shouldSeed) {
|
||||
return;
|
||||
}
|
||||
|
||||
final seeds = await _loadSeedItems();
|
||||
if (seeds.isEmpty) {
|
||||
AppLogger.info('store_seed.json 데이터가 비어 있어 시드를 건너뜁니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
await _applySeeds(
|
||||
restaurantBox: restaurantBox,
|
||||
seeds: seeds,
|
||||
generatedAt: meta.generatedAt,
|
||||
);
|
||||
await settingsBox.put(AppConstants.storeSeedVersionKey, meta.version);
|
||||
|
||||
AppLogger.info(
|
||||
'스토어 시드 적용 완료: version=${meta.version}, count=${meta.itemCount}',
|
||||
);
|
||||
}
|
||||
|
||||
Future<StoreSeedMeta?> _loadMeta() async {
|
||||
try {
|
||||
final metaJson = await rootBundle.loadString(
|
||||
AppConstants.storeSeedMetaAsset,
|
||||
);
|
||||
final decoded = jsonDecode(metaJson) as Map<String, dynamic>;
|
||||
return StoreSeedMeta.fromJson(decoded);
|
||||
} catch (e, stack) {
|
||||
AppLogger.error(
|
||||
'store_seed.meta.json 로딩 실패',
|
||||
error: e,
|
||||
stackTrace: stack,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<StoreSeedItem>> _loadSeedItems() async {
|
||||
try {
|
||||
final dataJson = await rootBundle.loadString(
|
||||
AppConstants.storeSeedDataAsset,
|
||||
);
|
||||
final decoded = jsonDecode(dataJson);
|
||||
if (decoded is! List) {
|
||||
throw const FormatException('store_seed.json 포맷이 배열이 아닙니다.');
|
||||
}
|
||||
return decoded
|
||||
.cast<Map<String, dynamic>>()
|
||||
.map(StoreSeedItem.fromJson)
|
||||
.toList();
|
||||
} catch (e, stack) {
|
||||
AppLogger.error('store_seed.json 로딩 실패', error: e, stackTrace: stack);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _applySeeds({
|
||||
required Box<Restaurant> restaurantBox,
|
||||
required List<StoreSeedItem> seeds,
|
||||
required DateTime generatedAt,
|
||||
}) async {
|
||||
final seedMap = {for (final seed in seeds) _buildId(seed.storeId): seed};
|
||||
int added = 0;
|
||||
int updated = 0;
|
||||
|
||||
for (final entry in seedMap.entries) {
|
||||
final id = entry.key;
|
||||
final seed = entry.value;
|
||||
final existing = restaurantBox.get(id);
|
||||
|
||||
if (existing == null) {
|
||||
final restaurant = _buildRestaurant(seed, generatedAt);
|
||||
await restaurantBox.put(id, restaurant);
|
||||
added++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existing.source == DataSource.PRESET) {
|
||||
final description = _buildDescription(seed, existing.description);
|
||||
final restaurant = existing.copyWith(
|
||||
name: seed.name,
|
||||
category: existing.category.isNotEmpty ? existing.category : '기타',
|
||||
subCategory: existing.subCategory.isNotEmpty
|
||||
? existing.subCategory
|
||||
: '기타',
|
||||
description: description,
|
||||
roadAddress: seed.roadAddress,
|
||||
jibunAddress: seed.address.isNotEmpty
|
||||
? seed.address
|
||||
: seed.roadAddress,
|
||||
latitude: seed.latitude,
|
||||
longitude: seed.longitude,
|
||||
updatedAt: generatedAt,
|
||||
);
|
||||
await restaurantBox.put(id, restaurant);
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
|
||||
final unchanged = restaurantBox.length - added - updated;
|
||||
AppLogger.debug(
|
||||
'스토어 시드 결과 - 추가: $added, 업데이트: $updated, 기존 유지: '
|
||||
'$unchanged',
|
||||
);
|
||||
}
|
||||
|
||||
Restaurant _buildRestaurant(StoreSeedItem seed, DateTime generatedAt) {
|
||||
return Restaurant(
|
||||
id: _buildId(seed.storeId),
|
||||
name: seed.name,
|
||||
category: '기타',
|
||||
subCategory: '기타',
|
||||
description: _buildDescription(seed, null),
|
||||
phoneNumber: null,
|
||||
roadAddress: seed.roadAddress,
|
||||
jibunAddress: seed.address.isNotEmpty ? seed.address : seed.roadAddress,
|
||||
latitude: seed.latitude,
|
||||
longitude: seed.longitude,
|
||||
lastVisitDate: null,
|
||||
source: DataSource.PRESET,
|
||||
createdAt: generatedAt,
|
||||
updatedAt: generatedAt,
|
||||
naverPlaceId: null,
|
||||
naverUrl: null,
|
||||
businessHours: null,
|
||||
lastVisited: null,
|
||||
visitCount: 0,
|
||||
);
|
||||
}
|
||||
|
||||
String _buildId(int storeId) => 'store-$storeId';
|
||||
|
||||
String? _buildDescription(StoreSeedItem seed, String? existingDescription) {
|
||||
if (existingDescription != null && existingDescription.isNotEmpty) {
|
||||
return existingDescription;
|
||||
}
|
||||
|
||||
if (seed.title.isNotEmpty && seed.title != seed.name) {
|
||||
return seed.title;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user