feat: Items API is_active 필터/반환 및 동적 필드 지원
- 목록 조회에 is_active 필드 반환 및 필터 파라미터 추가 - 상세 조회에서 options 동적 필드 플랫 전개 - 생성/수정 시 동적 필드 options 저장 지원
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Constants\SystemFields;
|
||||
use App\Helpers\ItemTypeHelper;
|
||||
use App\Models\ItemMaster\ItemField;
|
||||
use App\Models\Materials\Material;
|
||||
use App\Models\Products\Product;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -11,6 +13,128 @@
|
||||
|
||||
class ItemsService extends Service
|
||||
{
|
||||
/**
|
||||
* 고정 필드 목록 조회 (SystemFields + ItemField 기반)
|
||||
*/
|
||||
private function getKnownFields(string $sourceTable): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
// 1. SystemFields에서 테이블 고정 컬럼
|
||||
$systemFields = SystemFields::getReservedKeys($sourceTable);
|
||||
|
||||
// 2. ItemField에서 storage_type='column'인 필드의 field_key 조회
|
||||
$columnFields = ItemField::where('tenant_id', $tenantId)
|
||||
->where('source_table', $sourceTable)
|
||||
->where('storage_type', 'column')
|
||||
->whereNotNull('field_key')
|
||||
->pluck('field_key')
|
||||
->toArray();
|
||||
|
||||
// 3. 추가적인 API 전용 필드
|
||||
$apiFields = ['item_type', 'type_code', 'bom', 'product_type'];
|
||||
|
||||
return array_unique(array_merge($systemFields, $columnFields, $apiFields));
|
||||
}
|
||||
|
||||
/**
|
||||
* 동적 필드 추출
|
||||
*/
|
||||
private function extractDynamicOptions(array $params, string $sourceTable): array
|
||||
{
|
||||
$knownFields = $this->getKnownFields($sourceTable);
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 동적 필드를 options에 병합하고 정규화
|
||||
*/
|
||||
private function processDynamicOptions(array &$data, string $sourceTable): void
|
||||
{
|
||||
$dynamicOptions = $this->extractDynamicOptions($data, $sourceTable);
|
||||
if (! empty($dynamicOptions)) {
|
||||
$existingOptions = $data['options'] ?? [];
|
||||
$data['options'] = $this->mergeOptionsWithDynamic($existingOptions, $dynamicOptions);
|
||||
}
|
||||
|
||||
if (isset($data['options'])) {
|
||||
$data['options'] = $this->normalizeOptions($data['options']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 통합 품목 조회 (materials + products UNION)
|
||||
*
|
||||
@@ -27,6 +151,7 @@ public function getItems(array $filters = [], int $perPage = 20, bool $includeDe
|
||||
$types = $filters['type'] ?? ['FG', 'PT', 'SM', 'RM', 'CS'];
|
||||
$search = trim($filters['search'] ?? $filters['q'] ?? '');
|
||||
$categoryId = $filters['category_id'] ?? null;
|
||||
$isActive = $filters['is_active'] ?? null; // null: 전체, true/1: 활성만, false/0: 비활성만
|
||||
|
||||
// 타입을 배열로 변환 (문자열인 경우 쉼표로 분리)
|
||||
if (is_string($types)) {
|
||||
@@ -43,9 +168,14 @@ public function getItems(array $filters = [], int $perPage = 20, bool $includeDe
|
||||
if (! empty($productTypes)) {
|
||||
$productsQuery = Product::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('product_type', $productTypes)
|
||||
->where('is_active', 1)
|
||||
->select([
|
||||
->whereIn('product_type', $productTypes);
|
||||
|
||||
// is_active 필터 적용 (null이면 전체 조회)
|
||||
if ($isActive !== null) {
|
||||
$productsQuery->where('is_active', filter_var($isActive, FILTER_VALIDATE_BOOLEAN) ? 1 : 0);
|
||||
}
|
||||
|
||||
$productsQuery->select([
|
||||
'id',
|
||||
'product_type as item_type',
|
||||
'code',
|
||||
@@ -55,6 +185,7 @@ public function getItems(array $filters = [], int $perPage = 20, bool $includeDe
|
||||
'category_id',
|
||||
'product_type as type_code',
|
||||
'attributes',
|
||||
'is_active',
|
||||
'created_at',
|
||||
'deleted_at',
|
||||
]);
|
||||
@@ -83,8 +214,14 @@ public function getItems(array $filters = [], int $perPage = 20, bool $includeDe
|
||||
if (! empty($materialTypes)) {
|
||||
$materialsQuery = Material::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('material_type', $materialTypes)
|
||||
->select([
|
||||
->whereIn('material_type', $materialTypes);
|
||||
|
||||
// is_active 필터 적용 (null이면 전체 조회)
|
||||
if ($isActive !== null) {
|
||||
$materialsQuery->where('is_active', filter_var($isActive, FILTER_VALIDATE_BOOLEAN) ? 1 : 0);
|
||||
}
|
||||
|
||||
$materialsQuery->select([
|
||||
'id',
|
||||
'material_type as item_type',
|
||||
'material_code as code',
|
||||
@@ -94,6 +231,7 @@ public function getItems(array $filters = [], int $perPage = 20, bool $includeDe
|
||||
'category_id',
|
||||
'material_type as type_code',
|
||||
'attributes',
|
||||
'is_active',
|
||||
'created_at',
|
||||
'deleted_at',
|
||||
]);
|
||||
@@ -230,17 +368,33 @@ public function getItem(
|
||||
}
|
||||
|
||||
/**
|
||||
* attributes JSON 필드를 최상위로 플랫 전개
|
||||
* attributes/options JSON 필드를 최상위로 플랫 전개
|
||||
*/
|
||||
private function flattenAttributes(array $data): array
|
||||
{
|
||||
// attributes 플랫 전개
|
||||
$attributes = $data['attributes'] ?? [];
|
||||
if (is_string($attributes)) {
|
||||
$attributes = json_decode($attributes, true) ?? [];
|
||||
}
|
||||
unset($data['attributes']);
|
||||
|
||||
return array_merge($data, $attributes);
|
||||
// options 플랫 전개 ([{label, value, unit}] 형태 → {label: value} 형태로 변환)
|
||||
$options = $data['options'] ?? [];
|
||||
if (is_string($options)) {
|
||||
$options = json_decode($options, true) ?? [];
|
||||
}
|
||||
$flatOptions = [];
|
||||
if (is_array($options)) {
|
||||
foreach ($options as $opt) {
|
||||
if (is_array($opt) && isset($opt['label'])) {
|
||||
$flatOptions[$opt['label']] = $opt['value'] ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
// options는 원본 유지 (프론트에서 필요할 수 있음)
|
||||
|
||||
return array_merge($data, $attributes, $flatOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,6 +451,9 @@ private function createProduct(array $data, int $tenantId, int $userId): Product
|
||||
// 품목 코드 중복 시 자동 증가
|
||||
$data['code'] = $this->resolveUniqueCode($data['code'], $tenantId);
|
||||
|
||||
// 동적 필드를 options에 병합
|
||||
$this->processDynamicOptions($data, 'products');
|
||||
|
||||
$payload = $data;
|
||||
$payload['tenant_id'] = $tenantId;
|
||||
$payload['created_by'] = $userId;
|
||||
@@ -317,6 +474,9 @@ private function createMaterial(array $data, int $tenantId, int $userId, string
|
||||
$code = $data['code'] ?? $data['material_code'] ?? '';
|
||||
$data['material_code'] = $this->resolveUniqueMaterialCode($code, $tenantId);
|
||||
|
||||
// 동적 필드를 options에 병합
|
||||
$this->processDynamicOptions($data, 'materials');
|
||||
|
||||
$payload = [
|
||||
'tenant_id' => $tenantId,
|
||||
'created_by' => $userId,
|
||||
@@ -483,6 +643,9 @@ private function updateProduct(int $id, array $data): Product
|
||||
}
|
||||
}
|
||||
|
||||
// 동적 필드를 options에 병합
|
||||
$this->processDynamicOptions($data, 'products');
|
||||
|
||||
// item_type은 DB 필드가 아니므로 제거
|
||||
unset($data['item_type']);
|
||||
$data['updated_by'] = $userId;
|
||||
@@ -522,6 +685,9 @@ private function updateMaterial(int $id, array $data): Material
|
||||
$data['material_code'] = $newCode;
|
||||
}
|
||||
|
||||
// 동적 필드를 options에 병합
|
||||
$this->processDynamicOptions($data, 'materials');
|
||||
|
||||
// item_type, code는 DB 필드가 아니므로 제거
|
||||
unset($data['item_type'], $data['code']);
|
||||
$data['updated_by'] = $userId;
|
||||
|
||||
Reference in New Issue
Block a user