diff --git a/app/Http/Controllers/TriggerAuditController.php b/app/Http/Controllers/TriggerAuditController.php index 4a5c5679..898f95f9 100644 --- a/app/Http/Controllers/TriggerAuditController.php +++ b/app/Http/Controllers/TriggerAuditController.php @@ -233,6 +233,86 @@ private function buildReinsertSQL(TriggerAuditLog $log, \PDO $pdo): string return "INSERT INTO `{$log->table_name}` ({$cols}) VALUES ({$vals})"; } + // ── 4.3 일괄 롤백 (Operation 단위) ────────────────── + + /** + * Operation 단위 상세 (일괄 롤백 미리보기) + */ + public function operation(string $operationId): View + { + $logs = TriggerAuditLog::forOperation($operationId) + ->orderBy('created_at') + ->orderBy('id') + ->get(); + + if ($logs->isEmpty()) { + abort(404, '해당 작업 ID의 로그가 없습니다.'); + } + + // 각 로그별 롤백 SQL 생성 + $rollbackItems = $logs->reverse()->map(function (TriggerAuditLog $log) { + return [ + 'log' => $log, + 'sql' => $this->generateRollbackSQL($log), + 'diff' => $this->calculateDiff($log), + ]; + }); + + // 요약 정보 + $summary = [ + 'operation_id' => $operationId, + 'total_count' => $logs->count(), + 'tables' => $logs->groupBy('table_name')->map->count(), + 'dml_types' => $logs->groupBy('dml_type')->map->count(), + 'actor_id' => $logs->first()->actor_id, + 'session_info' => $logs->first()->session_info, + 'started_at' => $logs->first()->created_at, + 'ended_at' => $logs->last()->created_at, + ]; + + return view('trigger-audit.operation', compact('rollbackItems', 'summary')); + } + + /** + * Operation 단위 일괄 롤백 실행 + */ + public function batchRollbackExecute(Request $request, string $operationId) + { + $request->validate(['confirm' => 'required|accepted']); + + $logs = TriggerAuditLog::forOperation($operationId) + ->orderBy('created_at') + ->orderBy('id') + ->get(); + + if ($logs->isEmpty()) { + abort(404, '해당 작업 ID의 로그가 없습니다.'); + } + + // 역순으로 롤백 SQL 생성 + $sqls = $logs->reverse()->map(fn (TriggerAuditLog $log) => $this->generateRollbackSQL($log)); + + DB::statement('SET @disable_audit_trigger = 1'); + + try { + DB::transaction(function () use ($sqls) { + foreach ($sqls as $sql) { + if (! str_starts_with($sql, '--')) { + DB::statement($sql); + } + } + }); + + return redirect()->route('trigger-audit.operation', $operationId) + ->with('success', "일괄 롤백이 성공적으로 실행되었습니다. ({$logs->count()}건)"); + } catch (\Throwable $e) { + return redirect()->route('trigger-audit.operation', $operationId) + ->with('error', '일괄 롤백 실패: '.$e->getMessage()); + } finally { + DB::statement('SET @disable_audit_trigger = NULL'); + } + } + // ── 4.4 트리거 관리 ────────────────────────────── /** diff --git a/app/Models/Audit/TriggerAuditLog.php b/app/Models/Audit/TriggerAuditLog.php index 8968bb7a..877410c3 100644 --- a/app/Models/Audit/TriggerAuditLog.php +++ b/app/Models/Audit/TriggerAuditLog.php @@ -51,4 +51,9 @@ public function scopeForRecord($query, string $tableName, string $rowId) { return $query->where('table_name', $tableName)->where('row_id', $rowId); } + + public function scopeForOperation($query, string $operationId) + { + return $query->where('operation_id', $operationId); + } } diff --git a/app/Services/TriggerManagementService.php b/app/Services/TriggerManagementService.php index 249280fd..232af49d 100644 --- a/app/Services/TriggerManagementService.php +++ b/app/Services/TriggerManagementService.php @@ -220,8 +220,8 @@ private function createTriggersForTable(string $dbName, string $table): void CREATE TRIGGER `trg_{$table}_ai` AFTER INSERT ON `{$table}` FOR EACH ROW BEGIN {$guard} - INSERT INTO trigger_audit_logs (table_name, row_id, dml_type, old_values, new_values, changed_columns, tenant_id, actor_id, session_info, db_user, created_at) - VALUES ('{$table}', NEW.`{$pkCol}`, 'INSERT', NULL, {$newJson}, NULL, {$tenantExpr}, @sam_actor_id, @sam_session_info, CURRENT_USER(), NOW()); + INSERT INTO trigger_audit_logs (table_name, row_id, dml_type, old_values, new_values, changed_columns, tenant_id, actor_id, session_info, operation_id, db_user, created_at) + VALUES ('{$table}', NEW.`{$pkCol}`, 'INSERT', NULL, {$newJson}, NULL, {$tenantExpr}, @sam_actor_id, @sam_session_info, @sam_operation_id, CURRENT_USER(), NOW()); END IF; END "); @@ -231,8 +231,8 @@ private function createTriggersForTable(string $dbName, string $table): void FOR EACH ROW BEGIN {$guard} IF {$changeCheck} THEN - INSERT INTO trigger_audit_logs (table_name, row_id, dml_type, old_values, new_values, changed_columns, tenant_id, actor_id, session_info, db_user, created_at) - VALUES ('{$table}', NEW.`{$pkCol}`, 'UPDATE', {$oldJson}, {$newJson}, JSON_ARRAY({$changedCols}), {$tenantExpr}, @sam_actor_id, @sam_session_info, CURRENT_USER(), NOW()); + INSERT INTO trigger_audit_logs (table_name, row_id, dml_type, old_values, new_values, changed_columns, tenant_id, actor_id, session_info, operation_id, db_user, created_at) + VALUES ('{$table}', NEW.`{$pkCol}`, 'UPDATE', {$oldJson}, {$newJson}, JSON_ARRAY({$changedCols}), {$tenantExpr}, @sam_actor_id, @sam_session_info, @sam_operation_id, CURRENT_USER(), NOW()); END IF; END IF; END @@ -242,8 +242,8 @@ private function createTriggersForTable(string $dbName, string $table): void CREATE TRIGGER `trg_{$table}_ad` AFTER DELETE ON `{$table}` FOR EACH ROW BEGIN {$guard} - INSERT INTO trigger_audit_logs (table_name, row_id, dml_type, old_values, new_values, changed_columns, tenant_id, actor_id, session_info, db_user, created_at) - VALUES ('{$table}', OLD.`{$pkCol}`, 'DELETE', {$oldJson}, NULL, NULL, {$tenantExprOld}, @sam_actor_id, @sam_session_info, CURRENT_USER(), NOW()); + INSERT INTO trigger_audit_logs (table_name, row_id, dml_type, old_values, new_values, changed_columns, tenant_id, actor_id, session_info, operation_id, db_user, created_at) + VALUES ('{$table}', OLD.`{$pkCol}`, 'DELETE', {$oldJson}, NULL, NULL, {$tenantExprOld}, @sam_actor_id, @sam_session_info, @sam_operation_id, CURRENT_USER(), NOW()); END IF; END "); diff --git a/resources/views/trigger-audit/index.blade.php b/resources/views/trigger-audit/index.blade.php index 20df7a93..bdce3b92 100644 --- a/resources/views/trigger-audit/index.blade.php +++ b/resources/views/trigger-audit/index.blade.php @@ -135,7 +135,7 @@ class="border rounded-lg px-3 py-2 text-sm w-32">
+ Operation ID: {{ $summary['operation_id'] }} +
+{{ $item['sql'] }}
+