346 lines
14 KiB
PHP
346 lines
14 KiB
PHP
@extends('admin.layouts.app')
|
|
|
|
@section('title', '회원 관리')
|
|
@section('page_title', '회원 관리')
|
|
@section('page_desc', '회원상태/기본정보/인증을 조회합니다.')
|
|
@section('content_class', 'a-content--full')
|
|
|
|
@push('head')
|
|
<style>
|
|
/* members index only */
|
|
.bar{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap;}
|
|
.bar__left .t{font-weight:900;font-size:16px;}
|
|
.bar__left .d{font-size:12px;margin-top:4px;}
|
|
.bar__right{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;}
|
|
|
|
.filters{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end;}
|
|
.filters .q{width:260px;}
|
|
.filters .qf{width:140px;}
|
|
.filters .st{width:220px;}
|
|
.filters .dt{width:150px;}
|
|
|
|
.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;}
|
|
.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);}
|
|
|
|
.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;}
|
|
.table td{vertical-align:top;}
|
|
.badges{display:flex;gap:6px;flex-wrap:wrap;}
|
|
.nameLine{font-weight:900;}
|
|
.ageBox{display:flex;gap:8px;flex-wrap:wrap;align-items:center;}
|
|
.ageChip{padding:4px 8px;border-radius:999px;border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.06);font-size:12px;}
|
|
</style>
|
|
@endpush
|
|
|
|
@section('content')
|
|
@php
|
|
// stat_3 매핑 (1~6)
|
|
$stat3Map = $stat3Map ?? [
|
|
'1' => '1. 로그인정상',
|
|
'2' => '2. 로그인만가능',
|
|
'3' => '3. 로그인불가(접근금지)',
|
|
'4' => '4. 탈퇴완료(아이디보관)',
|
|
'5' => '5. 탈퇴완료',
|
|
'6' => '6. 휴면회원',
|
|
];
|
|
|
|
// stat_3 pill 색상
|
|
$stat3Pill = function(string $s3): string {
|
|
return match ($s3) {
|
|
'1' => 'pill--ok',
|
|
'2' => 'pill--warn',
|
|
'3' => 'pill--bad',
|
|
'4', '5' => 'pill--muted',
|
|
'6' => 'pill--warn',
|
|
default => 'pill--muted',
|
|
};
|
|
};
|
|
|
|
// 성별 라벨
|
|
$genderLabel = function(?string $g): string {
|
|
$g = (string)($g ?? '');
|
|
return match ($g) {
|
|
'1' => '남자',
|
|
'0' => '여자',
|
|
default => '-',
|
|
};
|
|
};
|
|
|
|
// 세는나이: 현재년도 - 출생년도 + 1
|
|
$koreanAge = function($birth): ?int {
|
|
$b = (string)($birth ?? '');
|
|
if ($b === '' || $b === '0000-00-00') return null;
|
|
try {
|
|
$y = \Carbon\Carbon::parse($b)->year;
|
|
if ($y < 1900) return null;
|
|
return (int) now()->year - $y + 1;
|
|
} catch (\Throwable $e) {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// 만나이: 생일 지났으면 diffInYears 그대로, 아니면 -1 반영되는 Carbon diffInYears 사용
|
|
$manAge = function($birth): ?int {
|
|
$b = (string)($birth ?? '');
|
|
if ($b === '' || $b === '0000-00-00') return null;
|
|
try {
|
|
$dob = \Carbon\Carbon::parse($b);
|
|
if ($dob->year < 1900) return null;
|
|
return (int) $dob->diffInYears(now()); // full years
|
|
} catch (\Throwable $e) {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// authMap 어떤 형태든 Y만 뽑기 (email/cell/account만, vow/otp는 제외)
|
|
$pickAuthOk = function($auth): array {
|
|
$out = ['email'=>false,'cell'=>false,'account'=>false];
|
|
|
|
$stateOf = function($v): string {
|
|
if (is_string($v)) return $v;
|
|
if (is_array($v)) {
|
|
if (isset($v['auth_state'])) return (string)$v['auth_state'];
|
|
if (isset($v['state'])) return (string)$v['state'];
|
|
return '';
|
|
}
|
|
if (is_object($v)) {
|
|
if (isset($v->auth_state)) return (string)$v->auth_state;
|
|
if (isset($v->state)) return (string)$v->state;
|
|
return '';
|
|
}
|
|
return '';
|
|
};
|
|
|
|
// assoc
|
|
if (is_array($auth) && (array_key_exists('email',$auth) || array_key_exists('cell',$auth) || array_key_exists('account',$auth))) {
|
|
foreach (['email','cell','account'] as $t) {
|
|
$out[$t] = ($stateOf($auth[$t] ?? '') === 'Y');
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
// rows list
|
|
if (is_array($auth)) {
|
|
foreach ($auth as $row) {
|
|
$type = '';
|
|
$state = '';
|
|
if (is_array($row)) {
|
|
$type = (string)($row['auth_type'] ?? $row['type'] ?? '');
|
|
$state = (string)($row['auth_state'] ?? $row['state'] ?? '');
|
|
} elseif (is_object($row)) {
|
|
$type = (string)($row->auth_type ?? $row->type ?? '');
|
|
$state = (string)($row->auth_state ?? $row->state ?? '');
|
|
}
|
|
if (in_array($type, ['email','cell','account'], true) && $state === 'Y') {
|
|
$out[$type] = true;
|
|
}
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
return $out;
|
|
};
|
|
|
|
$qf = (string)($filters['qf'] ?? 'all');
|
|
@endphp
|
|
|
|
<div class="a-card" style="padding:16px; margin-bottom:16px;">
|
|
<div class="bar">
|
|
<div class="bar__left">
|
|
<div class="t">회원 관리</div>
|
|
<div class="a-muted d">회원상태/기본정보/인증을 조회합니다.</div>
|
|
</div>
|
|
|
|
<div class="bar__right">
|
|
<form method="GET" action="{{ route('admin.members.index') }}" class="filters">
|
|
<div>
|
|
<select class="a-input qf" name="qf" id="qf">
|
|
<option value="mem_no" {{ $qf==='mem_no'?'selected':'' }}>회원번호</option>
|
|
<option value="name" {{ $qf==='name'?'selected':'' }}>이름</option>
|
|
<option value="email" {{ $qf==='email'?'selected':'' }}>이메일</option>
|
|
<option value="phone" {{ $qf==='phone'?'selected':'' }}>휴대폰</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<input class="a-input q"
|
|
id="q"
|
|
name="q"
|
|
value="{{ $filters['q'] ?? '' }}"
|
|
placeholder="회원번호/이름/이메일/휴대폰">
|
|
</div>
|
|
|
|
<div>
|
|
<select class="a-input st" name="stat_3">
|
|
<option value="">회원상태 전체</option>
|
|
@foreach($stat3Map as $k=>$label)
|
|
<option value="{{ $k }}" {{ (($filters['stat_3'] ?? '')===(string)$k)?'selected':'' }}>
|
|
{{ $label }}
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
<div><input class="a-input dt" type="date" name="date_from" value="{{ $filters['date_from'] ?? '' }}"></div>
|
|
<div><input class="a-input dt" type="date" name="date_to" value="{{ $filters['date_to'] ?? '' }}"></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="{{ route('admin.members.index') }}">초기화</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</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:1100px;">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:90px;">MEM_NO</th>
|
|
<th style="width:220px;">성명</th>
|
|
<th style="width:180px;">성별/나이</th>
|
|
<th>이메일</th>
|
|
<th style="width:200px;">회원상태</th>
|
|
<th style="width:240px;">인증(Y만)</th>
|
|
<th style="width:180px;">가입일</th>
|
|
<th style="width:90px; text-align:right;">관리</th>
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody>
|
|
@forelse($page as $m)
|
|
@php
|
|
$no = (int)($m->mem_no ?? 0);
|
|
|
|
$gender = $genderLabel($m->gender ?? null);
|
|
$ageK = $koreanAge($m->birth ?? null);
|
|
$ageM = $manAge($m->birth ?? null);
|
|
|
|
$s3 = (string)($m->stat_3 ?? '');
|
|
$s3Label = $stat3Map[$s3] ?? ($s3 !== '' ? $s3 : '-');
|
|
$s3Pill = $stat3Pill($s3);
|
|
|
|
$authRaw = $authMap[$no] ?? ($authMap[(string)$no] ?? null);
|
|
$ok = $pickAuthOk($authRaw);
|
|
|
|
$joinAt = $m->dt_reg ?? '-';
|
|
@endphp
|
|
|
|
<tr>
|
|
<td class="a-muted">{{ $no }}</td>
|
|
|
|
{{-- 성명 --}}
|
|
<td><div class="nameLine">{{ $m->name ?? '-' }}</div></td>
|
|
|
|
{{-- 성별/나이(세는나이 + 만나이) --}}
|
|
<td>
|
|
<div class="ageBox">
|
|
<span class="mono">{{ $gender }}</span>
|
|
@if($ageK !== null || $ageM !== null)
|
|
<span class="ageChip">
|
|
{{ $ageK !== null ? "{$ageK}세" : '-' }}
|
|
<span class="a-muted" style="margin-left:6px;">(만 {{ $ageM !== null ? "{$ageM}세" : '-' }})</span>
|
|
</span>
|
|
@else
|
|
<span class="a-muted">-</span>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
|
|
{{-- 이메일 --}}
|
|
<td>
|
|
@if(!empty($m->email))
|
|
<span class="mono">{{ $m->email }}</span>
|
|
@else
|
|
<span class="a-muted">-</span>
|
|
@endif
|
|
</td>
|
|
|
|
{{-- 회원상태(색상 분기) --}}
|
|
<td>
|
|
<span class="pill {{ $s3Pill }}">● {{ $s3Label }}</span>
|
|
</td>
|
|
|
|
{{-- 인증: Y만 표시 --}}
|
|
<td>
|
|
<div class="badges">
|
|
@if($ok['email'])
|
|
<span class="a-chip">이메일</span>
|
|
@endif
|
|
@if($ok['cell'])
|
|
<span class="a-chip">휴대폰</span>
|
|
@endif
|
|
@if($ok['account'])
|
|
<span class="a-chip">계좌</span>
|
|
@endif
|
|
|
|
@if(!$ok['email'] && !$ok['cell'] && !$ok['account'])
|
|
<span class="a-muted">-</span>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
|
|
{{-- 가입일 --}}
|
|
<td class="a-muted">{{ $joinAt }}</td>
|
|
|
|
<td style="text-align:right;">
|
|
<a class="lbtn lbtn--ghost lbtn--sm"
|
|
href="{{ route('admin.members.show', ['memNo'=>$no]) }}">
|
|
보기
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="8" class="a-muted" style="padding:18px;">데이터가 없습니다.</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div style="margin-top:12px;">
|
|
{{ $page->onEachSide(1)->links('vendor.pagination.admin') }}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function(){
|
|
const qf = document.getElementById('qf');
|
|
const q = document.getElementById('q');
|
|
if (!qf || !q) return;
|
|
|
|
const ph = {
|
|
all: '회원번호/이름/이메일/휴대폰',
|
|
mem_no: '회원번호',
|
|
name: '이름',
|
|
email: '이메일',
|
|
phone: '휴대폰(숫자만)',
|
|
};
|
|
|
|
const apply = () => {
|
|
const v = qf.value || 'all';
|
|
q.placeholder = ph[v] || ph.all;
|
|
};
|
|
|
|
qf.addEventListener('change', apply);
|
|
apply();
|
|
})();
|
|
</script>
|
|
@endsection
|