Flow Tester AI 프롬프트 템플릿 개선
- config.apiKey 필드를 JSON에서 제거 (서버 자동 주입) - config.baseUrl을 빈 문자열로 설정 (서버 기본값 사용) - 프롬프트 템플릿에 더 명확한 규칙 추가 - 로그인 스텝 포함한 완전한 예시 제공 - 예시 프롬프트 간소화
This commit is contained in:
@@ -168,33 +168,44 @@ public function validateJson(Request $request)
|
||||
]);
|
||||
}
|
||||
|
||||
// meta 정보 추출
|
||||
// meta 정보 추출 (최상위 또는 meta 객체에서)
|
||||
$meta = $data['meta'] ?? [];
|
||||
$tags = $meta['tags'] ?? [];
|
||||
|
||||
// 카테고리 추론: tags[0] 또는 첫 번째 endpoint에서 추출
|
||||
$category = $tags[0] ?? null;
|
||||
// 카테고리 추론: 최상위 category > tags[0] > endpoint에서 추출 (login 제외)
|
||||
$category = $data['category'] ?? $tags[0] ?? null;
|
||||
if (! $category && ! empty($data['steps'])) {
|
||||
$firstEndpoint = $data['steps'][0]['endpoint'] ?? '';
|
||||
// /item-master/pages → item-master
|
||||
if (preg_match('#^/([^/]+)#', $firstEndpoint, $matches)) {
|
||||
$category = $matches[1];
|
||||
// login, auth, refresh, logout 등 인증 관련 엔드포인트는 건너뛰고 추출
|
||||
$authEndpoints = ['/login', '/logout', '/refresh', '/auth'];
|
||||
foreach ($data['steps'] as $step) {
|
||||
$endpoint = $step['endpoint'] ?? '';
|
||||
// 인증 엔드포인트가 아닌 첫 번째 스텝에서 카테고리 추출
|
||||
if (! in_array($endpoint, $authEndpoints) && preg_match('#^/([^/]+)#', $endpoint, $matches)) {
|
||||
$category = $matches[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 이름 추론: meta.name 또는 description 첫 부분
|
||||
$name = $meta['name'] ?? null;
|
||||
if (! $name && ! empty($meta['description'])) {
|
||||
// 설명의 첫 번째 줄 또는 50자까지
|
||||
$name = mb_substr(strtok($meta['description'], "\n"), 0, 50);
|
||||
// 이름 추론: 최상위 name > meta.name > description 첫 부분
|
||||
$name = $data['name'] ?? $meta['name'] ?? null;
|
||||
if (! $name) {
|
||||
$description = $data['description'] ?? $meta['description'] ?? null;
|
||||
if ($description) {
|
||||
// 설명의 첫 번째 줄 또는 50자까지
|
||||
$name = mb_substr(strtok($description, "\n"), 0, 50);
|
||||
}
|
||||
}
|
||||
|
||||
// 설명 추론: 최상위 description > meta.description
|
||||
$description = $data['description'] ?? $meta['description'] ?? null;
|
||||
|
||||
return response()->json([
|
||||
'valid' => true,
|
||||
'stepCount' => count($data['steps'] ?? []),
|
||||
'extracted' => [
|
||||
'name' => $name,
|
||||
'description' => $meta['description'] ?? null,
|
||||
'description' => $description,
|
||||
'category' => $category,
|
||||
'author' => $meta['author'] ?? null,
|
||||
'tags' => $tags,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Department;
|
||||
use App\Models\Role;
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Services\UserService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
@@ -33,7 +34,14 @@ public function create(): View
|
||||
$roles = $tenantId ? Role::where('tenant_id', $tenantId)->orderBy('name')->get() : collect();
|
||||
$departments = $tenantId ? Department::where('tenant_id', $tenantId)->where('is_active', true)->orderBy('name')->get() : collect();
|
||||
|
||||
return view('users.create', compact('roles', 'departments'));
|
||||
// 본사 테넌트 여부 확인 (본사: 이메일 인증, 그 외: 비밀번호 직접 입력)
|
||||
$isHQ = false;
|
||||
if ($tenantId) {
|
||||
$tenant = Tenant::find($tenantId);
|
||||
$isHQ = $tenant?->tenant_type === 'HQ';
|
||||
}
|
||||
|
||||
return view('users.create', compact('roles', 'departments', 'isHQ'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Tenants\Tenant;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreUserRequest extends FormRequest
|
||||
@@ -14,6 +15,21 @@ public function authorize(): bool
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 선택된 테넌트가 본사(HQ)인지 확인
|
||||
*/
|
||||
protected function isHQTenant(): bool
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
if (! $tenantId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tenant = Tenant::find($tenantId);
|
||||
|
||||
return $tenant?->tenant_type === 'HQ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
* 일반관리자가 슈퍼관리자를 생성하려는 경우 is_super_admin 필드 제거
|
||||
@@ -35,12 +51,11 @@ protected function prepareForValidation(): void
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
$rules = [
|
||||
'user_id' => 'nullable|string|max:50|unique:users,user_id',
|
||||
'name' => 'required|string|max:100',
|
||||
'email' => 'required|email|max:255|unique:users,email',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
// password는 시스템이 자동 생성하므로 입력 받지 않음
|
||||
'role' => 'nullable|string|max:50',
|
||||
'is_active' => 'nullable|boolean',
|
||||
'is_super_admin' => 'nullable|boolean',
|
||||
@@ -49,6 +64,13 @@ public function rules(): array
|
||||
'department_ids' => 'nullable|array',
|
||||
'department_ids.*' => 'integer|exists:departments,id',
|
||||
];
|
||||
|
||||
// 비본사 테넌트: 비밀번호 직접 입력 필수
|
||||
if (! $this->isHQTenant()) {
|
||||
$rules['password'] = 'required|string|min:8|max:100|confirmed';
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -82,15 +82,28 @@ public function getUserById(int $id): ?User
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 생성 (관리자용: 임의 비밀번호 생성 + 메일 발송)
|
||||
* 사용자 생성
|
||||
* - 본사(HQ): 임의 비밀번호 생성 + 메일 발송
|
||||
* - 비본사: 입력된 비밀번호 사용 (메일 발송 안 함)
|
||||
*/
|
||||
public function createUser(array $data): User
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
// 임의 비밀번호 생성 (8자리 영문+숫자)
|
||||
$plainPassword = $this->generateRandomPassword();
|
||||
$data['password'] = Hash::make($plainPassword);
|
||||
// 비밀번호 처리: 입력된 비밀번호가 있으면 사용, 없으면 자동 생성
|
||||
$passwordProvided = ! empty($data['password']);
|
||||
if ($passwordProvided) {
|
||||
// 비본사: 입력된 비밀번호 사용
|
||||
$data['password'] = Hash::make($data['password']);
|
||||
$plainPassword = null; // 메일 발송하지 않음
|
||||
} else {
|
||||
// 본사: 임의 비밀번호 생성 (8자리 영문+숫자)
|
||||
$plainPassword = $this->generateRandomPassword();
|
||||
$data['password'] = Hash::make($plainPassword);
|
||||
}
|
||||
|
||||
// password_confirmation은 User 모델의 fillable이 아니므로 제거
|
||||
unset($data['password_confirmation']);
|
||||
|
||||
// is_active 처리
|
||||
$data['is_active'] = isset($data['is_active']) && $data['is_active'] == '1';
|
||||
@@ -120,8 +133,10 @@ public function createUser(array $data): User
|
||||
$this->syncDepartments($user, $tenantId, $departmentIds);
|
||||
}
|
||||
|
||||
// 비밀번호 안내 메일 발송
|
||||
$this->sendPasswordMail($user, $plainPassword, true);
|
||||
// 본사만 비밀번호 안내 메일 발송 (비본사는 관리자가 직접 알려줌)
|
||||
if ($plainPassword !== null) {
|
||||
$this->sendPasswordMail($user, $plainPassword, true);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
@@ -57,10 +57,10 @@ class="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
"description": "플로우 설명", // 상세 설명 (선택)
|
||||
"version": "1.0",
|
||||
"config": {
|
||||
"baseUrl": "https://api.sam.kr/api/v1",
|
||||
"apiKey": "@{{$env.API_KEY}}", // API 키 (선택)
|
||||
"baseUrl": "", // 빈 문자열 (서버 기본값 사용)
|
||||
"timeout": 30000,
|
||||
"stopOnFailure": true
|
||||
// apiKey는 서버에서 자동 주입 (JSON에 포함하지 않음)
|
||||
},
|
||||
"variables": {
|
||||
"user_id": "@{{$env.FLOW_TESTER_USER_ID}}", // 환경변수 참조
|
||||
@@ -350,24 +350,19 @@ class="absolute top-2 right-2 px-3 py-1 text-xs bg-gray-700 hover:bg-gray-600 te
|
||||
## 테스트 목적
|
||||
[테스트하려는 기능/시나리오 설명]
|
||||
|
||||
## API 서버 정보
|
||||
- Base URL: https://api.sam.kr/api/v1
|
||||
- API Key: 별도 제공 (config.apiKey에 설정)
|
||||
- 인증: user_id, user_pwd로 로그인 후 Bearer Token 사용
|
||||
|
||||
## 테스트 플로우
|
||||
1. [첫 번째 단계]
|
||||
2. [두 번째 단계] - 1번에서 추출한 값 사용
|
||||
3. ...
|
||||
|
||||
## JSON 형식 (반드시 이 형식으로!)
|
||||
## JSON 형식 (아래 형식 그대로 사용!)
|
||||
```json
|
||||
{
|
||||
"name": "플로우 이름 (50자 이내)",
|
||||
"description": "플로우 상세 설명",
|
||||
"version": "1.0",
|
||||
"config": {
|
||||
"baseUrl": "https://api.sam.kr/api/v1",
|
||||
"apiKey": "YOUR_API_KEY",
|
||||
"baseUrl": "",
|
||||
"timeout": 30000,
|
||||
"stopOnFailure": true
|
||||
},
|
||||
@@ -377,27 +372,44 @@ class="absolute top-2 right-2 px-3 py-1 text-xs bg-gray-700 hover:bg-gray-600 te
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "unique_step_id",
|
||||
"name": "단계 이름",
|
||||
"id": "login",
|
||||
"name": "로그인",
|
||||
"method": "POST",
|
||||
"endpoint": "/path",
|
||||
"body": { ... },
|
||||
"endpoint": "/login",
|
||||
"body": {
|
||||
"user_id": "@{{user_id}}",
|
||||
"user_pwd": "@{{user_pwd}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": { "$.access_token": "@isString" }
|
||||
},
|
||||
"extract": { "token": "$.access_token" }
|
||||
},
|
||||
{
|
||||
"id": "step2",
|
||||
"name": "다음 단계",
|
||||
"method": "GET",
|
||||
"endpoint": "/path",
|
||||
"headers": {
|
||||
"Authorization": "Bearer @{{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": { "$.success": true }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 주의사항
|
||||
- name: 최상위 필드로, 플로우 이름으로 저장됨
|
||||
- description: 최상위 필드로, 설명으로 저장됨
|
||||
- config.apiKey: API 인증키 설정
|
||||
- variables에서 @{{$env.XXX}} 형식으로 환경변수 참조 가능
|
||||
- extract로 추출한 값은 이후 스텝에서 @{{stepId.변수명}}으로 사용
|
||||
- headers에 Authorization: Bearer @{{login.token}} 형식으로 토큰 전달</pre>
|
||||
## 중요 규칙 (반드시 준수!)
|
||||
1. **config.baseUrl**: 빈 문자열 "" 로 설정 (서버에서 .env 기본값 사용)
|
||||
2. **config.apiKey**: 이 필드는 JSON에 포함하지 마세요 (서버에서 자동 주입)
|
||||
3. **variables**: @{{$env.XXX}} 형식으로 환경변수 참조 (user_id, user_pwd)
|
||||
4. **steps[].body**: @{{변수명}} 형식으로 variables 값 참조
|
||||
5. **headers.Authorization**: Bearer @{{login.token}} 형식으로 추출한 토큰 사용
|
||||
6. **extract**: 다음 스텝에서 @{{stepId.변수명}}으로 참조됨</pre>
|
||||
</div>
|
||||
|
||||
<h4 class="text-lg font-semibold mt-6 mb-3">실제 예시: 인증 플로우 테스트</h4>
|
||||
@@ -411,10 +423,6 @@ class="absolute top-2 right-2 px-3 py-1 text-xs bg-gray-700 hover:bg-gray-600 te
|
||||
## 테스트 목적
|
||||
로그인, 프로필 조회, 토큰 갱신, 로그아웃 전체 인증 플로우 검증
|
||||
|
||||
## API 서버
|
||||
- Base URL: https://api.sam.kr/api/v1
|
||||
- 인증: user_id/user_pwd → access_token/refresh_token
|
||||
|
||||
## 테스트 시나리오
|
||||
1. 로그인 → access_token, refresh_token 추출
|
||||
2. 프로필 조회 → Authorization 헤더에 access_token 사용
|
||||
@@ -427,24 +435,11 @@ class="absolute top-2 right-2 px-3 py-1 text-xs bg-gray-700 hover:bg-gray-600 te
|
||||
- POST /refresh - { refresh_token } → { access_token }
|
||||
- POST /logout - Authorization: Bearer {token}
|
||||
|
||||
## JSON 형식
|
||||
{
|
||||
"name": "인증 플로우 테스트",
|
||||
"description": "로그인, 프로필 조회, 토큰 갱신, 로그아웃 플로우를 테스트합니다.",
|
||||
"version": "1.0",
|
||||
"config": {
|
||||
"baseUrl": "https://api.sam.kr/api/v1",
|
||||
"apiKey": "YOUR_API_KEY",
|
||||
"timeout": 30000,
|
||||
"stopOnFailure": true
|
||||
},
|
||||
"variables": {
|
||||
"user_id": "@{{$env.FLOW_TESTER_USER_ID}}",
|
||||
"user_pwd": "@{{$env.FLOW_TESTER_USER_PWD}}"
|
||||
}
|
||||
}
|
||||
|
||||
extract로 token을 추출하고 다음 스텝 headers에서 사용해줘.</pre>
|
||||
## 중요 규칙
|
||||
- config.baseUrl: "" (빈 문자열, 서버 기본값 사용)
|
||||
- config.apiKey: JSON에 포함하지 않음
|
||||
- variables의 user_id, user_pwd: @{{$env.XXX}} 형식
|
||||
- extract로 token 추출 → 다음 스텝에서 @{{login.token}} 사용</pre>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
|
||||
@@ -66,20 +66,44 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 비밀번호 안내 -->
|
||||
<!-- 비밀번호 -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">비밀번호</h2>
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-blue-500 mt-0.5 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm text-blue-800 font-medium">임시 비밀번호가 자동 생성됩니다</p>
|
||||
<p class="text-sm text-blue-700 mt-1">사용자 생성 시 임시 비밀번호가 생성되어 입력한 이메일 주소로 발송됩니다.</p>
|
||||
@if($isHQ)
|
||||
{{-- 본사: 임시 비밀번호 자동 생성 + 이메일 발송 --}}
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-blue-500 mt-0.5 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm text-blue-800 font-medium">임시 비밀번호가 자동 생성됩니다</p>
|
||||
<p class="text-sm text-blue-700 mt-1">사용자 생성 시 임시 비밀번호가 생성되어 입력한 이메일 주소로 발송됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
{{-- 비본사: 비밀번호 직접 입력 --}}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
비밀번호 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="password" name="password" required minlength="8" maxlength="100"
|
||||
placeholder="최소 8자 이상"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<p class="text-xs text-gray-500 mt-1">영문, 숫자를 포함하여 8자 이상 입력하세요.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
비밀번호 확인 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="password" name="password_confirmation" required minlength="8" maxlength="100"
|
||||
placeholder="비밀번호 재입력"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- 역할 설정 -->
|
||||
|
||||
Reference in New Issue
Block a user