feat(app): seed restaurants, geocode addresses, refresh sharing
This commit is contained in:
229
tool/store_db_to_seed.dart
Normal file
229
tool/store_db_to_seed.dart
Normal file
@@ -0,0 +1,229 @@
|
||||
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<String> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> sourceSignature;
|
||||
|
||||
StoreSeedMeta({
|
||||
required this.version,
|
||||
required this.generatedAt,
|
||||
required this.sourceDb,
|
||||
required this.itemCount,
|
||||
required this.sourceSignature,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'version': version,
|
||||
'generatedAt': generatedAt,
|
||||
'sourceDb': sourceDb,
|
||||
'itemCount': itemCount,
|
||||
'sourceSignature': sourceSignature,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> main(List<String> 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<String?> _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<List<Map<String, dynamic>>> _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<Map<String, dynamic>>();
|
||||
}
|
||||
|
||||
Future<void> _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<int> bytes) {
|
||||
int hash = 0;
|
||||
for (final byte in bytes) {
|
||||
hash = (hash * 31 + byte) & 0x7fffffff;
|
||||
}
|
||||
return hash.toRadixString(16).padLeft(8, '0');
|
||||
}
|
||||
Reference in New Issue
Block a user