tenantId(); // 1. 기본 고정 필드 $baseFields = [ 'id', 'tenant_id', 'item_type', 'code', 'name', 'unit', 'category_id', 'bom', 'attributes', 'attributes_archive', 'options', 'description', 'is_active', 'created_by', 'updated_by', 'deleted_by', 'created_at', 'updated_at', 'deleted_at', ]; // 2. ItemField에서 storage_type='column'인 필드의 field_key 조회 $columnFields = ItemField::where('tenant_id', $tenantId) ->where('source_table', 'items') ->where('storage_type', 'column') ->whereNotNull('field_key') ->pluck('field_key') ->toArray(); // 3. API 전용 필드 $apiFields = ['type_code']; return array_unique(array_merge($baseFields, $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; } /** * 카테고리 트리 전체 조회 */ public function getCategory($request) { $parentId = $request->parentId ?? null; return $this->fetchCategoryTree($parentId); } /** * 내부 재귀 함수 (하위 카테고리 트리 구조로 구성) */ protected function fetchCategoryTree(?int $parentId = null) { $tenantId = $this->tenantId(); $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; } /** * 목록/검색 */ public function index(array $params): LengthAwarePaginator { $tenantId = $this->tenantId(); $size = (int) ($params['size'] ?? $params['per_page'] ?? 20); $q = trim((string) ($params['q'] ?? $params['search'] ?? '')); $categoryId = $params['category_id'] ?? null; $itemType = $params['item_type'] ?? null; $active = $params['active'] ?? null; $query = Item::query() ->with(['category:id,name', 'details']) ->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); } // item_type 필터 if ($itemType) { $query->where('item_type', strtoupper($itemType)); } // 활성 상태 if ($active !== null && $active !== '') { $query->where('is_active', (bool) $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): Item { $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']); } // tenant별 code 유니크 체크 $dup = Item::query() ->where('tenant_id', $tenantId) ->where('code', $data['code']) ->exists(); if ($dup) { throw new BadRequestHttpException(__('error.duplicate_key')); } // items 테이블 데이터 $itemData = [ 'tenant_id' => $tenantId, 'item_type' => strtoupper($data['item_type']), 'code' => $data['code'], 'name' => $data['name'], 'unit' => $data['unit'] ?? null, 'category_id' => $data['category_id'] ?? null, 'bom' => $data['bom'] ?? null, 'attributes' => $data['attributes'] ?? null, 'options' => $data['options'] ?? null, 'description' => $data['description'] ?? null, 'is_active' => $data['is_active'] ?? true, 'created_by' => $userId, ]; $item = Item::create($itemData); // item_details 테이블 데이터 $detailData = $this->extractDetailData($data); $detailData['item_id'] = $item->id; ItemDetail::create($detailData); return $item->load('details'); } /** * 단건 조회 */ public function show(int $id): Item { $tenantId = $this->tenantId(); $item = Item::query() ->with(['category:id,name', 'details']) ->where('tenant_id', $tenantId) ->find($id); if (! $item) { throw new BadRequestHttpException(__('error.not_found')); } return $item; } /** * 수정 */ public function update(int $id, array $data): Item { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $item = Item::query()->where('tenant_id', $tenantId)->find($id); if (! $item) { 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']); } // code 변경 시 중복 체크 if (isset($data['code']) && $data['code'] !== $item->code) { $dup = Item::query() ->where('tenant_id', $tenantId) ->where('code', $data['code']) ->exists(); if ($dup) { throw new BadRequestHttpException(__('error.duplicate_key')); } } // items 테이블 업데이트 $itemData = array_intersect_key($data, array_flip([ 'item_type', 'code', 'name', 'unit', 'category_id', 'bom', 'attributes', 'options', 'description', 'is_active', ])); $itemData['updated_by'] = $userId; if (isset($itemData['item_type'])) { $itemData['item_type'] = strtoupper($itemData['item_type']); } $item->update($itemData); // item_details 테이블 업데이트 $detailData = $this->extractDetailData($data); if (! empty($detailData)) { $item->details()->updateOrCreate( ['item_id' => $item->id], $detailData ); } return $item->load('details')->refresh(); } /** * 삭제 (soft delete) */ public function destroy(int $id): void { $tenantId = $this->tenantId(); $item = Item::query()->where('tenant_id', $tenantId)->find($id); if (! $item) { throw new BadRequestHttpException(__('error.not_found')); } $item->delete(); } /** * 간편 검색 (모달/드롭다운) */ public function search(array $params) { $tenantId = $this->tenantId(); $q = trim((string) ($params['q'] ?? '')); $limit = (int) ($params['limit'] ?? 20); $itemType = $params['item_type'] ?? null; $query = Item::query()->where('tenant_id', $tenantId); if ($q !== '') { $query->where(function ($w) use ($q) { $w->where('name', 'like', "%{$q}%") ->orWhere('code', 'like', "%{$q}%"); }); } if ($itemType) { $query->where('item_type', strtoupper($itemType)); } return $query->orderBy('name') ->limit($limit) ->get(['id', 'code', 'name', 'item_type', 'category_id']); } /** * 활성/비활성 토글 */ public function toggle(int $id): array { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $item = Item::query()->where('tenant_id', $tenantId)->find($id); if (! $item) { throw new BadRequestHttpException(__('error.not_found')); } $item->is_active = ! $item->is_active; $item->updated_by = $userId; $item->save(); return ['id' => $item->id, 'is_active' => $item->is_active]; } /** * item_details 데이터 추출 */ private function extractDetailData(array $data): array { $detailFields = [ // Products 전용 'is_sellable', 'is_purchasable', 'is_producible', 'safety_stock', 'lead_time', 'is_variable_size', 'product_category', 'part_type', 'bending_diagram', 'bending_details', 'specification_file', 'specification_file_name', 'certification_file', 'certification_file_name', 'certification_number', 'certification_start_date', 'certification_end_date', // Materials 전용 'is_inspection', 'item_name', 'specification', 'search_tag', 'remarks', ]; return array_intersect_key($data, array_flip($detailFields)); } }