2025-12-17 22:06:28 +09:00
|
|
|
<?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);
|
|
|
|
|
|
2025-12-18 15:42:01 +09:00
|
|
|
// 세션에 저장된 토큰
|
|
|
|
|
$savedToken = session('api_explorer_token');
|
|
|
|
|
|
2025-12-17 22:06:28 +09:00
|
|
|
return view('dev-tools.api-explorer.index', compact(
|
|
|
|
|
'endpoints',
|
|
|
|
|
'tags',
|
|
|
|
|
'bookmarks',
|
|
|
|
|
'environments',
|
2025-12-18 15:42:01 +09:00
|
|
|
'defaultEnv',
|
|
|
|
|
'savedToken'
|
2025-12-17 22:06:28 +09:00
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| 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',
|
2025-12-18 15:42:01 +09:00
|
|
|
'token' => 'nullable|string',
|
|
|
|
|
'user_id' => 'nullable|integer',
|
2025-12-17 22:06:28 +09:00
|
|
|
]);
|
|
|
|
|
|
2025-12-18 15:42:01 +09:00
|
|
|
// Bearer 토큰 처리
|
|
|
|
|
$token = null;
|
|
|
|
|
$headers = $validated['headers'] ?? [];
|
|
|
|
|
|
|
|
|
|
// 1. 직접 입력된 토큰
|
|
|
|
|
if (! empty($validated['token'])) {
|
|
|
|
|
$token = $validated['token'];
|
|
|
|
|
session(['api_explorer_token' => $token]);
|
|
|
|
|
}
|
|
|
|
|
// 2. 사용자 선택 시 Sanctum 토큰 발급
|
|
|
|
|
elseif (! empty($validated['user_id'])) {
|
|
|
|
|
$user = \App\Models\User::find($validated['user_id']);
|
|
|
|
|
if ($user) {
|
|
|
|
|
$token = $user->createToken('api-explorer', ['*'])->plainTextToken;
|
|
|
|
|
session(['api_explorer_token' => $token]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 3. 세션에 저장된 토큰 재사용
|
|
|
|
|
elseif (session('api_explorer_token')) {
|
|
|
|
|
$token = session('api_explorer_token');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Authorization 헤더 추가 (사용자 입력 토큰이 우선)
|
|
|
|
|
if ($token) {
|
2025-12-18 16:08:59 +09:00
|
|
|
$headers['Authorization'] = 'Bearer '.$token;
|
2025-12-18 15:42:01 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-17 22:06:28 +09:00
|
|
|
// API 실행
|
|
|
|
|
$result = $this->requester->execute(
|
|
|
|
|
$validated['method'],
|
|
|
|
|
$validated['url'],
|
2025-12-18 15:42:01 +09:00
|
|
|
$headers,
|
2025-12-17 22:06:28 +09:00
|
|
|
$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]);
|
|
|
|
|
}
|
2025-12-18 15:42:01 +09:00
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| Users (for Authentication)
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 현재 테넌트의 사용자 목록
|
|
|
|
|
*/
|
|
|
|
|
public function users(): JsonResponse
|
|
|
|
|
{
|
|
|
|
|
$tenantId = auth()->user()->tenant_id;
|
|
|
|
|
|
|
|
|
|
$users = \App\Models\User::where('tenant_id', $tenantId)
|
|
|
|
|
->select(['id', 'name', 'email'])
|
|
|
|
|
->orderBy('name')
|
|
|
|
|
->limit(100)
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
return response()->json($users);
|
|
|
|
|
}
|
2025-12-17 22:06:28 +09:00
|
|
|
}
|