Files
sam-manage/app/Http/Controllers/DevTools/ApiExplorerController.php
2026-02-25 11:45:01 +09:00

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);
}
}