feat: [bending] 절곡품 전용 테이블 분리 API
- bending_items 전용 테이블 생성 (items.options → 정규 컬럼 승격) - bending_models 전용 테이블 생성 (가이드레일/케이스/하단마감재 통합) - bending_data JSON 통합 (별도 테이블 → bending_items.bending_data 컬럼) - bending_item_mappings 테이블 DROP (bending_items.code에 흡수) - BendingItemService/BendingCodeService → BendingItem 모델 전환 - GuiderailModelService component 이미지 자동 복사 - ItemsFileController bending_items/bending_models 폴백 지원 - Swagger 스키마 업데이트
This commit is contained in:
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Items\Item;
|
||||
use App\Models\BendingModel;
|
||||
use App\Models\Commons\File;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class GuiderailModelService extends Service
|
||||
{
|
||||
@@ -11,75 +13,109 @@ class GuiderailModelService extends Service
|
||||
|
||||
public function list(array $params): LengthAwarePaginator
|
||||
{
|
||||
return Item::whereIn('item_category', self::CATEGORIES)
|
||||
->when($params['item_category'] ?? null, fn ($q, $v) => $q->where('item_category', $v))
|
||||
->when($params['item_sep'] ?? null, fn ($q, $v) => $q->where('options->item_sep', $v))
|
||||
->when($params['model_UA'] ?? null, fn ($q, $v) => $q->where('options->model_UA', $v))
|
||||
->when($params['check_type'] ?? null, fn ($q, $v) => $q->where('options->check_type', $v))
|
||||
->when($params['model_name'] ?? null, fn ($q, $v) => $q->where('options->model_name', $v))
|
||||
return BendingModel::whereIn('model_type', self::CATEGORIES)
|
||||
->when($params['item_category'] ?? null, fn ($q, $v) => $q->where('model_type', $v))
|
||||
->when($params['item_sep'] ?? null, fn ($q, $v) => $q->where('item_sep', $v))
|
||||
->when($params['model_UA'] ?? null, fn ($q, $v) => $q->where('model_UA', $v))
|
||||
->when($params['check_type'] ?? null, fn ($q, $v) => $q->where('check_type', $v))
|
||||
->when($params['model_name'] ?? null, fn ($q, $v) => $q->where('model_name', $v))
|
||||
->when($params['exit_direction'] ?? null, fn ($q, $v) => $q->where('exit_direction', $v))
|
||||
->when($params['search'] ?? null, fn ($q, $v) => $q->where(
|
||||
fn ($q2) => $q2
|
||||
->where('name', 'like', "%{$v}%")
|
||||
->orWhere('code', 'like', "%{$v}%")
|
||||
->orWhere('options->model_name', 'like', "%{$v}%")
|
||||
->orWhere('options->search_keyword', 'like', "%{$v}%")
|
||||
->orWhere('model_name', 'like', "%{$v}%")
|
||||
->orWhere('search_keyword', 'like', "%{$v}%")
|
||||
))
|
||||
->orderBy('code')
|
||||
->orderByDesc('id')
|
||||
->paginate($params['size'] ?? 50);
|
||||
}
|
||||
|
||||
public function filters(): array
|
||||
{
|
||||
$items = Item::whereIn('item_category', self::CATEGORIES)->select('options')->get();
|
||||
|
||||
return [
|
||||
'item_sep' => $items->pluck('options.item_sep')->filter()->unique()->sort()->values(),
|
||||
'model_UA' => $items->pluck('options.model_UA')->filter()->unique()->sort()->values(),
|
||||
'check_type' => $items->pluck('options.check_type')->filter()->unique()->sort()->values(),
|
||||
'model_name' => $items->pluck('options.model_name')->filter()->unique()->sort()->values(),
|
||||
'finishing_type' => $items->pluck('options.finishing_type')->filter()->unique()->sort()->values(),
|
||||
'item_sep' => BendingModel::whereIn('model_type', self::CATEGORIES)->whereNotNull('item_sep')->distinct()->pluck('item_sep')->sort()->values(),
|
||||
'model_UA' => BendingModel::whereIn('model_type', self::CATEGORIES)->whereNotNull('model_UA')->distinct()->pluck('model_UA')->sort()->values(),
|
||||
'check_type' => BendingModel::whereIn('model_type', self::CATEGORIES)->whereNotNull('check_type')->distinct()->pluck('check_type')->sort()->values(),
|
||||
'model_name' => BendingModel::whereIn('model_type', self::CATEGORIES)->whereNotNull('model_name')->distinct()->pluck('model_name')->sort()->values(),
|
||||
'finishing_type' => BendingModel::whereIn('model_type', self::CATEGORIES)->whereNotNull('finishing_type')->distinct()->pluck('finishing_type')->sort()->values(),
|
||||
];
|
||||
}
|
||||
|
||||
public function find(int $id): Item
|
||||
public function find(int $id): BendingModel
|
||||
{
|
||||
return Item::whereIn('item_category', self::CATEGORIES)->findOrFail($id);
|
||||
return BendingModel::whereIn('model_type', self::CATEGORIES)->findOrFail($id);
|
||||
}
|
||||
|
||||
public function create(array $data): Item
|
||||
public function create(array $data): BendingModel
|
||||
{
|
||||
$options = $this->buildOptions($data);
|
||||
// component 이미지 복사 (기초관리 원본 → 독립 복사본)
|
||||
if (! empty($data['components'])) {
|
||||
$data['components'] = $this->copyComponentImages($data['components']);
|
||||
}
|
||||
|
||||
return Item::create([
|
||||
return BendingModel::create([
|
||||
'tenant_id' => $this->tenantId(),
|
||||
'item_type' => 'FG',
|
||||
'item_category' => $data['item_category'] ?? 'GUIDERAIL_MODEL',
|
||||
'model_type' => $data['item_category'] ?? 'GUIDERAIL_MODEL',
|
||||
'code' => $data['code'],
|
||||
'name' => $data['name'],
|
||||
'unit' => 'SET',
|
||||
'options' => $options,
|
||||
'model_name' => $data['model_name'] ?? null,
|
||||
'model_UA' => $data['model_UA'] ?? null,
|
||||
'item_sep' => $data['item_sep'] ?? null,
|
||||
'finishing_type' => $data['finishing_type'] ?? null,
|
||||
'check_type' => $data['check_type'] ?? null,
|
||||
'rail_width' => $data['rail_width'] ?? null,
|
||||
'rail_length' => $data['rail_length'] ?? null,
|
||||
'exit_direction' => $data['exit_direction'] ?? null,
|
||||
'front_bottom_width' => $data['front_bottom_width'] ?? null,
|
||||
'box_width' => $data['box_width'] ?? null,
|
||||
'box_height' => $data['box_height'] ?? null,
|
||||
'bar_width' => $data['bar_width'] ?? null,
|
||||
'bar_height' => $data['bar_height'] ?? null,
|
||||
'components' => $data['components'] ?? null,
|
||||
'material_summary' => $data['material_summary'] ?? null,
|
||||
'search_keyword' => $data['search_keyword'] ?? null,
|
||||
'author' => $data['author'] ?? null,
|
||||
'memo' => $data['memo'] ?? null,
|
||||
'registration_date' => $data['registration_date'] ?? null,
|
||||
'options' => $this->buildOptions($data),
|
||||
'is_active' => true,
|
||||
'created_by' => $this->apiUserId(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(int $id, array $data): Item
|
||||
public function update(int $id, array $data): BendingModel
|
||||
{
|
||||
$item = Item::whereIn('item_category', self::CATEGORIES)->findOrFail($id);
|
||||
$item = BendingModel::whereIn('model_type', self::CATEGORIES)->findOrFail($id);
|
||||
|
||||
if (isset($data['code'])) {
|
||||
$item->code = $data['code'];
|
||||
}
|
||||
if (isset($data['name'])) {
|
||||
$item->name = $data['name'];
|
||||
}
|
||||
$columns = [
|
||||
'code', 'name', 'model_name', 'model_UA', 'item_sep', 'finishing_type',
|
||||
'check_type', 'rail_width', 'rail_length',
|
||||
'exit_direction', 'front_bottom_width', 'box_width', 'box_height',
|
||||
'bar_width', 'bar_height',
|
||||
'components', 'material_summary',
|
||||
'search_keyword', 'author', 'registration_date',
|
||||
];
|
||||
|
||||
foreach (self::OPTION_KEYS as $key) {
|
||||
if (array_key_exists($key, $data)) {
|
||||
$item->setOption($key, $data[$key]);
|
||||
foreach ($columns as $col) {
|
||||
if (array_key_exists($col, $data)) {
|
||||
// components 저장 시 이미지 복사
|
||||
if ($col === 'components' && ! empty($data[$col])) {
|
||||
$item->{$col} = $this->copyComponentImages($data[$col]);
|
||||
} else {
|
||||
$item->{$col} = $data[$col];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// memo → options
|
||||
if (array_key_exists('memo', $data)) {
|
||||
$item->setOption('memo', $data['memo']);
|
||||
}
|
||||
if (array_key_exists('modified_by', $data)) {
|
||||
$item->setOption('modified_by', $data['modified_by']);
|
||||
}
|
||||
|
||||
$item->updated_by = $this->apiUserId();
|
||||
$item->save();
|
||||
|
||||
@@ -88,33 +124,79 @@ public function update(int $id, array $data): Item
|
||||
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
$item = Item::whereIn('item_category', self::CATEGORIES)->findOrFail($id);
|
||||
$item = BendingModel::whereIn('model_type', self::CATEGORIES)->findOrFail($id);
|
||||
$item->deleted_by = $this->apiUserId();
|
||||
$item->save();
|
||||
|
||||
return $item->delete();
|
||||
}
|
||||
|
||||
private function buildOptions(array $data): array
|
||||
/**
|
||||
* component의 image_file_id가 bending_item 원본이면 복사본 생성
|
||||
*/
|
||||
private function copyComponentImages(array $components): array
|
||||
{
|
||||
$options = [];
|
||||
foreach (self::OPTION_KEYS as $key) {
|
||||
if (isset($data[$key])) {
|
||||
$options[$key] = $data[$key];
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
foreach ($components as &$comp) {
|
||||
$fileId = $comp['image_file_id'] ?? null;
|
||||
if (! $fileId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$source = File::find($fileId);
|
||||
if (! $source || ! $source->file_path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 이미 component_image면 복사 불필요 (이미 독립 복사본)
|
||||
if ($source->field_key === 'component_image') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// bending_item 원본이면 복사
|
||||
try {
|
||||
$extension = pathinfo($source->stored_name, PATHINFO_EXTENSION);
|
||||
$storedName = bin2hex(random_bytes(8)) . '.' . $extension;
|
||||
$directory = sprintf('%d/bending/model-parts/%s/%s', $tenantId, date('Y'), date('m'));
|
||||
$newPath = $directory . '/' . $storedName;
|
||||
|
||||
Storage::disk('r2')->put($newPath, Storage::disk('r2')->get($source->file_path));
|
||||
|
||||
$newFile = File::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'display_name' => $source->display_name,
|
||||
'stored_name' => $storedName,
|
||||
'file_path' => $newPath,
|
||||
'file_size' => $source->file_size,
|
||||
'mime_type' => $source->mime_type,
|
||||
'file_type' => 'image',
|
||||
'field_key' => 'component_image',
|
||||
'document_id' => 0,
|
||||
'document_type' => 'bending_model',
|
||||
'is_temp' => false,
|
||||
'uploaded_by' => $this->apiUserId(),
|
||||
'created_by' => $this->apiUserId(),
|
||||
]);
|
||||
|
||||
$comp['image_file_id'] = $newFile->id;
|
||||
} catch (\Throwable $e) {
|
||||
// 복사 실패 시 원본 ID 유지
|
||||
}
|
||||
}
|
||||
unset($comp);
|
||||
|
||||
return $options;
|
||||
return $components;
|
||||
}
|
||||
|
||||
private const OPTION_KEYS = [
|
||||
'model_name', 'check_type', 'rail_width', 'rail_length',
|
||||
'finishing_type', 'item_sep', 'model_UA', 'search_keyword',
|
||||
'author', 'memo', 'registration_date',
|
||||
'components', 'material_summary',
|
||||
// 케이스(SHUTTERBOX_MODEL) 전용
|
||||
'exit_direction', 'front_bottom_width', 'box_width', 'box_height',
|
||||
// 하단마감재(BOTTOMBAR_MODEL) 전용
|
||||
'bar_width', 'bar_height',
|
||||
];
|
||||
private function buildOptions(array $data): ?array
|
||||
{
|
||||
$opts = [];
|
||||
foreach (['memo', 'modified_by'] as $key) {
|
||||
if (! empty($data[$key])) {
|
||||
$opts[$key] = $data[$key];
|
||||
}
|
||||
}
|
||||
return empty($opts) ? null : $opts;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user