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