# 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
``` --- ## 9. 파일 구조 ``` mng/ ├── app/ │ ├── Http/ │ │ └── Controllers/ │ │ └── FlowTesterController.php │ ├── Models/ │ │ ├── AdminApiFlow.php │ │ └── AdminApiFlowRun.php │ └── Services/ │ └── FlowTester/ │ ├── FlowTesterService.php # 메인 서비스 │ ├── FlowExecutor.php # 실행 엔진 │ ├── VariableBinder.php # 변수 바인딩 │ ├── DependencyResolver.php # 의존성 정렬 │ ├── ResponseValidator.php # 응답 검증 │ └── HttpClient.php # HTTP 클라이언트 래퍼 ├── database/ │ └── migrations/ │ └── xxxx_create_admin_api_flow_tables.php ├── resources/ │ └── views/ │ └── dev-tools/ │ └── flow-tester/ │ ├── index.blade.php # 플로우 목록 │ ├── create.blade.php # 생성 폼 │ ├── edit.blade.php # 편집 폼 │ ├── run.blade.php # 실행 화면 │ ├── history.blade.php # 실행 이력 │ └── partials/ │ ├── flow-list.blade.php │ ├── step-result.blade.php │ └── run-status.blade.php └── public/ └── js/ └── flow-tester/ ├── json-editor.js # JSON 에디터 기능 └── flow-runner.js # 실행 UI 제어 ``` --- ## 10. 구현 일정 (4-5일) ### Day 1: 기반 구축 - [ ] 데이터베이스 마이그레이션 생성 및 실행 - [ ] Model 클래스 생성 (AdminApiFlow, AdminApiFlowRun) - [ ] 사이드바 메뉴 추가 ("개발 도구" 그룹) - [ ] 기본 라우트 설정 - [ ] FlowTesterController 스캐폴딩 ### Day 2: 핵심 서비스 - [ ] VariableBinder 구현 (변수 바인딩 엔진) - [ ] DependencyResolver 구현 (의존성 정렬) - [ ] ResponseValidator 구현 (응답 검증) - [ ] HttpClient 구현 (API 호출 래퍼) - [ ] FlowExecutor 구현 (실행 엔진) ### Day 3: CRUD UI - [ ] 플로우 목록 화면 (index.blade.php) - [ ] 플로우 생성/편집 화면 (create.blade.php, edit.blade.php) - [ ] JSON 에디터 통합 (CodeMirror 또는 Monaco) - [ ] JSON 실시간 검증 (HTMX) - [ ] 플로우 삭제/복제 기능 ### Day 4: 실행 및 모니터링 - [ ] 플로우 실행 화면 (run.blade.php) - [ ] 실시간 진행상황 표시 (Polling/SSE) - [ ] 단계별 결과 표시 - [ ] 실행 이력 화면 (history.blade.php) - [ ] 실행 상세 보기 ### Day 5: 마무리 및 테스트 - [ ] 에러 핸들링 강화 - [ ] UI 폴리싱 - [ ] 테스트 플로우 작성 (ItemMaster 예제) - [ ] 문서화 - [ ] 버그 수정 --- ## 11. 기술 스택 | 영역 | 기술 | |------|------| | Backend | Laravel 12 + PHP 8.4 | | Database | MySQL (samdb - admin_* 접두사 테이블) | | Frontend | Blade + Tailwind CSS + DaisyUI | | Interactivity | HTMX 1.9 | | JSON Editor | CodeMirror 6 또는 Monaco Editor (lite) | | HTTP Client | Guzzle 또는 Laravel HTTP Client | --- ## 12. 보안 고려사항 ### 12.1 접근 제어 - MNG 관리자 인증 필수 - 특정 권한 보유자만 접근 가능 (설정 가능) ### 12.2 API 호출 보안 - 저장된 API 키 사용 (환경변수에서 로드) - 외부 URL 호출 제한 (화이트리스트) - 민감 데이터 마스킹 (로그에서 Authorization 헤더 등) ### 12.3 데이터 보호 - 실행 로그 보존 기간 설정 - 민감 정보 저장 금지 (실제 비밀번호 등) --- ## 13. 확장 가능성 (향후) ### Phase 2 (선택적) - 스케줄 기반 자동 실행 - Slack/Teams 알림 연동 - 플로우 템플릿 공유 - 환경별 설정 (dev/staging/prod) ### Phase 3 (AI 통합 - A 버전) - Claude API 연동으로 플로우 자동 생성 - 에러 분석 및 수정 제안 - 테스트 데이터 자동 생성 --- ## 14. 참고 자료 ### 유사 도구 - Postman Collections & Runner - Insomnia Request Chaining - Bruno Sequential Requests - k6 Load Testing Scripts ### 참고 문서 - [HTMX 공식 문서](https://htmx.org/docs/) - [Laravel HTTP Client](https://laravel.com/docs/12.x/http-client) - [JSONPath 표준](https://goessner.net/articles/JsonPath/) --- **문서 끝**