['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'); $token = session('api_access_token', ''); return Http::baseUrl($baseUrl) ->withoutVerifying() ->withHeaders([ 'X-API-KEY' => config('services.api.key') ?: '42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a', 'X-TENANT-ID' => session('selected_tenant_id', 1), ]) ->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; } }