availableYears(2018, $currentYear); $year = (int)($query['year'] ?? $currentYear); if (!in_array($year, $years, true)) { // 테이블 없거나 허용 범위 밖이면 최신년도 fallback $year = !empty($years) ? max($years) : $currentYear; } $filters = [ 'year' => $year, 'date_from' => $this->safeDate($query['date_from'] ?? ''), 'date_to' => $this->safeDate($query['date_to'] ?? ''), 'mem_no' => $this->safeInt($query['mem_no'] ?? null), 'sf' => $this->safeEnum($query['sf'] ?? '', ['s','f']), // 성공/실패 'conn' => $this->safeEnum($query['conn'] ?? '', ['1','2']), // pc/mobile 'ip4' => $this->safeIpPrefix($query['ip4'] ?? ''), 'ip4_c' => $this->safeIpPrefix($query['ip4_c'] ?? ''), 'error_code'=> $this->safeStr($query['error_code'] ?? '', 10), 'platform' => $this->safeStr($query['platform'] ?? '', 30), 'browser' => $this->safeStr($query['browser'] ?? '', 40), ]; // 기간 역전 방지 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->paginateByYear($year, $filters, 30); // 화면 가공(라벨/링크 등) $items = []; foreach ($page->items() as $it) { $r = is_array($it) ? $it : (array)$it; $sf = (string)($r['sf'] ?? 's'); $conn = (string)($r['conn'] ?? '1'); $items[] = array_merge($r, [ 'sf_label' => $sf === 'f' ? '실패' : '성공', 'sf_badge' => $sf === 'f' ? 'badge--bad' : 'badge--ok', 'conn_label' => $conn === '2' ? 'M' : 'PC', 'conn_badge' => $conn === '2' ? 'badge--mvno' : 'badge--muted', 'mem_no_int' => (int)($r['mem_no'] ?? 0), 'mem_link' => ((int)($r['mem_no'] ?? 0) > 0) ? ('/members/' . (int)$r['mem_no']) : null, // 실패가 아니면 에러코드 화면에서 비워도 됨(뷰에서 처리) ]); } return [ 'years' => $years, 'filters' => $filters, 'page' => $page, 'items' => $items, ]; } private function availableYears(int $from, int $to): array { $out = []; for ($y = $from; $y <= $to; $y++) { $t = 'mem_login_' . $y; if (Schema::hasTable($t)) $out[] = $y; } // 없으면 그냥 범위라도 반환(운영/개발에서 초기 셋업 대비) return !empty($out) ? $out : [$to]; } 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 : ''; } }