# API Flow Tester 설계문서 > **Version**: 1.0 > **Date**: 2025-11-27 > **Author**: Claude Code > **Status**: Draft --- ## 1. 개요 ### 1.1 목적 API Flow Tester는 MNG 관리자 패널에서 복수의 API를 순차적으로 실행하고, 이전 응답 데이터를 다음 요청에 바인딩하여 통합 API 플로우를 테스트하는 도구입니다. ### 1.2 핵심 기능 (B 버전) - **플로우 관리**: 생성, 조회, 수정, 삭제 (CRUD) - **JSON 에디터**: 구문 강조 기능이 있는 플로우/데이터 편집기 - **변수 바인딩**: `{{stepN.response.path}}` 형식으로 이전 응답 참조 - **순차 실행**: 의존성 기반 순서대로 API 호출 - **결과 추적**: 단계별 성공/실패 표시 및 실행 결과 저장 ### 1.3 범위 제외 (B 버전) - 시스템 내 AI 통합 (사용자가 외부에서 Claude로 JSON 생성) - 자동 플로우 생성 - AI 기반 오류 분석 --- ## 2. 시스템 아키텍처 ### 2.1 전체 구조 ``` ┌─────────────────────────────────────────────────────────────────┐ │ MNG Application │ ├─────────────────────────────────────────────────────────────────┤ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │ │ Flow List │ │ Flow Editor │ │ Flow Executor │ │ │ │ (Index) │ │ (Create/ │ │ (Run/Monitor) │ │ │ │ │ │ Edit) │ │ │ │ │ └──────┬───────┘ └──────┬───────┘ └──────────┬───────────┘ │ │ │ │ │ │ │ ┌──────▼─────────────────▼──────────────────────▼───────────┐ │ │ │ FlowTesterController │ │ │ └───────────────────────────┬───────────────────────────────┘ │ │ │ │ │ ┌───────────────────────────▼───────────────────────────────┐ │ │ │ FlowTesterService │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │ │ │ │ FlowManager │ │ Executor │ │ VariableBinder │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │ │ └───────────────────────────┬───────────────────────────────┘ │ │ │ │ │ ┌───────────────────────────▼───────────────────────────────┐ │ │ │ Database (MySQL - samdb) │ │ │ │ ┌──────────────────┐ ┌────────────────────────────┐ │ │ │ │ │ admin_api_flows │ │ admin_api_flow_runs │ │ │ │ │ └──────────────────┘ └────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ External API Server │ │ (SAM API: api.sam.kr/api/v1) │ └─────────────────────────────────────────────────────────────────┘ ``` ### 2.2 컴포넌트 설명 | 컴포넌트 | 역할 | |---------|------| | Flow List | 등록된 플로우 목록 조회, 검색, 필터링 | | Flow Editor | JSON 기반 플로우 정의 생성/수정, 구문 검증 | | Flow Executor | 플로우 실행, 실시간 진행상황 표시 | | FlowTesterService | 비즈니스 로직 처리 (CRUD, 실행, 바인딩) | | VariableBinder | `{{...}}` 변수 파싱 및 치환 | --- ## 3. 데이터베이스 스키마 ### 3.1 admin_api_flows 테이블 ```sql CREATE TABLE admin_api_flows ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(100) NOT NULL, -- 플로우 이름 description TEXT, -- 설명 category VARCHAR(50), -- 카테고리 (선택) flow_definition JSON NOT NULL, -- 플로우 정의 (JSON) is_active BOOLEAN DEFAULT 1, -- 활성화 여부 created_by INTEGER, -- 생성자 updated_by INTEGER, -- 수정자 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 인덱스 CREATE INDEX idx_admin_api_flows_category ON admin_api_flows(category); CREATE INDEX idx_admin_api_flows_is_active ON admin_api_flows(is_active); CREATE INDEX idx_admin_api_flows_name ON admin_api_flows(name); ``` ### 3.2 admin_api_flow_runs 테이블 ```sql CREATE TABLE admin_api_flow_runs ( id INTEGER PRIMARY KEY AUTOINCREMENT, flow_id INTEGER NOT NULL, -- 플로우 ID status VARCHAR(20) DEFAULT 'PENDING', -- PENDING, RUNNING, SUCCESS, FAILED, PARTIAL started_at TIMESTAMP, -- 시작 시간 completed_at TIMESTAMP, -- 완료 시간 duration_ms INTEGER, -- 실행 시간 (ms) total_steps INTEGER, -- 총 단계 수 completed_steps INTEGER DEFAULT 0, -- 완료된 단계 수 failed_step INTEGER, -- 실패한 단계 (있는 경우) execution_log JSON, -- 실행 로그 (단계별 결과) input_variables JSON, -- 입력 변수 (실행 시 제공) error_message TEXT, -- 에러 메시지 executed_by INTEGER, -- 실행자 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (flow_id) REFERENCES admin_api_flows(id) ON DELETE CASCADE ); -- 인덱스 CREATE INDEX idx_admin_api_flow_runs_flow_id ON admin_api_flow_runs(flow_id); CREATE INDEX idx_admin_api_flow_runs_status ON admin_api_flow_runs(status); CREATE INDEX idx_admin_api_flow_runs_created_at ON admin_api_flow_runs(created_at); ``` ### 3.3 상태값 정의 ```php // FlowRun Status const STATUS_PENDING = 'PENDING'; // 대기 중 const STATUS_RUNNING = 'RUNNING'; // 실행 중 const STATUS_SUCCESS = 'SUCCESS'; // 모든 단계 성공 const STATUS_FAILED = 'FAILED'; // 단계 실패로 중단 const STATUS_PARTIAL = 'PARTIAL'; // 일부 성공 (선택적 단계 실패) ``` --- ## 4. JSON 플로우 정의 스키마 ### 4.1 플로우 정의 구조 ```json { "version": "1.0", "meta": { "author": "사용자명", "created": "2025-11-27", "tags": ["item-master", "integration-test"] }, "config": { "baseUrl": "https://api.sam.kr/api/v1", "timeout": 30000, "stopOnFailure": true, "headers": { "Accept": "application/json", "Content-Type": "application/json" } }, "variables": { "testPrefix": "TEST_", "timestamp": "{{$timestamp}}" }, "steps": [ { "id": "step1", "name": "페이지 생성", "description": "새 아이템 페이지 생성", "method": "POST", "endpoint": "/item-master/pages", "headers": {}, "body": { "page_name": "{{variables.testPrefix}}Page_{{variables.timestamp}}", "item_type": "PRODUCT", "absolute_path": "/test/page" }, "expect": { "status": [200, 201], "jsonPath": { "$.success": true, "$.data.id": "@isNumber" } }, "extract": { "pageId": "$.data.id", "pageName": "$.data.page_name" }, "continueOnFailure": false, "retries": 0, "delay": 0 }, { "id": "step2", "name": "섹션 생성", "description": "페이지에 섹션 추가", "dependsOn": ["step1"], "method": "POST", "endpoint": "/item-master/pages/{{step1.pageId}}/sections", "body": { "title": "기본 정보", "type": "form" }, "expect": { "status": [200, 201] }, "extract": { "sectionId": "$.data.id" } }, { "id": "step3", "name": "필드 생성", "description": "섹션에 필드 추가", "dependsOn": ["step2"], "method": "POST", "endpoint": "/item-master/sections/{{step2.sectionId}}/fields", "body": { "field_name": "제품명", "field_type": "text", "is_required": true }, "expect": { "status": [200, 201] }, "extract": { "fieldId": "$.data.id" } }, { "id": "cleanup", "name": "테스트 데이터 정리", "description": "생성된 페이지 삭제", "dependsOn": ["step3"], "method": "DELETE", "endpoint": "/item-master/pages/{{step1.pageId}}", "expect": { "status": [200, 204] }, "continueOnFailure": true } ] } ``` ### 4.2 스키마 상세 설명 #### 4.2.1 Step 속성 | 속성 | 타입 | 필수 | 설명 | |------|------|------|------| | id | string | O | 고유 식별자 (변수 참조용) | | name | string | O | 단계 이름 (UI 표시용) | | description | string | X | 상세 설명 | | dependsOn | string[] | X | 의존하는 이전 단계 ID 목록 | | method | string | O | HTTP 메서드 (GET, POST, PUT, PATCH, DELETE) | | endpoint | string | O | API 엔드포인트 (변수 치환 가능) | | headers | object | X | 추가 헤더 | | body | object | X | 요청 바디 (변수 치환 가능) | | expect | object | X | 기대 응답 검증 | | extract | object | X | 응답에서 추출할 변수 | | continueOnFailure | boolean | X | 실패 시 계속 진행 여부 (기본: false) | | retries | number | X | 재시도 횟수 (기본: 0) | | delay | number | X | 실행 전 지연 시간 (ms) | #### 4.2.2 Expect 검증 옵션 | 속성 | 타입 | 설명 | |------|------|------| | status | number[] | 허용되는 HTTP 상태 코드 | | jsonPath | object | JSONPath 기반 값 검증 | **JSONPath 검증 연산자**: - 직접 값: `"$.success": true` - 타입 체크: `"$.data.id": "@isNumber"` - 존재 체크: `"$.data.items": "@exists"` - 배열 길이: `"$.data.items": "@minLength:1"` - 정규식: `"$.data.code": "@regex:^[A-Z]+$"` #### 4.2.3 변수 바인딩 문법 | 패턴 | 설명 | 예시 | |------|------|------| | `{{variables.xxx}}` | 전역 변수 | `{{variables.testPrefix}}` | | `{{stepN.xxx}}` | 이전 단계 추출값 | `{{step1.pageId}}` | | `{{stepN.response.xxx}}` | 이전 단계 전체 응답 | `{{step1.response.data.name}}` | | `{{$timestamp}}` | 현재 타임스탬프 | `1701100800` | | `{{$uuid}}` | 랜덤 UUID | `550e8400-e29b-41d4-a716-446655440000` | | `{{$random:N}}` | N자리 랜덤 숫자 | `{{$random:6}}` → `482916` | --- ## 5. 변수 바인딩 엔진 ### 5.1 처리 흐름 ``` ┌─────────────────────────────────────────────────────────────┐ │ Variable Binding Engine │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Input: "POST /pages/{{step1.pageId}}/sections" │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────┐ │ │ │ 1. Pattern Detection │ │ │ │ /\{\{([^}]+)\}\}/g │ │ │ │ Found: ["{{step1.pageId}}"] │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────┐ │ │ │ 2. Reference Resolution │ │ │ │ "step1.pageId" → context.steps.step1 │ │ │ │ → extracted.pageId = 42 │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────┐ │ │ │ 3. Value Substitution │ │ │ │ "{{step1.pageId}}" → "42" │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ Output: "POST /pages/42/sections" │ │ │ └─────────────────────────────────────────────────────────────┘ ``` ### 5.2 구현 클래스 ```php context, $key, $value); } /** * 단계 결과 저장 */ public function setStepResult(string $stepId, array $extracted, array $fullResponse): void { $this->context['steps'][$stepId] = [ 'extracted' => $extracted, 'response' => $fullResponse, ]; } /** * 문자열 내 모든 변수 치환 */ public function bind(mixed $input): mixed { if (is_string($input)) { return $this->bindString($input); } if (is_array($input)) { return array_map(fn($v) => $this->bind($v), $input); } return $input; } /** * 문자열 내 변수 패턴 치환 */ private function bindString(string $input): string { // 내장 변수 처리 $input = $this->resolveBuiltins($input); // 컨텍스트 변수 처리 return preg_replace_callback( '/\{\{([^}]+)\}\}/', fn($matches) => $this->resolveReference($matches[1]), $input ); } /** * 내장 변수 처리 ($timestamp, $uuid, $random:N) */ private function resolveBuiltins(string $input): string { $input = str_replace('{{$timestamp}}', time(), $input); $input = str_replace('{{$uuid}}', (string) \Illuminate\Support\Str::uuid(), $input); $input = preg_replace_callback( '/\{\{\$random:(\d+)\}\}/', fn($m) => str_pad(random_int(0, pow(10, $m[1]) - 1), $m[1], '0', STR_PAD_LEFT), $input ); return $input; } /** * 참조 경로 해석 (step1.pageId → 실제 값) */ private function resolveReference(string $path): mixed { // variables.xxx → $this->context['variables']['xxx'] if (str_starts_with($path, 'variables.')) { return data_get($this->context, $path, ''); } // stepN.xxx → $this->context['steps']['stepN']['extracted']['xxx'] if (preg_match('/^(step\w+)\.(.+)$/', $path, $m)) { $stepId = $m[1]; $subPath = $m[2]; // stepN.response.xxx → 전체 응답에서 추출 if (str_starts_with($subPath, 'response.')) { $responsePath = substr($subPath, 9); return data_get($this->context['steps'][$stepId]['response'] ?? [], $responsePath, ''); } // stepN.xxx → extracted에서 추출 return data_get($this->context['steps'][$stepId]['extracted'] ?? [], $subPath, ''); } return data_get($this->context, $path, ''); } } ``` --- ## 6. 플로우 실행 엔진 ### 6.1 실행 흐름 ``` ┌──────────────────────────────────────────────────────────────────┐ │ Flow Execution Engine │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Load Flow │───▶│ Build Order │───▶│ Init Binder │ │ │ │ Definition │ │ (TopSort) │ │ + Variables │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ For Each Step (Ordered) │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ │ │ 1. Check Dependencies (all completed?) │ │ │ │ │ │ 2. Apply Delay (if configured) │ │ │ │ │ │ 3. Bind Variables (endpoint, headers, body) │ │ │ │ │ │ 4. Execute HTTP Request │ │ │ │ │ │ 5. Validate Response (expect) │ │ │ │ │ │ 6. Extract Variables (extract) │ │ │ │ │ │ 7. Store Result in Context │ │ │ │ │ │ 8. Update Progress │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ │ ┌─────────────┴─────────────┐ │ │ │ │ ▼ ▼ │ │ │ │ ┌──────────┐ ┌──────────┐ │ │ │ │ │ Success │ │ Failed │ │ │ │ │ │ → Next │ │ → Check │ │ │ │ │ │ Step │ │ Config │ │ │ │ │ └──────────┘ └────┬─────┘ │ │ │ │ │ │ │ │ │ ┌───────────────┴───────────────┐ │ │ │ │ ▼ ▼ │ │ │ │ ┌────────────┐ ┌──────────┐│ │ │ │ │ Continue │ │ Stop ││ │ │ │ │ On Failure │ │ Execution││ │ │ │ └────────────┘ └──────────┘│ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────┐ │ │ │ Save Run │ │ │ │ Results │ │ │ └─────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘ ``` ### 6.2 의존성 정렬 (Topological Sort) ```php $degree) { if ($degree === 0) { $queue[] = $id; } } $sorted = []; while (!empty($queue)) { $current = array_shift($queue); $sorted[] = $current; foreach ($graph[$current] as $neighbor) { $inDegree[$neighbor]--; if ($inDegree[$neighbor] === 0) { $queue[] = $neighbor; } } } if (count($sorted) !== count($steps)) { throw new \Exception("Circular dependency detected in flow"); } return $sorted; } } ``` ### 6.3 응답 검증기 ```php formatList($allowedStatus)}, got {$response['status']}"; } } // JSONPath 검증 if (isset($expect['jsonPath'])) { foreach ($expect['jsonPath'] as $path => $expected) { $actual = data_get($response['body'], ltrim($path, '$.')); $pathError = $this->validateValue($actual, $expected, $path); if ($pathError) { $errors[] = $pathError; } } } return [ 'success' => empty($errors), 'errors' => $errors, ]; } /** * 개별 값 검증 */ private function validateValue(mixed $actual, mixed $expected, string $path): ?string { // 직접 값 비교 if (!is_string($expected) || !str_starts_with($expected, '@')) { if ($actual !== $expected) { return "Path {$path}: expected " . json_encode($expected) . ", got " . json_encode($actual); } return null; } // 연산자 처리 $operator = substr($expected, 1); return match (true) { $operator === 'exists' => $actual === null ? "Path {$path}: expected to exist" : null, $operator === 'isNumber' => !is_numeric($actual) ? "Path {$path}: expected number, got " . gettype($actual) : null, $operator === 'isString' => !is_string($actual) ? "Path {$path}: expected string, got " . gettype($actual) : null, $operator === 'isArray' => !is_array($actual) ? "Path {$path}: expected array, got " . gettype($actual) : null, $operator === 'isBoolean' => !is_bool($actual) ? "Path {$path}: expected boolean, got " . gettype($actual) : null, str_starts_with($operator, 'minLength:') => $this->validateMinLength($actual, $operator, $path), str_starts_with($operator, 'regex:') => $this->validateRegex($actual, $operator, $path), default => "Path {$path}: unknown operator @{$operator}", }; } private function validateMinLength(mixed $actual, string $operator, string $path): ?string { $min = (int) substr($operator, 10); if (!is_array($actual) || count($actual) < $min) { return "Path {$path}: expected array with min length {$min}"; } return null; } private function validateRegex(mixed $actual, string $operator, string $path): ?string { $pattern = '/' . substr($operator, 6) . '/'; if (!is_string($actual) || !preg_match($pattern, $actual)) { return "Path {$path}: value does not match pattern {$pattern}"; } return null; } private function formatList(array $items): string { return '[' . implode(', ', $items) . ']'; } } ``` --- ## 7. UI 설계 ### 7.1 화면 구성 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ SAM MNG admin@sam.kr ▼ │ ├─────────────┬───────────────────────────────────────────────────────────┤ │ │ │ │ 대시보드 │ API Flow Tester │ │ │ ─────────────────────────────────────────────────────── │ │ 시스템관리 │ │ │ ├ 사용자 │ ┌─────────────────────────────────────────────────────┐ │ │ ├ 역할 │ │ 플로우 목록 [+ 새 플로우] │ │ │ └ 권한 │ ├─────────────────────────────────────────────────────┤ │ │ │ │ □ 이름 카테고리 상태 최근실행 액션 │ │ │ 개발 도구 │ │ ─────────────────────────────────────────────────── │ │ │ ├ API 플로우│ │ □ ItemMaster item-master 성공 5분 전 ▶ ✎ │ │ │ 테스터 │ │ □ Auth Flow auth 실패 1시간전 ▶ ✎ │ │ │ │ │ □ BOM Test bom 대기 - ▶ ✎ │ │ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ └─────────────┴───────────────────────────────────────────────────────────┘ ``` ### 7.2 플로우 편집기 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 플로우 편집: ItemMaster Integration Test [저장] [×] │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 기본 정보 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 이름: [ItemMaster Integration Test ] │ │ │ │ 카테고리: [item-master ▼] 설명: [페이지-섹션-필드 통합 테스트]│ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 플로우 정의 (JSON) [검증] [포맷] │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 1 { │ │ │ │ 2 "version": "1.0", │ │ │ │ 3 "config": { │ │ │ │ 4 "baseUrl": "https://api.sam.kr/api/v1", │ │ │ │ 5 "timeout": 30000 │ │ │ │ 6 }, │ │ │ │ 7 "steps": [ │ │ │ │ 8 { │ │ │ │ 9 "id": "step1", │ │ │ │ 10 "name": "페이지 생성", │ │ │ │ 11 "method": "POST", │ │ │ │ ... ... │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ ✓ JSON 문법 유효 │ 스텝 4개 │ 의존성 그래프 유효 │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 7.3 플로우 실행 화면 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 플로우 실행: ItemMaster Integration Test [×] │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 실행 상태 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ ████████████████░░░░░░░░ 3/4 단계 완료 │ │ │ │ 경과 시간: 2.4s │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 단계별 결과 │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ ✅ step1: 페이지 생성 201 Created │ │ │ │ └─ pageId: 42, pageName: "TEST_Page_1701100800" │ │ │ │ │ │ │ │ ✅ step2: 섹션 생성 201 Created │ │ │ │ └─ sectionId: 108 │ │ │ │ │ │ │ │ ✅ step3: 필드 생성 201 Created │ │ │ │ └─ fieldId: 256 │ │ │ │ │ │ │ │ 🔄 cleanup: 테스트 데이터 정리 실행 중... │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 상세 로그 ─────────────────────────────────────────── │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ [14:32:01] step1 시작 │ │ │ │ [14:32:01] POST /item-master/pages │ │ │ │ [14:32:01] Response: 201 Created (234ms) │ │ │ │ [14:32:01] Extracted: pageId=42, pageName=TEST_Page_1701100800 │ │ │ │ [14:32:01] step2 시작 │ │ │ │ ... │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ [실행 중지] [다시 실행] │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 7.4 실행 이력 화면 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 실행 이력: ItemMaster Integration Test │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 실행 ID 상태 시작 시간 소요시간 실행자 │ │ │ │ ─────────────────────────────────────────────────────────────── │ │ │ │ #127 ✅ 성공 2025-11-27 14:32:01 2.8s admin │ │ │ │ #126 ❌ 실패 2025-11-27 14:28:15 1.2s admin │ │ │ │ #125 ✅ 성공 2025-11-27 13:45:00 2.6s admin │ │ │ │ #124 ⚠️ 부분 2025-11-27 11:20:33 3.1s admin │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 실행 #126 상세 ──────────────────────────────── │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 실패 단계: step2 (섹션 생성) │ │ │ │ 에러 메시지: Expected status [200, 201], got 422 │ │ │ │ │ │ │ │ 응답 내용: │ │ │ │ { │ │ │ │ "success": false, │ │ │ │ "message": "validation_error", │ │ │ │ "errors": { "type": ["type 필드는 필수입니다."] } │ │ │ │ } │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## 8. API 엔드포인트 ### 8.1 MNG 내부 라우트 ```php // routes/web.php Route::prefix('dev-tools')->name('dev-tools.')->middleware(['auth'])->group(function () { // 플로우 목록 Route::get('/flow-tester', [FlowTesterController::class, 'index']) ->name('flow-tester.index'); // 플로우 생성 폼 Route::get('/flow-tester/create', [FlowTesterController::class, 'create']) ->name('flow-tester.create'); // 플로우 저장 Route::post('/flow-tester', [FlowTesterController::class, 'store']) ->name('flow-tester.store'); // 플로우 상세/편집 Route::get('/flow-tester/{id}', [FlowTesterController::class, 'edit']) ->name('flow-tester.edit'); // 플로우 수정 Route::put('/flow-tester/{id}', [FlowTesterController::class, 'update']) ->name('flow-tester.update'); // 플로우 삭제 Route::delete('/flow-tester/{id}', [FlowTesterController::class, 'destroy']) ->name('flow-tester.destroy'); // 플로우 복제 Route::post('/flow-tester/{id}/clone', [FlowTesterController::class, 'clone']) ->name('flow-tester.clone'); // JSON 검증 (HTMX) Route::post('/flow-tester/validate-json', [FlowTesterController::class, 'validateJson']) ->name('flow-tester.validate-json'); // 플로우 실행 Route::post('/flow-tester/{id}/run', [FlowTesterController::class, 'run']) ->name('flow-tester.run'); // 실행 상태 조회 (Polling/SSE) Route::get('/flow-tester/runs/{runId}/status', [FlowTesterController::class, 'runStatus']) ->name('flow-tester.run-status'); // 실행 이력 Route::get('/flow-tester/{id}/history', [FlowTesterController::class, 'history']) ->name('flow-tester.history'); // 실행 상세 Route::get('/flow-tester/runs/{runId}', [FlowTesterController::class, 'runDetail']) ->name('flow-tester.run-detail'); }); ``` ### 8.2 HTMX 통합 ```html