Files
lunchpick/lib/data/sample/store_dataset_seeder.dart

244 lines
6.9 KiB
Dart

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;
}
}