isFillable('created_by') && ! $model->created_by) { $model->created_by = $actorId; } if ($model->isFillable('updated_by') && ! $model->updated_by) { $model->updated_by = $actorId; } } }); static::updating(function ($model) { $actorId = static::resolveActorId(); if ($actorId && $model->isFillable('updated_by')) { $model->updated_by = $actorId; } }); static::deleting(function ($model) { $actorId = static::resolveActorId(); if ($actorId && $model->isFillable('deleted_by')) { $model->deleted_by = $actorId; $model->saveQuietly(); } }); static::created(function ($model) { $model->logAuditEvent('created', null, $model->toAuditSnapshot()); }); static::updated(function ($model) { $dirty = $model->getChanges(); $excluded = $model->getAuditExcludedFields(); $changed = array_diff_key($dirty, array_flip($excluded)); if (empty($changed)) { return; } $before = []; $after = []; foreach ($changed as $key => $value) { $before[$key] = $model->getOriginal($key); $after[$key] = $value; } $model->logAuditEvent('updated', $before, $after); }); static::deleted(function ($model) { $model->logAuditEvent('deleted', $model->toAuditSnapshot(), null); }); } public function getAuditExcludedFields(): array { $defaults = [ 'created_at', 'updated_at', 'deleted_at', 'created_by', 'updated_by', 'deleted_by', ]; $custom = property_exists($this, 'auditExclude') ? $this->auditExclude : []; return array_merge($defaults, $custom); } public function getAuditTargetType(): string { $className = class_basename(static::class); return Str::snake($className); } protected function toAuditSnapshot(): array { $excluded = $this->getAuditExcludedFields(); return array_diff_key($this->attributesToArray(), array_flip($excluded)); } protected function logAuditEvent(string $action, ?array $before, ?array $after): void { try { $tenantId = $this->tenant_id ?? null; if (! $tenantId) { return; } $request = request(); AuditLog::create([ 'tenant_id' => $tenantId, 'target_type' => $this->getAuditTargetType(), 'target_id' => $this->getKey(), 'action' => $action, 'before' => $before, 'after' => $after, 'actor_id' => static::resolveActorId(), 'ip' => $request?->ip(), 'ua' => $request?->userAgent(), 'created_at' => now(), ]); } catch (\Throwable $e) { // 감사 로그 실패는 업무 흐름을 방해하지 않음 } } protected static function resolveActorId(): ?int { return auth()->id(); } }