- layouts/app.blade.php에 SweetAlert2 CDN 및 전역 헬퍼 함수 추가 - showToast(): 토스트 알림 (success, error, warning, info) - showConfirm(): 확인 대화상자 - showDeleteConfirm(): 삭제 확인 (경고 아이콘) - showPermanentDeleteConfirm(): 영구 삭제 확인 (빨간색 경고) - showSuccess(), showError(): 성공/에러 알림 - 변환된 파일 목록 (48개 Blade 파일): - menus/* (6개), boards/* (2개), posts/* (3개) - daily-logs/* (3개), project-management/* (6개) - dev-tools/flow-tester/* (6개) - quote-formulas/* (4개), permission-analyze/* (1개) - archived-records/* (1개), profile/* (1개) - roles/* (3개), permissions/* (3개) - departments/* (3개), tenants/* (3개), users/* (3개) - 주요 개선사항: - Tailwind CSS 테마와 일관된 디자인 - 비동기 콜백 패턴으로 리팩토링 - 삭제/복원/영구삭제 각각 다른 스타일 적용
532 lines
36 KiB
PHP
532 lines
36 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', '실행 상세 - #' . $run->id)
|
|
|
|
|
|
@section('content')
|
|
<!-- 페이지 헤더 -->
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div class="flex items-center gap-4">
|
|
<a href="{{ route('dev-tools.flow-tester.history', $run->flow_id) }}"
|
|
class="p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
</a>
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-800">실행 상세 #{{ $run->id }}</h1>
|
|
<p class="text-sm text-gray-500">{{ $run->flow->name }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
@if($run->status === 'FAILED' || $run->status === 'PARTIAL')
|
|
<!-- AI에게 오류 복사 버튼 -->
|
|
<button onclick="copyErrorForAI()"
|
|
id="copy-error-btn"
|
|
class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transition flex items-center gap-2">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
|
</svg>
|
|
AI에게 복사
|
|
</button>
|
|
@endif
|
|
<!-- 다시 실행 버튼 -->
|
|
<button onclick="runFlow({{ $run->flow_id }})"
|
|
id="run-btn"
|
|
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition flex items-center gap-2">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
다시 실행
|
|
</button>
|
|
<!-- 편집 버튼 -->
|
|
<a href="{{ route('dev-tools.flow-tester.edit', $run->flow_id) }}"
|
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition flex items-center gap-2">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
편집
|
|
</a>
|
|
<span class="px-3 py-1 text-sm font-medium rounded {{ $run->status_color }}">
|
|
{{ $run->status_label }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- 요약 정보 -->
|
|
<div class="lg:col-span-1">
|
|
<div class="bg-white rounded-lg shadow-sm p-6">
|
|
<h2 class="text-lg font-semibold text-gray-800 mb-4">실행 정보</h2>
|
|
|
|
<dl class="space-y-3">
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500">상태</dt>
|
|
<dd>
|
|
<span class="px-2 py-1 text-xs font-medium rounded {{ $run->status_color }}">
|
|
{{ $run->status_label }}
|
|
</span>
|
|
</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500">진행률</dt>
|
|
<dd class="text-gray-900">{{ $run->progress }}%</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500">완료 스텝</dt>
|
|
<dd class="text-gray-900">{{ $run->completed_steps }}/{{ $run->total_steps ?? '-' }}</dd>
|
|
</div>
|
|
@if($run->failed_step)
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500">실패 스텝</dt>
|
|
<dd class="text-red-600">Step {{ $run->failed_step }}</dd>
|
|
</div>
|
|
@endif
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500">소요시간</dt>
|
|
<dd class="text-gray-900">
|
|
@if($run->duration_ms)
|
|
{{ number_format($run->duration_ms / 1000, 2) }}s
|
|
@else
|
|
-
|
|
@endif
|
|
</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500">시작시간</dt>
|
|
<dd class="text-gray-900">{{ $run->started_at?->format('H:i:s') ?? '-' }}</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500">완료시간</dt>
|
|
<dd class="text-gray-900">{{ $run->completed_at?->format('H:i:s') ?? '-' }}</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500">실행일</dt>
|
|
<dd class="text-gray-900">{{ $run->created_at->format('Y-m-d') }}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
@if($run->error_message)
|
|
<div class="mt-4 bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<h3 class="text-sm font-medium text-red-800 mb-2">에러 메시지</h3>
|
|
<p class="text-sm text-red-700">{{ $run->error_message }}</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- 실행 로그 -->
|
|
<div class="lg:col-span-2">
|
|
<div class="bg-white rounded-lg shadow-sm">
|
|
<!-- 헤더 -->
|
|
<div class="px-6 py-4 border-b">
|
|
<h2 class="text-lg font-semibold text-gray-800">실행 로그</h2>
|
|
</div>
|
|
|
|
<!-- 스텝 카드들 -->
|
|
<div class="p-6">
|
|
@php
|
|
// 전체 스텝 목록 (flow_definition에서)
|
|
$allSteps = $run->flow->flow_definition['steps'] ?? [];
|
|
|
|
// execution_log를 stepId로 인덱싱
|
|
$executionByStepId = [];
|
|
foreach ($run->execution_log ?? [] as $log) {
|
|
$stepId = $log['stepId'] ?? $log['step_id'] ?? null;
|
|
if ($stepId) $executionByStepId[$stepId] = $log;
|
|
}
|
|
|
|
// api_logs를 URI 기준으로 그룹화
|
|
$apiLogsByUri = [];
|
|
foreach ($run->api_logs ?? [] as $apiLog) {
|
|
$uri = $apiLog['uri'] ?? '';
|
|
if ($uri) {
|
|
$apiLogsByUri[$uri][] = $apiLog;
|
|
}
|
|
}
|
|
@endphp
|
|
|
|
@if(count($allSteps) > 0)
|
|
<div class="space-y-3">
|
|
@foreach($allSteps as $index => $step)
|
|
@php
|
|
$stepId = $step['id'] ?? 'step_'.($index + 1);
|
|
$stepName = $step['name'] ?? '';
|
|
$stepMethod = $step['method'] ?? '';
|
|
$stepEndpoint = $step['endpoint'] ?? '';
|
|
|
|
$executed = $executionByStepId[$stepId] ?? null;
|
|
// null = 미실행, true = 성공, false = 실패
|
|
$status = $executed ? ($executed['success'] ?? false) : null;
|
|
|
|
// 실패 스텝의 api_logs 찾기
|
|
$stepApiLogs = [];
|
|
if ($status === false && $stepEndpoint) {
|
|
foreach ($apiLogsByUri as $uri => $logs) {
|
|
if (str_contains($uri, $stepEndpoint) || str_contains($stepEndpoint, $uri)) {
|
|
$stepApiLogs = array_merge($stepApiLogs, $logs);
|
|
}
|
|
}
|
|
}
|
|
@endphp
|
|
|
|
{{-- 미실행 카드 (회색) --}}
|
|
@if($status === null)
|
|
<div class="border rounded-lg border-gray-200 bg-gray-50 p-4 opacity-60">
|
|
<div class="flex items-center gap-2">
|
|
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<circle cx="12" cy="12" r="10" stroke-width="2" />
|
|
</svg>
|
|
<span class="font-medium text-gray-500">{{ $stepId }}</span>
|
|
<span class="text-gray-400">{{ $stepName }}</span>
|
|
<code class="text-xs text-gray-400 bg-white bg-opacity-50 px-2 py-0.5 rounded">
|
|
{{ $stepMethod }} {{ Str::limit($stepEndpoint, 30) }}
|
|
</code>
|
|
<span class="ml-auto text-xs text-gray-400">(미실행)</span>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 성공 카드 (녹색, 항상 펼쳐짐) --}}
|
|
@elseif($status === true)
|
|
<div class="border rounded-lg border-green-200 bg-green-50">
|
|
{{-- 헤더 --}}
|
|
<div class="flex justify-between items-center p-4">
|
|
<div class="flex items-center gap-2">
|
|
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
<span class="font-medium">{{ $stepId }}</span>
|
|
<span class="text-gray-500">{{ $executed['name'] ?? $executed['stepName'] ?? $stepName }}</span>
|
|
<code class="text-xs text-gray-600 bg-white bg-opacity-50 px-2 py-0.5 rounded">
|
|
{{ $executed['request']['method'] ?? $stepMethod }} {{ Str::limit($executed['request']['endpoint'] ?? $stepEndpoint, 30) }}
|
|
</code>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
@if(isset($executed['response']['status']))
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded bg-green-600 text-white">
|
|
{{ $executed['response']['status'] }}
|
|
</span>
|
|
@endif
|
|
<span class="text-sm text-gray-500">{{ $executed['duration'] ?? '' }}ms</span>
|
|
</div>
|
|
</div>
|
|
{{-- 상세 (항상 표시) --}}
|
|
<div class="px-4 pb-4 border-t border-green-200">
|
|
<div class="pt-3 space-y-3">
|
|
@if(isset($executed['request']))
|
|
<div class="text-sm">
|
|
<div class="font-medium text-gray-700 mb-1">Request</div>
|
|
<div class="bg-white bg-opacity-60 rounded p-2">
|
|
<code class="text-gray-800">{{ $executed['request']['method'] ?? '' }} {{ $executed['request']['endpoint'] ?? '' }}</code>
|
|
@if(!empty($executed['request']['body']))
|
|
<details class="mt-2">
|
|
<summary class="cursor-pointer text-xs text-gray-500 hover:text-gray-700">요청 데이터</summary>
|
|
<pre class="mt-1 p-2 bg-gray-100 rounded text-xs overflow-x-auto">{{ json_encode($executed['request']['body'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
|
|
</details>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
@if(isset($executed['response']))
|
|
<div class="text-sm">
|
|
<div class="font-medium text-gray-700 mb-1">Response</div>
|
|
<div class="bg-white bg-opacity-60 rounded p-2">
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded bg-green-100 text-green-700">{{ $executed['response']['status'] ?? '-' }}</span>
|
|
@if(!empty($executed['response']['body']))
|
|
<details class="mt-2">
|
|
<summary class="cursor-pointer text-xs text-gray-500 hover:text-gray-700">응답 데이터</summary>
|
|
<pre class="mt-1 p-2 bg-gray-100 rounded text-xs overflow-x-auto max-h-60 overflow-y-auto">{{ json_encode($executed['response']['body'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
|
|
</details>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
@if(isset($executed['reason']))
|
|
<div class="text-sm text-green-700 bg-green-100 px-3 py-2 rounded">
|
|
<span class="font-medium">성공 이유:</span> {{ $executed['reason'] }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 실패 카드 (빨간색, 항상 펼쳐짐 + Laravel Log 버튼) --}}
|
|
@else
|
|
<div class="border rounded-lg border-red-200 bg-red-50">
|
|
{{-- 헤더 --}}
|
|
<div class="flex justify-between items-center p-4">
|
|
<div class="flex items-center gap-2">
|
|
<svg class="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
<span class="font-medium">{{ $stepId }}</span>
|
|
<span class="text-gray-500">{{ $executed['name'] ?? $executed['stepName'] ?? $stepName }}</span>
|
|
<code class="text-xs text-gray-600 bg-white bg-opacity-50 px-2 py-0.5 rounded">
|
|
{{ $executed['request']['method'] ?? $stepMethod }} {{ Str::limit($executed['request']['endpoint'] ?? $stepEndpoint, 30) }}
|
|
</code>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
@if(isset($executed['response']['status']))
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded bg-red-600 text-white">
|
|
{{ $executed['response']['status'] }}
|
|
</span>
|
|
@endif
|
|
<span class="text-sm text-gray-500">{{ $executed['duration'] ?? '' }}ms</span>
|
|
</div>
|
|
</div>
|
|
{{-- 상세 (항상 표시) --}}
|
|
<div class="px-4 pb-4 border-t border-red-200">
|
|
<div class="pt-3 space-y-3">
|
|
@if(isset($executed['request']))
|
|
<div class="text-sm">
|
|
<div class="font-medium text-gray-700 mb-1">Request</div>
|
|
<div class="bg-white bg-opacity-60 rounded p-2">
|
|
<code class="text-gray-800">{{ $executed['request']['method'] ?? '' }} {{ $executed['request']['endpoint'] ?? '' }}</code>
|
|
@if(!empty($executed['request']['body']))
|
|
<details class="mt-2">
|
|
<summary class="cursor-pointer text-xs text-gray-500 hover:text-gray-700">요청 데이터</summary>
|
|
<pre class="mt-1 p-2 bg-gray-100 rounded text-xs overflow-x-auto">{{ json_encode($executed['request']['body'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
|
|
</details>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
@if(isset($executed['response']))
|
|
<div class="text-sm">
|
|
<div class="font-medium text-gray-700 mb-1">Response</div>
|
|
<div class="bg-white bg-opacity-60 rounded p-2">
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded bg-red-100 text-red-700">{{ $executed['response']['status'] ?? '-' }}</span>
|
|
@if(isset($executed['expect']['status']))
|
|
<span class="text-xs text-gray-500">
|
|
예상: {{ is_array($executed['expect']['status']) ? implode(', ', $executed['expect']['status']) : $executed['expect']['status'] }}
|
|
</span>
|
|
@endif
|
|
</div>
|
|
@if(!empty($executed['response']['body']))
|
|
<details class="mt-2">
|
|
<summary class="cursor-pointer text-xs text-gray-500 hover:text-gray-700">응답 데이터</summary>
|
|
<pre class="mt-1 p-2 bg-gray-100 rounded text-xs overflow-x-auto max-h-60 overflow-y-auto">{{ json_encode($executed['response']['body'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
|
|
</details>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
@if(isset($executed['reason']))
|
|
<div class="text-sm text-red-700 bg-red-100 px-3 py-2 rounded">
|
|
<span class="font-medium">실패 이유:</span> {{ $executed['reason'] }}
|
|
</div>
|
|
@endif
|
|
@if(isset($executed['error']))
|
|
<div class="text-sm bg-red-100 rounded p-3">
|
|
<div class="font-medium text-red-800 mb-1">에러</div>
|
|
<div class="text-red-700">{{ $executed['error'] }}</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Laravel Log 보기 (DaisyUI collapse) --}}
|
|
@if(!empty($stepApiLogs))
|
|
<div class="pt-2 border-t border-red-200">
|
|
<details class="collapse collapse-arrow bg-purple-50 rounded-lg">
|
|
<summary class="collapse-title text-sm text-purple-600 font-medium min-h-0 py-2 px-3 flex items-center gap-2">
|
|
Laravel Log 보기 (API 서버)
|
|
<span class="px-1.5 py-0.5 text-xs bg-purple-100 text-purple-700 rounded">{{ count($stepApiLogs) }}</span>
|
|
</summary>
|
|
<div class="collapse-content px-3 pb-3">
|
|
<div class="space-y-2 pt-2">
|
|
@foreach($stepApiLogs as $apiLog)
|
|
@php
|
|
$isReq = ($apiLog['type'] ?? '') === 'request';
|
|
$isErr = !$isReq && (($apiLog['status'] ?? 0) >= 400);
|
|
@endphp
|
|
<div class="bg-white rounded p-3 border {{ $isErr ? 'border-red-300' : 'border-gray-200' }}">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded {{ $isReq ? 'bg-blue-100 text-blue-700' : ($isErr ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700') }}">
|
|
{{ $isReq ? 'REQ' : 'RES' }}
|
|
</span>
|
|
@if($isReq)
|
|
<code class="text-xs text-gray-700">{{ $apiLog['method'] ?? '' }} {{ $apiLog['uri'] ?? '' }}</code>
|
|
@else
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded {{ $isErr ? 'bg-red-600 text-white' : 'bg-green-600 text-white' }}">
|
|
{{ $apiLog['status'] ?? '-' }}
|
|
</span>
|
|
@endif
|
|
<span class="text-xs text-gray-400 ml-auto">{{ $apiLog['timestamp'] ?? '' }}</span>
|
|
</div>
|
|
{{-- 원본 로그 표시 --}}
|
|
@if(!empty($apiLog['raw']))
|
|
<pre class="p-2 bg-gray-900 text-gray-100 rounded text-xs overflow-x-auto whitespace-pre-wrap break-all font-mono">{{ $apiLog['raw'] }}</pre>
|
|
@else
|
|
{{-- raw가 없는 경우 기존 방식 --}}
|
|
@if($isReq && !empty($apiLog['input']))
|
|
<details class="mt-1">
|
|
<summary class="cursor-pointer text-xs text-gray-500 hover:text-gray-700">요청 데이터</summary>
|
|
<pre class="mt-1 p-2 bg-gray-100 rounded text-xs overflow-x-auto max-h-32">{{ json_encode($apiLog['input'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
|
|
</details>
|
|
@endif
|
|
@if(!$isReq && isset($apiLog['message']))
|
|
<div class="text-xs {{ $isErr ? 'text-red-600' : 'text-green-600' }}">{{ $apiLog['message'] }}</div>
|
|
@endif
|
|
@if(!$isReq && isset($apiLog['error']))
|
|
<details class="mt-1" open>
|
|
<summary class="cursor-pointer text-xs text-red-600 font-medium">오류 상세</summary>
|
|
<pre class="mt-1 p-2 bg-red-50 rounded text-xs overflow-x-auto max-h-40 text-red-800">{{ is_array($apiLog['error']) ? json_encode($apiLog['error'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : $apiLog['error'] }}</pre>
|
|
</details>
|
|
@endif
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<div class="text-center py-8 text-gray-500">
|
|
<p>스텝 정보가 없습니다.</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script>
|
|
// AI에게 오류 정보 복사
|
|
function copyErrorForAI() {
|
|
const errorData = {
|
|
url: window.location.href,
|
|
flowName: @json($run->flow->name),
|
|
runId: {{ $run->id }},
|
|
status: @json($run->status),
|
|
statusLabel: @json($run->status_label),
|
|
errorMessage: @json($run->error_message),
|
|
failedStep: @json($run->failed_step),
|
|
completedSteps: {{ $run->completed_steps }},
|
|
totalSteps: {{ $run->total_steps ?? 0 }},
|
|
executionLog: @json($run->execution_log),
|
|
};
|
|
|
|
// 실패한 스텝들 추출
|
|
const failedSteps = (errorData.executionLog || [])
|
|
.filter(log => !log.success)
|
|
.map(log => ({
|
|
stepId: log.step_id || 'Unknown',
|
|
name: log.name || '',
|
|
reason: log.reason || '',
|
|
error: log.error || '',
|
|
request: log.request || null,
|
|
response: log.response || null,
|
|
}));
|
|
|
|
// 마크다운 포맷 생성
|
|
let markdown = `## Flow Tester 오류 보고서\n\n`;
|
|
markdown += `**URL:** ${errorData.url}\n`;
|
|
markdown += `**플로우:** ${errorData.flowName}\n`;
|
|
markdown += `**실행 ID:** #${errorData.runId}\n`;
|
|
markdown += `**상태:** ${errorData.statusLabel} (${errorData.status})\n`;
|
|
markdown += `**진행률:** ${errorData.completedSteps}/${errorData.totalSteps} 스텝 완료\n`;
|
|
|
|
if (errorData.failedStep) {
|
|
markdown += `**실패 스텝:** Step ${errorData.failedStep}\n`;
|
|
}
|
|
|
|
if (errorData.errorMessage) {
|
|
markdown += `\n### 에러 메시지\n\`\`\`\n${errorData.errorMessage}\n\`\`\`\n`;
|
|
}
|
|
|
|
if (failedSteps.length > 0) {
|
|
markdown += `\n### 실패한 스텝 상세\n`;
|
|
failedSteps.forEach((step, index) => {
|
|
markdown += `\n#### ${index + 1}. ${step.stepId} - ${step.name}\n`;
|
|
if (step.reason) {
|
|
markdown += `- **실패 이유:** ${step.reason}\n`;
|
|
}
|
|
if (step.error) {
|
|
markdown += `- **에러:** ${step.error}\n`;
|
|
}
|
|
if (step.request) {
|
|
markdown += `- **요청:** \`${step.request.method || ''} ${step.request.endpoint || ''}\`\n`;
|
|
if (step.request.body && Object.keys(step.request.body).length > 0) {
|
|
markdown += `- **요청 데이터:**\n\`\`\`json\n${JSON.stringify(step.request.body, null, 2)}\n\`\`\`\n`;
|
|
}
|
|
}
|
|
if (step.response) {
|
|
markdown += `- **응답 상태:** ${step.response.status || '-'}\n`;
|
|
if (step.response.body) {
|
|
markdown += `- **응답 데이터:**\n\`\`\`json\n${JSON.stringify(step.response.body, null, 2)}\n\`\`\`\n`;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
markdown += `\n---\n*이 오류를 분석하고 해결 방법을 제안해주세요.*\n`;
|
|
|
|
// 클립보드에 복사
|
|
navigator.clipboard.writeText(markdown).then(() => {
|
|
const btn = document.getElementById('copy-error-btn');
|
|
const originalHtml = btn.innerHTML;
|
|
btn.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
</svg> 복사 완료!`;
|
|
btn.classList.remove('bg-purple-600', 'hover:bg-purple-700');
|
|
btn.classList.add('bg-green-600');
|
|
|
|
setTimeout(() => {
|
|
btn.innerHTML = originalHtml;
|
|
btn.classList.remove('bg-green-600');
|
|
btn.classList.add('bg-purple-600', 'hover:bg-purple-700');
|
|
}, 2000);
|
|
}).catch(err => {
|
|
showToast('복사 실패: ' + err.message, 'error');
|
|
});
|
|
}
|
|
|
|
function runFlow(id) {
|
|
showConfirm('이 플로우를 다시 실행하시겠습니까?', () => {
|
|
const btn = document.getElementById('run-btn');
|
|
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: {
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// 새 실행 결과 페이지로 이동
|
|
if (data.run_id) {
|
|
window.location.href = `/dev-tools/flow-tester/runs/${data.run_id}`;
|
|
} else {
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalHtml;
|
|
showToast(data.message || '실행 완료', 'success');
|
|
location.reload();
|
|
}
|
|
})
|
|
.catch(error => {
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalHtml;
|
|
showToast('오류 발생: ' + error.message, 'error');
|
|
});
|
|
}, { title: '플로우 재실행', icon: 'question' });
|
|
}
|
|
</script>
|
|
@endpush
|