feat:DB 트리거 기반 데이터 변경 추적 시스템 구현
Phase 1: DB 기반 구축 - trigger_audit_logs 테이블 (RANGE 파티셔닝 15개, 3개 인덱스) - 789개 MySQL AFTER 트리거 (263 테이블 × INSERT/UPDATE/DELETE) - SetAuditSessionVariables 미들웨어 (@sam_actor_id, @sam_session_info) Phase 2: 복구 메커니즘 - TriggerAuditLog 모델, TriggerAuditLogService, AuditRollbackService - 6개 API 엔드포인트 (index, show, stats, history, rollback-preview, rollback) - FormRequest 검증 (TriggerAuditLogIndexRequest, TriggerAuditRollbackRequest) Phase 3: 관리 도구 - v_unified_audit VIEW (APP + TRIGGER 통합, COLLATE 처리) - audit:partitions 커맨드 (파티션 추가/삭제, dry-run) - audit:triggers 커맨드 (트리거 재생성, 테이블별/전체) - 월 1회 파티션 자동 관리 스케줄러 등록 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
97
app/Services/Audit/TriggerAuditLogService.php
Normal file
97
app/Services/Audit/TriggerAuditLogService.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Audit;
|
||||
|
||||
use App\Models\Audit\TriggerAuditLog;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
class TriggerAuditLogService extends Service
|
||||
{
|
||||
public function paginate(array $filters): LengthAwarePaginator
|
||||
{
|
||||
$page = (int) ($filters['page'] ?? 1);
|
||||
$size = (int) ($filters['size'] ?? 20);
|
||||
$sort = $filters['sort'] ?? 'created_at';
|
||||
$order = $filters['order'] ?? 'desc';
|
||||
|
||||
$q = TriggerAuditLog::query();
|
||||
|
||||
// 테넌트 필터 (선택적: 전체 조회도 가능)
|
||||
if (! empty($filters['tenant_id'])) {
|
||||
$q->where('tenant_id', (int) $filters['tenant_id']);
|
||||
}
|
||||
if (! empty($filters['table_name'])) {
|
||||
$q->where('table_name', $filters['table_name']);
|
||||
}
|
||||
if (! empty($filters['row_id'])) {
|
||||
$q->where('row_id', $filters['row_id']);
|
||||
}
|
||||
if (! empty($filters['dml_type'])) {
|
||||
$q->where('dml_type', $filters['dml_type']);
|
||||
}
|
||||
if (! empty($filters['actor_id'])) {
|
||||
$q->where('actor_id', (int) $filters['actor_id']);
|
||||
}
|
||||
if (! empty($filters['db_user'])) {
|
||||
$q->where('db_user', 'like', "%{$filters['db_user']}%");
|
||||
}
|
||||
if (! empty($filters['from'])) {
|
||||
$q->where('created_at', '>=', $filters['from']);
|
||||
}
|
||||
if (! empty($filters['to'])) {
|
||||
$q->where('created_at', '<=', $filters['to']);
|
||||
}
|
||||
|
||||
return $q->orderBy($sort, $order)->paginate($size, ['*'], 'page', $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 레코드의 변경 이력 조회
|
||||
*/
|
||||
public function recordHistory(string $tableName, string $rowId): \Illuminate\Support\Collection
|
||||
{
|
||||
return TriggerAuditLog::forRecord($tableName, $rowId)
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블별 변경 통계
|
||||
*/
|
||||
public function stats(?int $tenantId = null): array
|
||||
{
|
||||
$q = TriggerAuditLog::query();
|
||||
|
||||
if ($tenantId) {
|
||||
$q->where('tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
$total = (clone $q)->count();
|
||||
|
||||
$byDmlType = (clone $q)
|
||||
->selectRaw('dml_type, COUNT(*) as count')
|
||||
->groupBy('dml_type')
|
||||
->pluck('count', 'dml_type')
|
||||
->toArray();
|
||||
|
||||
$topTables = (clone $q)
|
||||
->selectRaw('table_name, COUNT(*) as count')
|
||||
->groupBy('table_name')
|
||||
->orderByDesc('count')
|
||||
->limit(10)
|
||||
->pluck('count', 'table_name')
|
||||
->toArray();
|
||||
|
||||
$today = (clone $q)
|
||||
->whereDate('created_at', today())
|
||||
->count();
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'today' => $today,
|
||||
'by_dml_type' => $byDmlType,
|
||||
'top_tables' => $topTables,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user