Files
sam-manage/resources/views/dev-tools/flow-tester/index.blade.php
hskwon ff943ab728 feat: API Flow Tester 기능 기반 구조 추가
- 모델: AdminApiFlow, AdminApiFlowRun
- 컨트롤러: FlowTesterController
- 뷰: index, create, edit, history, run-detail
- 사이드바 메뉴에 "개발 도구" 그룹 추가
- 라우트 설정
2025-11-27 19:02:18 +09:00

219 lines
12 KiB
PHP

@extends('layouts.app')
@section('title', 'API 플로우 테스터')
@section('content')
<!-- 페이지 헤더 -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">API 플로우 테스터</h1>
<a href="{{ route('dev-tools.flow-tester.create') }}"
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="M12 4v16m8-8H4" />
</svg>
플로우
</a>
</div>
<!-- 필터 영역 -->
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
<form method="GET" class="flex gap-4">
<!-- 카테고리 선택 -->
<div class="w-40">
<select name="category"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">전체 카테고리</option>
<option value="item-master" {{ request('category') === 'item-master' ? 'selected' : '' }}>item-master</option>
<option value="auth" {{ request('category') === 'auth' ? 'selected' : '' }}>auth</option>
<option value="bom" {{ request('category') === 'bom' ? 'selected' : '' }}>bom</option>
</select>
</div>
<!-- 검색 -->
<div class="flex-1">
<input type="text"
name="search"
value="{{ request('search') }}"
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>
<!-- 검색 버튼 -->
<button type="submit" class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition">
검색
</button>
</form>
</div>
<!-- 플로우 목록 -->
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
@if($flows->isEmpty())
<div class="text-center py-12 text-gray-500">
<svg class="w-16 h-16 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<p class="text-lg font-medium">등록된 플로우가 없습니다</p>
<p class="text-sm mt-1"> 플로우를 생성하여 API 테스트를 시작하세요.</p>
</div>
@else
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">이름</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">카테고리</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">스텝</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">최근 실행</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">상태</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">액션</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@foreach($flows as $flow)
@php
$latestRun = $flow->runs->first();
@endphp
<tr class="hover:bg-gray-50">
<td class="px-6 py-4">
<div class="font-medium text-gray-900">{{ $flow->name }}</div>
@if($flow->description)
<div class="text-sm text-gray-500 truncate max-w-xs">{{ $flow->description }}</div>
@endif
</td>
<td class="px-6 py-4">
@if($flow->category)
<span class="px-2 py-1 text-xs font-medium bg-gray-100 text-gray-700 rounded">{{ $flow->category }}</span>
@else
<span class="text-gray-400">-</span>
@endif
</td>
<td class="px-6 py-4 text-sm text-gray-500">
{{ $flow->step_count }}
</td>
<td class="px-6 py-4 text-sm text-gray-500">
@if($latestRun)
{{ $latestRun->created_at->diffForHumans() }}
@else
<span class="text-gray-400">-</span>
@endif
</td>
<td class="px-6 py-4">
@if($latestRun)
<span class="px-2 py-1 text-xs font-medium rounded {{ $latestRun->status_color }}">
{{ $latestRun->status_label }}
</span>
@else
<span class="px-2 py-1 text-xs font-medium bg-gray-100 text-gray-500 rounded">대기</span>
@endif
</td>
<td class="px-6 py-4 text-right">
<div class="flex justify-end gap-2">
<!-- 실행 버튼 -->
<button onclick="runFlow({{ $flow->id }})"
class="p-2 text-green-600 hover:bg-green-50 rounded-lg transition"
title="실행">
<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', $flow->id) }}"
class="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition"
title="편집">
<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>
<!-- 이력 버튼 -->
<a href="{{ route('dev-tools.flow-tester.history', $flow->id) }}"
class="p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition"
title="실행 이력">
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</a>
<!-- 복제 버튼 -->
<form action="{{ route('dev-tools.flow-tester.clone', $flow->id) }}" method="POST" class="inline">
@csrf
<button type="submit"
class="p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition"
title="복제">
<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 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</form>
<!-- 삭제 버튼 -->
<button onclick="confirmDelete({{ $flow->id }}, '{{ $flow->name }}')"
class="p-2 text-red-600 hover:bg-red-50 rounded-lg transition"
title="삭제">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
<!-- 페이지네이션 -->
@if($flows->hasPages())
<div class="px-6 py-4 border-t border-gray-200">
{{ $flows->links() }}
</div>
@endif
@endif
</div>
@endsection
@push('scripts')
<script>
function runFlow(id) {
if (!confirm('이 플로우를 실행하시겠습니까?')) return;
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.success) {
alert(data.message);
location.reload();
} else {
alert('실행 실패: ' + (data.message || '알 수 없는 오류'));
}
})
.catch(error => {
alert('오류 발생: ' + error.message);
});
}
function confirmDelete(id, name) {
if (!confirm(`"${name}" 플로우를 삭제하시겠습니까?`)) return;
fetch(`/dev-tools/flow-tester/${id}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
},
})
.then(response => {
if (response.ok) {
location.reload();
} else {
alert('삭제 실패');
}
})
.catch(error => {
alert('오류 발생: ' + error.message);
});
}
</script>
@endpush