276 lines
12 KiB
PHP
276 lines
12 KiB
PHP
@extends('admin.layouts.app')
|
|
|
|
@section('title', '관리자 정보 수정')
|
|
@section('page_title', '관리자 정보 수정')
|
|
@section('page_desc', '기본정보/상태/2FA/역할을 관리합니다.')
|
|
|
|
@push('head')
|
|
<style>
|
|
/* admins edit only */
|
|
.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--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);}
|
|
.lbtn--danger{background:rgba(244,63,94,.88);border-color:rgba(244,63,94,.95);color:#fff;}
|
|
.lbtn--danger:hover{background:rgba(244,63,94,.98);}
|
|
.lbtn--ghost{background:transparent;}
|
|
.lbtn--sm{padding:7px 10px;font-size:12px;border-radius:11px;}
|
|
.lbtn--wide{padding:10px 14px;font-weight:800;}
|
|
|
|
.pill{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);}
|
|
.pill--ok{border-color:rgba(34,197,94,.35);background:rgba(34,197,94,.12);}
|
|
.pill--bad{border-color:rgba(244,63,94,.35);background:rgba(244,63,94,.10);}
|
|
.pill--warn{border-color:rgba(245,158,11,.35);background:rgba(245,158,11,.12);}
|
|
.pill--muted{opacity:.9;}
|
|
.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;}
|
|
.kvgrid{display:grid;grid-template-columns:1fr;gap:12px;}
|
|
@media (min-width: 980px){ .kvgrid{grid-template-columns:1fr 1fr 1fr;} }
|
|
.kv{padding:14px;border-radius:16px;border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.04);}
|
|
.kv .k{font-size:12px;opacity:.8;margin-bottom:6px;}
|
|
.kv .v{font-weight:900;}
|
|
.actions{position:sticky;bottom:10px;z-index:5;margin-top:12px;
|
|
display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;align-items:center;
|
|
padding:12px;border-radius:16px;border:1px solid rgba(255,255,255,.10);background:rgba(0,0,0,.25);backdrop-filter:blur(10px);}
|
|
.actions__right{display:flex;gap:8px;flex-wrap:wrap;align-items:center;}
|
|
.checks{display:flex;flex-wrap:wrap;gap:10px;}
|
|
</style>
|
|
@endpush
|
|
|
|
@section('content')
|
|
@php
|
|
$ip = !empty($admin->last_login_ip) ? inet_ntop($admin->last_login_ip) : '-';
|
|
|
|
$lockedUntil = $admin->locked_until ?? null;
|
|
$isLocked = false;
|
|
if (!empty($lockedUntil)) {
|
|
try { $isLocked = \Carbon\Carbon::parse($lockedUntil)->isFuture(); }
|
|
catch (\Throwable $e) { $isLocked = true; }
|
|
}
|
|
|
|
$st = (string)($admin->status ?? 'active');
|
|
$statusLabel = $st === 'active' ? '활성' : '비활성';
|
|
$statusPill = $st === 'active' ? 'pill--ok' : 'pill--bad';
|
|
|
|
$hasSecret = !empty($admin->totp_secret_enc);
|
|
$isModeOtp = (int)($admin->totp_enabled ?? 0) === 1;
|
|
@endphp
|
|
|
|
{{-- 요약 카드 --}}
|
|
<div class="a-card" style="padding:16px; margin-bottom:16px;">
|
|
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px; flex-wrap:wrap;">
|
|
<div>
|
|
<div style="font-weight:900; font-size:16px;">관리자 정보 수정</div>
|
|
<div class="a-muted" style="font-size:12px; margin-top:4px;">
|
|
#{{ $admin->id ?? '-' }} / {{ $admin->email ?? '-' }}
|
|
</div>
|
|
</div>
|
|
|
|
<a class="lbtn lbtn--ghost lbtn--sm"
|
|
href="{{ route('admin.admins.index', request()->only(['q','status','page'])) }}">
|
|
← 목록
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- KV 그리드 --}}
|
|
<div class="kvgrid" style="margin-bottom:16px;">
|
|
<div class="kv">
|
|
<div class="k">상태</div>
|
|
<div class="v">
|
|
<span class="pill {{ $statusPill }}">● {{ $statusLabel }}</span>
|
|
@if($st !== 'active')
|
|
<span class="a-muted" style="margin-left:6px; font-size:12px;">(로그인 불가)</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<div class="kv">
|
|
<div class="k">계정 잠금</div>
|
|
<div class="v">
|
|
@if($isLocked)
|
|
<span class="pill pill--bad">● 잠김</span>
|
|
<div class="a-muted" style="font-size:12px; margin-top:6px;">{{ $admin->locked_until }}</div>
|
|
@else
|
|
<span class="pill pill--ok">● 정상</span>
|
|
<div class="a-muted" style="font-size:12px; margin-top:6px;">※ 3회 실패 시 잠김</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<div class="kv">
|
|
<div class="k">로그인 실패 횟수</div>
|
|
<div class="v">{{ (int)($admin->failed_login_count ?? 0) }}</div>
|
|
</div>
|
|
|
|
<div class="kv">
|
|
<div class="k">최근 로그인 IP</div>
|
|
<div class="v"><span class="mono">{{ $ip }}</span></div>
|
|
</div>
|
|
|
|
<div class="kv">
|
|
<div class="k">최근 로그인 시간</div>
|
|
<div class="v">{{ $admin->last_login_at ?? '-' }}</div>
|
|
</div>
|
|
|
|
<div class="kv">
|
|
<div class="k">2FA</div>
|
|
<div class="v">
|
|
@if($hasSecret)
|
|
<span class="pill pill--ok">OTP 등록됨</span>
|
|
@else
|
|
<span class="pill pill--muted">OTP 미등록</span>
|
|
@endif
|
|
<span class="a-muted" style="margin-left:6px; font-size:12px;">
|
|
현재모드: <b>{{ $isModeOtp ? 'OTP' : 'SMS' }}</b>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="kv">
|
|
<div class="k">생성</div>
|
|
<div class="v">{{ $admin->created_at ?? '-' }}</div>
|
|
</div>
|
|
|
|
<div class="kv">
|
|
<div class="k">최근 수정</div>
|
|
<div class="v">{{ $admin->updated_at ?? '-' }}</div>
|
|
</div>
|
|
|
|
<div class="kv">
|
|
<div class="k">비활성 처리자</div>
|
|
<div class="v">{{ $admin->deleted_by ?? '-' }}</div>
|
|
</div>
|
|
|
|
<div class="kv">
|
|
<div class="k">현재 역할</div>
|
|
<div class="v">
|
|
@forelse($roles as $rr)
|
|
<span class="a-chip">{{ $rr['name'] ?? ($rr['code'] ?? '-') }}</span>
|
|
@empty
|
|
<span class="a-muted">-</span>
|
|
@endforelse
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 수정 폼 --}}
|
|
<form id="adminEditForm"
|
|
method="POST"
|
|
action="{{ route('admin.admins.update', ['id'=>$admin->id]) }}"
|
|
onsubmit="this.querySelector('button[type=submit][data-submit=save]')?.setAttribute('disabled','disabled');">
|
|
@csrf
|
|
|
|
<div class="a-card" style="padding:16px;">
|
|
<div style="display:grid; grid-template-columns:1fr; gap:12px; max-width:880px;">
|
|
<div class="a-field">
|
|
<label class="a-label">닉네임</label>
|
|
<input class="a-input" name="nickname" value="{{ old('nickname', $admin->nickname ?? '') }}">
|
|
</div>
|
|
|
|
<div class="a-field">
|
|
<label class="a-label">성명</label>
|
|
<input class="a-input" name="name" value="{{ old('name', $admin->name ?? '') }}">
|
|
</div>
|
|
|
|
<div class="a-field">
|
|
<label class="a-label">휴대폰 (숫자만 10~11자리)</label>
|
|
<input class="a-input" name="phone" value="{{ old('phone', $phone ?? '') }}" placeholder="01012345678">
|
|
<div class="a-muted" style="font-size:12px; margin-top:6px;">
|
|
※ 저장 시 phone_hash + phone_enc 갱신
|
|
</div>
|
|
</div>
|
|
|
|
<div class="a-field">
|
|
<label class="a-label">상태</label>
|
|
<select class="a-input" name="status">
|
|
@php $stSel = old('status', $admin->status ?? 'active'); @endphp
|
|
<option value="active" {{ $stSel==='active'?'selected':'' }}>활성</option>
|
|
<option value="blocked" {{ $stSel==='blocked'?'selected':'' }}>비활성</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="a-field">
|
|
<label class="a-label">
|
|
2차 인증방법
|
|
@if($hasSecret)
|
|
<span class="pill pill--ok" style="margin-left:6px;">OTP 등록</span>
|
|
@else
|
|
<span class="pill pill--muted" style="margin-left:6px;">OTP 미등록</span>
|
|
@endif
|
|
</label>
|
|
|
|
<select class="a-input" name="totp_enabled">
|
|
<option value="0" {{ (int)old('totp_enabled', $admin->totp_enabled ?? 0)===0 ? 'selected' : '' }}>SMS 인증</option>
|
|
<option value="1"
|
|
{{ (int)old('totp_enabled', $admin->totp_enabled ?? 0)===1 ? 'selected' : '' }}
|
|
{{ !$hasSecret ? 'disabled' : '' }}
|
|
>Google OTP 인증</option>
|
|
</select>
|
|
|
|
@if(!$hasSecret)
|
|
<div class="a-muted" style="font-size:12px; margin-top:6px;">
|
|
※ OTP 미등록 상태라 선택할 수 없습니다. (등록은 ‘내 정보’에서만 가능)
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="a-field">
|
|
<label class="a-label">역할(Role)</label>
|
|
<div class="a-muted" style="font-size:12px; margin-bottom:8px;">여러 개 선택 가능</div>
|
|
|
|
<div class="checks">
|
|
@foreach($allRoles as $r)
|
|
@php $rid = (int)$r['id']; @endphp
|
|
<label class="a-check" style="margin:0;">
|
|
<input type="checkbox" name="role_ids[]" value="{{ $rid }}"
|
|
{{ in_array($rid, $roleIds ?? [], true) ? 'checked' : '' }}>
|
|
<span>{{ $r['name'] ?? $r['code'] }}</span>
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
{{-- 하단 액션바 --}}
|
|
<div class="actions">
|
|
<a class="lbtn lbtn--ghost"
|
|
href="{{ route('admin.admins.index', request()->only(['q','status','page'])) }}">
|
|
← 뒤로가기
|
|
</a>
|
|
|
|
<div class="actions__right">
|
|
@if(!empty($admin->locked_until))
|
|
<form method="POST"
|
|
action="{{ route('admin.admins.unlock', $admin->id) }}"
|
|
style="display:inline;"
|
|
data-confirm="이 계정의 잠금을 해제할까요? (locked_until 초기화 + 실패횟수 0)"
|
|
onsubmit="this.querySelector('button[type=submit]')?.setAttribute('disabled','disabled');">
|
|
@csrf
|
|
<button class="lbtn lbtn--danger" type="submit">잠금해제</button>
|
|
</form>
|
|
@endif
|
|
|
|
<form method="POST"
|
|
action="{{ route('admin.admins.reset_password', $admin->id) }}"
|
|
style="display:inline;"
|
|
data-confirm="비밀번호를 초기화할까요? 임시 비밀번호는 이메일로 설정됩니다. (다음 로그인 시 변경 강제)"
|
|
onsubmit="this.querySelector('button[type=submit]')?.setAttribute('disabled','disabled');">
|
|
@csrf
|
|
<button class="lbtn lbtn--danger" type="submit">비밀번호 초기화</button>
|
|
</form>
|
|
|
|
<button class="lbtn lbtn--primary lbtn--wide"
|
|
form="adminEditForm"
|
|
type="submit"
|
|
data-submit="save">
|
|
저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@endsection
|