# API Explorer 상세 설계서 > **문서 버전**: 1.0 > **작성일**: 2025-12-17 > **대상 프로젝트**: mng (Plain Laravel + Blade + Tailwind) --- ## 1. 개요 ### 1.1 목적 Swagger UI의 한계를 극복하고, 개발팀의 API 관리 효율성을 높이기 위한 커스텀 API Explorer 개발 ### 1.2 Swagger 대비 개선점 | 기능 | Swagger UI | API Explorer | |------|------------|--------------| | 검색 | 엔드포인트명만 | 풀텍스트 (설명, 파라미터 포함) | | 그룹핑 | 태그만 | 태그 + 상태 + 메서드 + 커스텀 | | 즐겨찾기 | ❌ | ⭐ 사용자별 북마크 | | 요청 템플릿 | ❌ | 💾 저장/공유 가능 | | 히스토리 | ❌ | 📋 최근 요청 + 재실행 | | 환경 전환 | 수동 | 🔄 원클릭 전환 | ### 1.3 기술 스택 - **Backend**: Laravel 12 (mng 프로젝트) - **Frontend**: Blade + Tailwind CSS + HTMX - **Data Source**: OpenAPI 3.0 JSON (`api/storage/api-docs/api-docs.json`) - **HTTP Client**: Guzzle (서버사이드 프록시) --- ## 2. 시스템 아키텍처 ### 2.1 전체 구조 ``` ┌─────────────────────────────────────────────────────────────────┐ │ API Explorer (mng) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ │ Browser │───>│ Laravel │───>│ API Server │ │ │ │ (HTMX) │<───│ (Proxy) │<───│ (api/) │ │ │ └──────────────┘ └──────────────┘ └──────────────────┘ │ │ │ │ │ │ │ ┌──────┴──────┐ │ │ │ │ │ │ │ │ ┌─────┴─────┐ ┌─────┴─────┐ │ │ │ │ SQLite │ │ OpenAPI │ │ │ │ │ (Local) │ │ JSON │ │ │ │ └───────────┘ └───────────┘ │ │ │ │ │ ┌──────┴───────────────────────────────────────────────────┐ │ │ │ Local Storage │ │ │ │ • 환경 설정 (현재 서버) │ │ │ │ • UI 상태 (패널 크기, 필터) │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### 2.2 데이터 흐름 ``` 1. OpenAPI 파싱 api-docs.json ──> OpenApiParserService ──> 구조화된 API 데이터 2. API 요청 프록시 Browser ──HTMX──> ApiExplorerController ──Guzzle──> API Server 3. 사용자 데이터 (즐겨찾기, 템플릿, 히스토리) Browser ──> ApiExplorerController ──> SQLite/MySQL ``` --- ## 3. 디렉토리 구조 ``` mng/ ├── app/ │ ├── Http/ │ │ └── Controllers/ │ │ └── DevTools/ │ │ └── ApiExplorerController.php │ ├── Services/ │ │ └── ApiExplorer/ │ │ ├── OpenApiParserService.php # OpenAPI JSON 파싱 │ │ ├── ApiRequestService.php # API 호출 프록시 │ │ └── ApiExplorerService.php # 비즈니스 로직 통합 │ └── Models/ │ └── DevTools/ │ ├── ApiBookmark.php # 즐겨찾기 │ ├── ApiTemplate.php # 요청 템플릿 │ └── ApiHistory.php # 요청 히스토리 │ ├── database/ │ └── migrations/ │ └── 2024_xx_xx_create_api_explorer_tables.php │ ├── resources/ │ └── views/ │ └── dev-tools/ │ └── api-explorer/ │ ├── index.blade.php # 메인 레이아웃 │ ├── partials/ │ │ ├── sidebar.blade.php # API 목록 + 검색/필터 │ │ ├── endpoint-item.blade.php # 개별 엔드포인트 항목 │ │ ├── request-panel.blade.php # 요청 작성 패널 │ │ ├── response-panel.blade.php # 응답 표시 패널 │ │ ├── template-modal.blade.php # 템플릿 저장/불러오기 │ │ └── history-drawer.blade.php # 히스토리 서랍 │ └── components/ │ ├── method-badge.blade.php # HTTP 메서드 배지 │ ├── param-input.blade.php # 파라미터 입력 필드 │ ├── json-editor.blade.php # JSON 편집기 │ └── json-viewer.blade.php # JSON 뷰어 (트리/Raw) │ ├── routes/ │ └── web.php # 라우트 추가 │ └── config/ └── api-explorer.php # 설정 파일 ``` --- ## 4. 데이터베이스 스키마 ### 4.1 ERD ``` ┌─────────────────────┐ ┌─────────────────────┐ │ api_bookmarks │ │ api_templates │ ├─────────────────────┤ ├─────────────────────┤ │ id (PK) │ │ id (PK) │ │ user_id (FK) │ │ user_id (FK) │ │ endpoint │ │ endpoint │ │ method │ │ method │ │ display_name │ │ name │ │ display_order │ │ description │ │ color │ │ headers (JSON) │ │ created_at │ │ path_params (JSON) │ │ updated_at │ │ query_params (JSON) │ └─────────────────────┘ │ body (JSON) │ │ is_shared │ │ created_at │ │ updated_at │ └─────────────────────┘ ┌─────────────────────┐ ┌─────────────────────┐ │ api_histories │ │ api_environments │ ├─────────────────────┤ ├─────────────────────┤ │ id (PK) │ │ id (PK) │ │ user_id (FK) │ │ user_id (FK) │ │ endpoint │ │ name │ │ method │ │ base_url │ │ request_headers │ │ api_key │ │ request_body │ │ auth_token │ │ response_status │ │ variables (JSON) │ │ response_headers │ │ is_default │ │ response_body │ │ created_at │ │ duration_ms │ │ updated_at │ │ environment │ └─────────────────────┘ │ created_at │ └─────────────────────┘ ``` ### 4.2 마이그레이션 ```php // api_bookmarks Schema::create('api_bookmarks', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->string('endpoint', 500); $table->string('method', 10); $table->string('display_name', 100)->nullable(); $table->integer('display_order')->default(0); $table->string('color', 20)->nullable(); $table->timestamps(); $table->unique(['user_id', 'endpoint', 'method']); $table->index('user_id'); }); // api_templates Schema::create('api_templates', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->string('endpoint', 500); $table->string('method', 10); $table->string('name', 100); $table->text('description')->nullable(); $table->json('headers')->nullable(); $table->json('path_params')->nullable(); $table->json('query_params')->nullable(); $table->json('body')->nullable(); $table->boolean('is_shared')->default(false); $table->timestamps(); $table->index(['user_id', 'endpoint', 'method']); $table->index('is_shared'); }); // api_histories Schema::create('api_histories', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->string('endpoint', 500); $table->string('method', 10); $table->json('request_headers')->nullable(); $table->json('request_body')->nullable(); $table->integer('response_status'); $table->json('response_headers')->nullable(); $table->longText('response_body')->nullable(); $table->integer('duration_ms'); $table->string('environment', 50); $table->timestamp('created_at'); $table->index(['user_id', 'created_at']); $table->index(['endpoint', 'method']); }); // api_environments Schema::create('api_environments', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->string('name', 50); $table->string('base_url', 500); $table->string('api_key', 500)->nullable(); $table->text('auth_token')->nullable(); $table->json('variables')->nullable(); $table->boolean('is_default')->default(false); $table->timestamps(); $table->index('user_id'); }); ``` --- ## 5. UI 설계 ### 5.1 메인 레이아웃 (3-Panel) ``` ┌────────────────────────────────────────────────────────────────────────────┐ │ 🔍 Search... │ [로컬 ▼] │ 📋 History │ ⚙️ Settings │ ├────────────────────────────────────────────────────────────────────────────┤ │ │ │ │ │ API Sidebar │ Request Panel │ Response Panel │ │ (resizable) │ (resizable) │ (resizable) │ │ │ │ │ │ ┌───────────────────┐ │ ┌──────────────────────┐ │ ┌──────────────────────┐ │ │ │ 🔍 필터 │ │ │ POST /api/v1/login │ │ │ Status: 200 OK ✓ │ │ │ │ [GET][POST][PUT] │ │ │ │ │ │ Time: 45ms │ │ │ │ [DELETE][PATCH] │ │ │ ┌─ Headers ─────────┐│ │ │ │ │ │ │ │ │ │ │ Authorization: [] ││ │ │ ┌─ Headers ─────────┐│ │ │ │ ⭐ 즐겨찾기 (3) │ │ │ │ Content-Type: [] ││ │ │ │ content-type: ... ││ │ │ │ POST login │ │ │ └──────────────────┘│ │ │ │ x-request-id: ... ││ │ │ │ GET users │ │ │ │ │ │ └──────────────────┘│ │ │ │ POST logout │ │ │ ┌─ Path Params ────┐│ │ │ │ │ │ │ │ │ │ │ (none) ││ │ │ ┌─ Body ─────────────┐│ │ │ │ 📁 Auth │ │ │ └──────────────────┘│ │ │ │ { ││ │ │ │ ├ POST login │ │ │ │ │ │ │ "success": true, ││ │ │ │ ├ POST logout │ │ │ ┌─ Query Params ───┐│ │ │ │ "data": { ││ │ │ │ └ GET me │ │ │ │ (none) ││ │ │ │ "token": "..." ││ │ │ │ │ │ │ └──────────────────┘│ │ │ │ } ││ │ │ │ 📁 Users │ │ │ │ │ │ │ } ││ │ │ │ ├ GET list │ │ │ ┌─ Body (JSON) ────┐│ │ │ └──────────────────┘│ │ │ │ ├ GET {id} │ │ │ │ { ││ │ │ │ │ │ │ ├ POST create │ │ │ │ "user_id": "", ││ │ │ [Raw] [Pretty] [Tree]│ │ │ │ ├ PUT {id} │ │ │ │ "user_pwd": "" ││ │ │ │ │ │ │ └ DELETE {id} │ │ │ │ } ││ │ │ [📋 Copy] [💾 Save] │ │ │ │ │ │ │ └──────────────────┘│ │ │ │ │ │ │ 📁 Products │ │ │ │ │ └──────────────────────┘ │ │ │ └ ... │ │ │ [📋 템플릿] [▶ 실행]│ │ │ │ └───────────────────┘ │ └──────────────────────┘ │ │ │ │ │ │ └────────────────────────────────────────────────────────────────────────────┘ ``` ### 5.2 컬러 스킴 ```css /* HTTP 메서드 배지 */ .method-get { @apply bg-green-100 text-green-800; } .method-post { @apply bg-blue-100 text-blue-800; } .method-put { @apply bg-yellow-100 text-yellow-800; } .method-patch { @apply bg-orange-100 text-orange-800; } .method-delete { @apply bg-red-100 text-red-800; } /* 상태 코드 */ .status-2xx { @apply text-green-600; } /* 성공 */ .status-3xx { @apply text-blue-600; } /* 리다이렉트 */ .status-4xx { @apply text-yellow-600; } /* 클라이언트 에러 */ .status-5xx { @apply text-red-600; } /* 서버 에러 */ ``` ### 5.3 반응형 동작 | 화면 크기 | 동작 | |-----------|------| | Desktop (≥1280px) | 3-Panel 표시 | | Tablet (768-1279px) | 2-Panel (사이드바 접힘 가능) | | Mobile (<768px) | 1-Panel (탭 전환) | --- ## 6. API 설계 ### 6.1 라우트 정의 ```php // routes/web.php Route::prefix('dev-tools/api-explorer') ->middleware(['auth']) ->name('api-explorer.') ->group(function () { // 메인 페이지 Route::get('/', [ApiExplorerController::class, 'index'])->name('index'); // API 목록 (HTMX partial) Route::get('/endpoints', [ApiExplorerController::class, 'endpoints'])->name('endpoints'); Route::get('/endpoints/{operationId}', [ApiExplorerController::class, 'endpoint'])->name('endpoint'); // API 실행 (프록시) Route::post('/execute', [ApiExplorerController::class, 'execute'])->name('execute'); // 즐겨찾기 Route::get('/bookmarks', [ApiExplorerController::class, 'bookmarks'])->name('bookmarks'); Route::post('/bookmarks', [ApiExplorerController::class, 'addBookmark'])->name('bookmarks.add'); Route::delete('/bookmarks/{id}', [ApiExplorerController::class, 'removeBookmark'])->name('bookmarks.remove'); Route::put('/bookmarks/reorder', [ApiExplorerController::class, 'reorderBookmarks'])->name('bookmarks.reorder'); // 템플릿 Route::get('/templates', [ApiExplorerController::class, 'templates'])->name('templates'); Route::get('/templates/{endpoint}', [ApiExplorerController::class, 'templatesForEndpoint'])->name('templates.endpoint'); Route::post('/templates', [ApiExplorerController::class, 'saveTemplate'])->name('templates.save'); Route::delete('/templates/{id}', [ApiExplorerController::class, 'deleteTemplate'])->name('templates.delete'); // 히스토리 Route::get('/history', [ApiExplorerController::class, 'history'])->name('history'); Route::delete('/history', [ApiExplorerController::class, 'clearHistory'])->name('history.clear'); Route::post('/history/{id}/replay', [ApiExplorerController::class, 'replayHistory'])->name('history.replay'); // 환경 Route::get('/environments', [ApiExplorerController::class, 'environments'])->name('environments'); Route::post('/environments', [ApiExplorerController::class, 'saveEnvironment'])->name('environments.save'); Route::delete('/environments/{id}', [ApiExplorerController::class, 'deleteEnvironment'])->name('environments.delete'); Route::post('/environments/{id}/default', [ApiExplorerController::class, 'setDefaultEnvironment'])->name('environments.default'); }); ``` ### 6.2 Controller 메서드 시그니처 ```php class ApiExplorerController extends Controller { public function __construct( private OpenApiParserService $parser, private ApiRequestService $requester, private ApiExplorerService $explorer ) {} // GET /dev-tools/api-explorer public function index(): View // GET /dev-tools/api-explorer/endpoints?search=&tags[]=&methods[]= public function endpoints(Request $request): View // HTMX partial // GET /dev-tools/api-explorer/endpoints/{operationId} public function endpoint(string $operationId): View // HTMX partial // POST /dev-tools/api-explorer/execute public function execute(ExecuteApiRequest $request): JsonResponse // Bookmarks CRUD... // Templates CRUD... // History CRUD... // Environments CRUD... } ``` ### 6.3 Service 클래스 ```php // OpenApiParserService - OpenAPI JSON 파싱 class OpenApiParserService { public function parse(): array; // 전체 스펙 파싱 public function getEndpoints(): Collection; // 엔드포인트 목록 public function getEndpoint(string $operationId): ?array; // 단일 엔드포인트 public function getTags(): array; // 태그 목록 public function search(string $query): Collection; // 검색 public function filter(array $filters): Collection; // 필터링 } // ApiRequestService - API 호출 프록시 class ApiRequestService { public function execute( string $method, string $url, array $headers = [], array $query = [], ?array $body = null ): ApiResponse; } // ApiExplorerService - 비즈니스 로직 통합 class ApiExplorerService { // Bookmark operations public function getBookmarks(int $userId): Collection; public function addBookmark(int $userId, array $data): ApiBookmark; public function removeBookmark(int $bookmarkId): void; // Template operations public function getTemplates(int $userId, ?string $endpoint = null): Collection; public function saveTemplate(int $userId, array $data): ApiTemplate; public function deleteTemplate(int $templateId): void; // History operations public function logRequest(int $userId, array $data): ApiHistory; public function getHistory(int $userId, int $limit = 50): Collection; public function clearHistory(int $userId): void; // Environment operations public function getEnvironments(int $userId): Collection; public function saveEnvironment(int $userId, array $data): ApiEnvironment; } ``` --- ## 7. 핵심 기능 상세 ### 7.1 스마트 검색 ```php // 검색 대상 필드 $searchFields = [ 'endpoint', // /api/v1/users 'summary', // "사용자 목록 조회" 'description', // 상세 설명 'operationId', // getUserList 'parameters.*.name', // 파라미터명 'parameters.*.description', // 파라미터 설명 'tags', // 태그 ]; // 검색 알고리즘 1. 정확히 일치 → 최상위 2. 시작 부분 일치 → 높은 순위 3. 포함 → 일반 순위 4. Fuzzy 매칭 → 낮은 순위 (선택적) ``` ### 7.2 필터링 옵션 ```php $filters = [ 'methods' => ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], 'tags' => ['Auth', 'Users', 'Products', ...], 'status' => ['stable', 'beta', 'deprecated'], 'hasBody' => true|false, 'requiresAuth' => true|false, ]; ``` ### 7.3 요청 템플릿 시스템 ```json // 템플릿 저장 형식 { "name": "로그인 테스트", "description": "테스트 계정으로 로그인", "endpoint": "/api/v1/login", "method": "POST", "headers": { "X-API-KEY": "{{API_KEY}}" }, "body": { "user_id": "test", "user_pwd": "testpass" }, "is_shared": false } ``` ### 7.4 환경 변수 시스템 ```json // 환경 설정 형식 { "name": "로컬", "base_url": "http://api.sam.kr", "api_key": "your-api-key", "auth_token": null, "variables": { "TENANT_ID": "1", "USER_ID": "test" } } // 변수 치환: {{VARIABLE_NAME}} // 예: "Authorization": "Bearer {{AUTH_TOKEN}}" ``` --- ## 8. HTMX 통합 ### 8.1 주요 HTMX 패턴 ```html