- LoginToken 모델 수정 - items-bom/crud/search 플로우 데이터 업데이트 - API_FLOW_TESTER_DESIGN 문서 업데이트 - example-flows 뷰 업데이트
49 KiB
49 KiB
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 테이블
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 테이블
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 상태값 정의
// 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 플로우 정의 구조
{
"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
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
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
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 내부 라우트
// 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 통합
<!-- 플로우 목록 새로고침 -->
<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
참고 문서
문서 끝