- api_request_logs 테이블 사용으로 변경 (실제 API 호출 기록) - 기존 admin_api_histories는 API Explorer 테스트 기록용으로 유지 - ApiRequestLog 모델 추가 - URL에서 엔드포인트 경로 추출 (REGEXP_REPLACE 사용) - DB facade 사용으로 Eloquent accessor 충돌 방지 변경 전: 테스트 호출 2건만 표시 변경 후: 실제 API 호출 857건+ 표시 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
265 lines
7.9 KiB
PHP
265 lines
7.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services\ApiExplorer;
|
|
|
|
use App\Models\DevTools\ApiDeprecation;
|
|
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
|
|
{
|
|
// URL에서 경로만 추출 (도메인 및 쿼리스트링 제거)
|
|
// 예: https://api.sam.kr/api/v1/users?page=1 → /api/v1/users
|
|
// Note: DB facade 사용 (Eloquent accessor 충돌 방지)
|
|
return DB::table('api_request_logs')
|
|
->selectRaw("REGEXP_REPLACE(SUBSTRING_INDEX(url, '?', 1), '^https?://[^/]+', '') as endpoint")
|
|
->addSelect('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')
|
|
->groupByRaw("REGEXP_REPLACE(SUBSTRING_INDEX(url, '?', 1), '^https?://[^/]+', ''), 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 (DB facade 사용 - Eloquent accessor 충돌 방지)
|
|
$recentlyUsed = DB::table('api_request_logs')
|
|
->selectRaw("REGEXP_REPLACE(SUBSTRING_INDEX(url, '?', 1), '^https?://[^/]+', '') as endpoint")
|
|
->addSelect('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 DB::table('api_request_logs')
|
|
->select(DB::raw('DATE(created_at) as date'))
|
|
->selectRaw('COUNT(*) as call_count')
|
|
->selectRaw("COUNT(DISTINCT CONCAT(REGEXP_REPLACE(SUBSTRING_INDEX(url, '?', 1), '^https?://[^/]+', ''), '|', method)) as unique_endpoints")
|
|
->where('created_at', '>=', now()->subDays($days))
|
|
->groupBy('date')
|
|
->orderBy('date')
|
|
->get();
|
|
}
|
|
}
|