Files
sam-api/app/Services/BendingItemService.php

305 lines
12 KiB
PHP

<?php
namespace App\Services;
use App\Models\BendingItem;
use Illuminate\Pagination\LengthAwarePaginator;
class BendingItemService extends Service
{
public function list(array $params): LengthAwarePaginator
{
return BendingItem::query()
->when($params['item_sep'] ?? null, fn ($q, $v) => $q->where('item_sep', $v))
->when($params['item_bending'] ?? null, fn ($q, $v) => $q->where('item_bending', $v))
->when($params['material'] ?? null, fn ($q, $v) => $q->where('material', 'like', "%{$v}%"))
->when($params['model_UA'] ?? null, fn ($q, $v) => $q->where('model_UA', $v))
->when($params['model_name'] ?? null, fn ($q, $v) => $q->where('model_name', $v))
->when($params['legacy_bending_num'] ?? $params['legacy_bending_id'] ?? null, fn ($q, $v) => $q->where('legacy_bending_id', (int) $v))
->when($params['search'] ?? null, fn ($q, $v) => $q->where(
fn ($q2) => $q2
->where('item_name', 'like', "%{$v}%")
->orWhere('code', 'like', "%{$v}%")
->orWhere('item_spec', 'like', "%{$v}%")
->orWhere('legacy_code', 'like', "%{$v}%")
))
->orderByDesc('id')
->paginate($params['size'] ?? 50);
}
public function filters(): array
{
return [
'item_sep' => BendingItem::whereNotNull('item_sep')->distinct()->pluck('item_sep')->sort()->values(),
'item_bending' => BendingItem::whereNotNull('item_bending')->distinct()->pluck('item_bending')->sort()->values(),
'material' => BendingItem::whereNotNull('material')->distinct()->pluck('material')->sort()->values(),
'model_UA' => BendingItem::whereNotNull('model_UA')->distinct()->pluck('model_UA')->sort()->values(),
'model_name' => BendingItem::whereNotNull('model_name')->distinct()->pluck('model_name')->sort()->values(),
];
}
public function find(int $id): BendingItem
{
return BendingItem::findOrFail($id);
}
public function create(array $data): BendingItem
{
$code = $data['code'] ?? '';
// BD-XX 접두사 → 자동 채번 (BD-XX 또는 BD-XX.nn)
if (preg_match('/^BD-([A-Z]{2})$/i', $code, $m)) {
$code = $this->generateCode($m[1]);
}
return BendingItem::create([
'tenant_id' => $this->tenantId(),
'code' => $code,
'legacy_code' => $data['legacy_code'] ?? null,
'legacy_bending_id' => $data['legacy_bending_id'] ?? null,
'item_name' => $data['item_name'] ?? $data['name'] ?? '',
'item_sep' => $data['item_sep'] ?? null,
'item_bending' => $data['item_bending'] ?? null,
'material' => $data['material'] ?? null,
'item_spec' => $data['item_spec'] ?? null,
'model_name' => $data['model_name'] ?? null,
'model_UA' => $data['model_UA'] ?? null,
'rail_width' => $data['rail_width'] ?? null,
'exit_direction' => $data['exit_direction'] ?? null,
'box_width' => $data['box_width'] ?? null,
'box_height' => $data['box_height'] ?? null,
'front_bottom' => $data['front_bottom'] ?? $data['front_bottom_width'] ?? null,
'inspection_door' => $data['inspection_door'] ?? null,
'length_code' => $data['length_code'] ?? null,
'length_mm' => $data['length_mm'] ?? null,
'bending_data' => $data['bendingData'] ?? null,
'options' => $this->buildOptions($data),
'is_active' => true,
'created_by' => $this->apiUserId(),
]);
}
public function update(int $id, array $data): BendingItem
{
$item = BendingItem::findOrFail($id);
// code 변경 시 중복 검사
if (array_key_exists('code', $data) && $data['code'] && $data['code'] !== $item->code) {
$exists = BendingItem::withoutGlobalScopes()
->where('code', $data['code'])
->where('id', '!=', $id)
->exists();
if ($exists) {
throw new \Illuminate\Validation\ValidationException(
validator([], []),
response()->json([
'success' => false,
'message' => "코드 '{$data['code']}'가 이미 존재합니다.",
'errors' => ['code' => ["코드 '{$data['code']}'는 이미 사용 중입니다. 다른 코드를 입력하세요."]],
], 422)
);
}
$item->code = $data['code'];
}
$columns = [
'item_name', 'item_sep', 'item_bending',
'material', 'item_spec', 'model_name', 'model_UA',
'rail_width', 'exit_direction', 'box_width', 'box_height',
'front_bottom', 'inspection_door', 'length_code', 'length_mm',
];
foreach ($columns as $col) {
if (array_key_exists($col, $data)) {
$item->{$col} = $data[$col];
}
}
if (array_key_exists('front_bottom_width', $data) && ! array_key_exists('front_bottom', $data)) {
$item->front_bottom = $data['front_bottom_width'];
}
// 전개도 (JSON 직접 저장)
if (array_key_exists('bendingData', $data)) {
$item->bending_data = $data['bendingData'];
}
// 비정형 속성
foreach (self::OPTION_KEYS as $key) {
if (array_key_exists($key, $data)) {
$item->setOption($key, $data[$key]);
}
}
$item->updated_by = $this->apiUserId();
$item->save();
return $item;
}
/**
* 기초자료 복사 — 같은 분류코드의 다음 번호 자동 채번 + 이미지 복사
*/
public function duplicate(int $id): BendingItem
{
$source = BendingItem::findOrFail($id);
// 분류코드 추출 (BD-CL.001 → CL)
preg_match('/^BD-([A-Z]{2})/', $source->code, $m);
$prefix = $m[1] ?? 'XX';
$newCode = $this->generateCode($prefix);
$newItem = BendingItem::create([
'tenant_id' => $source->tenant_id,
'code' => $newCode,
'item_name' => $source->item_name,
'item_sep' => $source->item_sep,
'item_bending' => $source->item_bending,
'material' => $source->material,
'item_spec' => $source->item_spec,
'model_name' => $source->model_name,
'model_UA' => $source->model_UA,
'rail_width' => $source->rail_width,
'exit_direction' => $source->exit_direction,
'box_width' => $source->box_width,
'box_height' => $source->box_height,
'front_bottom' => $source->front_bottom,
'inspection_door' => $source->inspection_door,
'length_code' => $source->length_code,
'length_mm' => $source->length_mm,
'bending_data' => $source->bending_data,
'options' => $source->options,
'is_active' => true,
'created_by' => $this->apiUserId(),
]);
// 이미지 파일 복사 (R2)
$this->duplicateFiles($source, $newItem);
return $newItem;
}
/**
* 원본 아이템의 파일을 R2에서 복사하여 새 아이템에 연결
*/
private function duplicateFiles(BendingItem $source, BendingItem $target): void
{
$files = \App\Models\Commons\File::where('document_id', $source->id)
->where('document_type', 'bending_item')
->whereNull('deleted_at')
->get();
foreach ($files as $file) {
try {
$disk = \Illuminate\Support\Facades\Storage::disk('r2');
$newStoredName = bin2hex(random_bytes(8)).'.'.pathinfo($file->stored_name, PATHINFO_EXTENSION);
$dir = dirname($file->file_path);
$newPath = $dir.'/'.$newStoredName;
// R2 내 파일 복사
if ($file->file_path && $disk->exists($file->file_path)) {
$disk->copy($file->file_path, $newPath);
}
// 새 File 레코드 생성
\App\Models\Commons\File::create([
'tenant_id' => $target->tenant_id,
'document_id' => $target->id,
'document_type' => 'bending_item',
'field_key' => $file->field_key,
'file_path' => $newPath,
'stored_name' => $newStoredName,
'original_name' => $file->original_name,
'display_name' => $file->display_name,
'file_size' => $file->file_size,
'mime_type' => $file->mime_type,
'file_type' => $file->file_type,
'created_by' => $this->apiUserId(),
]);
} catch (\Throwable $e) {
\Illuminate\Support\Facades\Log::warning('BendingItem file duplicate failed', [
'source_id' => $source->id,
'file_id' => $file->id,
'error' => $e->getMessage(),
]);
}
}
}
public function delete(int $id): bool
{
$item = BendingItem::findOrFail($id);
$item->deleted_by = $this->apiUserId();
$item->save();
return $item->delete();
}
private function buildOptions(array $data): ?array
{
$options = [];
foreach (self::OPTION_KEYS as $key) {
if (isset($data[$key])) {
$options[$key] = $data[$key];
}
}
return empty($options) ? null : $options;
}
private const OPTION_KEYS = [
'search_keyword', 'registration_date', 'author', 'memo',
'parent_num', 'modified_by',
];
/**
* 기초자료 코드 자동 채번
*
* BD-XX.001 : 대표(표준) 형상 — 재공품 코드(BD-XX-길이)의 기준
* BD-XX.002~: 표준 대비 변형 (주문 수정 형상, 최대 999종)
*
* 항상 .001부터 시작, .001 = 대표 번호
*/
private function generateCode(string $prefix): string
{
$prefix = strtoupper($prefix);
$lastCode = BendingItem::withoutGlobalScopes()
->where('code', 'like', "BD-{$prefix}.%")
->orderByRaw('CAST(SUBSTRING(code, ?) AS UNSIGNED) DESC', [strlen("BD-{$prefix}.") + 1])
->value('code');
$nextSeq = 1;
if ($lastCode && preg_match('/\.(\d+)$/', $lastCode, $m)) {
$nextSeq = (int) $m[1] + 1;
}
return "BD-{$prefix}.".str_pad($nextSeq, 3, '0', STR_PAD_LEFT);
}
/**
* 사용 가능한 분류코드 접두사 목록
*/
public function prefixes(): array
{
return BendingItem::withoutGlobalScopes()
->where('code', 'like', 'BD-%')
->selectRaw("CASE WHEN code LIKE 'BD-__.%' THEN SUBSTRING(code, 4, 2) ELSE SUBSTRING(code, 4, 2) END as prefix, COUNT(*) as cnt")
->groupBy('prefix')
->orderBy('prefix')
->pluck('cnt', 'prefix')
->toArray();
}
/** 분류코드 접두사 정의 */
public const PREFIX_LABELS = [
'RS' => '가이드레일 SUS마감재', 'RM' => '가이드레일 본체/보강', 'RC' => '가이드레일 C형',
'RD' => '가이드레일 D형', 'RE' => '가이드레일 측면마감', 'RT' => '가이드레일 절단판',
'RH' => '가이드레일 뒷보강', 'RN' => '가이드레일 비인정',
'CP' => '케이스 밑면판/점검구', 'CF' => '케이스 전면판', 'CB' => '케이스 후면코너/후면부',
'CL' => '케이스 린텔', 'CX' => '케이스 상부덮개',
'BS' => '하단마감재 SUS', 'BE' => '하단마감재 EGI', 'BH' => '하단마감재 보강평철',
'TS' => '철재 하단마감재 SUS', 'TE' => '철재 하단마감재 EGI',
'XE' => '마구리', 'LE' => 'L-BAR',
'ZP' => '특수 밑면/점검구', 'ZF' => '특수 전면판', 'ZB' => '특수 후면',
];
}