Files
sam-manage/docs/API_FLOW_TESTER_DESIGN.md

1037 lines
49 KiB
Markdown
Raw Normal View History

# 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: 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://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://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/)
---
**문서 끝**