/// License → Maintenance 마이그레이션 실행 스크립트 /// /// 사용법: /// dart run lib/core/migrations/execute_migration.dart /// /// 옵션: /// --dry-run: 실제 데이터 변경 없이 시뮬레이션만 수행 /// --rollback: 이전 백업에서 롤백 실행 /// --validate: 마이그레이션 후 데이터 검증만 수행 library; import 'dart:io'; import 'dart:convert'; import 'package:dio/dio.dart'; import 'license_to_maintenance_migration.dart'; import 'maintenance_data_validator.dart'; class MigrationExecutor { static const String apiBaseUrl = 'http://43.201.34.104:8080/api/v1'; static const String backupPath = './migration_backup.json'; final Dio _dio; String? _authToken; MigrationExecutor() : _dio = Dio(BaseOptions( baseUrl: apiBaseUrl, connectTimeout: const Duration(seconds: 30), receiveTimeout: const Duration(seconds: 30), )); /// 마이그레이션 실행 Future execute({bool isDryRun = false}) async { print('=' * 60); print('License → Maintenance 마이그레이션 시작'); print('=' * 60); print('모드: ${isDryRun ? "DRY RUN (시뮬레이션)" : "실제 실행"}'); print('시작 시간: ${DateTime.now()}'); print('-' * 60); try { // 1. 인증 print('\n[1/7] API 인증 중...'); await _authenticate(); // 2. 기존 License 데이터 가져오기 print('\n[2/7] License 데이터 로딩 중...'); final licenseData = await _fetchLicenseData(); print(' → ${licenseData.length}개 License 발견'); // 3. Equipment 및 Equipment History 데이터 가져오기 print('\n[3/7] Equipment 관련 데이터 로딩 중...'); final equipmentData = await _fetchEquipmentData(); final equipmentHistoryData = await _fetchEquipmentHistoryData(); print(' → ${equipmentData.length}개 Equipment 발견'); print(' → ${equipmentHistoryData.length}개 Equipment History 발견'); // 4. 마이그레이션 실행 print('\n[4/7] 데이터 변환 중...'); final result = await LicenseToMaintenanceMigration.migrate( licenseData: licenseData, equipmentData: equipmentData, equipmentHistoryData: equipmentHistoryData, ); if (!result.success) { throw Exception('마이그레이션 실패: ${result.error}'); } // 5. 백업 저장 print('\n[5/7] 백업 저장 중...'); if (!isDryRun) { await _saveBackup(result.backup!); print(' → 백업 저장 완료: $backupPath'); } else { print(' → [DRY RUN] 백업 저장 건너뜀'); } // 6. Maintenance 데이터 저장 print('\n[6/7] Maintenance 데이터 저장 중...'); if (!isDryRun) { await _saveMaintenanceData(result.maintenanceData!); print(' → ${result.maintenanceData!.length}개 Maintenance 저장 완료'); } else { print(' → [DRY RUN] 실제 저장 건너뜀'); _printSampleData(result.maintenanceData!); } // 7. 검증 print('\n[7/7] 데이터 검증 중...'); final validationReport = await MaintenanceDataValidator.validate( maintenanceData: result.maintenanceData!, equipmentHistoryData: [], // TODO: 실제 equipment history 데이터 로드 ); _printValidationReport(validationReport); // 완료 print('\n${'=' * 60}'); print('마이그레이션 ${isDryRun ? "시뮬레이션" : "실행"} 완료!'); print('=' * 60); print('통계:'); print(' - 총 License: ${result.statistics!.totalCount}개'); print(' - 활성: ${result.statistics!.activeCount}개'); print(' - 만료 예정: ${result.statistics!.upcomingCount}개'); print(' - 만료됨: ${result.statistics!.expiredCount}개'); print('종료 시간: ${DateTime.now()}'); } catch (e) { print('\n❌ 마이그레이션 실패!'); print('오류: $e'); print('\n롤백이 필요한 경우 다음 명령을 실행하세요:'); print('dart run lib/core/migrations/execute_migration.dart --rollback'); exit(1); } } /// 롤백 실행 Future rollback() async { print('=' * 60); print('License → Maintenance 롤백 시작'); print('=' * 60); try { // 백업 파일 로드 final backupFile = File(backupPath); if (!await backupFile.exists()) { throw Exception('백업 파일을 찾을 수 없습니다: $backupPath'); } final backupContent = await backupFile.readAsString(); final backup = jsonDecode(backupContent) as Map; print('백업 정보:'); print(' - 생성 시간: ${backup['timestamp']}'); print(' - 버전: ${backup['version']}'); print(' - 데이터 수: ${(backup['data'] as List).length}개'); // 롤백 확인 print('\n정말로 롤백하시겠습니까? (y/n)'); final confirm = stdin.readLineSync(); if (confirm?.toLowerCase() != 'y') { print('롤백 취소됨'); return; } // 롤백 실행 print('\n롤백 실행 중...'); final success = await LicenseToMaintenanceMigration.rollback(backup); if (success) { print('✅ 롤백 완료!'); // Maintenance 데이터 삭제 print('Maintenance 데이터 정리 중...'); await _deleteMaintenanceData(); // License 데이터 복원 print('License 데이터 복원 중...'); await _restoreLicenseData(backup['data'] as List); print('\n롤백이 성공적으로 완료되었습니다.'); } else { throw Exception('롤백 실패'); } } catch (e) { print('\n❌ 롤백 실패!'); print('오류: $e'); print('\n수동 복구가 필요할 수 있습니다.'); exit(1); } } /// API 인증 Future _authenticate() async { try { final response = await _dio.post('/auth/login', data: { 'email': 'admin@example.com', 'password': 'password123', }); _authToken = response.data['token']; _dio.options.headers['Authorization'] = 'Bearer $_authToken'; } catch (e) { throw Exception('인증 실패: $e'); } } /// License 데이터 가져오기 Future>> _fetchLicenseData() async { try { // 실제 환경에서는 API 호출 // 여기서는 더미 데이터 반환 (실제 구현 시 수정 필요) return [ { 'id': 1, 'equipment_id': 1, 'license_type': 'O', 'period_months': 12, 'cost': 1000000, 'vendor_name': '삼성전자서비스', 'vendor_contact': '1588-3366', 'start_date': '2024-01-01T00:00:00Z', 'expiry_date': '2024-12-31T23:59:59Z', 'created_at': '2024-01-01T00:00:00Z', }, // 추가 데이터... ]; } catch (e) { throw Exception('License 데이터 로딩 실패: $e'); } } /// Equipment 데이터 가져오기 Future>> _fetchEquipmentData() async { try { final response = await _dio.get('/equipments'); return (response.data['data'] as List) .map((e) => e as Map) .toList(); } catch (e) { throw Exception('Equipment 데이터 로딩 실패: $e'); } } /// Equipment History 데이터 가져오기 Future>> _fetchEquipmentHistoryData() async { try { final response = await _dio.get('/equipment_history'); return (response.data['data'] as List) .map((e) => e as Map) .toList(); } catch (e) { // Equipment History가 아직 구현되지 않았을 수 있음 print(' ⚠️ Equipment History API 미구현, 빈 데이터 사용'); return []; } } /// 백업 저장 Future _saveBackup(Map backup) async { final file = File(backupPath); await file.writeAsString(jsonEncode(backup)); } /// Maintenance 데이터 저장 Future _saveMaintenanceData(List> data) async { for (final maintenance in data) { try { await _dio.post('/maintenances', data: maintenance); } catch (e) { print(' ⚠️ Maintenance #${maintenance['id']} 저장 실패: $e'); } } } /// Maintenance 데이터 삭제 Future _deleteMaintenanceData() async { try { // 모든 Maintenance 데이터 삭제 await _dio.delete('/maintenances/all'); } catch (e) { print(' ⚠️ Maintenance 데이터 삭제 실패: $e'); } } /// License 데이터 복원 Future _restoreLicenseData(List data) async { for (final license in data) { try { await _dio.post('/licenses', data: license); } catch (e) { print(' ⚠️ License #${license['id']} 복원 실패: $e'); } } } /// 샘플 데이터 출력 (DRY RUN용) void _printSampleData(List> data) { print('\n 샘플 변환 데이터 (처음 3개):'); for (var i = 0; i < (data.length > 3 ? 3 : data.length); i++) { final item = data[i]; print(' ${i + 1}. Maintenance #${item['id']}'); print(' - Equipment History ID: ${item['equipment_history_id']}'); print(' - Type: ${item['maintenance_type']}'); print(' - Period: ${item['period_months']} months'); print(' - Status: ${item['status']}'); print(' - Next Date: ${item['next_date']}'); } } /// 검증 보고서 출력 void _printValidationReport(ValidationReport report) { print('\n 검증 결과:'); print(' - 전체 검증: ${report.isValid ? "✅ 통과" : "❌ 실패"}'); print(' - 데이터 무결성: ${report.dataIntegrity ? "✅" : "❌"}'); print(' - FK 관계: ${report.foreignKeyIntegrity ? "✅" : "❌"}'); print(' - 비즈니스 규칙: ${report.businessRulesValid ? "✅" : "❌"}'); if (report.warnings.isNotEmpty) { print('\n 경고:'); for (final warning in report.warnings) { print(' ⚠️ $warning'); } } if (report.errors.isNotEmpty) { print('\n 오류:'); for (final error in report.errors) { print(' ❌ $error'); } } } } /// 메인 실행 함수 Future main(List args) async { final executor = MigrationExecutor(); if (args.contains('--rollback')) { await executor.rollback(); } else { final isDryRun = args.contains('--dry-run'); await executor.execute(isDryRun: isDryRun); } }