129 lines
4.5 KiB
PHP
129 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin\Members;
|
|
|
|
use App\Services\Admin\Member\AdminMemberMarketingService;
|
|
use Illuminate\Http\Request;
|
|
use App\Services\Admin\AdminAuditService;
|
|
|
|
|
|
final class AdminMemberMarketingController
|
|
{
|
|
public function __construct(
|
|
private readonly AdminMemberMarketingService $service,
|
|
private readonly AdminAuditService $audit,
|
|
) {}
|
|
|
|
public function index(Request $request)
|
|
{
|
|
$data = $this->service->index($request->all());
|
|
return view('admin.members.marketing', $data);
|
|
}
|
|
|
|
public function export(Request $request)
|
|
{
|
|
$data = $request->validate([
|
|
'zip_password' => ['required', 'string', 'min:4', 'max:64'],
|
|
|
|
// filters
|
|
'qf' => ['nullable','string','in:all,mem_no,name,email,phone'],
|
|
'q' => ['nullable','string','max:100'],
|
|
|
|
'stat_3' => ['nullable','string','in:1,2,3,4,5,6'],
|
|
|
|
'reg_from' => ['nullable','date'],
|
|
'reg_to' => ['nullable','date'],
|
|
|
|
'no_login' => ['nullable','string','in:0,1'],
|
|
'inactive_days_gte' => ['nullable','integer','min:0','max:36500'],
|
|
|
|
'has_purchase' => ['nullable','string','in:all,1,0'],
|
|
'recent_purchase' => ['nullable','string','in:all,30,90'],
|
|
'min_purchase_count' => ['nullable','integer','min:0','max:999999'],
|
|
'min_purchase_amount' => ['nullable','integer','min:0','max:999999999999'],
|
|
|
|
// 추가 옵션(접기 영역)
|
|
'optin_sms' => ['nullable','string','in:all,1,0'],
|
|
'optin_email' => ['nullable','string','in:all,1,0'],
|
|
'has_phone' => ['nullable','string','in:all,1,0'],
|
|
'has_email' => ['nullable','string','in:all,1,0'],
|
|
]);
|
|
|
|
$actorAdminId = (int) auth('admin')->id();
|
|
$ip = (string) $request->ip();
|
|
$ua = (string) $request->userAgent();
|
|
|
|
$zipPassword = (string) $data['zip_password'];
|
|
unset($data['zip_password']);
|
|
|
|
// 감사로그용 필터 스냅샷(민감정보 제외: zip_password는 이미 제거됨)
|
|
$auditFilters = $data;
|
|
|
|
// q 마스킹(전화/이메일 검색인 경우만)
|
|
$qf = (string)($auditFilters['qf'] ?? 'all');
|
|
$q = (string)($auditFilters['q'] ?? '');
|
|
if ($q !== '') {
|
|
if ($qf === 'phone') {
|
|
$digits = preg_replace('/\D+/', '', $q);
|
|
$auditFilters['q'] = $digits !== '' ? (substr($digits, 0, 3) . '****' . substr($digits, -2)) : '***';
|
|
} elseif ($qf === 'email' && str_contains($q, '@')) {
|
|
[$local, $domain] = explode('@', $q, 2);
|
|
$auditFilters['q'] = (mb_substr($local, 0, 2) . '***@' . $domain);
|
|
}
|
|
}
|
|
|
|
$res = $this->service->exportZip($data, $zipPassword);
|
|
|
|
if (!($res['ok'] ?? false)) {
|
|
// 실패도 기록(원인 추적용) — 비밀번호는 기록하지 않음
|
|
$this->audit->log(
|
|
actorAdminId: $actorAdminId,
|
|
action: 'admin.member.export.fail',
|
|
targetType: 'member',
|
|
targetId: 0,
|
|
before: ['filters' => $auditFilters],
|
|
after: [
|
|
'ok' => false,
|
|
'message' => $res['message'] ?? '파일 생성 실패',
|
|
],
|
|
ip: $ip,
|
|
ua: $ua,
|
|
);
|
|
|
|
return redirect()->back()->with('toast', [
|
|
'type' => 'danger',
|
|
'title' => '다운로드 실패',
|
|
'message' => $res['message'] ?? '파일 생성에 실패했습니다.',
|
|
]);
|
|
}
|
|
|
|
$zipPath = (string)($res['zip_path'] ?? '');
|
|
$downloadName = (string)($res['download_name'] ?? 'members.zip');
|
|
|
|
// 성공 기록
|
|
$bytes = (is_string($zipPath) && $zipPath !== '' && file_exists($zipPath)) ? @filesize($zipPath) : null;
|
|
|
|
$this->audit->log(
|
|
actorAdminId: $actorAdminId,
|
|
action: 'admin.member.export',
|
|
targetType: 'member',
|
|
targetId: 0,
|
|
before: ['filters' => $auditFilters],
|
|
after: [
|
|
'ok' => true,
|
|
'download_name' => $downloadName,
|
|
'bytes' => $bytes,
|
|
// exportZip에서 제공 가능하면 같이 남겨두면 좋아
|
|
'row_count' => $res['row_count'] ?? null,
|
|
],
|
|
ip: $ip,
|
|
ua: $ua,
|
|
);
|
|
|
|
return response()
|
|
->download($zipPath, $downloadName)
|
|
->deleteFileAfterSend(true);
|
|
}
|
|
|
|
}
|