feat: [flow-tester] 플로우 실행 기능 완성
- FlowTesterController에 FlowExecutor 연결 - 실행 결과를 AdminApiFlowRun에 저장 - 실행 중 로딩 인디케이터 추가 - 결과 모달로 상세 실행 결과 표시 - 상태, 소요 시간, 완료 스텝 표시 - 각 스텝별 성공/실패 로그 표시 - 상세 보기 링크 제공
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Admin\AdminApiFlow;
|
||||
use App\Models\Admin\AdminApiFlowRun;
|
||||
use App\Services\FlowTester\FlowExecutor;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
@@ -216,14 +217,57 @@ public function run(int $id)
|
||||
'executed_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
// TODO: 실제 플로우 실행 로직은 Service 클래스로 분리
|
||||
// 현재는 스캐폴딩만 제공
|
||||
try {
|
||||
// FlowExecutor로 실제 실행
|
||||
$executor = new FlowExecutor();
|
||||
$result = $executor->execute($flow->flow_definition);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'run_id' => $run->id,
|
||||
'message' => '플로우 실행이 시작되었습니다.',
|
||||
]);
|
||||
// 실행 결과 저장
|
||||
$run->update([
|
||||
'status' => $result['status'],
|
||||
'completed_at' => now(),
|
||||
'duration_ms' => $result['duration'],
|
||||
'completed_steps' => $result['completedSteps'],
|
||||
'failed_step' => $result['failedStep'],
|
||||
'execution_log' => $result['executionLog'],
|
||||
'error_message' => $result['errorMessage'],
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => $result['status'] === 'SUCCESS',
|
||||
'run_id' => $run->id,
|
||||
'status' => $result['status'],
|
||||
'message' => $this->getResultMessage($result),
|
||||
'result' => $result,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 예외 발생 시 실패 처리
|
||||
$run->update([
|
||||
'status' => AdminApiFlowRun::STATUS_FAILED,
|
||||
'completed_at' => now(),
|
||||
'error_message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'run_id' => $run->id,
|
||||
'status' => 'FAILED',
|
||||
'message' => '실행 오류: '.$e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 실행 결과 메시지 생성
|
||||
*/
|
||||
private function getResultMessage(array $result): string
|
||||
{
|
||||
return match ($result['status']) {
|
||||
'SUCCESS' => "플로우 실행 완료! ({$result['completedSteps']}/{$result['totalSteps']} 스텝 성공)",
|
||||
'FAILED' => "플로우 실행 실패: ".($result['errorMessage'] ?? '알 수 없는 오류'),
|
||||
'PARTIAL' => "부분 성공: {$result['completedSteps']}/{$result['totalSteps']} 스텝 완료",
|
||||
default => "실행 완료 (상태: {$result['status']})",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -196,6 +196,15 @@ class="p-2 text-red-600 hover:bg-red-50 rounded-lg transition"
|
||||
function runFlow(id) {
|
||||
if (!confirm('이 플로우를 실행하시겠습니까?')) return;
|
||||
|
||||
// 실행 버튼을 로딩 상태로 변경
|
||||
const btn = document.querySelector(`button[onclick="runFlow(${id})"]`);
|
||||
const originalHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>`;
|
||||
|
||||
fetch(`/dev-tools/flow-tester/${id}/run`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -205,18 +214,114 @@ function runFlow(id) {
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
} else {
|
||||
alert('실행 실패: ' + (data.message || '알 수 없는 오류'));
|
||||
}
|
||||
// 버튼 복원
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalHtml;
|
||||
|
||||
// 결과 모달 표시
|
||||
showResultModal(data);
|
||||
})
|
||||
.catch(error => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalHtml;
|
||||
alert('오류 발생: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function showResultModal(data) {
|
||||
const isSuccess = data.status === 'SUCCESS';
|
||||
const isFailed = data.status === 'FAILED';
|
||||
const isPartial = data.status === 'PARTIAL';
|
||||
|
||||
const statusColor = isSuccess ? 'green' : (isFailed ? 'red' : 'yellow');
|
||||
const statusIcon = isSuccess
|
||||
? `<svg class="w-12 h-12 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>`
|
||||
: (isFailed
|
||||
? `<svg class="w-12 h-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>`
|
||||
: `<svg class="w-12 h-12 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>`);
|
||||
|
||||
const result = data.result || {};
|
||||
const duration = result.duration ? `${(result.duration / 1000).toFixed(2)}초` : '-';
|
||||
|
||||
// 실행 로그 요약
|
||||
let logSummary = '';
|
||||
if (result.executionLog && result.executionLog.length > 0) {
|
||||
logSummary = result.executionLog.map((step, idx) => {
|
||||
const stepIcon = step.success
|
||||
? '<span class="text-green-500">✓</span>'
|
||||
: '<span class="text-red-500">✗</span>';
|
||||
return `<div class="flex items-center gap-2 py-1 text-sm ${!step.success ? 'text-red-600' : ''}">
|
||||
${stepIcon}
|
||||
<span class="font-medium">${step.stepName || step.stepId}</span>
|
||||
<span class="text-gray-400">${step.duration}ms</span>
|
||||
${step.error ? `<span class="text-red-500 text-xs ml-2">${step.error}</span>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'fixed inset-0 z-50 overflow-y-auto';
|
||||
modal.innerHTML = `
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75" onclick="this.parentElement.remove()"></div>
|
||||
<div class="flex min-h-full items-center justify-center p-4">
|
||||
<div class="relative bg-white rounded-lg shadow-xl max-w-lg w-full p-6">
|
||||
<div class="text-center mb-4">
|
||||
${statusIcon}
|
||||
<h3 class="mt-4 text-lg font-semibold text-gray-900">${data.message}</h3>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 rounded-lg p-4 mb-4">
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="text-gray-500">상태:</span>
|
||||
<span class="ml-2 font-medium text-${statusColor}-600">${data.status}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">소요 시간:</span>
|
||||
<span class="ml-2 font-medium">${duration}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">완료 스텝:</span>
|
||||
<span class="ml-2 font-medium">${result.completedSteps || 0}/${result.totalSteps || 0}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">실행 ID:</span>
|
||||
<span class="ml-2 font-medium">#${data.run_id}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${logSummary ? `
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-2">실행 로그</h4>
|
||||
<div class="bg-gray-50 rounded-lg p-3 max-h-48 overflow-y-auto">
|
||||
${logSummary}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button onclick="this.closest('.fixed').remove(); location.reload();"
|
||||
class="flex-1 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition">
|
||||
닫기
|
||||
</button>
|
||||
<a href="/dev-tools/flow-tester/runs/${data.run_id}"
|
||||
class="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-center rounded-lg transition">
|
||||
상세 보기
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
function confirmDelete(id, name) {
|
||||
if (!confirm(`"${name}" 플로우를 삭제하시겠습니까?`)) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user