option('table'); $dropOnly = $this->option('drop-only'); $dryRun = $this->option('dry-run'); $dbName = config('database.connections.mysql.database'); $this->info('=== 트리거 감사 로그 트리거 '.($dropOnly ? '삭제' : '재생성').' ==='); $this->newLine(); // 대상 테이블 목록 $tables = $this->getTargetTables($dbName, $specificTable); $this->info('대상 테이블: '.count($tables).'개'); if ($dryRun) { foreach ($tables as $t) { $this->line(" - {$t}"); } $this->newLine(); $this->info('[DRY-RUN] 실제 변경 없음'); return self::SUCCESS; } $dropped = 0; $created = 0; foreach ($tables as $table) { // 기존 트리거 삭제 foreach (['ai', 'au', 'ad'] as $suffix) { $triggerName = "trg_{$table}_{$suffix}"; DB::unprepared("DROP TRIGGER IF EXISTS `{$triggerName}`"); $dropped++; } if (! $dropOnly) { // 트리거 재생성 $this->createTriggersForTable($dbName, $table); $created += 3; } $this->line(" {$table}: ".($dropOnly ? '삭제 완료' : '재생성 완료')); } $this->newLine(); $this->info("결과: 삭제 {$dropped}개, 생성 {$created}개"); return self::SUCCESS; } private function getTargetTables(string $dbName, ?string $specificTable): array { if ($specificTable) { if (in_array($specificTable, $this->excludeTables)) { $this->error("{$specificTable}은(는) 트리거 제외 테이블입니다."); return []; } return [$specificTable]; } $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 { // 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]); $pkCol = $pkRow?->COLUMN_NAME ?? 'id'; // 컬럼 목록 (제외: created_at, updated_at, deleted_at, remember_token) $excludeCols = ['created_at', 'updated_at', 'deleted_at', 'remember_token']; $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, $excludeCols)) ->values() ->toArray(); if (empty($cols)) { return; } // JSON_OBJECT 표현식 $newJson = 'JSON_OBJECT('.collect($cols)->map(fn ($c) => "'{$c}', NEW.`{$c}`")->implode(', ').')'; $oldJson = 'JSON_OBJECT('.collect($cols)->map(fn ($c) => "'{$c}', OLD.`{$c}`")->implode(', ').')'; // changed_columns (UPDATE용) $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'; // INSERT trigger 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 "); // UPDATE trigger 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 "); // DELETE trigger 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 "); } }