- operation 상세 페이지 및 일괄 롤백 실행 기능 추가
- TriggerAuditLog에 scopeForOperation 스코프 추가
- 트리거 INSERT/UPDATE/DELETE에 operation_id 컬럼 포함
- 감사로그 목록에 작업 단위 링크 컬럼 추가
- 라우트: operation/{id}, batch-rollback 추가
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
204 lines
9.3 KiB
PHP
204 lines
9.3 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', 'DB 트리거 감사 로그')
|
|
|
|
@section('content')
|
|
<!-- 페이지 헤더 -->
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h1 class="text-2xl font-bold text-gray-800">DB 트리거 감사 로그</h1>
|
|
<div class="flex items-center gap-2 text-sm text-gray-500">
|
|
<span class="inline-flex items-center px-2 py-1 bg-green-100 text-green-700 rounded">트리거 {{ $triggerCount }}개 활성</span>
|
|
</div>
|
|
</div>
|
|
|
|
@include('trigger-audit.partials.sub-nav')
|
|
|
|
<!-- 통계 카드 -->
|
|
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6">
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
<div class="text-sm text-gray-500">전체 기록</div>
|
|
<div class="text-2xl font-bold text-gray-800">{{ number_format($stats['total']) }}</div>
|
|
</div>
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
<div class="text-sm text-gray-500">오늘 기록</div>
|
|
<div class="text-2xl font-bold text-blue-600">{{ number_format($stats['today']) }}</div>
|
|
</div>
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
<div class="text-sm text-gray-500">INSERT</div>
|
|
<div class="text-2xl font-bold text-green-600">{{ number_format($stats['by_dml_type']['INSERT'] ?? 0) }}</div>
|
|
</div>
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
<div class="text-sm text-gray-500">UPDATE</div>
|
|
<div class="text-2xl font-bold text-yellow-600">{{ number_format($stats['by_dml_type']['UPDATE'] ?? 0) }}</div>
|
|
</div>
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
<div class="text-sm text-gray-500">DELETE</div>
|
|
<div class="text-2xl font-bold text-red-600">{{ number_format($stats['by_dml_type']['DELETE'] ?? 0) }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 상위 테이블 + 파티션 현황 -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
|
<!-- 상위 테이블 -->
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">변경 빈도 상위 테이블</h3>
|
|
<div class="space-y-2">
|
|
@foreach($stats['top_tables'] as $table => $count)
|
|
<div class="flex justify-between items-center">
|
|
<a href="{{ route('trigger-audit.index', ['table_name' => $table]) }}"
|
|
class="text-sm text-blue-600 hover:underline">{{ $table }}</a>
|
|
<span class="text-sm text-gray-500">{{ number_format($count) }}</span>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 파티션 현황 -->
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">파티션 현황 (저장소: {{ $stats['storage_mb'] }}MB)</h3>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-xs">
|
|
<thead>
|
|
<tr class="text-gray-500">
|
|
<th class="text-left py-1">파티션</th>
|
|
<th class="text-right py-1">행 수</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach($partitions as $p)
|
|
<tr class="border-t">
|
|
<td class="py-1">{{ $p->PARTITION_NAME }}</td>
|
|
<td class="text-right py-1">{{ number_format($p->TABLE_ROWS) }}</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 필터 -->
|
|
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
|
<form method="GET" class="flex flex-wrap gap-4 items-end">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">테이블</label>
|
|
<select name="table_name" onchange="this.form.submit()" class="border rounded-lg px-3 py-2 text-sm">
|
|
<option value="">전체</option>
|
|
@foreach($tables as $table => $cnt)
|
|
<option value="{{ $table }}" {{ request('table_name') === $table ? 'selected' : '' }}>{{ $table }} ({{ number_format($cnt) }})</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">DML 타입</label>
|
|
<select name="dml_type" onchange="this.form.submit()" class="border rounded-lg px-3 py-2 text-sm">
|
|
<option value="">전체</option>
|
|
@foreach(['INSERT', 'UPDATE', 'DELETE'] as $type)
|
|
<option value="{{ $type }}" {{ request('dml_type') === $type ? 'selected' : '' }}>{{ $type }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Row ID</label>
|
|
<input type="text" name="row_id" value="{{ request('row_id') }}" placeholder="레코드 ID"
|
|
class="border rounded-lg px-3 py-2 text-sm w-32">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">시작일</label>
|
|
<input type="date" name="from" value="{{ request('from') }}" class="border rounded-lg px-3 py-2 text-sm">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">종료일</label>
|
|
<input type="date" name="to" value="{{ request('to') }}" class="border rounded-lg px-3 py-2 text-sm">
|
|
</div>
|
|
|
|
<div>
|
|
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm">검색</button>
|
|
</div>
|
|
<div>
|
|
<a href="{{ route('trigger-audit.index') }}" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-lg text-sm inline-block">초기화</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- 로그 테이블 -->
|
|
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-gray-50 text-gray-600">
|
|
<tr>
|
|
<th class="px-4 py-3 text-left">ID</th>
|
|
<th class="px-4 py-3 text-left">테이블</th>
|
|
<th class="px-4 py-3 text-left">Row ID</th>
|
|
<th class="px-4 py-3 text-center">DML</th>
|
|
<th class="px-4 py-3 text-left">변경 컬럼</th>
|
|
<th class="px-4 py-3 text-center">작업 단위</th>
|
|
<th class="px-4 py-3 text-left">일시</th>
|
|
<th class="px-4 py-3 text-center">액션</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100">
|
|
@forelse($logs as $log)
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-4 py-3 text-gray-500">{{ $log->id }}</td>
|
|
<td class="px-4 py-3">
|
|
<a href="{{ route('trigger-audit.index', ['table_name' => $log->table_name]) }}"
|
|
class="text-blue-600 hover:underline">{{ $log->table_name }}</a>
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<a href="{{ route('trigger-audit.history', [$log->table_name, $log->row_id]) }}"
|
|
class="text-blue-600 hover:underline">{{ $log->row_id }}</a>
|
|
</td>
|
|
<td class="px-4 py-3 text-center">
|
|
<span class="px-2 py-1 rounded text-xs font-medium
|
|
{{ $log->dml_type === 'INSERT' ? 'bg-green-100 text-green-700' : '' }}
|
|
{{ $log->dml_type === 'UPDATE' ? 'bg-yellow-100 text-yellow-700' : '' }}
|
|
{{ $log->dml_type === 'DELETE' ? 'bg-red-100 text-red-700' : '' }}">
|
|
{{ $log->dml_type }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-gray-600 text-xs">
|
|
@if($log->changed_columns)
|
|
{{ implode(', ', array_slice($log->changed_columns, 0, 3)) }}
|
|
@if(count($log->changed_columns) > 3)
|
|
<span class="text-gray-400">+{{ count($log->changed_columns) - 3 }}</span>
|
|
@endif
|
|
@else
|
|
<span class="text-gray-400">-</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-4 py-3 text-center">
|
|
@if($log->operation_id)
|
|
<a href="{{ route('trigger-audit.operation', $log->operation_id) }}"
|
|
class="inline-block bg-purple-100 text-purple-700 px-2 py-0.5 rounded text-xs hover:bg-purple-200"
|
|
title="{{ $log->operation_id }}">
|
|
{{ substr($log->operation_id, 0, 8) }}
|
|
</a>
|
|
@else
|
|
<span class="text-gray-400 text-xs">-</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-4 py-3 text-gray-500 text-xs">{{ $log->created_at->format('m/d H:i:s') }}</td>
|
|
<td class="px-4 py-3 text-center">
|
|
<a href="{{ route('trigger-audit.show', $log->id) }}"
|
|
class="text-blue-600 hover:underline text-xs">상세</a>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="9" class="px-4 py-8 text-center text-gray-400">감사 로그가 없습니다.</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- 페이지네이션 -->
|
|
<div class="px-4 py-3 bg-gray-50 border-t">
|
|
{{ $logs->links() }}
|
|
</div>
|
|
</div>
|
|
@endsection
|