From 844a0458adb6fb8ebc638f00243376c0408a96e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Fri, 20 Mar 2026 00:36:48 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20R2=20presigned=20URL=20=EC=97=94?= =?UTF-8?q?=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FileStorageController에 presignedUrl() 메서드 추가 (30분 유효) - GET /api/v1/files/{id}/presigned-url 라우트 추가 - 파일 프록시 스트리밍 대신 R2 직접 접근 지원 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Api/V1/FileStorageController.php | 23 +++++++++++++++++++ routes/api/v1/files.php | 1 + 2 files changed, 24 insertions(+) diff --git a/app/Http/Controllers/Api/V1/FileStorageController.php b/app/Http/Controllers/Api/V1/FileStorageController.php index 95e4e4d0..41e6486c 100644 --- a/app/Http/Controllers/Api/V1/FileStorageController.php +++ b/app/Http/Controllers/Api/V1/FileStorageController.php @@ -9,6 +9,7 @@ use App\Http\Requests\Api\V1\ShareLinkRequest; use App\Services\FileStorageService; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Storage; class FileStorageController extends Controller { @@ -122,6 +123,28 @@ public function view(int $id, Request $request) return $file->download(inline: true); } + /** + * R2 Presigned URL 발급 (30분 유효) + */ + public function presignedUrl(int $id, Request $request) + { + $this->ensureContext($request); + + $service = new FileStorageService; + $file = $service->getFile($id); + + if (! $file->file_path || ! Storage::disk('r2')->exists($file->file_path)) { + abort(404, 'File not found in storage'); + } + + $url = Storage::disk('r2')->temporaryUrl( + $file->file_path, + now()->addMinutes(30) + ); + + return ApiResponse::handle(fn () => ['url' => $url]); + } + /** * Soft delete file */ diff --git a/routes/api/v1/files.php b/routes/api/v1/files.php index 4f48bc91..bed1b821 100644 --- a/routes/api/v1/files.php +++ b/routes/api/v1/files.php @@ -22,6 +22,7 @@ Route::get('/{id}', [FileStorageController::class, 'show'])->name('v1.files.show'); // 파일 상세 Route::get('/{id}/download', [FileStorageController::class, 'download'])->name('v1.files.download'); // 파일 다운로드 Route::get('/{id}/view', [FileStorageController::class, 'view'])->name('v1.files.view'); // 파일 인라인 보기 (이미지/PDF) + Route::get('/{id}/presigned-url', [FileStorageController::class, 'presignedUrl'])->name('v1.files.presigned-url'); // R2 Presigned URL 발급 Route::delete('/{id}', [FileStorageController::class, 'destroy'])->name('v1.files.destroy'); // 파일 삭제 (soft) Route::post('/{id}/restore', [FileStorageController::class, 'restore'])->name('v1.files.restore'); // 파일 복구 Route::delete('/{id}/permanent', [FileStorageController::class, 'permanentDelete'])->name('v1.files.permanent'); // 파일 영구 삭제