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(): View { $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); } }