- OpenAPI 스펙 파일을 mng/storage/api-docs/로 복사 - config 경로를 storage_path()로 변경 (Docker 호환) - 라우트 이름 수정 (dev-tools.api-explorer.* 접두사) - HTMX 트리거 수정 (input changed, autocomplete off) - openSettingsModal, showToast 함수 추가
249 lines
12 KiB
PHP
249 lines
12 KiB
PHP
<div class="panel-header flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<span class="method-badge method-{{ strtolower($endpoint['method']) }}">
|
|
{{ $endpoint['method'] }}
|
|
</span>
|
|
<h3 class="font-semibold text-gray-700">{{ $endpoint['path'] }}</h3>
|
|
</div>
|
|
<button onclick="toggleBookmark('{{ $endpoint['path'] }}', '{{ $endpoint['method'] }}', this)"
|
|
class="{{ $isBookmarked ? 'text-yellow-500' : 'text-gray-400' }} hover:text-yellow-500 p-1">
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="panel-content">
|
|
<form onsubmit="executeApi(event)" class="space-y-4">
|
|
<input type="hidden" name="method" value="{{ $endpoint['method'] }}">
|
|
<input type="hidden" name="endpoint" value="{{ $endpoint['path'] }}">
|
|
|
|
{{-- 설명 --}}
|
|
@if($endpoint['summary'] || $endpoint['description'])
|
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
|
@if($endpoint['summary'])
|
|
<p class="font-medium text-blue-800">{{ $endpoint['summary'] }}</p>
|
|
@endif
|
|
@if($endpoint['description'])
|
|
<p class="text-sm text-blue-600 mt-1">{{ $endpoint['description'] }}</p>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Deprecated 경고 --}}
|
|
@if($endpoint['deprecated'] ?? false)
|
|
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
|
<p class="text-yellow-800 font-medium">⚠️ 이 API는 더 이상 사용되지 않습니다 (Deprecated)</p>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Path Parameters --}}
|
|
@php
|
|
$pathParams = collect($endpoint['parameters'])->where('in', 'path');
|
|
@endphp
|
|
@if($pathParams->isNotEmpty())
|
|
<div>
|
|
<h4 class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
|
|
<svg class="w-4 h-4 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
|
</svg>
|
|
Path Parameters
|
|
</h4>
|
|
<div class="space-y-2">
|
|
@foreach($pathParams as $param)
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">
|
|
{{ $param['name'] }}
|
|
@if($param['required'] ?? false)
|
|
<span class="text-red-500">*</span>
|
|
@endif
|
|
</label>
|
|
<input type="text"
|
|
name="path_{{ $param['name'] }}"
|
|
placeholder="{{ $param['description'] ?? $param['name'] }}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
{{ ($param['required'] ?? false) ? 'required' : '' }}>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Query Parameters --}}
|
|
@php
|
|
$queryParams = collect($endpoint['parameters'])->where('in', 'query');
|
|
@endphp
|
|
@if($queryParams->isNotEmpty())
|
|
<div>
|
|
<h4 class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
|
|
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
Query Parameters
|
|
</h4>
|
|
<div class="space-y-2">
|
|
@foreach($queryParams as $param)
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">
|
|
{{ $param['name'] }}
|
|
@if($param['required'] ?? false)
|
|
<span class="text-red-500">*</span>
|
|
@endif
|
|
</label>
|
|
<input type="text"
|
|
name="query_{{ $param['name'] }}"
|
|
placeholder="{{ $param['description'] ?? $param['name'] }}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
{{ ($param['required'] ?? false) ? 'required' : '' }}>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Request Body --}}
|
|
@if(in_array($endpoint['method'], ['POST', 'PUT', 'PATCH']) && ($endpoint['requestBody'] ?? null))
|
|
<div>
|
|
<h4 class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
|
|
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
Request Body (JSON)
|
|
</h4>
|
|
@php
|
|
// 스키마에서 예제 데이터 추출 시도
|
|
$schema = $endpoint['requestBody']['content']['application/json']['schema'] ?? [];
|
|
$example = $schema['example'] ?? null;
|
|
$properties = $schema['properties'] ?? [];
|
|
|
|
if (!$example && $properties) {
|
|
$example = [];
|
|
foreach ($properties as $propName => $propDef) {
|
|
$example[$propName] = $propDef['example'] ?? ($propDef['type'] === 'string' ? '' : null);
|
|
}
|
|
}
|
|
@endphp
|
|
<textarea name="body"
|
|
class="json-editor"
|
|
placeholder='{ "key": "value" }'>{{ $example ? json_encode($example, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : '' }}</textarea>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- 템플릿 선택 --}}
|
|
@if($templates->isNotEmpty())
|
|
<div>
|
|
<h4 class="text-sm font-semibold text-gray-700 mb-2">저장된 템플릿</h4>
|
|
<select onchange="loadTemplate(this.value)" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
|
<option value="">템플릿 선택...</option>
|
|
@foreach($templates as $template)
|
|
<option value="{{ $template->id }}">{{ $template->name }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- 실행 버튼 --}}
|
|
<div class="flex items-center gap-2 pt-2">
|
|
<button type="submit"
|
|
class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition flex items-center justify-center gap-2">
|
|
<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="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>
|
|
<button type="button"
|
|
onclick="saveAsTemplate()"
|
|
class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition"
|
|
title="템플릿으로 저장">
|
|
<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="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<script>
|
|
// 템플릿 로드
|
|
async function loadTemplate(templateId) {
|
|
if (!templateId) return;
|
|
|
|
const response = await fetch(`/dev-tools/api-explorer/templates/${encodeURIComponent('{{ $endpoint['path'] }}')}?method={{ $endpoint['method'] }}`);
|
|
const templates = await response.json();
|
|
const template = templates.find(t => t.id == templateId);
|
|
|
|
if (!template) return;
|
|
|
|
// 폼 필드 채우기
|
|
if (template.path_params) {
|
|
Object.entries(template.path_params).forEach(([key, value]) => {
|
|
const input = document.querySelector(`[name="path_${key}"]`);
|
|
if (input) input.value = value;
|
|
});
|
|
}
|
|
|
|
if (template.query_params) {
|
|
Object.entries(template.query_params).forEach(([key, value]) => {
|
|
const input = document.querySelector(`[name="query_${key}"]`);
|
|
if (input) input.value = value;
|
|
});
|
|
}
|
|
|
|
if (template.body) {
|
|
const bodyInput = document.querySelector('[name="body"]');
|
|
if (bodyInput) bodyInput.value = JSON.stringify(template.body, null, 2);
|
|
}
|
|
|
|
showToast(`템플릿 "${template.name}"이(가) 로드되었습니다.`);
|
|
}
|
|
|
|
// 템플릿 저장
|
|
async function saveAsTemplate() {
|
|
const name = prompt('템플릿 이름을 입력하세요:');
|
|
if (!name) return;
|
|
|
|
const form = document.querySelector('form');
|
|
const formData = new FormData(form);
|
|
|
|
// 파라미터 수집
|
|
const pathParams = {};
|
|
const queryParams = {};
|
|
|
|
formData.forEach((value, key) => {
|
|
if (key.startsWith('path_')) pathParams[key.replace('path_', '')] = value;
|
|
if (key.startsWith('query_')) queryParams[key.replace('query_', '')] = value;
|
|
});
|
|
|
|
let body = null;
|
|
const bodyText = formData.get('body');
|
|
if (bodyText) {
|
|
try {
|
|
body = JSON.parse(bodyText);
|
|
} catch (e) {}
|
|
}
|
|
|
|
const response = await fetch('{{ route("dev-tools.api-explorer.templates.store") }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
|
},
|
|
body: JSON.stringify({
|
|
endpoint: '{{ $endpoint['path'] }}',
|
|
method: '{{ $endpoint['method'] }}',
|
|
name: name,
|
|
path_params: pathParams,
|
|
query_params: queryParams,
|
|
body: body
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
showToast('템플릿이 저장되었습니다.');
|
|
} else {
|
|
showToast('템플릿 저장에 실패했습니다.', 'error');
|
|
}
|
|
}
|
|
</script>
|