import 'dart:convert'; import 'dart:io'; class StoreSeedConfig { final String dbPath; final String dataOutputPath; final String metaOutputPath; final String version; StoreSeedConfig({ required this.dbPath, required this.dataOutputPath, required this.metaOutputPath, required this.version, }); factory StoreSeedConfig.fromArgs(List args) { String dbPath = 'doc/restaurant_data/store.db'; String dataOutputPath = 'assets/data/store_seed.json'; String metaOutputPath = 'assets/data/store_seed.meta.json'; String version = DateTime.now().toUtc().toIso8601String(); for (final arg in args) { if (arg.startsWith('--db=')) { dbPath = arg.substring('--db='.length); } else if (arg.startsWith('--data=')) { dataOutputPath = arg.substring('--data='.length); } else if (arg.startsWith('--meta=')) { metaOutputPath = arg.substring('--meta='.length); } else if (arg.startsWith('--version=')) { version = arg.substring('--version='.length); } } return StoreSeedConfig( dbPath: dbPath, dataOutputPath: dataOutputPath, metaOutputPath: metaOutputPath, version: version, ); } } class StoreSeedRow { final int storeId; final String province; final String district; final String name; final String title; final String address; final String roadAddress; final double latitude; final double longitude; StoreSeedRow({ required this.storeId, required this.province, required this.district, required this.name, required this.title, required this.address, required this.roadAddress, required this.latitude, required this.longitude, }); factory StoreSeedRow.fromMap(Map map) { return StoreSeedRow( 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(), ); } Map toJson() { return { 'storeId': storeId, 'province': province, 'district': district, 'name': name, 'title': title, 'address': address, 'roadAddress': roadAddress, 'latitude': latitude, 'longitude': longitude, }; } } class StoreSeedMeta { final String version; final String generatedAt; final String sourceDb; final int itemCount; final Map sourceSignature; StoreSeedMeta({ required this.version, required this.generatedAt, required this.sourceDb, required this.itemCount, required this.sourceSignature, }); Map toJson() { return { 'version': version, 'generatedAt': generatedAt, 'sourceDb': sourceDb, 'itemCount': itemCount, 'sourceSignature': sourceSignature, }; } } Future main(List args) async { final config = StoreSeedConfig.fromArgs(args); final dbFile = File(config.dbPath); if (!dbFile.existsSync()) { stderr.writeln('DB 파일을 찾을 수 없습니다: ${config.dbPath}'); exit(1); } final sqlitePath = await _findSqliteBinary(); if (sqlitePath == null) { stderr.writeln('sqlite3 바이너리를 찾을 수 없습니다. 시스템에 설치되어 있는지 확인하세요.'); exit(1); } final rows = await _fetchRows(sqlitePath, dbFile.path); if (rows.isEmpty) { stderr.writeln('restaurants 테이블에서 가져온 행이 없습니다.'); } final seeds = rows.map(StoreSeedRow.fromMap).toList(); final sourceBytes = await dbFile.readAsBytes(); final sourceSignature = _buildSignature(sourceBytes); await _writeJson( config.dataOutputPath, seeds.map((e) => e.toJson()).toList(), ); final generatedAt = DateTime.now().toUtc().toIso8601String(); final meta = StoreSeedMeta( version: config.version.isNotEmpty ? config.version : sourceSignature, generatedAt: generatedAt, sourceDb: dbFile.path, itemCount: seeds.length, sourceSignature: { 'hash': sourceSignature, 'size': sourceBytes.length, 'modifiedMs': dbFile.lastModifiedSync().millisecondsSinceEpoch, }, ); await _writeJson(config.metaOutputPath, meta.toJson()); stdout.writeln( '변환 완료: ${seeds.length}개 항목 → ' '${config.dataOutputPath} / ${config.metaOutputPath}', ); } 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; } 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) { stderr.writeln('sqlite3 실행 실패: ${result.stderr}'); exit(result.exitCode); } final output = result.stdout as String; final decoded = jsonDecode(output); if (decoded is! List) { stderr.writeln('예상치 못한 JSON 포맷입니다: ${decoded.runtimeType}'); exit(1); } return decoded.cast>(); } Future _writeJson(String path, Object data) async { final file = File(path); await file.parent.create(recursive: true); final encoder = const JsonEncoder.withIndent(' '); final content = encoder.convert(data); await file.writeAsString('$content\n'); } String _buildSignature(List bytes) { int hash = 0; for (final byte in bytes) { hash = (hash * 31 + byte) & 0x7fffffff; } return hash.toRadixString(16).padLeft(8, '0'); }