244 lines
6.9 KiB
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;
|
|
}
|
|
}
|