leftJoin('admin_users as au', 'au.id', '=', 'l.actor_admin_user_id') ->select([ 'l.id', 'l.actor_admin_user_id', 'l.action', 'l.target_type', 'l.target_id', 'l.ip', 'l.created_at', 'au.email as actor_email', 'au.name as actor_name', ]); // 기간 if (!empty($filters['date_from'])) { $q->where('l.created_at', '>=', $filters['date_from'] . ' 00:00:00'); } if (!empty($filters['date_to'])) { $q->where('l.created_at', '<=', $filters['date_to'] . ' 23:59:59'); } // actor 검색 (email/name) $actorQ = trim((string)($filters['actor_q'] ?? '')); if ($actorQ !== '') { $like = '%' . $this->escapeLike($actorQ) . '%'; $q->where(function ($qq) use ($like) { $qq->where('au.email', 'like', $like) ->orWhere('au.name', 'like', $like); }); } // action (prefix match) $action = trim((string)($filters['action'] ?? '')); if ($action !== '') { $q->where('l.action', 'like', $this->escapeLike($action) . '%'); } // target_type (prefix match) $tt = trim((string)($filters['target_type'] ?? '')); if ($tt !== '') { $q->where('l.target_type', 'like', $this->escapeLike($tt) . '%'); } // ip (prefix match) $ip = trim((string)($filters['ip'] ?? '')); if ($ip !== '') { $q->where('l.ip', 'like', $this->escapeLike($ip) . '%'); } // ✅ 최신순 $q->orderByDesc('l.created_at')->orderByDesc('l.id'); return $q->paginate($perPage)->withQueryString(); } public function findOne(int $id): ?array { $row = DB::table(self::TABLE . ' as l') ->leftJoin('admin_users as au', 'au.id', '=', 'l.actor_admin_user_id') ->select([ 'l.id', 'l.actor_admin_user_id', 'l.action', 'l.target_type', 'l.target_id', 'l.before_json', 'l.after_json', 'l.ip', 'l.user_agent', 'l.created_at', 'au.email as actor_email', 'au.name as actor_name', ]) ->where('l.id', $id) ->first(); return $row ? (array)$row : null; } private function escapeLike(string $s): string { // MySQL LIKE escape (%, _) return str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $s); } }