- StatAlert 모델에 ai_analysis_summary accessor 추가 - 알림 상세 아코디언에 'AI 분석용 복사' 버튼 추가 - 클립보드 복사 시 심각도/도메인/유형/메시지 등 포맷팅 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
243 lines
12 KiB
PHP
243 lines
12 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', '시스템 알림')
|
|
|
|
@section('content')
|
|
<!-- 페이지 헤더 -->
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h1 class="text-2xl font-bold text-gray-800">시스템 알림</h1>
|
|
<div class="flex items-center gap-2">
|
|
@if($stats['unread'] > 0)
|
|
<button hx-post="{{ route('system.alerts.read-all') }}"
|
|
hx-confirm="모든 알림을 읽음 처리하시겠습니까?"
|
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm">
|
|
전체 읽음 ({{ $stats['unread'] }})
|
|
</button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 통계 카드 -->
|
|
<div class="grid grid-cols-1 md:grid-cols-4 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['unread']) }}</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-yellow-600">{{ number_format($stats['unresolved']) }}</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-red-600">{{ number_format($stats['critical']) }}</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="domain" onchange="this.form.submit()" class="border rounded-lg px-3 py-2 text-sm">
|
|
<option value="">전체</option>
|
|
@foreach($domains as $domain)
|
|
<option value="{{ $domain }}" {{ request('domain') === $domain ? 'selected' : '' }}>
|
|
{{ match($domain) {
|
|
'backup' => '백업',
|
|
'sales' => '매출',
|
|
'finance' => '재무',
|
|
'production' => '생산',
|
|
'system' => '시스템',
|
|
default => $domain,
|
|
} }}
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">심각도</label>
|
|
<select name="severity" onchange="this.form.submit()" class="border rounded-lg px-3 py-2 text-sm">
|
|
<option value="">전체</option>
|
|
<option value="critical" {{ request('severity') === 'critical' ? 'selected' : '' }}>긴급</option>
|
|
<option value="warning" {{ request('severity') === 'warning' ? 'selected' : '' }}>경고</option>
|
|
<option value="info" {{ request('severity') === 'info' ? 'selected' : '' }}>정보</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">상태</label>
|
|
<select name="status" onchange="this.form.submit()" class="border rounded-lg px-3 py-2 text-sm">
|
|
<option value="">전체</option>
|
|
<option value="unread" {{ request('status') === 'unread' ? 'selected' : '' }}>미읽음</option>
|
|
<option value="unresolved" {{ request('status') === 'unresolved' ? 'selected' : '' }}>미해결</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">시작일</label>
|
|
<input type="date" name="date_from" value="{{ request('date_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="date_to" value="{{ request('date_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>
|
|
<a href="{{ route('system.alerts.index') }}" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-lg text-sm ml-1">
|
|
초기화
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- 알림 목록 -->
|
|
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase w-20">심각도</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase w-20">도메인</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">제목</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase w-36">시간</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase w-16">읽음</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase w-16">해결</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase w-32">작업</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
@forelse($alerts as $alert)
|
|
<tr class="{{ !$alert->is_read ? 'bg-blue-50' : '' }} hover:bg-gray-50 cursor-pointer"
|
|
onclick="toggleDetail({{ $alert->id }})">
|
|
<td class="px-4 py-3 whitespace-nowrap">
|
|
<span class="px-2 py-1 text-xs font-medium rounded {{ $alert->severity_color }}">
|
|
{{ $alert->severity_label }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-600">
|
|
{{ $alert->domain_label }}
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-gray-900 {{ !$alert->is_read ? 'font-semibold' : '' }}">
|
|
{{ $alert->title }}
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
|
{{ $alert->created_at->format('m/d H:i') }}
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap text-center">
|
|
@if($alert->is_read)
|
|
<span class="text-green-500 text-xs">O</span>
|
|
@else
|
|
<span class="text-gray-300 text-xs">-</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap text-center">
|
|
@if($alert->is_resolved)
|
|
<span class="text-green-500 text-xs">O</span>
|
|
@else
|
|
<span class="text-gray-300 text-xs">-</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap text-sm" onclick="event.stopPropagation()">
|
|
@if(!$alert->is_read)
|
|
<button hx-post="{{ route('system.alerts.read', $alert->id) }}"
|
|
class="text-blue-600 hover:text-blue-800 text-xs mr-2">
|
|
읽음
|
|
</button>
|
|
@endif
|
|
@if(!$alert->is_resolved)
|
|
<button hx-post="{{ route('system.alerts.resolve', $alert->id) }}"
|
|
hx-confirm="이 알림을 해결 처리하시겠습니까?"
|
|
class="text-green-600 hover:text-green-800 text-xs">
|
|
해결
|
|
</button>
|
|
@else
|
|
<span class="text-gray-400 text-xs">{{ $alert->resolved_at?->format('m/d H:i') }}</span>
|
|
@endif
|
|
</td>
|
|
</tr>
|
|
<!-- 상세 내용 -->
|
|
<tr id="detail-{{ $alert->id }}" class="hidden">
|
|
<td colspan="7" class="px-4 py-4 bg-slate-50 border-t border-b border-slate-200">
|
|
<div class="text-sm text-gray-700">
|
|
<div class="mb-2 flex items-center justify-between">
|
|
<div>
|
|
<span class="font-medium text-gray-500">유형:</span>
|
|
{{ $alert->alert_type }}
|
|
</div>
|
|
<button onclick="event.stopPropagation(); copyAiAnalysis({{ $alert->id }})"
|
|
class="inline-flex items-center gap-1 px-3 py-1.5 bg-purple-600 hover:bg-purple-700 text-white text-xs rounded-lg transition">
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
|
</svg>
|
|
AI 분석용 복사
|
|
</button>
|
|
</div>
|
|
@if($alert->message)
|
|
<div class="mb-2">
|
|
<span class="font-medium text-gray-500">상세:</span>
|
|
<pre class="mt-1 bg-gray-900 text-green-400 text-xs rounded-lg p-3 font-mono whitespace-pre-wrap">{{ $alert->message }}</pre>
|
|
</div>
|
|
@endif
|
|
@if($alert->current_value || $alert->threshold_value)
|
|
<div class="flex gap-4 text-xs text-gray-500">
|
|
<span><strong>현재값:</strong> {{ $alert->current_value }}</span>
|
|
<span><strong>임계값:</strong> {{ $alert->threshold_value }}</span>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="7" class="px-4 py-8 text-center text-gray-500">
|
|
시스템 알림이 없습니다.
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- 페이지네이션 -->
|
|
@if($alerts->hasPages())
|
|
<div class="px-4 py-3 border-t">
|
|
{{ $alerts->links() }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- AI 분석용 데이터 -->
|
|
@foreach($alerts as $alert)
|
|
<textarea id="ai-analysis-{{ $alert->id }}" class="hidden">{{ $alert->ai_analysis_summary }}</textarea>
|
|
@endforeach
|
|
|
|
<script>
|
|
function toggleDetail(id) {
|
|
const detail = document.getElementById('detail-' + id);
|
|
if (detail) {
|
|
detail.classList.toggle('hidden');
|
|
}
|
|
}
|
|
|
|
function copyAiAnalysis(id) {
|
|
const textarea = document.getElementById('ai-analysis-' + id);
|
|
if (textarea) {
|
|
navigator.clipboard.writeText(textarea.value).then(() => {
|
|
alert('AI 분석용 내용이 클립보드에 복사되었습니다.\nClaude나 ChatGPT에 붙여넣기 하세요.');
|
|
}).catch(err => {
|
|
console.error('복사 실패:', err);
|
|
alert('복사에 실패했습니다.');
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
@endsection |