diff --git a/app/Http/Controllers/BendingBaseController.php b/app/Http/Controllers/BendingBaseController.php new file mode 100644 index 00000000..c12bf0ea --- /dev/null +++ b/app/Http/Controllers/BendingBaseController.php @@ -0,0 +1,276 @@ + config('services.api.key') ?: '42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a', + 'X-TENANT-ID' => session('selected_tenant_id', 1), + ]; + + // Docker: internal_url(nginx 컨테이너) 경유, Host 헤더로 서버 블록 라우팅 + if ($internalUrl) { + $headers['Host'] = parse_url($baseUrl, PHP_URL_HOST) ?: 'api.sam.kr'; + $baseUrl = $internalUrl; + } + + return Http::baseUrl($baseUrl) + ->withoutVerifying() + ->withHeaders($headers) + ->withToken($token) + ->timeout(10); + } + + public function index(Request $request): View|\Illuminate\Http\Response + { + $params = $request->only(['item_sep', 'item_bending', 'material', 'model_UA', 'item_name', 'search', 'page', 'size']); + $params['size'] = $params['size'] ?? 30; + + $response = $this->api()->get('/api/v1/bending-items', $params); + $body = $response->successful() ? $response->json('data', []) : []; + $data = [ + 'data' => $body['data'] ?? [], + 'total' => $body['total'] ?? 0, + 'current_page' => $body['current_page'] ?? 1, + 'last_page' => $body['last_page'] ?? 1, + ]; + + $filterResponse = $this->api()->get('/api/v1/bending-items/filters'); + $filterOptions = $filterResponse->successful() ? $filterResponse->json('data', []) : []; + + if ($request->header('HX-Request')) { + if ($request->header('HX-Target') === 'items-table') { + return view('bending.base.partials.table', ['items' => $data]); + } + + return response('', 200)->header('HX-Redirect', route('bending.base.index', $request->query())); + } + + return view('bending.base.index', [ + 'items' => $data, + 'filterOptions' => $filterOptions, + ]); + } + + public function show(int $id): View + { + $response = $this->api()->get("/api/v1/bending-items/{$id}"); + $item = $response->successful() ? $response->json('data') : null; + abort_unless($item, 404); + + $imageFile = $this->getImageFile($id); + + return view('bending.base.form', ['item' => $item, 'mode' => 'view', 'imageFile' => $imageFile]); + } + + public function create(): View + { + return view('bending.base.form', ['item' => null, 'mode' => 'create', 'imageFile' => null]); + } + + public function edit(int $id): View + { + $response = $this->api()->get("/api/v1/bending-items/{$id}"); + $item = $response->successful() ? $response->json('data') : null; + abort_unless($item, 404); + + $imageFile = $this->getImageFile($id); + + return view('bending.base.form', ['item' => $item, 'mode' => 'edit', 'imageFile' => $imageFile]); + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'code' => 'required|string|max:100', + 'name' => 'required|string|max:200', + 'item_sep' => 'required|in:스크린,철재', + 'item_bending' => 'required|string|max:50', + 'item_name' => 'required|string|max:50', + 'material' => 'required|string|max:50', + 'model_UA' => 'nullable|in:인정,비인정', + ], [ + 'code.required' => '코드를 입력하세요.', + 'name.required' => '이름을 입력하세요.', + 'item_sep.required' => '대분류를 선택하세요.', + 'item_sep.in' => '대분류는 스크린 또는 철재만 선택 가능합니다.', + 'item_bending.required' => '분류를 선택하세요.', + 'item_name.required' => '품명을 입력하세요.', + 'material.required' => '재질을 입력하세요.', + 'model_UA.in' => '인정여부는 인정 또는 비인정만 선택 가능합니다.', + ]); + + $data = $this->prepareApiData($request); + $response = $this->api()->post('/api/v1/bending-items', $data); + + if (! $response->successful()) { + $body = $response->json(); + \Log::error('BendingBase store API error', ['status' => $response->status(), 'body' => $body, 'sent_data' => $data]); + $apiErrors = $body['errors'] ?? $body['error']['details'] ?? $body['data']['errors'] ?? []; + $apiMessage = $body['message'] ?? 'API 오류'; + $errorBag = ['api' => "[{$response->status()}] {$apiMessage}"]; + foreach ($apiErrors as $field => $msgs) { + $fieldLabel = match ($field) { + 'code' => '코드', 'name' => '이름', 'item_sep' => '대분류', + 'item_bending' => '분류', 'item_name' => '품명', 'material' => '재질', + default => $field, + }; + $errorBag["api_{$field}"] = "[{$fieldLabel}] ".(is_array($msgs) ? implode(', ', $msgs) : $msgs); + } + + return back()->withErrors($errorBag)->withInput(); + } + + $itemId = $response->json('data.id'); + + if ($itemId) { + $this->handleImageUpload($request, $itemId); + } + + return redirect()->route('bending.base.index')->with('success', '절곡품이 등록되었습니다.'); + } + + public function update(Request $request, int $id) + { + $validated = $request->validate([ + 'code' => 'required|string|max:100', + 'name' => 'required|string|max:200', + 'item_sep' => 'required|in:스크린,철재', + 'item_bending' => 'required|string|max:50', + 'item_name' => 'required|string|max:50', + 'material' => 'required|string|max:50', + 'model_UA' => 'nullable|in:인정,비인정', + ], [ + 'code.required' => '코드를 입력하세요.', + 'name.required' => '이름을 입력하세요.', + 'item_sep.required' => '대분류를 선택하세요.', + 'item_sep.in' => '대분류는 스크린 또는 철재만 선택 가능합니다.', + 'item_bending.required' => '분류를 선택하세요.', + 'item_name.required' => '품명을 입력하세요.', + 'material.required' => '재질을 입력하세요.', + 'model_UA.in' => '인정여부는 인정 또는 비인정만 선택 가능합니다.', + ]); + + $data = $this->prepareApiData($request); + $response = $this->api()->put("/api/v1/bending-items/{$id}", $data); + + if (! $response->successful()) { + return back()->withErrors(['api' => $response->json('message', 'API 오류')])->withInput(); + } + + $this->handleImageUpload($request, $id); + + return redirect()->route('bending.base.show', $id)->with('success', '절곡품이 수정되었습니다.'); + } + + public function destroy(int $id) + { + $this->api()->delete("/api/v1/bending-items/{$id}"); + + return redirect()->route('bending.base.index')->with('success', '절곡품이 삭제되었습니다.'); + } + + private function getImageFile(int $itemId): ?array + { + $response = $this->api()->get("/api/v1/items/{$itemId}/files", ['field_key' => 'bending_diagram']); + + if (! $response->successful()) { + return null; + } + + $files = $response->json('data.bending_diagram', []); + + return ! empty($files) ? $files[0] : null; + } + + private function uploadImage(int $itemId, \Illuminate\Http\UploadedFile $file): ?array + { + $existing = $this->getImageFile($itemId); + + $postData = ['field_key' => 'bending_diagram']; + if ($existing) { + $postData['file_id'] = $existing['id']; + } + + $response = $this->api() + ->attach('file', $file->getContent(), $file->getClientOriginalName()) + ->post("/api/v1/items/{$itemId}/files", $postData); + + if (! $response->successful()) { + \Log::error('Bending image upload failed', [ + 'itemId' => $itemId, + 'status' => $response->status(), + 'body' => $response->json(), + ]); + } + + return $response->successful() ? $response->json('data') : null; + } + + /** + * 파일 업로드 또는 Canvas Base64 이미지 처리 + */ + private function handleImageUpload(Request $request, int $itemId): void + { + if ($request->hasFile('image')) { + $this->uploadImage($itemId, $request->file('image')); + } elseif ($request->filled('canvas_image')) { + $this->uploadCanvasImage($itemId, $request->input('canvas_image')); + } + } + + /** + * Canvas Base64 DataURL → 임시 파일 → API 업로드 + */ + private function uploadCanvasImage(int $itemId, string $dataURL): ?array + { + // data:image/png;base64,... 형식 파싱 + if (! preg_match('/^data:image\/(\w+);base64,/', $dataURL, $matches)) { + return null; + } + + $ext = $matches[1] === 'jpeg' ? 'jpg' : $matches[1]; + $base64 = substr($dataURL, strpos($dataURL, ',') + 1); + $binary = base64_decode($base64); + + if ($binary === false) { + return null; + } + + // 임시 파일 생성 + $tmpPath = tempnam(sys_get_temp_dir(), 'canvas_').'.'.$ext; + file_put_contents($tmpPath, $binary); + + try { + $file = new \Illuminate\Http\UploadedFile($tmpPath, "canvas.{$ext}", "image/{$ext}", null, true); + + return $this->uploadImage($itemId, $file); + } finally { + @unlink($tmpPath); + } + } + + private function prepareApiData(Request $request): array + { + $data = $request->except(['_token', '_method', 'image', 'canvas_image']); + + if (isset($data['bendingData']) && is_string($data['bendingData'])) { + $decoded = json_decode($data['bendingData'], true); + $data['bendingData'] = is_array($decoded) ? $decoded : null; + } + + // 빈 문자열도 전송 (기존 값 삭제 가능하도록) — null만 제거 + return array_filter($data, fn ($v) => $v !== null); + } +} diff --git a/app/Http/Controllers/BendingProductController.php b/app/Http/Controllers/BendingProductController.php new file mode 100644 index 00000000..761aaf8a --- /dev/null +++ b/app/Http/Controllers/BendingProductController.php @@ -0,0 +1,395 @@ + ['title' => '절곡품 (가이드레일)', 'prefix' => 'products', 'label' => '가이드레일'], + 'SHUTTERBOX_MODEL' => ['title' => '케이스 관리', 'prefix' => 'cases', 'label' => '케이스'], + 'BOTTOMBAR_MODEL' => ['title' => '하단마감재 관리', 'prefix' => 'bottombars', 'label' => '하단마감재'], + ]; + + private function api(): \Illuminate\Http\Client\PendingRequest + { + $baseUrl = config('services.api.base_url', 'https://api.sam.kr'); + $internalUrl = config('services.api.internal_url'); + $token = session('api_access_token', ''); + + $headers = [ + 'X-API-KEY' => config('services.api.key') ?: '42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a', + 'X-TENANT-ID' => session('selected_tenant_id', 1), + ]; + + if ($internalUrl) { + $headers['Host'] = parse_url($baseUrl, PHP_URL_HOST) ?: 'api.sam.kr'; + $baseUrl = $internalUrl; + } + + return Http::baseUrl($baseUrl) + ->withoutVerifying() + ->withHeaders($headers) + ->withToken($token) + ->timeout(10); + } + + private function getConfig(string $category): array + { + return self::TYPE_CONFIG[$category] ?? self::TYPE_CONFIG['GUIDERAIL_MODEL']; + } + + public function index(Request $request, string $category = 'GUIDERAIL_MODEL'): View|\Illuminate\Http\Response + { + $config = $this->getConfig($category); + $params = $request->only(['item_sep', 'model_UA', 'check_type', 'model_name', 'exit_direction', 'search', 'page', 'size']); + $params['size'] = $params['size'] ?? 30; + $params['item_category'] = $category; + + $response = $this->api()->get('/api/v1/guiderail-models', $params); + $body = $response->successful() ? $response->json('data', []) : []; + $data = [ + 'data' => $body['data'] ?? [], + 'total' => $body['total'] ?? 0, + 'current_page' => $body['current_page'] ?? 1, + 'last_page' => $body['last_page'] ?? 1, + ]; + + $filterResponse = $this->api()->get('/api/v1/guiderail-models/filters'); + $filterOptions = $filterResponse->successful() ? $filterResponse->json('data', []) : []; + + if ($request->header('HX-Request')) { + // 필터/검색 HTMX (hx-target="#items-table") → 파셜 반환 + if ($request->header('HX-Target') === 'items-table') { + return view('bending.products.partials.table', ['items' => $data, 'config' => $config, 'category' => $category]); + } + + // 사이드바 등 그 외 HTMX → 전체 페이지 리로드 + return response('', 200)->header('HX-Redirect', route("bending.{$config['prefix']}.index", $request->query())); + } + + return view('bending.products.index', [ + 'items' => $data, + 'filterOptions' => $filterOptions, + 'config' => $config, + 'category' => $category, + ]); + } + + public function show(int $id): View + { + $response = $this->api()->get("/api/v1/guiderail-models/{$id}"); + $item = $response->successful() ? $response->json('data') : null; + abort_unless($item, 404); + + $item = $this->enrichComponentsWithSamIds($item); + $config = $this->getConfig($item['item_category'] ?? 'GUIDERAIL_MODEL'); + $imageFile = $this->getImageFile($id, $item['image_file_id'] ?? null); + + return view('bending.products.form', ['item' => $item, 'mode' => 'view', 'config' => $config, 'imageFile' => $imageFile]); + } + + public function create(string $category = 'GUIDERAIL_MODEL'): View + { + $config = $this->getConfig($category); + + return view('bending.products.form', ['item' => null, 'mode' => 'create', 'config' => $config, 'category' => $category, 'imageFile' => null]); + } + + public function edit(int $id): View + { + $response = $this->api()->get("/api/v1/guiderail-models/{$id}"); + $item = $response->successful() ? $response->json('data') : null; + abort_unless($item, 404); + + $item = $this->enrichComponentsWithSamIds($item); + $config = $this->getConfig($item['item_category'] ?? 'GUIDERAIL_MODEL'); + $imageFile = $this->getImageFile($id, $item['image_file_id'] ?? null); + + return view('bending.products.form', ['item' => $item, 'mode' => 'edit', 'config' => $config, 'imageFile' => $imageFile]); + } + + public function store(Request $request, string $category = 'GUIDERAIL_MODEL') + { + $config = $this->getConfig($category); + + if ($category === 'SHUTTERBOX_MODEL') { + $rules = [ + 'exit_direction' => 'required|string', + 'box_width' => 'required|numeric', + 'box_height' => 'required|numeric', + ]; + $messages = [ + 'exit_direction.required' => '점검구 방향을 선택하세요.', + 'box_width.required' => '가로(폭)를 입력하세요.', + 'box_height.required' => '세로(높이)를 입력하세요.', + ]; + } else { + $rules = [ + 'item_sep' => 'required|string|max:20', + 'model_name' => 'required|string|max:50', + ]; + $messages = [ + 'item_sep.required' => '대분류를 선택하세요.', + 'model_name.required' => '모델을 선택하세요.', + ]; + } + + $request->validate($rules, $messages); + + $data = $this->prepareApiData($request); + $response = $this->api()->post('/api/v1/guiderail-models', $data); + + if ($response->successful()) { + $itemId = $response->json('data.id'); + + if ($itemId) { + $this->handleImageUpload($request, $itemId); + } + + if ($itemId && $request->input('_redirect') === 'edit') { + return redirect()->route("bending.{$config['prefix']}.edit", $itemId)->with('success', '등록 후 편집 모드로 전환되었습니다.'); + } + + return redirect()->route("bending.{$config['prefix']}.index")->with('success', "{$config['label']} 모델이 등록되었습니다."); + } + + $body = $response->json(); + $apiErrors = $body['errors'] ?? $body['error']['details'] ?? $body['data']['errors'] ?? []; + $apiMessage = $body['message'] ?? 'API 오류'; + $errorBag = ['api' => "[{$response->status()}] {$apiMessage}"]; + foreach ($apiErrors as $field => $msgs) { + $errorBag["api_{$field}"] = "[{$field}] ".(is_array($msgs) ? implode(', ', $msgs) : $msgs); + } + + return back()->withErrors($errorBag)->withInput(); + } + + public function update(Request $request, int $id) + { + $category = $request->input('item_category', 'GUIDERAIL_MODEL'); + + if ($category === 'SHUTTERBOX_MODEL') { + $rules = [ + 'exit_direction' => 'required|string', + 'box_width' => 'required|numeric', + 'box_height' => 'required|numeric', + ]; + $messages = [ + 'exit_direction.required' => '점검구 방향을 선택하세요.', + 'box_width.required' => '가로(폭)를 입력하세요.', + 'box_height.required' => '세로(높이)를 입력하세요.', + ]; + } else { + $rules = [ + 'item_sep' => 'required|string|max:20', + 'model_name' => 'required|string|max:50', + ]; + $messages = [ + 'item_sep.required' => '대분류를 선택하세요.', + 'model_name.required' => '모델을 선택하세요.', + ]; + } + + $request->validate($rules, $messages); + + $data = $this->prepareApiData($request); + $response = $this->api()->put("/api/v1/guiderail-models/{$id}", $data); + + if ($response->successful()) { + $item = $response->json('data'); + $config = $this->getConfig($item['item_category'] ?? 'GUIDERAIL_MODEL'); + + $this->handleImageUpload($request, $id); + + if ($request->input('_redirect') === 'edit') { + return redirect()->route("bending.{$config['prefix']}.edit", $id)->with('success', '저장되었습니다.'); + } + + return redirect()->route("bending.{$config['prefix']}.show", $id)->with('success', "{$config['label']} 모델이 수정되었습니다."); + } + + $body = $response->json(); + $apiErrors = $body['errors'] ?? $body['error']['details'] ?? $body['data']['errors'] ?? []; + $apiMessage = $body['message'] ?? 'API 오류'; + $errorBag = ['api' => "[{$response->status()}] {$apiMessage}"]; + foreach ($apiErrors as $field => $msgs) { + $errorBag["api_{$field}"] = "[{$field}] ".(is_array($msgs) ? implode(', ', $msgs) : $msgs); + } + + return back()->withErrors($errorBag)->withInput(); + } + + public function destroy(Request $request, int $id) + { + $response = $this->api()->get("/api/v1/guiderail-models/{$id}"); + $item = $response->successful() ? $response->json('data') : null; + $config = $this->getConfig($item['item_category'] ?? 'GUIDERAIL_MODEL'); + + $this->api()->delete("/api/v1/guiderail-models/{$id}"); + + return redirect()->route("bending.{$config['prefix']}.index")->with('success', "{$config['label']} 모델이 삭제되었습니다."); + } + + public function print(Request $request, int $id) + { + $response = $this->api()->get("/api/v1/guiderail-models/{$id}"); + $item = $response->successful() ? $response->json('data') : null; + abort_unless($item, 404); + + return view('bending.products.print', ['item' => $item]); + } + + public function searchBendingItems(Request $request) + { + $params = $request->only(['item_sep', 'item_bending', 'material', 'search', 'legacy_bending_num', 'size']); + $params['size'] = $params['size'] ?? 100; + + $response = $this->api()->get('/api/v1/bending-items', $params); + $body = $response->successful() ? $response->json('data', []) : []; + + return response()->json([ + 'data' => $body['data'] ?? [], + 'total' => $body['total'] ?? 0, + ]); + } + + private function enrichComponentsWithSamIds(array $item): array + { + if (empty($item['components'])) { + return $item; + } + + $legacyNums = array_filter(array_column($item['components'], 'legacy_bending_num')); + if (empty($legacyNums)) { + return $item; + } + + $response = $this->api()->get('/api/v1/bending-items', ['size' => 200]); + $allItems = $response->successful() ? ($response->json('data.data') ?? []) : []; + + $numToId = []; + foreach ($allItems as $samItem) { + $lbn = $samItem['legacy_bending_num'] ?? null; + if ($lbn !== null) { + $numToId[(string) $lbn] = $samItem['id']; + } + } + + foreach ($item['components'] as &$comp) { + $lbn = $comp['legacy_bending_num'] ?? null; + $comp['sam_item_id'] = $lbn !== null ? ($numToId[(string) $lbn] ?? null) : null; + } + unset($comp); + + return $item; + } + + private function getImageFile(int $itemId, ?int $imageFileId = null): ?array + { + // API Resource에서 image_file_id를 이미 반환 → 그대로 사용 + if ($imageFileId) { + return ['id' => $imageFileId]; + } + + // fallback: fileable 기반 조회 + $response = $this->api()->get("/api/v1/items/{$itemId}/files", ['field_key' => 'bending_diagram']); + if ($response->successful()) { + $files = $response->json('data.bending_diagram', []); + if (! empty($files)) { + return $files[0]; + } + } + + return null; + } + + private function uploadImage(int $itemId, \Illuminate\Http\UploadedFile $file): ?array + { + $existing = $this->getImageFile($itemId); + + $postData = ['field_key' => 'bending_diagram']; + if ($existing) { + $postData['file_id'] = $existing['id']; + } + + $response = $this->api() + ->attach('file', $file->getContent(), $file->getClientOriginalName()) + ->post("/api/v1/items/{$itemId}/files", $postData); + + if (! $response->successful()) { + \Log::error('Model image upload failed', [ + 'itemId' => $itemId, + 'status' => $response->status(), + 'body' => $response->json(), + ]); + } + + return $response->successful() ? $response->json('data') : null; + } + + private function handleImageUpload(Request $request, int $itemId): void + { + if ($request->hasFile('image')) { + $this->uploadImage($itemId, $request->file('image')); + } elseif ($request->filled('canvas_image')) { + $this->uploadCanvasImage($itemId, $request->input('canvas_image')); + } + } + + private function uploadCanvasImage(int $itemId, string $dataURL): ?array + { + if (! preg_match('/^data:image\/(\w+);base64,/', $dataURL, $matches)) { + return null; + } + + $ext = $matches[1] === 'jpeg' ? 'jpg' : $matches[1]; + $binary = base64_decode(substr($dataURL, strpos($dataURL, ',') + 1)); + + if ($binary === false) { + return null; + } + + $tmpPath = tempnam(sys_get_temp_dir(), 'canvas_').'.'.$ext; + file_put_contents($tmpPath, $binary); + + try { + $file = new \Illuminate\Http\UploadedFile($tmpPath, "canvas.{$ext}", "image/{$ext}", null, true); + + return $this->uploadImage($itemId, $file); + } finally { + @unlink($tmpPath); + } + } + + private function prepareApiData(Request $request): array + { + $data = $request->except(['_token', '_method', '_redirect', 'image', 'canvas_image']); + + if (isset($data['components']) && is_string($data['components'])) { + $decoded = json_decode($data['components'], true); + $data['components'] = is_array($decoded) ? $decoded : null; + } + if (isset($data['material_summary']) && is_string($data['material_summary'])) { + $decoded = json_decode($data['material_summary'], true); + $data['material_summary'] = is_array($decoded) ? $decoded : null; + } + + // 빈 문자열도 전송 (기존 값 삭제 가능하도록) — null만 제거 + $data = array_filter($data, fn ($v) => $v !== null); + + if (empty($data['code'])) { + $modelName = $data['model_name'] ?? ''; + $itemSep = $data['item_sep'] ?? ''; + $data['code'] = trim("{$itemSep}_{$modelName}_".date('ymd_His')); + } + if (empty($data['name'])) { + $data['name'] = $data['model_name'] ?? $data['code']; + } + + return $data; + } +} diff --git a/app/Http/Controllers/DocumentTemplateController.php b/app/Http/Controllers/DocumentTemplateController.php index 44688194..d99d25d8 100644 --- a/app/Http/Controllers/DocumentTemplateController.php +++ b/app/Http/Controllers/DocumentTemplateController.php @@ -130,6 +130,63 @@ public function blockEdit(int $id): View /** * 현재 선택된 테넌트 조회 */ + /** + * API에서 presigned URL 발급 + */ + private function getPresignedUrlFromApi(int $fileId): ?string + { + $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 = \Illuminate\Support\Facades\Http::baseUrl($baseUrl) + ->withoutVerifying() + ->withHeaders($headers) + ->withToken($token) + ->timeout(10) + ->get("/api/v1/files/{$fileId}/presigned-url"); + + return $response->successful() ? $response->json('data.url') : null; + } + + private function getPresignedUrlByPath(string $path): ?string + { + $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 = \Illuminate\Support\Facades\Http::baseUrl($baseUrl) + ->withoutVerifying() + ->withHeaders($headers) + ->withToken($token) + ->timeout(10) + ->post('/api/v1/files/presigned-url-by-path', ['path' => $path]); + + return $response->successful() ? $response->json('data.url') : null; + } + private function getCurrentTenant(): ?Tenant { $tenantId = session('selected_tenant_id'); diff --git a/app/Http/Controllers/FileViewController.php b/app/Http/Controllers/FileViewController.php new file mode 100644 index 00000000..1e15781a --- /dev/null +++ b/app/Http/Controllers/FileViewController.php @@ -0,0 +1,57 @@ +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); + } +}