input('logs', []); // 빈 요청이 browser.log 파일을 만들지 않도록 채널 해석 자체를 미룬다. if (! is_array($logs) || $logs === []) { return response()->json(['status' => 'logged']); } $logger = self::channel(); foreach ($logs as $log) { if (! is_array($log)) { continue; } $logger->log( self::level($log['type'] ?? 'log'), self::buildMessage($log['data'] ?? []), [ 'url' => $log['url'] ?? null, 'user_agent' => ($log['userAgent'] ?? null) ?: null, 'timestamp' => ($log['timestamp'] ?? null) ?: now()->toIso8601String(), ] ); } return response()->json(['status' => 'logged']); } /** * 브라우저 로그 타입을 PSR-3 로그 레벨로 변환합니다. * * boost 원본은 `$log['type']` 이 없으면 `match` 에서 UnhandledMatchError 로 터진다. * 여기서는 호출부에서 기본값을 채워 넘긴다. * * @param mixed $type 브라우저 로그 타입 * @return string PSR-3 로그 레벨 */ private static function level(mixed $type): string { return match ($type) { 'warn' => 'warning', 'log', 'table' => 'debug', 'window_error', 'uncaught_error', 'unhandled_rejection' => 'error', 'error', 'info', 'notice', 'critical', 'alert', 'emergency' => $type, default => 'debug', }; } /** * 브라우저 로그 data 배열을 사람이 읽을 수 있는 한 줄 메시지로 만듭니다. * * 중첩 배열(예: `{"message":"...","reason":{"name":"TypeError",...}}`)도 재귀 평탄화한다. * * @param mixed $data 로그 data 페이로드 * @return string 조립된 메시지 */ private static function buildMessage(mixed $data): string { if (! is_array($data)) { return self::stringify($data); } return implode(' ', array_map( static fn (mixed $value): string => is_array($value) ? self::buildMessage($value) : self::stringify($value), $data )); } /** * 스칼라/객체 값을 문자열로 변환합니다. * * @param mixed $value 변환할 값 * @return string 문자열 표현 */ private static function stringify(mixed $value): string { return match (true) { is_string($value) => $value, is_numeric($value) => (string) $value, is_bool($value) => $value ? 'true' : 'false', is_null($value) => 'null', default => (string) json_encode($value), }; } /** * 브라우저 로그 채널을 반환합니다 (미정의 시 즉석 생성). * * @return LoggerInterface 로그 채널 */ private static function channel(): LoggerInterface { if (config('logging.channels.browser') !== null) { return Log::channel('browser'); } // daily + 유한 보존일 — single 드라이버는 로테이션 없이 무한 append 되어 // browser.log 가 저장소 단일 최대 파일로 자란다 (실측 989MB). return Log::build([ 'driver' => 'daily', 'path' => storage_path('logs/browser.log'), 'days' => 7, 'level' => config('logging.level', 'debug'), ]); } }