giftcon_dev/resources/views/admin/log/AdminAuditLogController.blade.php

282 lines
14 KiB
PHP

{{-- resources/views/admin/log/AdminAuditLogController.blade.php --}}
@extends('admin.layouts.app')
@section('title', '관리자 감사 로그')
@section('page_title', '관리자 감사 로그')
@section('page_desc', '관리자 행위 감사로그를 조회합니다. (상세는 모달)')
@section('content_class', 'a-content--full')
@push('head')
<style>
.bar{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap;}
.filters{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end;}
.filters .inp{width:170px;}
.filters .inpWide{width:220px;}
.lbtn{padding:8px 12px;font-size:13px;border-radius:12px;line-height:1.1;text-decoration:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;
border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.06);color:inherit;cursor:pointer;}
.lbtn:hover{background:rgba(255,255,255,.10);text-decoration:none;}
.lbtn--ghost{background:transparent;}
.lbtn--sm{padding:7px 10px;font-size:12px;border-radius:11px;}
.mono{padding:4px 8px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.10);
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px;display:inline-block;}
.muted{opacity:.8;font-size:12px;}
.nowrap{white-space:nowrap;}
.lbtn--primary{background:rgba(59,130,246,.88);border-color:rgba(59,130,246,.95);color:#fff;}
.lbtn--primary:hover{background:rgba(59,130,246,.98);}
/* modal */
.mback{position:fixed;inset:0;display:none;align-items:center;justify-content:center;z-index:9999;
background:rgba(0,0,0,.55);backdrop-filter:blur(2px);}
.mback.is-open{display:flex;}
.modalx{width:min(1100px, 94vw);max-height:88vh;overflow:auto;border-radius:18px;
border:1px solid rgba(255,255,255,.12);background:rgba(18,18,18,.96);box-shadow:0 20px 60px rgba(0,0,0,.45);}
.modalx__head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;padding:16px 16px 10px;border-bottom:1px solid rgba(255,255,255,.10);}
.modalx__title{font-weight:900;font-size:16px;}
.modalx__desc{font-size:12px;opacity:.8;margin-top:4px;}
.modalx__body{padding:16px;}
.modalx__foot{display:flex;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid rgba(255,255,255,.10);}
.infoGrid{display:grid;grid-template-columns:1fr;gap:10px;margin-bottom:12px;}
@media (min-width: 920px){ .infoGrid{grid-template-columns:1fr 1fr;} }
.ibox{border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.03);border-radius:14px;padding:10px;}
.ibox .k{font-size:12px;opacity:.75;margin-bottom:4px;}
.ibox .v{font-size:13px;word-break:break-word;}
.jsonGrid{display:grid;grid-template-columns:1fr;gap:12px;}
@media (min-width: 1000px){ .jsonGrid{grid-template-columns:1fr 1fr;} }
.prebox{border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.03);border-radius:14px;padding:10px;overflow:auto;}
.prebox .k{font-size:12px;opacity:.75;margin-bottom:8px;font-weight:800;}
pre{margin:0;white-space:pre; font-size:12px; line-height:1.5;
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;}
</style>
@endpush
@section('content')
@php
$indexUrl = route('admin.systemlog.admin-audit-logs', [], false);
$showTpl = route('admin.systemlog.admin-audit-logs.show', ['id' => '__ID__'], false);
$f = $filters ?? [];
$dateFrom = (string)($f['date_from'] ?? '');
$dateTo = (string)($f['date_to'] ?? '');
$actorQ = (string)($f['actor_q'] ?? '');
$action = (string)($f['action'] ?? '');
$tt = (string)($f['target_type'] ?? '');
$ip = (string)($f['ip'] ?? '');
@endphp
<div class="a-card" style="padding:16px; margin-bottom:16px;">
<div class="bar">
<div>
<div style="font-weight:900;font-size:16px;">관리자 감사 로그</div>
<div class="muted" style="margin-top:4px;">검색/페이징 지원 · 상세는 AJAX 모달</div>
</div>
<form method="GET" action="{{ $indexUrl }}" class="filters">
<div>
<label class="muted">From</label>
<input class="a-input inp" type="date" name="date_from" value="{{ $dateFrom }}">
</div>
<div>
<label class="muted">To</label>
<input class="a-input inp" type="date" name="date_to" value="{{ $dateTo }}">
</div>
<div>
<label class="muted">Actor(이메일/이름)</label>
<input class="a-input inpWide" name="actor_q" value="{{ $actorQ }}" placeholder="ex) sungro / gmail">
</div>
<div>
<label class="muted">action</label>
<input class="a-input inp" name="action" value="{{ $action }}" placeholder="ex) admin_user_update">
</div>
<div>
<label class="muted">target_type</label>
<input class="a-input inp" name="target_type" value="{{ $tt }}" placeholder="ex) admin_users">
</div>
<div>
<label class="muted">ip</label>
<input class="a-input inp" name="ip" value="{{ $ip }}" placeholder="ex) 210.96.">
</div>
<div style="display:flex;gap:8px;align-items:flex-end;">
<button class="lbtn lbtn--ghost" type="submit">검색</button>
<a class="lbtn lbtn--ghost" href="{{ $indexUrl }}">초기화</a>
</div>
</form>
</div>
</div>
<div class="a-card" style="padding:16px;">
<div class="a-muted" style="margin-bottom:10px;"> <b>{{ $page->total() }}</b></div>
<div style="overflow:auto;">
<table class="a-table table" style="width:100%; min-width:1050px;">
<thead>
<tr>
<th style="width:90px;">ID</th>
<th style="width:260px;">Actor</th>
<th style="width:220px;">Action</th>
<th style="width:200px;">Target Type</th>
<th style="width:180px;">IP</th>
<th style="width:190px;">Created</th>
<th style="width:120px;">상세</th>
</tr>
</thead>
<tbody>
@forelse(($items ?? []) as $r0)
@php
$r = is_array($r0) ? $r0 : (array)$r0;
$id = (int)($r['id'] ?? 0);
$aEmail = (string)($r['actor_email'] ?? '');
$aName = (string)($r['actor_name'] ?? '');
$actorTxt = trim(($aName !== '' ? $aName : '-') . ($aEmail !== '' ? " ({$aEmail})" : ''));
@endphp
<tr>
<td class="a-muted">{{ $id }}</td>
<td>
<div style="font-weight:900;">{{ $aName !== '' ? $aName : '-' }}</div>
<div class="a-muted" style="font-size:12px;">{{ $aEmail !== '' ? $aEmail : 'admin_users 미조회' }}</div>
</td>
<td><span class="mono">{{ $r['action'] ?? '-' }}</span></td>
<td><span class="mono">{{ $r['target_type'] ?? '-' }}</span></td>
<td class="nowrap">{{ $r['ip'] ?? '-' }}</td>
<td class="a-muted">{{ $r['created_at'] ?? '-' }}</td>
<td style="text-align:right;">
<button type="button"
class="lbtn lbtn--sm btnAuditDetail"
data-id="{{ $id }}">
상세보기
</button>
</td>
</tr>
@empty
<tr><td colspan="7" class="a-muted" style="padding:16px;">데이터가 없습니다.</td></tr>
@endforelse
</tbody>
</table>
</div>
<div style="margin-top:12px;">
{{ $page->onEachSide(1)->links('vendor.pagination.admin') }}
</div>
</div>
@include('admin.log._audit_log_modal')
<script>
(function(){
const showTpl = @json($showTpl);
const modalBack = document.getElementById('auditModalBack');
const btnClose = document.getElementById('auditBtnClose');
const btnOk = document.getElementById('auditBtnOk');
const el = (id) => document.getElementById(id);
const setText = (node, text) => {
if (!node) return;
node.textContent = (text === null || text === undefined || text === '') ? '-' : String(text);
};
const openModal = () => {
modalBack.classList.add('is-open');
modalBack.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
};
const closeModal = () => {
modalBack.classList.remove('is-open');
modalBack.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
};
btnClose?.addEventListener('click', closeModal);
btnOk?.addEventListener('click', closeModal);
modalBack?.addEventListener('click', (e) => { if (e.target === modalBack) closeModal(); });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modalBack.classList.contains('is-open')) closeModal(); });
const fillLoading = () => {
setText(el('auditTitle'), '감사로그 상세');
setText(el('auditDesc'), '로딩 중...');
setText(el('audit_id'), '-');
setText(el('audit_actor'), '-');
setText(el('audit_action'), '-');
setText(el('audit_target'), '-');
setText(el('audit_ip'), '-');
setText(el('audit_created'), '-');
setText(el('audit_ua'), '-');
setText(el('audit_before'), '로딩 중...');
setText(el('audit_after'), '로딩 중...');
};
const fillItem = (item) => {
const actor = `${item.actor_name || '-'} (${item.actor_email || '-'}) [#${item.actor_admin_user_id || 0}]`;
setText(el('auditTitle'), `감사로그 #${item.id}`);
setText(el('auditDesc'), 'before/after JSON과 user_agent를 확인하세요.');
setText(el('audit_id'), item.id);
setText(el('audit_actor'), actor);
setText(el('audit_action'), item.action);
setText(el('audit_target'), `${item.target_type} #${item.target_id}`);
setText(el('audit_ip'), item.ip);
setText(el('audit_created'), item.created_at);
setText(el('audit_ua'), item.user_agent);
setText(el('audit_before'), item.before_pretty);
setText(el('audit_after'), item.after_pretty);
};
async function loadDetail(id){
const url = showTpl.replace('__ID__', encodeURIComponent(id));
const res = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
credentials: 'same-origin',
});
if (res.status === 401 || res.status === 419) throw new Error('AUTH');
if (res.status === 403) throw new Error('FORBIDDEN');
if (!res.ok) throw new Error('HTTP_' + res.status);
const ct = (res.headers.get('content-type') || '').toLowerCase();
if (!ct.includes('application/json')) throw new Error('NOT_JSON');
const payload = await res.json();
if (!payload?.ok || !payload?.item) throw new Error('NOT_FOUND');
return payload.item;
}
document.querySelectorAll('.btnAuditDetail').forEach(btn => {
btn.addEventListener('click', async () => {
const id = btn.getAttribute('data-id');
if (!id) return;
try {
btn.disabled = true;
fillLoading();
openModal();
const item = await loadDetail(id);
fillItem(item);
} catch (e) {
if (String(e.message) === 'AUTH') alert('세션이 만료되었습니다. 다시 로그인해 주세요.');
else if (String(e.message) === 'FORBIDDEN') alert('권한이 없습니다. (super_admin만 접근)');
else if (String(e.message) === 'NOT_JSON') alert('응답이 JSON이 아닙니다. (프록시/APP_URL/리다이렉트 가능)');
else alert('상세 정보를 불러오지 못했습니다.');
closeModal();
} finally {
btn.disabled = false;
}
});
});
})();
</script>
@endsection