specPath = config('api-explorer.openapi_path'); } /** * 전체 스펙 파싱 */ public function parse(): array { if ($this->spec !== null) { return $this->spec; } $cacheKey = config('api-explorer.cache.key', 'api_explorer_openapi_spec'); $cacheTtl = config('api-explorer.cache.ttl', 3600); if (config('api-explorer.cache.enabled', true)) { $fileModified = File::exists($this->specPath) ? File::lastModified($this->specPath) : 0; $cacheKeyWithMtime = $cacheKey.'_'.$fileModified; $this->spec = Cache::remember($cacheKeyWithMtime, $cacheTtl, function () { return $this->loadSpec(); }); } else { $this->spec = $this->loadSpec(); } return $this->spec; } /** * 스펙 파일 로드 */ private function loadSpec(): array { if (! File::exists($this->specPath)) { return [ 'openapi' => '3.0.0', 'info' => ['title' => 'API', 'version' => '1.0.0'], 'paths' => [], 'tags' => [], ]; } $content = File::get($this->specPath); $spec = json_decode($content, true); if (json_last_error() !== JSON_ERROR_NONE) { return [ 'openapi' => '3.0.0', 'info' => ['title' => 'API', 'version' => '1.0.0'], 'paths' => [], 'tags' => [], 'error' => 'JSON 파싱 오류: '.json_last_error_msg(), ]; } return $spec; } /** * 엔드포인트 목록 추출 */ public function getEndpoints(): Collection { $spec = $this->parse(); $endpoints = collect(); foreach ($spec['paths'] ?? [] as $path => $methods) { foreach ($methods as $method => $details) { if (! in_array(strtoupper($method), ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'])) { continue; } $operationId = $details['operationId'] ?? $this->generateOperationId($method, $path); $endpoints->push([ 'operationId' => $operationId, 'path' => $path, 'method' => strtoupper($method), 'summary' => $details['summary'] ?? '', 'description' => $details['description'] ?? '', 'tags' => $details['tags'] ?? [], 'parameters' => $details['parameters'] ?? [], 'requestBody' => $details['requestBody'] ?? null, 'responses' => $details['responses'] ?? [], 'security' => $details['security'] ?? [], 'deprecated' => $details['deprecated'] ?? false, ]); } } return $endpoints->sortBy([ ['tags', 'asc'], ['path', 'asc'], ['method', 'asc'], ])->values(); } /** * 단일 엔드포인트 조회 */ public function getEndpoint(string $operationId): ?array { return $this->getEndpoints()->firstWhere('operationId', $operationId); } /** * 태그 목록 추출 */ public function getTags(): array { $spec = $this->parse(); // 정의된 태그 $definedTags = collect($spec['tags'] ?? [])->keyBy('name'); // 실제 사용된 태그 $usedTags = $this->getEndpoints() ->pluck('tags') ->flatten() ->unique() ->filter() ->values(); // 병합 return $usedTags->map(function ($tagName) use ($definedTags) { $tagInfo = $definedTags->get($tagName, []); return [ 'name' => $tagName, 'description' => $tagInfo['description'] ?? '', ]; })->toArray(); } /** * 검색 */ public function search(string $query): Collection { if (empty($query)) { return $this->getEndpoints(); } $query = mb_strtolower($query); return $this->getEndpoints()->filter(function ($endpoint) use ($query) { // 엔드포인트 경로 if (str_contains(mb_strtolower($endpoint['path']), $query)) { return true; } // summary if (str_contains(mb_strtolower($endpoint['summary']), $query)) { return true; } // description if (str_contains(mb_strtolower($endpoint['description']), $query)) { return true; } // operationId if (str_contains(mb_strtolower($endpoint['operationId']), $query)) { return true; } // 태그 foreach ($endpoint['tags'] as $tag) { if (str_contains(mb_strtolower($tag), $query)) { return true; } } // 파라미터명 foreach ($endpoint['parameters'] as $param) { if (str_contains(mb_strtolower($param['name'] ?? ''), $query)) { return true; } if (str_contains(mb_strtolower($param['description'] ?? ''), $query)) { return true; } } return false; })->values(); } /** * 필터링 */ public function filter(array $filters): Collection { $endpoints = $this->getEndpoints(); // 메서드 필터 if (! empty($filters['methods'])) { $methods = array_map('strtoupper', (array) $filters['methods']); $endpoints = $endpoints->filter(fn ($e) => in_array($e['method'], $methods)); } // 태그 필터 if (! empty($filters['tags'])) { $tags = (array) $filters['tags']; $endpoints = $endpoints->filter(function ($e) use ($tags) { return count(array_intersect($e['tags'], $tags)) > 0; }); } // 검색어 if (! empty($filters['search'])) { $endpoints = $this->searchInCollection($endpoints, $filters['search']); } return $endpoints->values(); } /** * 컬렉션 내 검색 */ private function searchInCollection(Collection $endpoints, string $query): Collection { $query = mb_strtolower($query); return $endpoints->filter(function ($endpoint) use ($query) { return str_contains(mb_strtolower($endpoint['path']), $query) || str_contains(mb_strtolower($endpoint['summary']), $query) || str_contains(mb_strtolower($endpoint['description']), $query) || str_contains(mb_strtolower($endpoint['operationId']), $query); }); } /** * 태그별 그룹핑 */ public function getEndpointsByTag(): Collection { return $this->getEndpoints()->groupBy(function ($endpoint) { return $endpoint['tags'][0] ?? '기타'; }); } /** * operationId 생성 */ private function generateOperationId(string $method, string $path): string { $cleaned = preg_replace('/[^a-zA-Z0-9]/', '_', $path); $cleaned = preg_replace('/_+/', '_', $cleaned); $cleaned = trim($cleaned, '_'); return strtolower($method).'_'.$cleaned; } /** * API 서버 정보 */ public function getServers(): array { $spec = $this->parse(); return $spec['servers'] ?? []; } /** * API 기본 정보 */ public function getInfo(): array { $spec = $this->parse(); return $spec['info'] ?? []; } /** * 캐시 초기화 */ public function clearCache(): void { $cacheKey = config('api-explorer.cache.key', 'api_explorer_openapi_spec'); Cache::forget($cacheKey); // 파일 수정 시간 포함된 키도 삭제 if (File::exists($this->specPath)) { $fileModified = File::lastModified($this->specPath); Cache::forget($cacheKey.'_'.$fileModified); } $this->spec = null; } }