giftcon_dev/app/Http/Controllers/Admin/Members/AdminMemberMarketingController.php
2026-03-03 15:13:16 +09:00

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);
}
}