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"); if (! $response->successful()) { return null; } return $response->json('data.url'); }); if (! $url) { Cache::forget($cacheKey); abort(404); } 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'); } }