339 lines
16 KiB
PHP
339 lines
16 KiB
PHP
{{-- resources/views/admin/log/MemberPasswdModifyLogController.blade.php --}}
|
|
@extends('admin.layouts.app')
|
|
|
|
@section('title', '비밀번호 변경 로그')
|
|
@section('page_title', '비밀번호 변경 로그')
|
|
@section('page_desc', '로그인/2차 비밀번호 변경 및 비밀번호 찾기 변경 이력을 조회합니다.')
|
|
@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:160px;}
|
|
.filters .inpWide{width:220px;}
|
|
.filters .sel{width:140px;}
|
|
|
|
.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;}
|
|
|
|
.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;}
|
|
.ellipsis{max-width:360px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;vertical-align:bottom;}
|
|
|
|
.badge{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:12px;
|
|
border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.06);}
|
|
.badge--ok{border-color:rgba(34,197,94,.35);background:rgba(34,197,94,.10);}
|
|
.badge--warn{border-color:rgba(245,158,11,.35);background:rgba(245,158,11,.10);}
|
|
|
|
/* mem link white */
|
|
a.memlink{color:#fff;text-decoration:none;}
|
|
a.memlink:hover{color:#fff;text-decoration:underline;}
|
|
|
|
details{border:1px solid rgba(255,255,255,.10);border-radius:12px;background:rgba(255,255,255,.03);padding:10px;}
|
|
details summary{cursor:pointer;user-select:none;font-weight:800;font-size:12px;opacity:.9;}
|
|
pre{margin:10px 0 0;white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;
|
|
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;}
|
|
.kv{display:grid;grid-template-columns:160px 1fr;gap:8px;margin-top:10px;}
|
|
.kv .k{opacity:.75;font-size:12px;}
|
|
.kv .v{font-size:12px;word-break:break-word;}
|
|
|
|
/* json 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(980px, 92vw);max-height:88vh;overflow:hidden;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;overflow:auto;max-height:70vh;}
|
|
.modalx__foot{display:flex;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid rgba(255,255,255,.10);}
|
|
|
|
.kv{display:grid;grid-template-columns:170px 1fr;gap:8px;margin-bottom:12px;}
|
|
.kv .k{opacity:.75;font-size:12px;}
|
|
.kv .v{font-size:12px;word-break:break-all;} /* auth_key 같은 긴 문자열 깨짐 방지 */
|
|
.prebox{border:1px solid rgba(255,255,255,.10);border-radius:14px;background:rgba(255,255,255,.03);padding:12px;}
|
|
.prebox pre{margin:0;white-space:pre-wrap;word-break:break-word;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.member-passwd-modify-logs', [], false);
|
|
|
|
$f = $filters ?? [];
|
|
$stateMap = $stateMap ?? ['S'=>'직접변경','E'=>'비번찾기'];
|
|
|
|
$dateFrom = (string)($f['date_from'] ?? '');
|
|
$dateTo = (string)($f['date_to'] ?? '');
|
|
$state = (string)($f['state'] ?? '');
|
|
$memNo = (string)($f['mem_no'] ?? '');
|
|
$type = (string)($f['type'] ?? '');
|
|
$email = (string)($f['email'] ?? '');
|
|
$ip = (string)($f['ip'] ?? '');
|
|
$q = (string)($f['q'] ?? '');
|
|
@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;">구/신 JSON 혼재 → 핵심 필드 정규화 + 원본 JSON 펼쳐보기</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">state</label>
|
|
<select class="a-input sel" name="state">
|
|
<option value="">전체</option>
|
|
<option value="S" {{ $state==='S'?'selected':'' }}>직접변경(S)</option>
|
|
<option value="E" {{ $state==='E'?'selected':'' }}>비번찾기(E)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="muted">mem_no</label>
|
|
<input class="a-input inp" name="mem_no" value="{{ $memNo }}" inputmode="numeric" placeholder="5359">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="muted">type(신포맷)</label>
|
|
<input class="a-input inpWide" name="type" value="{{ $type }}" placeholder="로그인비밀번호변경 / 2차비밀번호변경">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="muted">email(구포맷)</label>
|
|
<input class="a-input inpWide" name="email" value="{{ $email }}" placeholder="naver.com">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="muted">ip</label>
|
|
<input class="a-input inp" name="ip" value="{{ $ip }}" placeholder="210.96.">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="muted">q(info 검색)</label>
|
|
<input class="a-input inpWide" name="q" value="{{ $q }}" placeholder="agent / auth_key ...">
|
|
</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:1300px;">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:90px;">SEQ</th>
|
|
<th style="width:190px;">rgdate</th>
|
|
<th style="width:130px;">state</th>
|
|
<th style="width:140px;">mem_no</th>
|
|
<th style="width:200px;">type</th>
|
|
<th style="width:220px;">ip</th>
|
|
<th style="width:240px;">email</th>
|
|
<th>JSON</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@forelse(($items ?? []) as $r0)
|
|
@php
|
|
$r = is_array($r0) ? $r0 : (array)$r0;
|
|
$seq = (int)($r['seq'] ?? 0);
|
|
|
|
$memNoInt = (int)($r['mem_no_int'] ?? 0);
|
|
$memLink = $r['mem_link'] ?? null;
|
|
|
|
$agent = (string)($r['agent_norm'] ?? '');
|
|
$emailN = (string)($r['email_norm'] ?? '');
|
|
$ipN = (string)($r['ip_norm'] ?? '');
|
|
|
|
$authKey = (string)($r['auth_key'] ?? '');
|
|
$authEff = (string)($r['auth_effective_time'] ?? '');
|
|
@endphp
|
|
<tr>
|
|
<td class="a-muted">{{ $seq }}</td>
|
|
<td class="a-muted nowrap">{{ $r['rgdate'] ?? '-' }}</td>
|
|
|
|
<td>
|
|
<span class="badge {{ $r['state_badge'] ?? 'badge--warn' }}">
|
|
{{ $r['state_label'] ?? ($stateMap[$r['state'] ?? ''] ?? '-') }}
|
|
</span>
|
|
</td>
|
|
|
|
<td>
|
|
@if($memNoInt > 0 && $memLink)
|
|
<a href="{{ $memLink }}" class="mono memlink" target="_blank" rel="noopener">{{ $memNoInt }}</a>
|
|
@else
|
|
<span class="mono">-</span>
|
|
@endif
|
|
</td>
|
|
|
|
<td><span class="mono">{{ $r['event_type'] ?? '-' }}</span></td>
|
|
|
|
<td class="nowrap">
|
|
@if($ipN !== '')
|
|
<span class="mono">{{ $ipN }}</span>
|
|
@else
|
|
<span class="a-muted">-</span>
|
|
@endif
|
|
</td>
|
|
|
|
<td>
|
|
@if($emailN !== '')
|
|
<span class="mono">{{ $emailN }}</span>
|
|
@else
|
|
<span class="a-muted">-</span>
|
|
@endif
|
|
</td>
|
|
|
|
@php
|
|
$agent = (string)($r['agent_norm'] ?? '');
|
|
$authKey = (string)($r['auth_key'] ?? '');
|
|
$authEff = (string)($r['auth_effective_time'] ?? '');
|
|
@endphp
|
|
|
|
<td class="nowrap" style="text-align:right;">
|
|
<button type="button"
|
|
class="lbtn lbtn--ghost lbtn--sm btnJson"
|
|
data-seq="{{ $seq }}"
|
|
data-agent="{{ e($agent) }}"
|
|
data-auth-key="{{ e($authKey) }}"
|
|
data-auth-eff="{{ e($authEff) }}">
|
|
JSON 보기
|
|
</button>
|
|
|
|
{{-- AJAX 없이: row별 JSON을 숨겨두고 모달에서 꺼내씀 --}}
|
|
<textarea id="jsonStore-{{ $seq }}" style="display:none;">{{ $r['info_pretty'] ?? ($r['info'] ?? '') }}</textarea>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr><td colspan="9" 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>
|
|
|
|
<div class="mback" id="jsonModalBack" aria-hidden="true">
|
|
<div class="modalx" role="dialog" aria-modal="true" aria-labelledby="jsonModalTitle">
|
|
<div class="modalx__head">
|
|
<div>
|
|
<div class="modalx__title" id="jsonModalTitle">JSON</div>
|
|
<div class="modalx__desc" id="jsonModalDesc">로그 상세 JSON</div>
|
|
</div>
|
|
<button class="lbtn lbtn--ghost lbtn--sm" type="button" id="btnJsonClose">닫기 ✕</button>
|
|
</div>
|
|
|
|
<div class="modalx__body">
|
|
<div class="kv" id="jsonModalKv"></div>
|
|
<div class="prebox">
|
|
<pre id="jsonModalPre"></pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modalx__foot">
|
|
<button class="lbtn lbtn--ghost" type="button" id="btnJsonCopy">복사</button>
|
|
<button class="lbtn lbtn--primary" type="button" id="btnJsonOk">확인</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
(function(){
|
|
const back = document.getElementById('jsonModalBack');
|
|
const pre = document.getElementById('jsonModalPre');
|
|
const kv = document.getElementById('jsonModalKv');
|
|
const btnClose = document.getElementById('btnJsonClose');
|
|
const btnOk = document.getElementById('btnJsonOk');
|
|
const btnCopy = document.getElementById('btnJsonCopy');
|
|
|
|
const open = () => {
|
|
back.classList.add('is-open');
|
|
back.setAttribute('aria-hidden', 'false');
|
|
document.body.style.overflow = 'hidden';
|
|
};
|
|
const close = () => {
|
|
back.classList.remove('is-open');
|
|
back.setAttribute('aria-hidden', 'true');
|
|
document.body.style.overflow = '';
|
|
};
|
|
|
|
btnClose?.addEventListener('click', close);
|
|
btnOk?.addEventListener('click', close);
|
|
back?.addEventListener('click', (e)=>{ if(e.target === back) close(); });
|
|
document.addEventListener('keydown', (e)=>{ if(e.key === 'Escape' && back.classList.contains('is-open')) close(); });
|
|
|
|
btnCopy?.addEventListener('click', async () => {
|
|
const text = pre.textContent || '';
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
btnCopy.textContent = '복사됨';
|
|
setTimeout(()=>btnCopy.textContent='복사', 900);
|
|
} catch (e) {
|
|
alert('복사를 지원하지 않는 환경입니다.');
|
|
}
|
|
});
|
|
|
|
const renderKv = (pairs) => {
|
|
kv.innerHTML = '';
|
|
pairs.filter(p => p.v && String(p.v).trim() !== '').forEach(p => {
|
|
const k = document.createElement('div'); k.className='k'; k.textContent=p.k;
|
|
const v = document.createElement('div'); v.className='v';
|
|
const span = document.createElement('span'); span.className='mono'; span.textContent=p.v;
|
|
v.appendChild(span);
|
|
kv.appendChild(k); kv.appendChild(v);
|
|
});
|
|
};
|
|
|
|
document.querySelectorAll('.btnJson').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const seq = btn.getAttribute('data-seq');
|
|
if (!seq) return;
|
|
|
|
const ta = document.getElementById('jsonStore-' + seq);
|
|
const raw = ta ? ta.value : '';
|
|
|
|
const agent = btn.getAttribute('data-agent') || '';
|
|
const authKey = btn.getAttribute('data-auth-key') || '';
|
|
const authEff = btn.getAttribute('data-auth-eff') || '';
|
|
|
|
renderKv([
|
|
{k:'agent', v: agent},
|
|
{k:'auth_key', v: authKey},
|
|
{k:'auth_effective_time', v: authEff},
|
|
]);
|
|
|
|
pre.textContent = raw || '';
|
|
open();
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
@endsection
|