715 lines
21 KiB
PHP
715 lines
21 KiB
PHP
<?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\ApiUsageService;
|
|
use App\Services\ApiExplorer\OpenApiParserService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\View\View;
|
|
|
|
/**
|
|
* API Explorer Controller
|
|
*/
|
|
class ApiExplorerController extends Controller
|
|
{
|
|
public function __construct(
|
|
private OpenApiParserService $parser,
|
|
private ApiRequestService $requester,
|
|
private ApiExplorerService $explorer,
|
|
private ApiUsageService $usageService
|
|
) {}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Main Pages
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* 메인 페이지
|
|
*/
|
|
public function index(Request $request): View|Response
|
|
{
|
|
// HTMX 요청 시 전체 페이지 리로드 (스크립트 로딩을 위해)
|
|
if ($request->header('HX-Request')) {
|
|
return response('', 200)->header('HX-Redirect', route('dev-tools.api-explorer.index'));
|
|
}
|
|
|
|
$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);
|
|
|
|
// 세션에 저장된 토큰
|
|
$savedToken = session('api_explorer_token');
|
|
|
|
return view('dev-tools.api-explorer.index', compact(
|
|
'endpoints',
|
|
'tags',
|
|
'bookmarks',
|
|
'environments',
|
|
'defaultEnv',
|
|
'savedToken'
|
|
));
|
|
}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| 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',
|
|
'token' => 'nullable|string',
|
|
'user_id' => 'nullable|integer',
|
|
]);
|
|
|
|
// 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) {
|
|
$headers['Authorization'] = 'Bearer '.$token;
|
|
}
|
|
|
|
// API 실행
|
|
$result = $this->requester->execute(
|
|
$validated['method'],
|
|
$validated['url'],
|
|
$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 storeBookmark(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'endpoint' => 'required|string|max:500',
|
|
'method' => 'required|string|max:10',
|
|
'display_name' => 'nullable|string|max:100',
|
|
]);
|
|
|
|
$bookmark = $this->explorer->addBookmark(auth()->id(), $validated);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'bookmark' => $bookmark,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 즐겨찾기 토글
|
|
*/
|
|
public function toggleBookmark(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]);
|
|
}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Users (for Authentication)
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* 현재 테넌트의 사용자 목록
|
|
* 시스템 헤더에서 선택한 테넌트 기준 (session('selected_tenant_id'))
|
|
* 관리자는 자신이 속하지 않은 테넌트의 사용자도 볼 수 있어야 함
|
|
*/
|
|
public function users(): JsonResponse
|
|
{
|
|
// 세션에서 직접 테넌트 ID 조회 (관리자가 선택한 테넌트)
|
|
$selectedTenantId = session('selected_tenant_id');
|
|
|
|
if (! $selectedTenantId) {
|
|
// 테넌트가 선택되지 않은 경우 로그인 사용자의 기본 테넌트 사용
|
|
$currentTenant = auth()->user()->tenants()
|
|
->where('is_default', true)
|
|
->first() ?? auth()->user()->tenants()->first();
|
|
|
|
if (! $currentTenant) {
|
|
return response()->json([]);
|
|
}
|
|
|
|
$selectedTenantId = $currentTenant->id;
|
|
}
|
|
|
|
// Tenant 모델에서 직접 조회 (사용자의 테넌트 관계와 무관하게)
|
|
$tenant = \App\Models\Tenants\Tenant::find($selectedTenantId);
|
|
|
|
if (! $tenant) {
|
|
return response()->json([]);
|
|
}
|
|
|
|
// 해당 테넌트에 속한 사용자 목록 조회
|
|
$users = \App\Models\User::whereHas('tenants', function ($query) use ($selectedTenantId) {
|
|
$query->where('tenant_id', $selectedTenantId);
|
|
})
|
|
->select(['id', 'name', 'email'])
|
|
->orderBy('name')
|
|
->limit(100)
|
|
->get();
|
|
|
|
return response()->json([
|
|
'tenant' => [
|
|
'id' => $tenant->id,
|
|
'name' => $tenant->company_name,
|
|
],
|
|
'users' => $users,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 사용자 선택 시 Sanctum 토큰 발급
|
|
*/
|
|
public function issueToken(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'user_id' => 'required|integer|exists:users,id',
|
|
]);
|
|
|
|
$user = \App\Models\User::find($validated['user_id']);
|
|
|
|
if (! $user) {
|
|
return response()->json(['error' => '사용자를 찾을 수 없습니다.'], 404);
|
|
}
|
|
|
|
// Sanctum 토큰 발급
|
|
$token = $user->createToken('api-explorer', ['*'])->plainTextToken;
|
|
session(['api_explorer_token' => $token]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'token' => $token,
|
|
'user' => [
|
|
'id' => $user->id,
|
|
'name' => $user->name,
|
|
'email' => $user->email,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 즐겨찾기 수정
|
|
*/
|
|
public function updateBookmark(Request $request, int $id): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'display_name' => 'nullable|string|max:100',
|
|
]);
|
|
|
|
$bookmark = $this->explorer->updateBookmark($id, $validated);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'bookmark' => $bookmark,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 즐겨찾기 삭제
|
|
*/
|
|
public function deleteBookmark(int $id): JsonResponse
|
|
{
|
|
$this->explorer->removeBookmark($id);
|
|
|
|
return response()->json(['success' => true]);
|
|
}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| API Usage & Deprecation
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* API 사용 현황 페이지
|
|
*/
|
|
public function usage(Request $request): View|Response
|
|
{
|
|
// HTMX 요청 시 전체 페이지 리로드
|
|
if ($request->header('HX-Request')) {
|
|
return response('', 200)->header('HX-Redirect', route('dev-tools.api-explorer.usage'));
|
|
}
|
|
|
|
$comparison = $this->usageService->getApiUsageComparison();
|
|
$deprecations = $this->usageService->getDeprecations();
|
|
$dailyTrend = $this->usageService->getDailyTrend(30);
|
|
|
|
return view('dev-tools.api-explorer.usage', compact(
|
|
'comparison',
|
|
'deprecations',
|
|
'dailyTrend'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* API 사용 통계 조회 (JSON)
|
|
*/
|
|
public function usageStats(): JsonResponse
|
|
{
|
|
$comparison = $this->usageService->getApiUsageComparison();
|
|
|
|
return response()->json($comparison);
|
|
}
|
|
|
|
/**
|
|
* 폐기 후보 목록 조회
|
|
*/
|
|
public function deprecations(Request $request): JsonResponse
|
|
{
|
|
$status = $request->input('status');
|
|
$deprecations = $this->usageService->getDeprecations($status);
|
|
|
|
return response()->json($deprecations);
|
|
}
|
|
|
|
/**
|
|
* 폐기 후보 추가
|
|
*/
|
|
public function addDeprecation(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'endpoint' => 'required|string|max:500',
|
|
'method' => 'required|string|max:10',
|
|
'reason' => 'nullable|string',
|
|
]);
|
|
|
|
$deprecation = $this->usageService->addDeprecationCandidate(
|
|
$validated['endpoint'],
|
|
$validated['method'],
|
|
$validated['reason'] ?? null,
|
|
auth()->id()
|
|
);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'deprecation' => $deprecation,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 미사용 API 전체를 폐기 후보로 추가
|
|
*/
|
|
public function addAllUnusedAsDeprecation(): JsonResponse
|
|
{
|
|
$count = $this->usageService->addAllUnusedAsCandidate(auth()->id());
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'added_count' => $count,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 폐기 상태 변경
|
|
*/
|
|
public function updateDeprecation(Request $request, int $id): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'status' => 'required|string|in:candidate,scheduled,deprecated,removed',
|
|
'reason' => 'nullable|string',
|
|
'scheduled_date' => 'nullable|date',
|
|
]);
|
|
|
|
$scheduledDate = ! empty($validated['scheduled_date'])
|
|
? new \DateTime($validated['scheduled_date'])
|
|
: null;
|
|
|
|
$deprecation = $this->usageService->updateDeprecationStatus(
|
|
$id,
|
|
$validated['status'],
|
|
$validated['reason'] ?? null,
|
|
$scheduledDate
|
|
);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'deprecation' => $deprecation,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 폐기 후보 삭제
|
|
*/
|
|
public function removeDeprecation(int $id): JsonResponse
|
|
{
|
|
$this->usageService->removeDeprecation($id);
|
|
|
|
return response()->json(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* 일별 API 호출 추이
|
|
*/
|
|
public function dailyTrend(Request $request): JsonResponse
|
|
{
|
|
$days = $request->input('days', 30);
|
|
$trend = $this->usageService->getDailyTrend($days);
|
|
|
|
return response()->json($trend);
|
|
}
|
|
|
|
/**
|
|
* 인기 API 목록
|
|
*/
|
|
public function popularApis(Request $request): JsonResponse
|
|
{
|
|
$limit = $request->input('limit', 20);
|
|
$popular = $this->usageService->getPopularApis($limit);
|
|
|
|
return response()->json($popular);
|
|
}
|
|
|
|
/**
|
|
* 오래된 API 목록 (N일 이상 미사용)
|
|
*/
|
|
public function staleApis(Request $request): JsonResponse
|
|
{
|
|
$days = $request->input('days', 30);
|
|
$stale = $this->usageService->getStaleApis($days);
|
|
|
|
return response()->json($stale);
|
|
}
|
|
}
|