import 'dart:convert'; import 'dart:io'; import 'package:build/build.dart'; import 'package:path/path.dart' as p; class StoreSeedBuilder implements Builder { StoreSeedBuilder(); @override final Map> buildExtensions = const { 'doc/restaurant_data/store.db': [ 'assets/data/store_seed.json', 'assets/data/store_seed.meta.json', ], }; @override Future build(BuildStep buildStep) async { final inputId = buildStep.inputId; final bytes = await buildStep.readAsBytes(inputId); if (bytes.isEmpty) { log.warning('store.db가 비어 있습니다. 시드를 건너뜁니다.'); return; } final tempDir = await Directory.systemTemp.createTemp('store_seed_'); final tempDbPath = p.join(tempDir.path, 'store.db'); await File(tempDbPath).writeAsBytes(bytes, flush: true); final sqlitePath = await _findSqliteBinary(); if (sqlitePath == null) { log.severe('sqlite3 바이너리를 찾을 수 없습니다. 설치 후 다시 시도하세요.'); return; } final rows = await _fetchRows(sqlitePath, tempDbPath); if (rows.isEmpty) { log.warning('restaurants 테이블에서 가져온 행이 없습니다.'); await tempDir.delete(recursive: true); return; } final newSeeds = rows.map(_seedFromMap).toList(); final merged = await _mergeWithExisting(buildStep, newSeeds); final signature = _buildSignature(bytes); final generatedAt = DateTime.now().toUtc().toIso8601String(); final meta = { 'version': signature, 'generatedAt': generatedAt, 'sourceDb': inputId.path, 'itemCount': merged.length, 'sourceSignature': {'hash': signature, 'size': bytes.length}, }; final encoder = const JsonEncoder.withIndent(' '); await buildStep.writeAsString( AssetId(inputId.package, 'assets/data/store_seed.json'), '${encoder.convert(merged)}\n', ); await buildStep.writeAsString( AssetId(inputId.package, 'assets/data/store_seed.meta.json'), '${encoder.convert(meta)}\n', ); await tempDir.delete(recursive: true); log.info( 'store_seed 생성 완료: ${merged.length}개 (sig: $signature, src: ${inputId.path})', ); } Future>> _fetchRows( String sqlitePath, String dbPath, ) async { const query = 'SELECT id, province, district, name, title, address, road_address, ' 'latitude, longitude FROM restaurants'; final result = await Process.run( sqlitePath, ['-json', dbPath, query], stdoutEncoding: utf8, stderrEncoding: utf8, ); if (result.exitCode != 0) { throw StateError('sqlite3 실행 실패: ${result.stderr}'); } final output = result.stdout as String; final decoded = jsonDecode(output); if (decoded is! List) { throw const FormatException('예상치 못한 JSON 포맷입니다.'); } return decoded.cast>(); } Map _seedFromMap(Map map) { return { 'storeId': map['id'] as int, 'province': (map['province'] as String).trim(), 'district': (map['district'] as String).trim(), 'name': (map['name'] as String).trim(), 'title': (map['title'] as String).trim(), 'address': (map['address'] as String).trim(), 'roadAddress': (map['road_address'] as String).trim(), 'latitude': (map['latitude'] as num).toDouble(), 'longitude': (map['longitude'] as num).toDouble(), }; } Future>> _mergeWithExisting( BuildStep buildStep, List> newSeeds, ) async { final existingId = AssetId( buildStep.inputId.package, 'assets/data/store_seed.json', ); List> existing = []; if (await buildStep.canRead(existingId)) { final raw = await buildStep.readAsString(existingId); try { final decoded = jsonDecode(raw); if (decoded is List) { existing = decoded.cast>(); } } catch (_) { log.warning('기존 store_seed.json 파싱 실패, 신규 데이터로 대체합니다.'); } } final byId = >{}; for (final seed in existing) { final id = _seedId(seed); byId[id] = seed; } for (final seed in newSeeds) { final id = _seedId(seed); if (!byId.containsKey(id)) { byId[id] = seed; continue; } final isDuplicateByNameAndAddress = byId.values.any((existingSeed) { return existingSeed['name'] == seed['name'] && existingSeed['roadAddress'] == seed['roadAddress']; }); if (!isDuplicateByNameAndAddress) { byId[id] = seed; // 같은 ID는 최신 값으로 교체 } } final merged = byId.values.toList() ..sort((a, b) => (_seedId(a)).compareTo(_seedId(b))); return merged; } String _seedId(Map seed) => 'store-${seed['storeId']}'; Future _findSqliteBinary() async { try { final result = await Process.run('which', ['sqlite3']); if (result.exitCode == 0) { final path = (result.stdout as String).trim(); if (path.isNotEmpty) { return path; } } } catch (_) { return null; } return null; } String _buildSignature(List bytes) { int hash = 0; for (final byte in bytes) { hash = (hash * 31 + byte) & 0x7fffffff; } return hash.toRadixString(16).padLeft(8, '0'); } } Builder storeSeedBuilder(BuilderOptions options) => StoreSeedBuilder(); // ignore_for_file: depend_on_referenced_packages