tenantId(); // 1. SystemFields에서 products 테이블 고정 컬럼 $systemFields = SystemFields::getReservedKeys(SystemFields::SOURCE_TABLE_PRODUCTS); // 2. ItemField에서 storage_type='column'인 필드의 field_key 조회 $columnFields = ItemField::where('tenant_id', $tenantId) ->where('source_table', 'products') ->where('storage_type', 'column') ->whereNotNull('field_key') ->pluck('field_key') ->toArray(); // 3. 추가적인 API 전용 필드 (DB 컬럼이 아니지만 API에서 사용하는 필드) $apiFields = ['item_type', 'type_code', 'bom']; return array_unique(array_merge($systemFields, $columnFields, $apiFields)); } /** * 정의된 필드 외의 동적 필드를 options로 추출 */ private function extractDynamicOptions(array $params): array { $knownFields = $this->getKnownFields(); $dynamicOptions = []; foreach ($params as $key => $value) { if (! in_array($key, $knownFields) && $value !== null && $value !== '') { $dynamicOptions[$key] = $value; } } return $dynamicOptions; } /** * 기존 options 배열과 동적 필드를 병합 */ private function mergeOptionsWithDynamic($existingOptions, array $dynamicOptions): array { if (! is_array($existingOptions) || empty($existingOptions)) { return $dynamicOptions; } $isAssoc = array_keys($existingOptions) !== range(0, count($existingOptions) - 1); if ($isAssoc) { return array_merge($existingOptions, $dynamicOptions); } foreach ($dynamicOptions as $key => $value) { $existingOptions[] = ['label' => $key, 'value' => $value]; } return $existingOptions; } /** * options 입력을 [{label, value, unit}] 형태로 정규화 */ private function normalizeOptions(?array $in): ?array { if (! $in) { return null; } $isAssoc = array_keys($in) !== range(0, count($in) - 1); if ($isAssoc) { $out = []; foreach ($in as $k => $v) { $label = trim((string) $k); $value = is_scalar($v) ? trim((string) $v) : json_encode($v, JSON_UNESCAPED_UNICODE); if ($label !== '' || $value !== '') { $out[] = ['label' => $label, 'value' => $value, 'unit' => '']; } } return $out ?: null; } $out = []; foreach ($in as $a) { if (! is_array($a)) { continue; } $label = trim((string) ($a['label'] ?? '')); $value = trim((string) ($a['value'] ?? '')); $unit = trim((string) ($a['unit'] ?? '')); if ($label === '' && $value === '') { continue; } $out[] = ['label' => $label, 'value' => $value, 'unit' => $unit]; } return $out ?: null; } /** * 카테고리 트리 전체 조회 (parent_id = null 기준) */ public function getCategory($request) { $parentId = $request->parentId ?? null; $group = $request->group ?? 'category'; // 재귀적으로 트리 구성 $list = $this->fetchCategoryTree($parentId, $group); return $list; } /** * 내부 재귀 함수 (하위 카테고리 트리 구조로 구성) */ protected function fetchCategoryTree(?int $parentId = null) { $tenantId = $this->tenantId(); // Base Service에서 상속받은 메서드 $query = Category::query() ->when($tenantId, fn ($q) => $q->where('tenant_id', $tenantId)) ->when( is_null($parentId), fn ($q) => $q->whereNull('parent_id'), fn ($q) => $q->where('parent_id', $parentId) ) ->where('is_active', 1) ->orderBy('sort_order'); $categories = $query->get(); foreach ($categories as $category) { $children = $this->fetchCategoryTree($category->id); $category->setRelation('children', $children); } return $categories; } /** * (예시) 기존의 flat 리스트 조회 */ public static function getCategoryFlat($group = 'category') { $query = CommonCode::where('code_group', $group)->whereNull('parent_id'); return $query->get(); } // 목록/검색 public function index(array $params) { $tenantId = $this->tenantId(); $size = (int) ($params['size'] ?? 20); $q = trim((string) ($params['q'] ?? '')); $categoryId = $params['category_id'] ?? null; $productType = $params['product_type'] ?? null; // PRODUCT|PART|SUBASSEMBLY... $active = $params['active'] ?? null; // 1/0 $query = Product::query() ->with('category:id,name') // 필요한 컬럼만 가져오기 ->where('tenant_id', $tenantId); if ($q !== '') { $query->where(function ($w) use ($q) { $w->where('name', 'like', "%{$q}%") ->orWhere('code', 'like', "%{$q}%") ->orWhere('description', 'like', "%{$q}%"); }); } if ($categoryId) { $query->where('category_id', (int) $categoryId); } if ($productType) { $query->where('product_type', $productType); } // Note: is_active 필드는 하이브리드 구조로 전환하면서 제거됨 // 필요시 attributes JSON이나 별도 필드로 관리 // if ($active !== null && $active !== '') { // $query->where('is_active', (int) $active); // } $paginator = $query->orderBy('id')->paginate($size); // 날짜 형식을 위해 분리 $paginator->setCollection( $paginator->getCollection()->transform(function ($item) { $arr = $item->toArray(); $arr['created_at'] = $item->created_at ? $item->created_at->format('Y-m-d') : null; return $arr; }) ); return $paginator; } // 생성 public function store(array $data) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); // 동적 필드를 options에 병합 $dynamicOptions = $this->extractDynamicOptions($data); if (! empty($dynamicOptions)) { $existingOptions = $data['options'] ?? []; $data['options'] = $this->mergeOptionsWithDynamic($existingOptions, $dynamicOptions); } // options 정규화 if (isset($data['options'])) { $data['options'] = $this->normalizeOptions($data['options']); } $payload = $data; // tenant별 code 유니크 수동 체크 $dup = Product::query() ->where('tenant_id', $tenantId) ->where('code', $payload['code']) ->exists(); if ($dup) { throw new BadRequestHttpException(__('error.duplicate_key')); } // 기본값 설정 $payload['tenant_id'] = $tenantId; $payload['created_by'] = $userId; $payload['is_sellable'] = $payload['is_sellable'] ?? true; $payload['is_purchasable'] = $payload['is_purchasable'] ?? false; $payload['is_producible'] = $payload['is_producible'] ?? true; return Product::create($payload); } // 단건 public function show(int $id) { $tenantId = $this->tenantId(); $p = Product::query()->where('tenant_id', $tenantId)->find($id); if (! $p) { throw new BadRequestHttpException(__('error.not_found')); } return $p; } // 수정 public function update(int $id, array $data) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $p = Product::query()->where('tenant_id', $tenantId)->find($id); if (! $p) { throw new BadRequestHttpException(__('error.not_found')); } // 동적 필드를 options에 병합 $dynamicOptions = $this->extractDynamicOptions($data); if (! empty($dynamicOptions)) { $existingOptions = $data['options'] ?? []; $data['options'] = $this->mergeOptionsWithDynamic($existingOptions, $dynamicOptions); } // options 정규화 if (isset($data['options'])) { $data['options'] = $this->normalizeOptions($data['options']); } $payload = $data; // code 변경 시 중복 체크 if (isset($payload['code']) && $payload['code'] !== $p->code) { $dup = Product::query() ->where('tenant_id', $tenantId) ->where('code', $payload['code']) ->exists(); if ($dup) { throw new BadRequestHttpException(__('error.duplicate_key')); } } $payload['updated_by'] = $userId; $p->update($payload); return $p->refresh(); } // 삭제(soft) public function destroy(int $id): void { $tenantId = $this->tenantId(); $p = Product::query()->where('tenant_id', $tenantId)->find($id); if (! $p) { throw new BadRequestHttpException(__('error.not_found')); } $p->delete(); } // 간편 검색(모달/드롭다운) public function search(array $params) { $tenantId = $this->tenantId(); $q = trim((string) ($params['q'] ?? '')); $lim = (int) ($params['limit'] ?? 20); $qr = Product::query()->where('tenant_id', $tenantId); if ($q !== '') { $qr->where(function ($w) use ($q) { $w->where('name', 'like', "%{$q}%") ->orWhere('code', 'like', "%{$q}%"); }); } return $qr->orderBy('name')->limit($lim)->get(['id', 'code', 'name', 'product_type', 'category_id']); } // Note: toggle 메서드는 is_active 필드 제거로 인해 비활성화됨 // 필요시 attributes JSON이나 별도 필드로 구현 // public function toggle(int $id) // { // $tenantId = $this->tenantId(); // $userId = $this->apiUserId(); // // $p = Product::query()->where('tenant_id', $tenantId)->find($id); // if (! $p) { // throw new BadRequestHttpException(__('error.not_found')); // } // // $p->is_active = $p->is_active ? 0 : 1; // $p->updated_by = $userId; // $p->save(); // // return ['id' => $p->id, 'is_active' => (int) $p->is_active]; // } }