$this->safeDate($query['date_from'] ?? ''), 'date_to' => $this->safeDate($query['date_to'] ?? ''), 'state' => $this->safeEnum($query['state'] ?? '', ['S','E']), // S 직접, E 찾기 'mem_no' => $this->safeInt($query['mem_no'] ?? null), 'type' => $this->safeStr($query['type'] ?? '', 40), // 신포맷 type 'email' => $this->safeStr($query['email'] ?? '', 80), // 구포맷 user_email 'ip' => $this->safeIpPrefix($query['ip'] ?? ''), // remote_addr or auth_key contains 'q' => $this->safeStr($query['q'] ?? '', 120), // info 전체 like ]; if ($filters['date_from'] && $filters['date_to']) { if (strcmp($filters['date_from'], $filters['date_to']) > 0) { [$filters['date_from'], $filters['date_to']] = [$filters['date_to'], $filters['date_from']]; } } $page = $this->repo->paginate($filters, 30); $items = []; foreach ($page->items() as $it) { $r = is_array($it) ? $it : (array)$it; $infoRaw = (string)($r['info'] ?? ''); $infoArr = $this->decodeJson($infoRaw); $state = (string)($r['state'] ?? ''); $memNo = (int)($infoArr['mem_no'] ?? 0); $type = trim((string)($infoArr['type'] ?? '')); $eventType = $type !== '' ? $type : ($state === 'E' ? '비밀번호찾기 변경' : '비밀번호변경'); $eventTime = trim((string)($infoArr['redate'] ?? '')); if ($eventTime === '') $eventTime = (string)($r['rgdate'] ?? ''); $ip = trim((string)($infoArr['remote_addr'] ?? '')); if ($ip === '') { $ip = $this->extractIpFromAuthKey((string)($infoArr['auth_key'] ?? '')) ?: ''; } $email = trim((string)($infoArr['user_email'] ?? '')); if ($email === '') $email = trim((string)($infoArr['email'] ?? '')); $agent = trim((string)($infoArr['agent'] ?? '')); $items[] = array_merge($r, [ 'info_arr' => $infoArr, 'mem_no_int' => $memNo, 'mem_link' => $memNo > 0 ? ('/members/' . $memNo) : null, 'state_label' => $state === 'E' ? '비번찾기' : '직접변경', 'state_badge' => $state === 'E' ? 'badge--warn' : 'badge--ok', 'event_type' => $eventType, 'event_time' => $eventTime, 'ip_norm' => $ip, 'email_norm' => $email, 'agent_norm' => $agent, // 펼침용(구포맷 핵심 필드) 'auth_key' => (string)($infoArr['auth_key'] ?? ''), 'auth_effective_time' => (string)($infoArr['auth_effective_time'] ?? ''), // raw pretty 'info_pretty' => $this->prettyJson($infoArr, $infoRaw), ]); } return [ 'filters' => $filters, 'page' => $page, 'items' => $items, 'stateMap' => [ 'S' => '직접변경', 'E' => '비번찾기', ], ]; } private function decodeJson(string $raw): array { $raw = trim($raw); if ($raw === '') return []; $arr = json_decode($raw, true); return is_array($arr) ? $arr : []; } private function prettyJson(array $arr, string $fallbackRaw): string { if (!empty($arr)) { return (string)json_encode($arr, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } return $fallbackRaw; } private function extractIpFromAuthKey(string $authKey): ?string { $authKey = trim($authKey); if ($authKey === '') return null; // auth_key 끝에 "1.85.101.111" 같은 IP가 붙는 케이스 대응 (마지막 매치 사용) if (preg_match_all('/(\d{1,3}(?:\.\d{1,3}){3})/', $authKey, $m) && !empty($m[1])) { return (string)end($m[1]); } return null; } private function safeStr(mixed $v, int $max): string { $s = trim((string)$v); if ($s === '') return ''; if (mb_strlen($s) > $max) $s = mb_substr($s, 0, $max); return $s; } private function safeEnum(mixed $v, array $allowed): string { $s = trim((string)$v); return in_array($s, $allowed, true) ? $s : ''; } private function safeInt(mixed $v): ?int { if ($v === null || $v === '') return null; if (!is_numeric($v)) return null; $n = (int)$v; return $n >= 0 ? $n : null; } private function safeDate(mixed $v): ?string { $s = trim((string)$v); if ($s === '') return null; return preg_match('/^\d{4}-\d{2}-\d{2}$/', $s) ? $s : null; } private function safeIpPrefix(mixed $v): string { $s = trim((string)$v); if ($s === '') return ''; return preg_match('/^[0-9.]+$/', $s) ? $s : ''; } }