feat: API 라우터 분리 및 버전 폴백 시스템 구현
- api.php를 13개 도메인별 파일로 분리 (1,479줄 → 61줄) - ApiVersionMiddleware 생성 (헤더/쿼리 기반 버전 선택) - v2 요청 시 v2 없으면 v1으로 자동 폴백 - 지원 헤더: Accept-Version, X-API-Version, api_version 쿼리 분리된 도메인: auth, admin, users, tenants, hr, finance, sales, inventory, production, design, files, boards, common Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
171
app/Http/Middleware/ApiVersionMiddleware.php
Normal file
171
app/Http/Middleware/ApiVersionMiddleware.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* API 버전 관리 미들웨어
|
||||
*
|
||||
* - 헤더 Accept-Version으로 버전 선택 (기본: v1)
|
||||
* - 요청된 버전의 라우트가 없으면 하위 버전으로 fallback
|
||||
* - 응답 헤더에 실제 사용된 버전 표시
|
||||
*/
|
||||
class ApiVersionMiddleware
|
||||
{
|
||||
/**
|
||||
* 지원하는 API 버전 목록 (우선순위 순)
|
||||
*/
|
||||
protected array $supportedVersions = ['v2', 'v1'];
|
||||
|
||||
/**
|
||||
* 기본 버전
|
||||
*/
|
||||
protected string $defaultVersion = 'v1';
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// 1. 요청된 버전 확인 (헤더 > 쿼리 파라미터 > 기본값)
|
||||
$requestedVersion = $this->getRequestedVersion($request);
|
||||
|
||||
// 2. 실제 사용할 버전 결정 (fallback 적용)
|
||||
$actualVersion = $this->resolveVersion($request, $requestedVersion);
|
||||
|
||||
// 3. 요청에 버전 정보 저장 (컨트롤러에서 사용 가능)
|
||||
$request->attributes->set('api_version', $actualVersion);
|
||||
$request->attributes->set('api_version_requested', $requestedVersion);
|
||||
$request->attributes->set('api_version_fallback', $actualVersion !== $requestedVersion);
|
||||
|
||||
// 4. 요청 처리
|
||||
$response = $next($request);
|
||||
|
||||
// 5. 응답 헤더에 버전 정보 추가
|
||||
$response->headers->set('X-API-Version', $actualVersion);
|
||||
if ($actualVersion !== $requestedVersion) {
|
||||
$response->headers->set('X-API-Version-Fallback', 'true');
|
||||
$response->headers->set('X-API-Version-Requested', $requestedVersion);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청에서 버전 정보 추출
|
||||
*/
|
||||
protected function getRequestedVersion(Request $request): string
|
||||
{
|
||||
// 1. Accept-Version 헤더 (권장)
|
||||
$version = $request->header('Accept-Version');
|
||||
if ($version && $this->isValidVersion($version)) {
|
||||
return $version;
|
||||
}
|
||||
|
||||
// 2. X-API-Version 헤더 (대안)
|
||||
$version = $request->header('X-API-Version');
|
||||
if ($version && $this->isValidVersion($version)) {
|
||||
return $version;
|
||||
}
|
||||
|
||||
// 3. 쿼리 파라미터 (테스트용)
|
||||
$version = $request->query('api_version');
|
||||
if ($version && $this->isValidVersion($version)) {
|
||||
return $version;
|
||||
}
|
||||
|
||||
return $this->defaultVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* 유효한 버전인지 확인
|
||||
*/
|
||||
protected function isValidVersion(string $version): bool
|
||||
{
|
||||
return in_array($version, $this->supportedVersions, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 실제 사용할 버전 결정 (fallback 로직)
|
||||
*/
|
||||
protected function resolveVersion(Request $request, string $requestedVersion): string
|
||||
{
|
||||
// 요청된 버전부터 하위 버전까지 순차 확인
|
||||
$startIndex = array_search($requestedVersion, $this->supportedVersions, true);
|
||||
|
||||
if ($startIndex === false) {
|
||||
return $this->defaultVersion;
|
||||
}
|
||||
|
||||
// 요청된 버전부터 하위 버전까지 체크
|
||||
for ($i = $startIndex; $i < count($this->supportedVersions); $i++) {
|
||||
$version = $this->supportedVersions[$i];
|
||||
|
||||
if ($this->versionRouteExists($request, $version)) {
|
||||
return $version;
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 버전에서 라우트를 찾지 못하면 기본값 반환
|
||||
return $this->defaultVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* 해당 버전의 라우트가 존재하는지 확인
|
||||
*/
|
||||
protected function versionRouteExists(Request $request, string $version): bool
|
||||
{
|
||||
$path = $request->path();
|
||||
|
||||
// URL에서 버전 부분 교체
|
||||
// /api/v1/users → /api/v2/users
|
||||
$versionedPath = preg_replace('/^api\/v\d+/', "api/{$version}", $path);
|
||||
|
||||
// 해당 경로의 라우트가 존재하는지 확인
|
||||
$routes = Route::getRoutes();
|
||||
|
||||
foreach ($routes as $route) {
|
||||
$routeUri = $route->uri();
|
||||
|
||||
// 정확히 일치하거나 파라미터 패턴 매칭
|
||||
if ($this->matchesRoute($versionedPath, $routeUri, $request->method())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 경로가 라우트와 일치하는지 확인
|
||||
*/
|
||||
protected function matchesRoute(string $path, string $routeUri, string $method): bool
|
||||
{
|
||||
// 라우트 URI의 파라미터를 정규식으로 변환
|
||||
// {id} → [^/]+
|
||||
$pattern = preg_replace('/\{[^}]+\}/', '[^/]+', $routeUri);
|
||||
$pattern = '#^'.$pattern.'$#';
|
||||
|
||||
return (bool) preg_match($pattern, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 지원 버전 목록 반환 (외부에서 사용)
|
||||
*/
|
||||
public function getSupportedVersions(): array
|
||||
{
|
||||
return $this->supportedVersions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 버전 반환
|
||||
*/
|
||||
public function getDefaultVersion(): string
|
||||
{
|
||||
return $this->defaultVersion;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user