context = []; } /** * 전역 변수 설정 * 환경변수 참조 ({{$env.XXX}})도 이 시점에 치환 */ public function setVariables(array $variables): void { // 환경변수 참조를 먼저 치환 $resolvedVariables = []; foreach ($variables as $key => $value) { if (is_string($value)) { $resolvedVariables[$key] = $this->resolveBuiltins($value); } else { $resolvedVariables[$key] = $value; } } $this->context['variables'] = $resolvedVariables; } /** * 컨텍스트에 변수 추가 */ 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, ]; // 편의를 위해 extracted 값을 stepId.key 형태로도 접근 가능하게 foreach ($extracted as $key => $value) { $this->context['steps'][$stepId][$key] = $value; } } /** * 문자열/배열 내 모든 변수 치환 */ 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, $env.XXX, $auth.token) */ private function resolveBuiltins(string $input): string { // {{$timestamp}} → 현재 타임스탬프 $input = str_replace('{{$timestamp}}', (string) time(), $input); // {{$uuid}} → 랜덤 UUID $input = str_replace('{{$uuid}}', (string) Str::uuid(), $input); // {{$random:N}} → N자리 랜덤 숫자 $input = preg_replace_callback( '/\{\{\$random:(\d+)\}\}/', function ($m) { $digits = (int) $m[1]; $max = (int) pow(10, $digits) - 1; return str_pad((string) random_int(0, $max), $digits, '0', STR_PAD_LEFT); }, $input ); // {{$date}} → 현재 날짜 (Y-m-d) $input = str_replace('{{$date}}', date('Y-m-d'), $input); // {{$datetime}} → 현재 날짜시간 (Y-m-d H:i:s) $input = str_replace('{{$datetime}}', date('Y-m-d H:i:s'), $input); // {{$env.VAR_NAME}} → 환경변수에서 읽기 $input = preg_replace_callback( '/\{\{\$env\.([A-Z_][A-Z0-9_]*)\}\}/i', fn ($m) => env($m[1], ''), $input ); // {{$auth.token}} → 현재 로그인 사용자의 API 토큰 if (str_contains($input, '{{$auth.token}}')) { $token = $this->getAuthToken(); $input = str_replace('{{$auth.token}}', $token, $input); } // {{$auth.apiKey}} → .env의 API Key $input = str_replace('{{$auth.apiKey}}', env('FLOW_TESTER_API_KEY', ''), $input); // {{$faker.xxx}} → Faker 기반 랜덤 데이터 생성 $input = $this->resolveFaker($input); return $input; } /** * Faker 인스턴스 가져오기 (지연 초기화) */ private function getFaker(): FakerGenerator { if ($this->faker === null) { $this->faker = FakerFactory::create('ko_KR'); } return $this->faker; } /** * Faker 변수 처리 * * 지원 패턴: * - {{$faker.name}} - 랜덤 이름 * - {{$faker.company}} - 랜덤 회사명 * - {{$faker.email}} - 랜덤 이메일 * - {{$faker.phone}} - 랜덤 전화번호 * - {{$faker.word}} - 랜덤 단어 * - {{$faker.sentence}} - 랜덤 문장 * - {{$faker.text}} - 랜덤 텍스트 * - {{$faker.number:MIN:MAX}} - 범위 내 랜덤 정수 * - {{$faker.price:MIN:MAX}} - 범위 내 랜덤 가격 (소수점 2자리) * - {{$faker.code:PREFIX:LENGTH}} - 코드 생성 (PREFIX + 숫자) * - {{$faker.itemCode:PREFIX}} - 품목코드 생성 (PREFIX + 6자리 숫자) * - {{$faker.address}} - 랜덤 주소 * - {{$faker.url}} - 랜덤 URL * - {{$faker.boolean}} - true/false */ private function resolveFaker(string $input): string { // {{$faker.xxx}} 또는 {{$faker.xxx:param1:param2}} 패턴 처리 return preg_replace_callback( '/\{\{\$faker\.([a-zA-Z]+)(?::([^}]*))?\}\}/', fn ($m) => $this->generateFakerValue($m[1], $m[2] ?? ''), $input ); } /** * Faker 값 생성 */ private function generateFakerValue(string $type, string $params): string { $faker = $this->getFaker(); $paramList = $params !== '' ? explode(':', $params) : []; return match ($type) { // 기본 텍스트 'name' => $faker->name(), 'firstName' => $faker->firstName(), 'lastName' => $faker->lastName(), 'company' => $faker->company(), 'email' => $faker->unique()->safeEmail(), 'phone' => $faker->phoneNumber(), 'word' => $faker->word(), 'sentence' => $faker->sentence(), 'text' => $faker->text(100), 'paragraph' => $faker->paragraph(), // 주소 'address' => $faker->address(), 'city' => $faker->city(), 'postcode' => $faker->postcode(), // 숫자 'number' => $this->fakerNumber($faker, $paramList), 'price' => $this->fakerPrice($faker, $paramList), 'quantity' => (string) $faker->numberBetween(1, 100), // 코드 생성 'code' => $this->fakerCode($paramList), 'itemCode' => $this->fakerItemCode($paramList), 'sku' => 'SKU-'.strtoupper($faker->bothify('??###')), 'barcode' => $faker->ean13(), // 기타 'url' => $faker->url(), 'boolean' => $faker->boolean() ? 'true' : 'false', 'uuid' => $faker->uuid(), 'date' => $faker->date('Y-m-d'), 'datetime' => $faker->dateTime()->format('Y-m-d H:i:s'), // 품목 관련 'productName' => $this->fakerProductName($faker), 'unit' => $faker->randomElement(['EA', 'SET', 'BOX', 'KG', 'M', 'L', 'PCS']), 'category' => $faker->randomElement(['원자재', '부품', '반제품', '완제품', '소모품']), default => "{{faker.{$type}}}", }; } /** * 범위 내 랜덤 정수 생성 */ private function fakerNumber(FakerGenerator $faker, array $params): string { $min = isset($params[0]) ? (int) $params[0] : 1; $max = isset($params[1]) ? (int) $params[1] : 1000; return (string) $faker->numberBetween($min, $max); } /** * 범위 내 랜덤 가격 생성 (소수점 2자리) */ private function fakerPrice(FakerGenerator $faker, array $params): string { $min = isset($params[0]) ? (float) $params[0] : 1000; $max = isset($params[1]) ? (float) $params[1] : 100000; return number_format($faker->randomFloat(2, $min, $max), 2, '.', ''); } /** * 커스텀 코드 생성 (PREFIX + 숫자) */ private function fakerCode(array $params): string { $prefix = $params[0] ?? 'CODE'; $length = isset($params[1]) ? (int) $params[1] : 6; $digits = str_pad((string) random_int(0, (int) pow(10, $length) - 1), $length, '0', STR_PAD_LEFT); return $prefix.$digits; } /** * 품목코드 생성 (PREFIX + 6자리 숫자) */ private function fakerItemCode(array $params): string { $prefix = $params[0] ?? 'ITEM'; $digits = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); return $prefix.'-'.$digits; } /** * 제품명 생성 (한글) */ private function fakerProductName(FakerGenerator $faker): string { $prefixes = ['고급', '프리미엄', '스탠다드', '베이직', '프로', '울트라']; $types = ['부품', '자재', '모듈', '키트', '세트', '패키지']; $models = ['A', 'B', 'C', 'X', 'Y', 'Z', 'Pro', 'Plus', 'Max']; return $faker->randomElement($prefixes).' ' .$faker->randomElement($types).' ' .$faker->randomElement($models).'-' .$faker->numberBetween(100, 999); } /** * 현재 로그인 사용자의 API 토큰 조회 */ private function getAuthToken(): string { $user = auth()->user(); if (! $user) { return env('FLOW_TESTER_API_TOKEN', ''); } // 사용자에게 저장된 API 토큰이 있으면 사용 if (! empty($user->api_token)) { return $user->api_token; } // 없으면 .env의 기본 토큰 사용 return env('FLOW_TESTER_API_TOKEN', ''); } /** * 참조 경로 해석 (step1.pageId 또는 page_create_1.pageId → 실제 값) * * 지원 패턴: * - {{variables.xxx}} - 명시적 변수 참조 * - {{xxx}} - 단축형 변수 참조 (variables.xxx로 폴백) * - {{stepId.xxx}} - 스텝 추출값 참조 * - {{stepId.response.xxx}} - 스텝 전체 응답 참조 */ private function resolveReference(string $path): string { $path = trim($path); // variables.xxx → $this->context['variables']['xxx'] if (str_starts_with($path, 'variables.')) { $value = data_get($this->context, $path, ''); return $this->valueToString($value); } // stepId.xxx 패턴 감지 (stepId는 등록된 step의 ID) // 첫 번째 점(.) 기준으로 분리 $dotPos = strpos($path, '.'); if ($dotPos !== false) { $potentialStepId = substr($path, 0, $dotPos); $subPath = substr($path, $dotPos + 1); // 등록된 step인지 확인 if (isset($this->context['steps'][$potentialStepId])) { // stepId.response.xxx → 전체 응답에서 추출 if (str_starts_with($subPath, 'response.')) { $responsePath = substr($subPath, 9); // "response." 제거 $value = data_get($this->context['steps'][$potentialStepId]['response'] ?? [], $responsePath, ''); return $this->valueToString($value); } // stepId.xxx → extracted 또는 직접 접근 $value = data_get($this->context['steps'][$potentialStepId] ?? [], $subPath, ''); return $this->valueToString($value); } } // 기타 경로는 context에서 직접 조회 $value = data_get($this->context, $path, null); // 값이 없으면 variables.xxx로 폴백 시도 (단축형 지원) // 예: {{user_id}} → variables.user_id if ($value === null && ! str_contains($path, '.')) { $value = data_get($this->context, 'variables.'.$path, ''); } return $this->valueToString($value ?? ''); } /** * 값을 문자열로 변환 (배열/객체는 JSON) */ private function valueToString(mixed $value): string { if (is_null($value)) { return ''; } if (is_bool($value)) { return $value ? 'true' : 'false'; } if (is_array($value) || is_object($value)) { return json_encode($value, JSON_UNESCAPED_UNICODE); } return (string) $value; } /** * 현재 컨텍스트 반환 (디버깅용) */ public function getContext(): array { return $this->context; } }