Files
sam-docs/features/api-explorer-spec.md
hskwon 1770c2ed23 API Explorer 기능 스펙 및 개발 계획 문서 추가
- features/api-explorer-spec.md: API Explorer 기능 스펙
- plans/api-explorer-development-plan.md: API Explorer 개발 계획
2025-12-18 20:29:09 +09:00

26 KiB

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 마이그레이션

// 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 컬러 스킴

/* 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 라우트 정의

// 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 메서드 시그니처

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 클래스

// 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 스마트 검색

// 검색 대상 필드
$searchFields = [
    'endpoint',      // /api/v1/users
    'summary',       // "사용자 목록 조회"
    'description',   // 상세 설명
    'operationId',   // getUserList
    'parameters.*.name',        // 파라미터명
    'parameters.*.description', // 파라미터 설명
    'tags',          // 태그
];

// 검색 알고리즘
1. 정확히 일치  최상위
2. 시작 부분 일치  높은 순위
3. 포함  일반 순위
4. Fuzzy 매칭  낮은 순위 (선택적)

7.2 필터링 옵션

$filters = [
    'methods' => ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
    'tags' => ['Auth', 'Users', 'Products', ...],
    'status' => ['stable', 'beta', 'deprecated'],
    'hasBody' => true|false,
    'requiresAuth' => true|false,
];

7.3 요청 템플릿 시스템

// 템플릿 저장 형식
{
    "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 환경 변수 시스템

// 환경 설정 형식
{
    "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 패턴

<!-- 엔드포인트 목록 갱신 -->
<div id="endpoint-list"
     hx-get="/dev-tools/api-explorer/endpoints"
     hx-trigger="load, search"
     hx-swap="innerHTML">
</div>

<!-- 검색 입력 -->
<input type="text"
       name="search"
       hx-get="/dev-tools/api-explorer/endpoints"
       hx-trigger="keyup changed delay:300ms"
       hx-target="#endpoint-list"
       hx-include="[name='methods[]'], [name='tags[]']">

<!-- 엔드포인트 선택 -->
<button hx-get="/dev-tools/api-explorer/endpoints/{{ $operationId }}"
        hx-target="#request-panel"
        hx-swap="innerHTML">
</button>

<!-- API 실행 -->
<form hx-post="/dev-tools/api-explorer/execute"
      hx-target="#response-panel"
      hx-indicator="#loading-spinner">
</form>

<!-- 즐겨찾기 토글 -->
<button hx-post="/dev-tools/api-explorer/bookmarks"
        hx-vals='{"endpoint": "{{ $endpoint }}", "method": "{{ $method }}"}'
        hx-swap="outerHTML"></button>

8.2 OOB (Out-of-Band) 업데이트

<!-- 응답 후 히스토리 자동 갱신 -->
<div id="history-count" hx-swap-oob="true">
    {{ $historyCount }}
</div>

9. 보안 고려사항

9.1 접근 제어

  • mng 프로젝트 로그인 필수 (auth 미들웨어)
  • 개발 환경에서만 접근 가능 (선택적)
  • API Key/Token은 서버사이드에서만 관리

9.2 민감 정보 처리

  • 환경 설정의 API Key는 암호화 저장
  • 히스토리에서 민감 헤더 마스킹 옵션
  • 공유 템플릿에서 인증 정보 자동 제외

9.3 프록시 보안

  • 허용된 base_url만 프록시 가능 (화이트리스트)
  • 요청 크기 제한 (body 최대 1MB)
  • 타임아웃 설정 (30초)

10. 구현 로드맵

Phase 1: 기본 구조 (3-4일)

  • 디렉토리 구조 생성
  • 마이그레이션 파일 작성
  • OpenApiParserService 구현
  • 기본 UI 레이아웃 (3-Panel)
  • 엔드포인트 목록 표시

Phase 2: 검색/필터/요청 (3일)

  • 풀텍스트 검색 구현
  • 메서드/태그 필터링
  • 요청 패널 UI
  • ApiRequestService (프록시)
  • 응답 표시 (JSON Viewer)

Phase 3: 사용자 데이터 (3일)

  • 즐겨찾기 CRUD
  • 템플릿 저장/불러오기
  • 히스토리 기록/재실행
  • 드래그&드롭 정렬

Phase 4: 환경/고급 기능 (2-3일)

  • 환경 관리 UI
  • 변수 치환 시스템
  • 키보드 단축키
  • UI 폴리싱
  • 반응형 최적화

Phase 5: 테스트/배포 (2일)

  • 기능 테스트
  • 성능 최적화
  • 문서화
  • 배포

예상 총 기간: 13-15일


11. 향후 확장 가능성

11.1 추가 기능 후보

  • Mock 서버: 테스트용 가짜 응답 생성
  • API 비교: 두 환경 간 응답 비교
  • 자동 테스트: 저장된 템플릿 일괄 실행
  • 변경 감지: OpenAPI 스펙 변경 알림
  • 문서 생성: Markdown/PDF 문서 자동 생성

11.2 통합 가능성

  • CI/CD: API 테스트 자동화
  • Slack/Teams: 알림 연동
  • Postman: 컬렉션 import/export

12. 참고 자료

12.1 유사 도구

12.2 기술 문서