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 json) { StoreSeedSourceSignature? signature; if (json['sourceSignature'] != null) { signature = StoreSeedSourceSignature.fromJson( json['sourceSignature'] as Map, ); } 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 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 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 seedIfNeeded() async { final restaurantBox = Hive.box(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 _loadMeta() async { try { final metaJson = await rootBundle.loadString( AppConstants.storeSeedMetaAsset, ); final decoded = jsonDecode(metaJson) as Map; return StoreSeedMeta.fromJson(decoded); } catch (e, stack) { AppLogger.error( 'store_seed.meta.json 로딩 실패', error: e, stackTrace: stack, ); return null; } } Future> _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(StoreSeedItem.fromJson) .toList(); } catch (e, stack) { AppLogger.error('store_seed.json 로딩 실패', error: e, stackTrace: stack); return []; } } Future _applySeeds({ required Box restaurantBox, required List 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; } }