125 lines
3.6 KiB
PHP
125 lines
3.6 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Traits;
|
||
|
|
|
||
|
|
use App\Models\Audit\AuditLog;
|
||
|
|
use Illuminate\Support\Str;
|
||
|
|
|
||
|
|
trait Auditable
|
||
|
|
{
|
||
|
|
protected static function bootAuditable(): void
|
||
|
|
{
|
||
|
|
static::creating(function ($model) {
|
||
|
|
$actorId = static::resolveActorId();
|
||
|
|
if ($actorId) {
|
||
|
|
if ($model->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();
|
||
|
|
}
|
||
|
|
}
|