feat: API 사용 현황 및 폐기 후보 관리 기능 추가
- API 사용 통계 조회 및 미사용 API 식별 기능 - 폐기 후보 등록/상태변경/삭제 기능 - API Explorer에서 사용 현황 페이지 링크 추가 - 북마크 토글 버그 수정 (라우트-컨트롤러 메서드명 일치)
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\ApiExplorer\ApiExplorerService;
|
||||
use App\Services\ApiExplorer\ApiRequestService;
|
||||
use App\Services\ApiExplorer\ApiUsageService;
|
||||
use App\Services\ApiExplorer\OpenApiParserService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -18,7 +19,8 @@ class ApiExplorerController extends Controller
|
||||
public function __construct(
|
||||
private OpenApiParserService $parser,
|
||||
private ApiRequestService $requester,
|
||||
private ApiExplorerService $explorer
|
||||
private ApiExplorerService $explorer,
|
||||
private ApiUsageService $usageService
|
||||
) {}
|
||||
|
||||
/*
|
||||
@@ -199,9 +201,28 @@ public function bookmarks(): View
|
||||
}
|
||||
|
||||
/**
|
||||
* 즐겨찾기 추가/토글
|
||||
* 즐겨찾기 저장
|
||||
*/
|
||||
public function addBookmark(Request $request): JsonResponse
|
||||
public function storeBookmark(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'endpoint' => 'required|string|max:500',
|
||||
'method' => 'required|string|max:10',
|
||||
'display_name' => 'nullable|string|max:100',
|
||||
]);
|
||||
|
||||
$bookmark = $this->explorer->addBookmark(auth()->id(), $validated);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'bookmark' => $bookmark,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 즐겨찾기 토글
|
||||
*/
|
||||
public function toggleBookmark(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'endpoint' => 'required|string|max:500',
|
||||
@@ -439,4 +460,182 @@ public function users(): JsonResponse
|
||||
|
||||
return response()->json($users);
|
||||
}
|
||||
|
||||
/**
|
||||
* 즐겨찾기 수정
|
||||
*/
|
||||
public function updateBookmark(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'display_name' => 'nullable|string|max:100',
|
||||
]);
|
||||
|
||||
$bookmark = $this->explorer->updateBookmark($id, $validated);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'bookmark' => $bookmark,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 즐겨찾기 삭제
|
||||
*/
|
||||
public function deleteBookmark(int $id): JsonResponse
|
||||
{
|
||||
$this->explorer->removeBookmark($id);
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Usage & Deprecation
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* API 사용 현황 페이지
|
||||
*/
|
||||
public function usage(): View
|
||||
{
|
||||
$comparison = $this->usageService->getApiUsageComparison();
|
||||
$deprecations = $this->usageService->getDeprecations();
|
||||
$dailyTrend = $this->usageService->getDailyTrend(30);
|
||||
|
||||
return view('dev-tools.api-explorer.usage', compact(
|
||||
'comparison',
|
||||
'deprecations',
|
||||
'dailyTrend'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* API 사용 통계 조회 (JSON)
|
||||
*/
|
||||
public function usageStats(): JsonResponse
|
||||
{
|
||||
$comparison = $this->usageService->getApiUsageComparison();
|
||||
|
||||
return response()->json($comparison);
|
||||
}
|
||||
|
||||
/**
|
||||
* 폐기 후보 목록 조회
|
||||
*/
|
||||
public function deprecations(Request $request): JsonResponse
|
||||
{
|
||||
$status = $request->input('status');
|
||||
$deprecations = $this->usageService->getDeprecations($status);
|
||||
|
||||
return response()->json($deprecations);
|
||||
}
|
||||
|
||||
/**
|
||||
* 폐기 후보 추가
|
||||
*/
|
||||
public function addDeprecation(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'endpoint' => 'required|string|max:500',
|
||||
'method' => 'required|string|max:10',
|
||||
'reason' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$deprecation = $this->usageService->addDeprecationCandidate(
|
||||
$validated['endpoint'],
|
||||
$validated['method'],
|
||||
$validated['reason'] ?? null,
|
||||
auth()->id()
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'deprecation' => $deprecation,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 미사용 API 전체를 폐기 후보로 추가
|
||||
*/
|
||||
public function addAllUnusedAsDeprecation(): JsonResponse
|
||||
{
|
||||
$count = $this->usageService->addAllUnusedAsCandidate(auth()->id());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'added_count' => $count,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 폐기 상태 변경
|
||||
*/
|
||||
public function updateDeprecation(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'status' => 'required|string|in:candidate,scheduled,deprecated,removed',
|
||||
'reason' => 'nullable|string',
|
||||
'scheduled_date' => 'nullable|date',
|
||||
]);
|
||||
|
||||
$scheduledDate = ! empty($validated['scheduled_date'])
|
||||
? new \DateTime($validated['scheduled_date'])
|
||||
: null;
|
||||
|
||||
$deprecation = $this->usageService->updateDeprecationStatus(
|
||||
$id,
|
||||
$validated['status'],
|
||||
$validated['reason'] ?? null,
|
||||
$scheduledDate
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'deprecation' => $deprecation,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 폐기 후보 삭제
|
||||
*/
|
||||
public function removeDeprecation(int $id): JsonResponse
|
||||
{
|
||||
$this->usageService->removeDeprecation($id);
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일별 API 호출 추이
|
||||
*/
|
||||
public function dailyTrend(Request $request): JsonResponse
|
||||
{
|
||||
$days = $request->input('days', 30);
|
||||
$trend = $this->usageService->getDailyTrend($days);
|
||||
|
||||
return response()->json($trend);
|
||||
}
|
||||
|
||||
/**
|
||||
* 인기 API 목록
|
||||
*/
|
||||
public function popularApis(Request $request): JsonResponse
|
||||
{
|
||||
$limit = $request->input('limit', 20);
|
||||
$popular = $this->usageService->getPopularApis($limit);
|
||||
|
||||
return response()->json($popular);
|
||||
}
|
||||
|
||||
/**
|
||||
* 오래된 API 목록 (N일 이상 미사용)
|
||||
*/
|
||||
public function staleApis(Request $request): JsonResponse
|
||||
{
|
||||
$days = $request->input('days', 30);
|
||||
$stale = $this->usageService->getStaleApis($days);
|
||||
|
||||
return response()->json($stale);
|
||||
}
|
||||
}
|
||||
|
||||
112
app/Models/DevTools/ApiDeprecation.php
Normal file
112
app/Models/DevTools/ApiDeprecation.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\DevTools;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* API 폐기 후보 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $endpoint
|
||||
* @property string $method
|
||||
* @property string $status
|
||||
* @property string|null $reason
|
||||
* @property \Illuminate\Support\Carbon|null $scheduled_date
|
||||
* @property \Illuminate\Support\Carbon|null $deprecated_date
|
||||
* @property int|null $created_by
|
||||
* @property \Illuminate\Support\Carbon $created_at
|
||||
* @property \Illuminate\Support\Carbon $updated_at
|
||||
*/
|
||||
class ApiDeprecation extends Model
|
||||
{
|
||||
protected $table = 'admin_api_deprecations';
|
||||
|
||||
protected $fillable = [
|
||||
'endpoint',
|
||||
'method',
|
||||
'status',
|
||||
'reason',
|
||||
'scheduled_date',
|
||||
'deprecated_date',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'scheduled_date' => 'date',
|
||||
'deprecated_date' => 'date',
|
||||
];
|
||||
|
||||
/**
|
||||
* 상태 상수
|
||||
*/
|
||||
public const STATUS_CANDIDATE = 'candidate'; // 삭제 후보
|
||||
public const STATUS_SCHEDULED = 'scheduled'; // 삭제 예정
|
||||
public const STATUS_DEPRECATED = 'deprecated'; // 폐기됨 (사용 중단)
|
||||
public const STATUS_REMOVED = 'removed'; // 완전 삭제
|
||||
|
||||
/**
|
||||
* 상태 라벨
|
||||
*/
|
||||
public static function statusLabels(): array
|
||||
{
|
||||
return [
|
||||
self::STATUS_CANDIDATE => '삭제 후보',
|
||||
self::STATUS_SCHEDULED => '삭제 예정',
|
||||
self::STATUS_DEPRECATED => '폐기됨',
|
||||
self::STATUS_REMOVED => '삭제됨',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 라벨 접근자
|
||||
*/
|
||||
public function getStatusLabelAttribute(): string
|
||||
{
|
||||
return self::statusLabels()[$this->status] ?? $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 색상 접근자
|
||||
*/
|
||||
public function getStatusColorAttribute(): string
|
||||
{
|
||||
return match ($this->status) {
|
||||
self::STATUS_CANDIDATE => 'yellow',
|
||||
self::STATUS_SCHEDULED => 'orange',
|
||||
self::STATUS_DEPRECATED => 'red',
|
||||
self::STATUS_REMOVED => 'gray',
|
||||
default => 'gray',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성자 관계
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 후보 상태 스코프
|
||||
*/
|
||||
public function scopeCandidates($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_CANDIDATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 상태 스코프 (삭제되지 않은 것들)
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->whereIn('status', [
|
||||
self::STATUS_CANDIDATE,
|
||||
self::STATUS_SCHEDULED,
|
||||
self::STATUS_DEPRECATED,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,26 @@ public function removeBookmark(int $bookmarkId): void
|
||||
ApiBookmark::where('id', $bookmarkId)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 즐겨찾기 수정
|
||||
*/
|
||||
public function updateBookmark(int $bookmarkId, array $data): ?ApiBookmark
|
||||
{
|
||||
$bookmark = ApiBookmark::find($bookmarkId);
|
||||
|
||||
if (! $bookmark) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($data['display_name'])) {
|
||||
$bookmark->display_name = $data['display_name'];
|
||||
}
|
||||
|
||||
$bookmark->save();
|
||||
|
||||
return $bookmark;
|
||||
}
|
||||
|
||||
/**
|
||||
* 즐겨찾기 순서 변경
|
||||
*/
|
||||
|
||||
257
app/Services/ApiExplorer/ApiUsageService.php
Normal file
257
app/Services/ApiExplorer/ApiUsageService.php
Normal file
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ApiExplorer;
|
||||
|
||||
use App\Models\DevTools\ApiDeprecation;
|
||||
use App\Models\DevTools\ApiHistory;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* API 사용 현황 및 통계 서비스
|
||||
*/
|
||||
class ApiUsageService
|
||||
{
|
||||
public function __construct(
|
||||
private OpenApiParserService $parser
|
||||
) {}
|
||||
|
||||
/**
|
||||
* API 사용 통계 조회
|
||||
*
|
||||
* @return Collection 엔드포인트별 사용 횟수
|
||||
*/
|
||||
public function getUsageStats(): Collection
|
||||
{
|
||||
return ApiHistory::select('endpoint', 'method')
|
||||
->selectRaw('COUNT(*) as call_count')
|
||||
->selectRaw('MAX(created_at) as last_called_at')
|
||||
->selectRaw('AVG(duration_ms) as avg_duration_ms')
|
||||
->selectRaw('SUM(CASE WHEN response_status >= 200 AND response_status < 300 THEN 1 ELSE 0 END) as success_count')
|
||||
->selectRaw('SUM(CASE WHEN response_status >= 400 THEN 1 ELSE 0 END) as error_count')
|
||||
->groupBy('endpoint', 'method')
|
||||
->orderByDesc('call_count')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 API 목록과 사용 현황 비교
|
||||
*
|
||||
* @return array ['used' => [...], 'unused' => [...], 'stats' => [...]]
|
||||
*/
|
||||
public function getApiUsageComparison(): array
|
||||
{
|
||||
// Swagger에서 전체 API 목록 조회
|
||||
$allEndpoints = $this->parser->getEndpoints();
|
||||
|
||||
// 사용된 API 통계
|
||||
$usageStats = $this->getUsageStats()->keyBy(function ($item) {
|
||||
return $item->endpoint . '|' . $item->method;
|
||||
});
|
||||
|
||||
// 폐기 후보 목록
|
||||
$deprecations = ApiDeprecation::active()
|
||||
->get()
|
||||
->keyBy(function ($item) {
|
||||
return $item->endpoint . '|' . $item->method;
|
||||
});
|
||||
|
||||
$used = [];
|
||||
$unused = [];
|
||||
|
||||
foreach ($allEndpoints as $endpoint) {
|
||||
$key = $endpoint['path'] . '|' . $endpoint['method'];
|
||||
$stats = $usageStats->get($key);
|
||||
$deprecation = $deprecations->get($key);
|
||||
|
||||
$item = [
|
||||
'endpoint' => $endpoint['path'],
|
||||
'method' => $endpoint['method'],
|
||||
'summary' => $endpoint['summary'] ?? '',
|
||||
'tags' => $endpoint['tags'] ?? [],
|
||||
'call_count' => $stats->call_count ?? 0,
|
||||
'last_called_at' => $stats->last_called_at ?? null,
|
||||
'avg_duration_ms' => $stats->avg_duration_ms ?? null,
|
||||
'success_count' => $stats->success_count ?? 0,
|
||||
'error_count' => $stats->error_count ?? 0,
|
||||
'deprecation' => $deprecation,
|
||||
];
|
||||
|
||||
if ($stats) {
|
||||
$used[] = $item;
|
||||
} else {
|
||||
$unused[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
// 사용된 API는 호출 횟수 순으로 정렬
|
||||
usort($used, fn ($a, $b) => $b['call_count'] <=> $a['call_count']);
|
||||
|
||||
return [
|
||||
'used' => $used,
|
||||
'unused' => $unused,
|
||||
'summary' => [
|
||||
'total' => count($allEndpoints),
|
||||
'used_count' => count($used),
|
||||
'unused_count' => count($unused),
|
||||
'deprecation_count' => $deprecations->count(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 미사용 API 목록 조회
|
||||
*/
|
||||
public function getUnusedApis(): Collection
|
||||
{
|
||||
$comparison = $this->getApiUsageComparison();
|
||||
|
||||
return collect($comparison['unused']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 폐기 후보 추가
|
||||
*/
|
||||
public function addDeprecationCandidate(
|
||||
string $endpoint,
|
||||
string $method,
|
||||
?string $reason = null,
|
||||
?int $userId = null
|
||||
): ApiDeprecation {
|
||||
return ApiDeprecation::updateOrCreate(
|
||||
['endpoint' => $endpoint, 'method' => strtoupper($method)],
|
||||
[
|
||||
'status' => ApiDeprecation::STATUS_CANDIDATE,
|
||||
'reason' => $reason,
|
||||
'created_by' => $userId,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 폐기 후보 일괄 추가 (미사용 API 전체)
|
||||
*/
|
||||
public function addAllUnusedAsCandidate(?int $userId = null): int
|
||||
{
|
||||
$unused = $this->getUnusedApis();
|
||||
$count = 0;
|
||||
|
||||
foreach ($unused as $api) {
|
||||
// 이미 등록되지 않은 것만 추가
|
||||
$exists = ApiDeprecation::where('endpoint', $api['endpoint'])
|
||||
->where('method', $api['method'])
|
||||
->exists();
|
||||
|
||||
if (! $exists) {
|
||||
$this->addDeprecationCandidate(
|
||||
$api['endpoint'],
|
||||
$api['method'],
|
||||
'미사용 API (자동 등록)',
|
||||
$userId
|
||||
);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 폐기 상태 변경
|
||||
*/
|
||||
public function updateDeprecationStatus(
|
||||
int $id,
|
||||
string $status,
|
||||
?string $reason = null,
|
||||
?\DateTime $scheduledDate = null
|
||||
): ApiDeprecation {
|
||||
$deprecation = ApiDeprecation::findOrFail($id);
|
||||
|
||||
$data = ['status' => $status];
|
||||
|
||||
if ($reason !== null) {
|
||||
$data['reason'] = $reason;
|
||||
}
|
||||
|
||||
if ($scheduledDate !== null) {
|
||||
$data['scheduled_date'] = $scheduledDate;
|
||||
}
|
||||
|
||||
if ($status === ApiDeprecation::STATUS_DEPRECATED) {
|
||||
$data['deprecated_date'] = now();
|
||||
}
|
||||
|
||||
$deprecation->update($data);
|
||||
|
||||
return $deprecation->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 폐기 후보 삭제
|
||||
*/
|
||||
public function removeDeprecation(int $id): void
|
||||
{
|
||||
ApiDeprecation::where('id', $id)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 폐기 후보 목록 조회
|
||||
*/
|
||||
public function getDeprecations(?string $status = null): Collection
|
||||
{
|
||||
$query = ApiDeprecation::with('creator')
|
||||
->orderByDesc('created_at');
|
||||
|
||||
if ($status) {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 인기 API 조회 (상위 N개)
|
||||
*/
|
||||
public function getPopularApis(int $limit = 20): Collection
|
||||
{
|
||||
return $this->getUsageStats()->take($limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 최근 사용되지 않은 API (N일 이상)
|
||||
*/
|
||||
public function getStaleApis(int $days = 30): Collection
|
||||
{
|
||||
$cutoffDate = now()->subDays($days);
|
||||
|
||||
// 최근 호출된 API
|
||||
$recentlyUsed = ApiHistory::select('endpoint', 'method')
|
||||
->where('created_at', '>=', $cutoffDate)
|
||||
->distinct()
|
||||
->get()
|
||||
->map(fn ($item) => $item->endpoint . '|' . $item->method)
|
||||
->toArray();
|
||||
|
||||
// 전체 사용 통계에서 최근 사용된 것 제외
|
||||
return $this->getUsageStats()
|
||||
->filter(function ($item) use ($recentlyUsed) {
|
||||
$key = $item->endpoint . '|' . $item->method;
|
||||
|
||||
return ! in_array($key, $recentlyUsed);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 일별 API 호출 추이
|
||||
*/
|
||||
public function getDailyTrend(int $days = 30): Collection
|
||||
{
|
||||
return ApiHistory::select(DB::raw('DATE(created_at) as date'))
|
||||
->selectRaw('COUNT(*) as call_count')
|
||||
->selectRaw('COUNT(DISTINCT CONCAT(endpoint, "|", method)) as unique_endpoints')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->groupBy('date')
|
||||
->orderBy('date')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
@@ -213,6 +213,13 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- API 사용 현황 버튼 -->
|
||||
<a href="{{ route('dev-tools.api-explorer.usage') }}" class="p-2 text-gray-600 hover:bg-gray-100 rounded-lg" title="API 사용 현황">
|
||||
<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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- 설정 버튼 -->
|
||||
<button onclick="openSettingsModal()" class="p-2 text-gray-600 hover:bg-gray-100 rounded-lg" title="설정">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
654
resources/views/dev-tools/api-explorer/usage.blade.php
Normal file
654
resources/views/dev-tools/api-explorer/usage.blade.php
Normal file
@@ -0,0 +1,654 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'API 사용 현황')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
/* 통계 카드 */
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* HTTP 메서드 배지 */
|
||||
.method-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.125rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
border-radius: 0.25rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.method-get { background: #dcfce7; color: #166534; }
|
||||
.method-post { background: #dbeafe; color: #1e40af; }
|
||||
.method-put { background: #fef3c7; color: #92400e; }
|
||||
.method-patch { background: #ffedd5; color: #9a3412; }
|
||||
.method-delete { background: #fee2e2; color: #991b1b; }
|
||||
|
||||
/* 상태 배지 */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.status-candidate { background: #fef3c7; color: #92400e; }
|
||||
.status-scheduled { background: #ffedd5; color: #9a3412; }
|
||||
.status-deprecated { background: #fee2e2; color: #991b1b; }
|
||||
.status-removed { background: #f3f4f6; color: #6b7280; }
|
||||
|
||||
/* 테이블 스타일 */
|
||||
.api-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.api-table th {
|
||||
background: #f9fafb;
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.api-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.api-table tbody tr:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.endpoint-path {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.8rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
/* 탭 네비게이션 */
|
||||
.tab-nav {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.tab-item:hover {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #3b82f6;
|
||||
border-bottom-color: #3b82f6;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 차트 컨테이너 */
|
||||
.chart-container {
|
||||
height: 200px;
|
||||
background: #f9fafb;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid py-4 px-6">
|
||||
<!-- 헤더 -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-800">API 사용 현황</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">Swagger 기반 API 사용 통계 및 폐기 관리</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('dev-tools.api-explorer.index') }}" class="btn btn-ghost btn-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
API Explorer
|
||||
</a>
|
||||
<button onclick="addAllUnused()" class="btn btn-warning btn-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
미사용 API 전체 등록
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 통계 요약 -->
|
||||
<div class="grid grid-cols-4 gap-4 mb-6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value text-gray-800">{{ $comparison['summary']['total'] }}</div>
|
||||
<div class="stat-label">전체 API</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value text-green-600">{{ $comparison['summary']['used_count'] }}</div>
|
||||
<div class="stat-label">사용중 API</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value text-orange-500">{{ $comparison['summary']['unused_count'] }}</div>
|
||||
<div class="stat-label">미사용 API</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value text-red-500">{{ $comparison['summary']['deprecation_count'] }}</div>
|
||||
<div class="stat-label">폐기 후보</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 탭 네비게이션 -->
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="tab-nav px-4">
|
||||
<div class="tab-item active" data-tab="used">
|
||||
사용중 API ({{ count($comparison['used']) }})
|
||||
</div>
|
||||
<div class="tab-item" data-tab="unused">
|
||||
미사용 API ({{ count($comparison['unused']) }})
|
||||
</div>
|
||||
<div class="tab-item" data-tab="deprecations">
|
||||
폐기 후보 ({{ count($deprecations) }})
|
||||
</div>
|
||||
<div class="tab-item" data-tab="trend">
|
||||
호출 추이
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 사용중 API 탭 -->
|
||||
<div class="tab-content active" id="tab-used">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="api-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80px;">메서드</th>
|
||||
<th>엔드포인트</th>
|
||||
<th style="width: 120px;">호출 수</th>
|
||||
<th style="width: 100px;">성공</th>
|
||||
<th style="width: 100px;">실패</th>
|
||||
<th style="width: 100px;">평균 응답</th>
|
||||
<th style="width: 150px;">마지막 호출</th>
|
||||
<th style="width: 80px;">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($comparison['used'] as $api)
|
||||
<tr>
|
||||
<td>
|
||||
<span class="method-badge method-{{ strtolower($api['method']) }}">
|
||||
{{ $api['method'] }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="endpoint-path">{{ $api['endpoint'] }}</div>
|
||||
@if($api['summary'])
|
||||
<div class="text-xs text-gray-400 mt-0.5">{{ $api['summary'] }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="font-semibold">{{ number_format($api['call_count']) }}</td>
|
||||
<td class="text-green-600">{{ number_format($api['success_count']) }}</td>
|
||||
<td class="text-red-500">{{ number_format($api['error_count']) }}</td>
|
||||
<td>
|
||||
@if($api['avg_duration_ms'])
|
||||
{{ number_format($api['avg_duration_ms'], 0) }}ms
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-gray-500 text-xs">
|
||||
@if($api['last_called_at'])
|
||||
{{ \Carbon\Carbon::parse($api['last_called_at'])->diffForHumans() }}
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if(!$api['deprecation'])
|
||||
<button onclick="addDeprecation('{{ $api['endpoint'] }}', '{{ $api['method'] }}')"
|
||||
class="btn btn-ghost btn-xs text-orange-500" title="폐기 후보 등록">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
||||
</svg>
|
||||
</button>
|
||||
@else
|
||||
<span class="status-badge status-{{ $api['deprecation']->status }}">
|
||||
{{ $api['deprecation']->status_label }}
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-8 text-gray-400">
|
||||
사용된 API가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 미사용 API 탭 -->
|
||||
<div class="tab-content" id="tab-unused">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="api-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80px;">메서드</th>
|
||||
<th>엔드포인트</th>
|
||||
<th>태그</th>
|
||||
<th>상태</th>
|
||||
<th style="width: 80px;">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($comparison['unused'] as $api)
|
||||
<tr>
|
||||
<td>
|
||||
<span class="method-badge method-{{ strtolower($api['method']) }}">
|
||||
{{ $api['method'] }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="endpoint-path">{{ $api['endpoint'] }}</div>
|
||||
@if($api['summary'])
|
||||
<div class="text-xs text-gray-400 mt-0.5">{{ $api['summary'] }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@foreach($api['tags'] ?? [] as $tag)
|
||||
<span class="badge badge-ghost badge-sm">{{ $tag }}</span>
|
||||
@endforeach
|
||||
</td>
|
||||
<td>
|
||||
@if($api['deprecation'])
|
||||
<span class="status-badge status-{{ $api['deprecation']->status }}">
|
||||
{{ $api['deprecation']->status_label }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if(!$api['deprecation'])
|
||||
<button onclick="addDeprecation('{{ $api['endpoint'] }}', '{{ $api['method'] }}')"
|
||||
class="btn btn-ghost btn-xs text-orange-500" title="폐기 후보 등록">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
</button>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-8 text-gray-400">
|
||||
미사용 API가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 폐기 후보 탭 -->
|
||||
<div class="tab-content" id="tab-deprecations">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="api-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80px;">메서드</th>
|
||||
<th>엔드포인트</th>
|
||||
<th>상태</th>
|
||||
<th>사유</th>
|
||||
<th>예정일</th>
|
||||
<th>등록자</th>
|
||||
<th>등록일</th>
|
||||
<th style="width: 120px;">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($deprecations as $dep)
|
||||
<tr>
|
||||
<td>
|
||||
<span class="method-badge method-{{ strtolower($dep->method) }}">
|
||||
{{ $dep->method }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="endpoint-path">{{ $dep->endpoint }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<select onchange="updateDeprecationStatus({{ $dep->id }}, this.value)"
|
||||
class="select select-bordered select-xs">
|
||||
@foreach(\App\Models\DevTools\ApiDeprecation::statusLabels() as $value => $label)
|
||||
<option value="{{ $value }}" {{ $dep->status === $value ? 'selected' : '' }}>
|
||||
{{ $label }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
<td class="text-gray-600 text-sm">{{ $dep->reason ?? '-' }}</td>
|
||||
<td class="text-gray-500 text-xs">
|
||||
{{ $dep->scheduled_date?->format('Y-m-d') ?? '-' }}
|
||||
</td>
|
||||
<td class="text-gray-500 text-xs">
|
||||
{{ $dep->creator?->name ?? '-' }}
|
||||
</td>
|
||||
<td class="text-gray-500 text-xs">
|
||||
{{ $dep->created_at->format('Y-m-d') }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex gap-1">
|
||||
<button onclick="editDeprecation({{ $dep->id }})"
|
||||
class="btn btn-ghost btn-xs" title="수정">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
</button>
|
||||
<button onclick="removeDeprecation({{ $dep->id }})"
|
||||
class="btn btn-ghost btn-xs text-red-500" title="삭제">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-8 text-gray-400">
|
||||
폐기 후보가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 호출 추이 탭 -->
|
||||
<div class="tab-content p-6" id="tab-trend">
|
||||
<div class="chart-container" id="trend-chart">
|
||||
<div class="text-center">
|
||||
<p class="mb-2">최근 30일 API 호출 추이</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="api-table mx-auto" style="max-width: 600px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>날짜</th>
|
||||
<th>호출 수</th>
|
||||
<th>고유 엔드포인트</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($dailyTrend as $day)
|
||||
<tr>
|
||||
<td>{{ $day->date }}</td>
|
||||
<td class="font-semibold">{{ number_format($day->call_count) }}</td>
|
||||
<td>{{ $day->unique_endpoints }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-4 text-gray-400">
|
||||
데이터가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 폐기 후보 등록 모달 -->
|
||||
<div id="deprecation-modal" class="fixed inset-0 z-50 hidden">
|
||||
<div class="fixed inset-0 bg-black/50" onclick="closeDeprecationModal()"></div>
|
||||
<div class="fixed inset-0 flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-md w-full p-6 relative">
|
||||
<!-- 헤더 -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">폐기 후보 등록</h3>
|
||||
<button onclick="closeDeprecationModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<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="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="deprecation-form">
|
||||
<input type="hidden" name="endpoint" id="dep-endpoint">
|
||||
<input type="hidden" name="method" id="dep-method">
|
||||
|
||||
<!-- API 정보 -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">API 정보</label>
|
||||
<div id="dep-api-info" class="bg-gray-100 p-3 rounded-lg font-mono text-sm"></div>
|
||||
</div>
|
||||
|
||||
<!-- 폐기 사유 -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">폐기 사유</label>
|
||||
<textarea name="reason" id="dep-reason" rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
placeholder="폐기 사유를 입력하세요 (선택)"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 버튼 -->
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" onclick="closeDeprecationModal()"
|
||||
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition">
|
||||
취소
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="px-4 py-2 text-white bg-orange-500 hover:bg-orange-600 rounded-lg transition">
|
||||
등록
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 탭 전환
|
||||
document.querySelectorAll('.tab-item').forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
const tabId = this.dataset.tab;
|
||||
|
||||
// 탭 버튼 활성화
|
||||
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// 탭 컨텐츠 전환
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
document.getElementById('tab-' + tabId).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// 폐기 후보 등록 폼
|
||||
document.getElementById('deprecation-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = {
|
||||
endpoint: document.getElementById('dep-endpoint').value,
|
||||
method: document.getElementById('dep-method').value,
|
||||
reason: document.getElementById('dep-reason').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('{{ route("dev-tools.api-explorer.deprecations.store") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
closeDeprecationModal();
|
||||
location.reload();
|
||||
} else {
|
||||
alert('등록 실패: ' + (result.message || '알 수 없는 오류'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('등록 중 오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 폐기 후보 등록 모달 열기
|
||||
function addDeprecation(endpoint, method) {
|
||||
document.getElementById('dep-endpoint').value = endpoint;
|
||||
document.getElementById('dep-method').value = method;
|
||||
document.getElementById('dep-api-info').innerHTML = `<span class="method-badge method-${method.toLowerCase()}">${method}</span> ${endpoint}`;
|
||||
document.getElementById('dep-reason').value = '';
|
||||
document.getElementById('deprecation-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 폐기 후보 등록 모달 닫기
|
||||
function closeDeprecationModal() {
|
||||
document.getElementById('deprecation-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 미사용 API 전체 등록
|
||||
async function addAllUnused() {
|
||||
if (!confirm('미사용 API를 모두 폐기 후보로 등록하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('{{ route("dev-tools.api-explorer.deprecations.bulk-unused") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert(`${result.added_count}개의 API가 폐기 후보로 등록되었습니다.`);
|
||||
location.reload();
|
||||
} else {
|
||||
alert('등록 실패: ' + (result.message || '알 수 없는 오류'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('등록 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 폐기 상태 변경
|
||||
async function updateDeprecationStatus(id, status) {
|
||||
try {
|
||||
const response = await fetch(`{{ url('dev-tools/api-explorer/deprecations') }}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify({ status })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
alert('상태 변경 실패');
|
||||
location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('상태 변경 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 폐기 후보 삭제
|
||||
async function removeDeprecation(id) {
|
||||
if (!confirm('이 폐기 후보를 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`{{ url('dev-tools/api-explorer/deprecations') }}/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('삭제 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 폐기 후보 수정 (TODO: 모달로 구현)
|
||||
function editDeprecation(id) {
|
||||
alert('수정 기능은 추후 구현 예정입니다.');
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -324,26 +324,36 @@
|
||||
Route::post('/bookmarks/reorder', [ApiExplorerController::class, 'reorderBookmarks'])->name('bookmarks.reorder');
|
||||
|
||||
// 템플릿
|
||||
Route::get('/templates/{endpoint}', [ApiExplorerController::class, 'templates'])->name('templates.index');
|
||||
Route::post('/templates', [ApiExplorerController::class, 'storeTemplate'])->name('templates.store');
|
||||
Route::put('/templates/{id}', [ApiExplorerController::class, 'updateTemplate'])->name('templates.update');
|
||||
Route::get('/templates/{endpoint}', [ApiExplorerController::class, 'templatesForEndpoint'])->name('templates.index');
|
||||
Route::post('/templates', [ApiExplorerController::class, 'saveTemplate'])->name('templates.store');
|
||||
Route::delete('/templates/{id}', [ApiExplorerController::class, 'deleteTemplate'])->name('templates.destroy');
|
||||
|
||||
// 히스토리
|
||||
Route::get('/history', [ApiExplorerController::class, 'history'])->name('history.index');
|
||||
Route::get('/history/{id}', [ApiExplorerController::class, 'historyDetail'])->name('history.show');
|
||||
Route::post('/history/{id}/replay', [ApiExplorerController::class, 'replayHistory'])->name('history.replay');
|
||||
Route::delete('/history/{id}', [ApiExplorerController::class, 'deleteHistory'])->name('history.destroy');
|
||||
Route::delete('/history', [ApiExplorerController::class, 'clearHistory'])->name('history.clear');
|
||||
|
||||
// 환경 설정
|
||||
Route::get('/environments', [ApiExplorerController::class, 'environments'])->name('environments.index');
|
||||
Route::post('/environments', [ApiExplorerController::class, 'storeEnvironment'])->name('environments.store');
|
||||
Route::put('/environments/{id}', [ApiExplorerController::class, 'updateEnvironment'])->name('environments.update');
|
||||
Route::post('/environments', [ApiExplorerController::class, 'saveEnvironment'])->name('environments.store');
|
||||
Route::delete('/environments/{id}', [ApiExplorerController::class, 'deleteEnvironment'])->name('environments.destroy');
|
||||
|
||||
// 사용자 목록 (인증용)
|
||||
Route::get('/users', [ApiExplorerController::class, 'users'])->name('users');
|
||||
|
||||
// API 사용 현황 및 폐기 관리
|
||||
Route::get('/usage', [ApiExplorerController::class, 'usage'])->name('usage');
|
||||
Route::get('/usage/stats', [ApiExplorerController::class, 'usageStats'])->name('usage.stats');
|
||||
Route::get('/usage/trend', [ApiExplorerController::class, 'dailyTrend'])->name('usage.trend');
|
||||
Route::get('/usage/popular', [ApiExplorerController::class, 'popularApis'])->name('usage.popular');
|
||||
Route::get('/usage/stale', [ApiExplorerController::class, 'staleApis'])->name('usage.stale');
|
||||
|
||||
// 폐기 후보 관리
|
||||
Route::get('/deprecations', [ApiExplorerController::class, 'deprecations'])->name('deprecations.index');
|
||||
Route::post('/deprecations', [ApiExplorerController::class, 'addDeprecation'])->name('deprecations.store');
|
||||
Route::post('/deprecations/bulk-unused', [ApiExplorerController::class, 'addAllUnusedAsDeprecation'])->name('deprecations.bulk-unused');
|
||||
Route::put('/deprecations/{id}', [ApiExplorerController::class, 'updateDeprecation'])->name('deprecations.update');
|
||||
Route::delete('/deprecations/{id}', [ApiExplorerController::class, 'removeDeprecation'])->name('deprecations.destroy');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user