feat(api-explorer): Phase 1 기본 구조 및 OpenAPI 파싱 구현
- Config: api-explorer.php (환경, 보안, 캐시 설정) - Migration: api_bookmarks, api_templates, api_histories, api_environments - Model: ApiBookmark, ApiTemplate, ApiHistory, ApiEnvironment - Service: OpenApiParserService, ApiRequestService, ApiExplorerService - Controller: ApiExplorerController (CRUD, 실행, 히스토리) - View: 3-Panel 레이아웃 (sidebar, request, response, history) - Route: 23개 엔드포인트 등록 Swagger UI 대체 개발 도구, HTMX 기반 SPA 경험
This commit is contained in:
387
app/Http/Controllers/DevTools/ApiExplorerController.php
Normal file
387
app/Http/Controllers/DevTools/ApiExplorerController.php
Normal file
@@ -0,0 +1,387 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\DevTools;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\ApiExplorer\ApiExplorerService;
|
||||
use App\Services\ApiExplorer\ApiRequestService;
|
||||
use App\Services\ApiExplorer\OpenApiParserService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* API Explorer Controller
|
||||
*/
|
||||
class ApiExplorerController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private OpenApiParserService $parser,
|
||||
private ApiRequestService $requester,
|
||||
private ApiExplorerService $explorer
|
||||
) {}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Main Pages
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* 메인 페이지
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$userId = auth()->id();
|
||||
|
||||
// 기본 환경 초기화
|
||||
$this->explorer->initializeDefaultEnvironments($userId);
|
||||
|
||||
$endpoints = $this->parser->getEndpointsByTag();
|
||||
$tags = $this->parser->getTags();
|
||||
$bookmarks = $this->explorer->getBookmarks($userId);
|
||||
$environments = $this->explorer->getEnvironments($userId);
|
||||
$defaultEnv = $this->explorer->getDefaultEnvironment($userId);
|
||||
|
||||
return view('dev-tools.api-explorer.index', compact(
|
||||
'endpoints',
|
||||
'tags',
|
||||
'bookmarks',
|
||||
'environments',
|
||||
'defaultEnv'
|
||||
));
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Endpoint Operations (HTMX partial)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* 엔드포인트 목록 (필터/검색)
|
||||
*/
|
||||
public function endpoints(Request $request): View
|
||||
{
|
||||
$filters = [
|
||||
'search' => $request->input('search'),
|
||||
'methods' => $request->input('methods', []),
|
||||
'tags' => $request->input('tags', []),
|
||||
];
|
||||
|
||||
$endpoints = $this->parser->filter($filters);
|
||||
$endpointsByTag = $endpoints->groupBy(fn ($e) => $e['tags'][0] ?? '기타');
|
||||
$bookmarks = $this->explorer->getBookmarks(auth()->id());
|
||||
|
||||
return view('dev-tools.api-explorer.partials.sidebar', compact(
|
||||
'endpointsByTag',
|
||||
'bookmarks'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 엔드포인트 상세 (요청 패널)
|
||||
*/
|
||||
public function endpoint(string $operationId): View
|
||||
{
|
||||
$endpoint = $this->parser->getEndpoint($operationId);
|
||||
|
||||
if (! $endpoint) {
|
||||
abort(404, '엔드포인트를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
$userId = auth()->id();
|
||||
$isBookmarked = $this->explorer->isBookmarked($userId, $endpoint['path'], $endpoint['method']);
|
||||
$templates = $this->explorer->getTemplates($userId, $endpoint['path'], $endpoint['method']);
|
||||
|
||||
return view('dev-tools.api-explorer.partials.request-panel', compact(
|
||||
'endpoint',
|
||||
'isBookmarked',
|
||||
'templates'
|
||||
));
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Execution
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* API 실행 (프록시)
|
||||
*/
|
||||
public function execute(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'method' => 'required|string|in:GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS',
|
||||
'url' => 'required|url',
|
||||
'headers' => 'nullable|array',
|
||||
'query' => 'nullable|array',
|
||||
'body' => 'nullable|array',
|
||||
'environment' => 'required|string',
|
||||
]);
|
||||
|
||||
// API 실행
|
||||
$result = $this->requester->execute(
|
||||
$validated['method'],
|
||||
$validated['url'],
|
||||
$validated['headers'] ?? [],
|
||||
$validated['query'] ?? [],
|
||||
$validated['body']
|
||||
);
|
||||
|
||||
// 히스토리 저장
|
||||
$parsedUrl = parse_url($validated['url']);
|
||||
$endpoint = $parsedUrl['path'] ?? '/';
|
||||
|
||||
$this->explorer->logRequest(auth()->id(), [
|
||||
'endpoint' => $endpoint,
|
||||
'method' => $validated['method'],
|
||||
'request_headers' => $this->requester->maskSensitiveHeaders($validated['headers'] ?? []),
|
||||
'request_body' => $validated['body'],
|
||||
'response_status' => $result['status'],
|
||||
'response_headers' => $result['headers'],
|
||||
'response_body' => is_string($result['body']) ? $result['body'] : json_encode($result['body']),
|
||||
'duration_ms' => $result['duration_ms'],
|
||||
'environment' => $validated['environment'],
|
||||
]);
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Bookmarks
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* 즐겨찾기 목록
|
||||
*/
|
||||
public function bookmarks(): View
|
||||
{
|
||||
$bookmarks = $this->explorer->getBookmarks(auth()->id());
|
||||
|
||||
return view('dev-tools.api-explorer.partials.bookmarks', compact('bookmarks'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 즐겨찾기 추가/토글
|
||||
*/
|
||||
public function addBookmark(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'endpoint' => 'required|string|max:500',
|
||||
'method' => 'required|string|max:10',
|
||||
'display_name' => 'nullable|string|max:100',
|
||||
]);
|
||||
|
||||
$result = $this->explorer->toggleBookmark(auth()->id(), $validated);
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 즐겨찾기 제거
|
||||
*/
|
||||
public function removeBookmark(int $id): JsonResponse
|
||||
{
|
||||
$this->explorer->removeBookmark($id);
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 즐겨찾기 순서 변경
|
||||
*/
|
||||
public function reorderBookmarks(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'order' => 'required|array',
|
||||
'order.*' => 'integer',
|
||||
]);
|
||||
|
||||
$this->explorer->reorderBookmarks(auth()->id(), $validated['order']);
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Templates
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* 템플릿 목록
|
||||
*/
|
||||
public function templates(Request $request): JsonResponse
|
||||
{
|
||||
$endpoint = $request->input('endpoint');
|
||||
$method = $request->input('method');
|
||||
|
||||
$templates = $this->explorer->getTemplates(auth()->id(), $endpoint, $method);
|
||||
|
||||
return response()->json($templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 엔드포인트의 템플릿 목록
|
||||
*/
|
||||
public function templatesForEndpoint(string $endpoint): JsonResponse
|
||||
{
|
||||
$method = request('method');
|
||||
$endpoint = urldecode($endpoint);
|
||||
|
||||
$templates = $this->explorer->getTemplates(auth()->id(), $endpoint, $method);
|
||||
|
||||
return response()->json($templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 저장
|
||||
*/
|
||||
public function saveTemplate(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'endpoint' => 'required|string|max:500',
|
||||
'method' => 'required|string|max:10',
|
||||
'name' => 'required|string|max:100',
|
||||
'description' => 'nullable|string',
|
||||
'headers' => 'nullable|array',
|
||||
'path_params' => 'nullable|array',
|
||||
'query_params' => 'nullable|array',
|
||||
'body' => 'nullable|array',
|
||||
'is_shared' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$template = $this->explorer->saveTemplate(auth()->id(), $validated);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'template' => $template,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 삭제
|
||||
*/
|
||||
public function deleteTemplate(int $id): JsonResponse
|
||||
{
|
||||
$this->explorer->deleteTemplate($id);
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| History
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* 히스토리 목록
|
||||
*/
|
||||
public function history(Request $request): View
|
||||
{
|
||||
$limit = $request->input('limit', 50);
|
||||
$histories = $this->explorer->getHistory(auth()->id(), $limit);
|
||||
|
||||
return view('dev-tools.api-explorer.partials.history-drawer', compact('histories'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 히스토리 전체 삭제
|
||||
*/
|
||||
public function clearHistory(): JsonResponse
|
||||
{
|
||||
$count = $this->explorer->clearHistory(auth()->id());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'deleted' => $count,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 히스토리 재실행
|
||||
*/
|
||||
public function replayHistory(int $id): JsonResponse
|
||||
{
|
||||
$history = $this->explorer->getHistoryItem($id);
|
||||
|
||||
if (! $history) {
|
||||
return response()->json(['error' => '히스토리를 찾을 수 없습니다.'], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'endpoint' => $history->endpoint,
|
||||
'method' => $history->method,
|
||||
'headers' => $history->request_headers,
|
||||
'body' => $history->request_body,
|
||||
]);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Environments
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* 환경 목록
|
||||
*/
|
||||
public function environments(): JsonResponse
|
||||
{
|
||||
$environments = $this->explorer->getEnvironments(auth()->id());
|
||||
|
||||
return response()->json($environments);
|
||||
}
|
||||
|
||||
/**
|
||||
* 환경 저장
|
||||
*/
|
||||
public function saveEnvironment(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'required|string|max:50',
|
||||
'base_url' => 'required|url|max:500',
|
||||
'api_key' => 'nullable|string|max:500',
|
||||
'auth_token' => 'nullable|string',
|
||||
'variables' => 'nullable|array',
|
||||
'is_default' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
if (! empty($validated['id'])) {
|
||||
$environment = $this->explorer->updateEnvironment($validated['id'], $validated);
|
||||
} else {
|
||||
$environment = $this->explorer->saveEnvironment(auth()->id(), $validated);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'environment' => $environment,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 환경 삭제
|
||||
*/
|
||||
public function deleteEnvironment(int $id): JsonResponse
|
||||
{
|
||||
$this->explorer->deleteEnvironment($id);
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 환경 설정
|
||||
*/
|
||||
public function setDefaultEnvironment(int $id): JsonResponse
|
||||
{
|
||||
$this->explorer->setDefaultEnvironment(auth()->id(), $id);
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user