feat: [flow-tester] API 로그 캡처 및 UI 개선

- ApiLogCapturer 추가: 플로우 실행 중 API 로그 캡처
- resolveBaseUrl() 추가: .env 환경변수 기반 baseUrl 지원
- 실행 상세 페이지: 스텝별 접기/펼치기 기능 (성공=접힘, 실패=펼침)
- JSON 가이드 및 예제 플로우 최신화
- AI 프롬프트 템플릿 업데이트
- bindExpectVariables() 추가: expect jsonPath 값에 변수 바인딩 적용
- areNumericEqual() 추가: 숫자 타입 유연 비교 ("2" == 2)
This commit is contained in:
2025-12-04 15:30:04 +09:00
parent 20cfa01579
commit fe10cae06c
9 changed files with 709 additions and 143 deletions

View File

@@ -26,6 +26,8 @@ class FlowExecutor
private HttpClient $httpClient;
private ApiLogCapturer $logCapturer;
/**
* 실행 로그
*/
@@ -48,16 +50,23 @@ class FlowExecutor
*/
private array $stepSuccessMap = [];
/**
* 캡처된 API 로그
*/
private array $apiLogs = [];
public function __construct(
?VariableBinder $binder = null,
?DependencyResolver $resolver = null,
?ResponseValidator $validator = null,
?HttpClient $httpClient = null
?HttpClient $httpClient = null,
?ApiLogCapturer $logCapturer = null
) {
$this->binder = $binder ?? new VariableBinder;
$this->resolver = $resolver ?? new DependencyResolver;
$this->validator = $validator ?? new ResponseValidator;
$this->httpClient = $httpClient ?? new HttpClient;
$this->logCapturer = $logCapturer ?? new ApiLogCapturer;
}
/**
@@ -72,6 +81,9 @@ public function execute(array $flowDefinition, array $inputVariables = []): arra
$startTime = microtime(true);
$this->reset();
// API 로그 캡처 시작
$this->logCapturer->start();
try {
// 1. 플로우 정의 검증
$this->validateFlowDefinition($flowDefinition);
@@ -242,6 +254,10 @@ private function executeStep(array $step): array
if (empty($expect) || ! isset($expect['status'])) {
$expect['status'] = [200, 201, 204];
}
// expect 값에도 변수 바인딩 적용 (jsonPath 값의 {{변수}} 치환)
$expect = $this->bindExpectVariables($expect);
$validation = $this->validator->validate($response, $expect);
// 6. 변수 추출
@@ -317,9 +333,10 @@ private function validateFlowDefinition(array $definition): void
*/
private function applyConfig(array $config): void
{
// Base URL - Docker 환경 자동 변환
if (isset($config['baseUrl'])) {
$baseUrl = $config['baseUrl'];
// Base URL 결정 - JSON에 있으면 사용, 없으면 .env에서
$baseUrl = $this->resolveBaseUrl($config['baseUrl'] ?? null);
if ($baseUrl) {
// Docker 환경에서 외부 URL을 내부 URL로 변환
if ($this->isDockerEnvironment()) {
@@ -395,6 +412,46 @@ private function getDefaultBearerToken(): ?string
return env('FLOW_TESTER_API_TOKEN');
}
/**
* Base URL 결정
*
* 우선순위:
* 1. JSON에 완전한 URL (도메인 포함) → 그대로 사용
* 2. JSON에 경로만 있거나 없음 → .env의 FLOW_TESTER_API_BASE_URL + 경로
*
* @param string|null $configBaseUrl JSON config의 baseUrl
* @return string|null 최종 Base URL
*/
private function resolveBaseUrl(?string $configBaseUrl): ?string
{
// 환경변수에서 기본 도메인 가져오기
$envBaseUrl = env('FLOW_TESTER_API_BASE_URL');
// JSON에 baseUrl이 없는 경우
if (empty($configBaseUrl)) {
return $envBaseUrl ?: null;
}
// JSON에 완전한 URL이 있는지 확인 (http:// 또는 https://로 시작)
if (preg_match('#^https?://#', $configBaseUrl)) {
// 완전한 URL이면 그대로 사용
return $configBaseUrl;
}
// 상대 경로만 있는 경우 (.env 도메인 + 상대 경로)
if ($envBaseUrl) {
// .env URL 끝의 슬래시 제거
$envBaseUrl = rtrim($envBaseUrl, '/');
// 상대 경로 시작의 슬래시 보장
$configBaseUrl = '/'.ltrim($configBaseUrl, '/');
return $envBaseUrl.$configBaseUrl;
}
// .env도 없고 상대 경로만 있으면 그대로 반환 (실패할 수 있음)
return $configBaseUrl;
}
/**
* Docker 환경인지 확인
*/
@@ -449,6 +506,9 @@ private function buildResult(
): array {
$duration = (int) ((microtime(true) - $startTime) * 1000);
// API 로그 캡처
$this->apiLogs = $this->logCapturer->capture();
return [
'status' => $status,
'duration' => $duration,
@@ -457,6 +517,7 @@ private function buildResult(
'failedStep' => $failedStep,
'errorMessage' => $errorMessage,
'executionLog' => $this->executionLog,
'apiLogs' => $this->apiLogs,
'startedAt' => date('Y-m-d H:i:s', (int) $startTime),
'completedAt' => date('Y-m-d H:i:s'),
];
@@ -472,6 +533,7 @@ private function reset(): void
$this->completedSteps = 0;
$this->totalSteps = 0;
$this->stepSuccessMap = [];
$this->apiLogs = [];
$this->binder->reset();
}
@@ -560,6 +622,27 @@ private function buildResultReason(bool $success, array $expect, int $actualStat
return $reason;
}
/**
* expect 값에 변수 바인딩 적용
*
* jsonPath 값들의 {{변수}} 플레이스홀더를 치환합니다.
* 예: "$.data.client_code": "{{test_client_code}}" → "TEST_CLIENT_123"
*/
private function bindExpectVariables(array $expect): array
{
// jsonPath 값들에 변수 바인딩 적용
if (isset($expect['jsonPath']) && is_array($expect['jsonPath'])) {
foreach ($expect['jsonPath'] as $path => $expected) {
// 문자열이고 {{}} 플레이스홀더가 있는 경우만 바인딩
if (is_string($expected) && str_contains($expected, '{{')) {
$expect['jsonPath'][$path] = $this->binder->bind($expected);
}
}
}
return $expect;
}
/**
* assertions 배열 형식을 expect 객체 형식으로 변환
*