- Config, Service, Controller, View 생성 - Model 4개 (admin_api_* 테이블 참조) - 3-Panel 레이아웃 (sidebar, request, response) - HTMX 기반 동적 UI - 마이그레이션은 api/ 프로젝트에서 관리
311 lines
8.5 KiB
PHP
311 lines
8.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services\ApiExplorer;
|
|
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\File;
|
|
|
|
/**
|
|
* OpenAPI JSON 파싱 서비스
|
|
*/
|
|
class OpenApiParserService
|
|
{
|
|
private ?array $spec = null;
|
|
|
|
private string $specPath;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->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;
|
|
}
|
|
}
|