2025-11-30 21:04:19 +09:00
|
|
|
|
# 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 │
|
2025-12-22 19:51:36 +09:00
|
|
|
|
│ (SAM API: api.sam.kr/api/v1) │
|
2025-11-30 21:04:19 +09:00
|
|
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 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": {
|
2025-12-22 19:51:36 +09:00
|
|
|
|
"baseUrl": "https://api.sam.kr/api/v1",
|
2025-11-30 21:04:19 +09:00
|
|
|
|
"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": { │ │
|
2025-12-22 19:51:36 +09:00
|
|
|
|
│ │ 4 "baseUrl": "https://api.sam.kr/api/v1", │ │
|
2025-11-30 21:04:19 +09:00
|
|
|
|
│ │ 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/)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
**문서 끝**
|