pluck('TABLE_NAME')->toArray(); // 추적 대상 테이블 $trackedTables = array_values(array_diff($allTables, $this->excludeTables)); // 현재 존재하는 트리거 조회 $triggers = collect(DB::select(" SELECT TRIGGER_NAME, EVENT_OBJECT_TABLE, EVENT_MANIPULATION FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA = ? AND TRIGGER_NAME LIKE 'trg_%' ORDER BY EVENT_OBJECT_TABLE, EVENT_MANIPULATION ", [$dbName])); // 테이블별 트리거 상태 매핑 $tableStatus = []; foreach ($trackedTables as $table) { // PK 조회 $pkRow = DB::selectOne(" SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_KEY = 'PRI' LIMIT 1 ", [$dbName, $table]); // 컬럼 수 조회 $colCount = DB::selectOne(" SELECT COUNT(*) as cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ", [$dbName, $table])->cnt; $tableTriggers = $triggers->where('EVENT_OBJECT_TABLE', $table); $tableStatus[] = [ 'table_name' => $table, 'pk_column' => $pkRow?->COLUMN_NAME ?? '-', 'column_count' => $colCount, 'has_insert' => $tableTriggers->contains('EVENT_MANIPULATION', 'INSERT'), 'has_update' => $tableTriggers->contains('EVENT_MANIPULATION', 'UPDATE'), 'has_delete' => $tableTriggers->contains('EVENT_MANIPULATION', 'DELETE'), ]; } return [ 'total_tables' => count($allTables), 'tracked_tables' => count($trackedTables), 'excluded_tables' => count($this->excludeTables), 'active_triggers' => $triggers->count(), 'table_status' => $tableStatus, 'exclude_tables' => $this->excludeTables, ]; } /** * 트리거 재생성 (단일 테이블 또는 전체) */ public function regenerate(?string $tableName = null): array { $dbName = config('database.connections.mysql.database'); $tables = $this->getTargetTables($dbName, $tableName); if (empty($tables)) { return ['success' => false, 'message' => '대상 테이블이 없습니다.']; } $dropped = 0; $created = 0; foreach ($tables as $table) { // 기존 트리거 삭제 foreach (['ai', 'au', 'ad'] as $suffix) { DB::unprepared("DROP TRIGGER IF EXISTS `trg_{$table}_{$suffix}`"); $dropped++; } // 트리거 생성 $this->createTriggersForTable($dbName, $table); $created += 3; } $label = $tableName ?? '전체'; return [ 'success' => true, 'message' => "[{$label}] 트리거 재생성 완료 (삭제: {$dropped}, 생성: {$created})", ]; } /** * 트리거 삭제 (단일 테이블 또는 전체) */ public function drop(?string $tableName = null): array { $dbName = config('database.connections.mysql.database'); $tables = $this->getTargetTables($dbName, $tableName); if (empty($tables)) { return ['success' => false, 'message' => '대상 테이블이 없습니다.']; } $dropped = 0; foreach ($tables as $table) { foreach (['ai', 'au', 'ad'] as $suffix) { DB::unprepared("DROP TRIGGER IF EXISTS `trg_{$table}_{$suffix}`"); $dropped++; } } $label = $tableName ?? '전체'; return [ 'success' => true, 'message' => "[{$label}] 트리거 삭제 완료 ({$dropped}개)", ]; } private function getTargetTables(string $dbName, ?string $tableName): array { if ($tableName) { if (in_array($tableName, $this->excludeTables)) { return []; } return [$tableName]; } $rows = DB::select(" SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME ", [$dbName]); return collect($rows) ->pluck('TABLE_NAME') ->reject(fn ($t) => in_array($t, $this->excludeTables)) ->values() ->toArray(); } private function createTriggersForTable(string $dbName, string $table): void { $pkRow = DB::selectOne(" SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_KEY = 'PRI' LIMIT 1 ", [$dbName, $table]); $pkCol = $pkRow?->COLUMN_NAME ?? 'id'; $columns = DB::select(' SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION ', [$dbName, $table]); $cols = collect($columns) ->pluck('COLUMN_NAME') ->reject(fn ($c) => in_array($c, $this->excludeColumns)) ->values() ->toArray(); if (empty($cols)) { return; } $newJson = 'JSON_OBJECT(' . collect($cols)->map(fn ($c) => "'{$c}', NEW.`{$c}`")->implode(', ') . ')'; $oldJson = 'JSON_OBJECT(' . collect($cols)->map(fn ($c) => "'{$c}', OLD.`{$c}`")->implode(', ') . ')'; $changedCols = collect($cols)->map(fn ($c) => "IF(NOT (NEW.`{$c}` <=> OLD.`{$c}`), '{$c}', NULL)")->implode(', '); $changeCheck = collect($cols)->map(fn ($c) => "NOT (NEW.`{$c}` <=> OLD.`{$c}`)")->implode(' OR '); $tenantExpr = in_array('tenant_id', $cols) ? 'NEW.`tenant_id`' : 'NULL'; $tenantExprOld = in_array('tenant_id', $cols) ? 'OLD.`tenant_id`' : 'NULL'; $guard = 'IF @disable_audit_trigger IS NULL OR @disable_audit_trigger != 1 THEN'; DB::unprepared(" 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()); END IF; END "); DB::unprepared(" CREATE TRIGGER `trg_{$table}_au` AFTER UPDATE ON `{$table}` 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()); END IF; END IF; END "); DB::unprepared(" 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()); END IF; END "); } }