diff --git a/app/Http/Controllers/FileViewController.php b/app/Http/Controllers/FileViewController.php index 1e15781a..a605ab68 100644 --- a/app/Http/Controllers/FileViewController.php +++ b/app/Http/Controllers/FileViewController.php @@ -54,4 +54,58 @@ public function show(int $id) return redirect($url); } + + /** + * 이미지 프록시 (Canvas 편집기용) + * + * R2에서 이미지를 서버 사이드로 다운로드하여 같은 도메인에서 스트리밍. + * crossOrigin 요청 없이 Canvas에서 taint 없이 사용 가능. + */ + public function proxy(int $id) + { + $cacheKey = "file_presigned_url:{$id}"; + + $url = Cache::remember($cacheKey, now()->addMinutes(5), function () use ($id) { + $baseUrl = config('services.api.base_url', 'https://api.sam.kr'); + $internalUrl = config('services.api.internal_url'); + $apiKey = config('services.api.key'); + $token = session('api_access_token', ''); + + $headers = [ + 'X-API-KEY' => $apiKey, + 'X-TENANT-ID' => session('selected_tenant_id', 1), + ]; + + if ($internalUrl) { + $headers['Host'] = parse_url($baseUrl, PHP_URL_HOST) ?: 'api.sam.kr'; + $baseUrl = $internalUrl; + } + + $response = Http::baseUrl($baseUrl) + ->withoutVerifying() + ->withHeaders($headers) + ->withToken($token) + ->timeout(10) + ->get("/api/v1/files/{$id}/presigned-url"); + + return $response->successful() ? $response->json('data.url') : null; + }); + + if (! $url) { + Cache::forget($cacheKey); + abort(404); + } + + $imageResponse = Http::withoutVerifying()->timeout(15)->get($url); + + if (! $imageResponse->successful()) { + abort(404); + } + + $contentType = $imageResponse->header('Content-Type') ?: 'image/png'; + + return response($imageResponse->body(), 200) + ->header('Content-Type', $contentType) + ->header('Cache-Control', 'public, max-age=300'); + } } diff --git a/resources/views/bending/base/form.blade.php b/resources/views/bending/base/form.blade.php index 253f74d6..8e47860d 100644 --- a/resources/views/bending/base/form.blade.php +++ b/resources/views/bending/base/form.blade.php @@ -247,7 +247,7 @@ class="w-full border border-gray-300 rounded px-3 py-1.5 text-sm {{ $isView ? 'b