Files
sam-manage/resources/views/dev-tools/api-explorer/partials/request-panel.blade.php
hskwon 9f3c5a98b9 fix: [api-explorer] Docker 환경 OpenAPI 경로 및 뷰 오류 수정
- OpenAPI 스펙 파일을 mng/storage/api-docs/로 복사
- config 경로를 storage_path()로 변경 (Docker 호환)
- 라우트 이름 수정 (dev-tools.api-explorer.* 접두사)
- HTMX 트리거 수정 (input changed, autocomplete off)
- openSettingsModal, showToast 함수 추가
2025-12-17 23:44:36 +09:00

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>