- LoginToken 모델 수정 - items-bom/crud/search 플로우 데이터 업데이트 - API_FLOW_TESTER_DESIGN 문서 업데이트 - example-flows 뷰 업데이트
1037 lines
49 KiB
Markdown
1037 lines
49 KiB
Markdown
# 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
|
||
<?php
|
||
|
||
namespace App\Services\FlowTester;
|
||
|
||
class VariableBinder
|
||
{
|
||
private array $context = [];
|
||
|
||
/**
|
||
* 컨텍스트에 변수 추가
|
||
*/
|
||
public function setVariable(string $key, mixed $value): void
|
||
{
|
||
data_set($this->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
|
||
<?php
|
||
|
||
namespace App\Services\FlowTester;
|
||
|
||
class DependencyResolver
|
||
{
|
||
/**
|
||
* 의존성 기반 실행 순서 결정
|
||
*
|
||
* @param array $steps 스텝 정의 배열
|
||
* @return array 정렬된 스텝 ID 배열
|
||
* @throws \Exception 순환 의존성 발견 시
|
||
*/
|
||
public function resolve(array $steps): array
|
||
{
|
||
$graph = [];
|
||
$inDegree = [];
|
||
|
||
// 그래프 초기화
|
||
foreach ($steps as $step) {
|
||
$id = $step['id'];
|
||
$graph[$id] = [];
|
||
$inDegree[$id] = 0;
|
||
}
|
||
|
||
// 간선 추가 (의존성)
|
||
foreach ($steps as $step) {
|
||
$id = $step['id'];
|
||
$deps = $step['dependsOn'] ?? [];
|
||
|
||
foreach ($deps as $dep) {
|
||
if (!isset($graph[$dep])) {
|
||
throw new \Exception("Unknown dependency: {$dep} in step {$id}");
|
||
}
|
||
$graph[$dep][] = $id;
|
||
$inDegree[$id]++;
|
||
}
|
||
}
|
||
|
||
// Kahn's Algorithm
|
||
$queue = [];
|
||
foreach ($inDegree as $id => $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
|
||
<?php
|
||
|
||
namespace App\Services\FlowTester;
|
||
|
||
class ResponseValidator
|
||
{
|
||
/**
|
||
* 응답 검증
|
||
*
|
||
* @param array $response HTTP 응답 [status, headers, body]
|
||
* @param array $expect 기대값 정의
|
||
* @return array [success, errors]
|
||
*/
|
||
public function validate(array $response, array $expect): array
|
||
{
|
||
$errors = [];
|
||
|
||
// 상태 코드 검증
|
||
if (isset($expect['status'])) {
|
||
$allowedStatus = (array) $expect['status'];
|
||
if (!in_array($response['status'], $allowedStatus)) {
|
||
$errors[] = "Expected status {$this->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
|
||
<!-- 플로우 목록 새로고침 -->
|
||
<div hx-get="{{ route('dev-tools.flow-tester.index') }}"
|
||
hx-trigger="load, flowUpdated from:body"
|
||
hx-target="#flow-list">
|
||
</div>
|
||
|
||
<!-- JSON 실시간 검증 -->
|
||
<textarea name="flow_definition"
|
||
hx-post="{{ route('dev-tools.flow-tester.validate-json') }}"
|
||
hx-trigger="keyup changed delay:500ms"
|
||
hx-target="#validation-result">
|
||
</textarea>
|
||
|
||
<!-- 실행 상태 폴링 -->
|
||
<div id="run-status"
|
||
hx-get="{{ route('dev-tools.flow-tester.run-status', $runId) }}"
|
||
hx-trigger="every 1s"
|
||
hx-swap="innerHTML">
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
## 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/)
|
||
|
||
---
|
||
|
||
**문서 끝**
|