feat: 품목 코드 중복 시 에러 반환 및 중복 ID 제공
- DuplicateCodeException 커스텀 예외 추가 - 등록/수정 시 자동 코드 증가 기능 제거 - 중복 발견 시 duplicate_id, duplicate_code 함께 반환 - resolveUniqueCode(), resolveUniqueMaterialCode() 메서드 제거
This commit is contained in:
51
app/Exceptions/DuplicateCodeException.php
Normal file
51
app/Exceptions/DuplicateCodeException.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 품목 코드 중복 예외
|
||||||
|
*
|
||||||
|
* 중복된 품목 ID를 함께 반환하기 위한 커스텀 예외
|
||||||
|
*/
|
||||||
|
class DuplicateCodeException extends Exception
|
||||||
|
{
|
||||||
|
protected int $duplicateId;
|
||||||
|
|
||||||
|
protected string $duplicateCode;
|
||||||
|
|
||||||
|
public function __construct(string $code, int $duplicateId, ?string $message = null)
|
||||||
|
{
|
||||||
|
$this->duplicateCode = $code;
|
||||||
|
$this->duplicateId = $duplicateId;
|
||||||
|
|
||||||
|
parent::__construct($message ?? __('error.item.duplicate_code'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDuplicateId(): int
|
||||||
|
{
|
||||||
|
return $this->duplicateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDuplicateCode(): string
|
||||||
|
{
|
||||||
|
return $this->duplicateCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP 응답으로 변환
|
||||||
|
*/
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $this->getMessage(),
|
||||||
|
'error' => [
|
||||||
|
'code' => 400,
|
||||||
|
],
|
||||||
|
'duplicate_id' => $this->duplicateId,
|
||||||
|
'duplicate_code' => $this->duplicateCode,
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Constants\SystemFields;
|
use App\Constants\SystemFields;
|
||||||
|
use App\Exceptions\DuplicateCodeException;
|
||||||
use App\Helpers\ItemTypeHelper;
|
use App\Helpers\ItemTypeHelper;
|
||||||
use App\Models\ItemMaster\ItemField;
|
use App\Models\ItemMaster\ItemField;
|
||||||
use App\Models\Materials\Material;
|
use App\Models\Materials\Material;
|
||||||
@@ -448,8 +449,16 @@ public function createItem(array $data): Product|Material
|
|||||||
*/
|
*/
|
||||||
private function createProduct(array $data, int $tenantId, int $userId): Product
|
private function createProduct(array $data, int $tenantId, int $userId): Product
|
||||||
{
|
{
|
||||||
// 품목 코드 중복 시 자동 증가
|
// 품목 코드 중복 체크
|
||||||
$data['code'] = $this->resolveUniqueCode($data['code'], $tenantId);
|
$code = $data['code'] ?? '';
|
||||||
|
$existingProduct = Product::withTrashed()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->where('code', $code)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existingProduct) {
|
||||||
|
throw new DuplicateCodeException($code, $existingProduct->id);
|
||||||
|
}
|
||||||
|
|
||||||
// 동적 필드를 options에 병합
|
// 동적 필드를 options에 병합
|
||||||
$this->processDynamicOptions($data, 'products');
|
$this->processDynamicOptions($data, 'products');
|
||||||
@@ -470,9 +479,16 @@ private function createProduct(array $data, int $tenantId, int $userId): Product
|
|||||||
*/
|
*/
|
||||||
private function createMaterial(array $data, int $tenantId, int $userId, string $materialType): Material
|
private function createMaterial(array $data, int $tenantId, int $userId, string $materialType): Material
|
||||||
{
|
{
|
||||||
// 품목 코드 중복 시 자동 증가 (Materials용)
|
// 품목 코드 중복 체크
|
||||||
$code = $data['code'] ?? $data['material_code'] ?? '';
|
$code = $data['code'] ?? $data['material_code'] ?? '';
|
||||||
$data['material_code'] = $this->resolveUniqueMaterialCode($code, $tenantId);
|
$existingMaterial = Material::withTrashed()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->where('material_code', $code)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existingMaterial) {
|
||||||
|
throw new DuplicateCodeException($code, $existingMaterial->id);
|
||||||
|
}
|
||||||
|
|
||||||
// 동적 필드를 options에 병합
|
// 동적 필드를 options에 병합
|
||||||
$this->processDynamicOptions($data, 'materials');
|
$this->processDynamicOptions($data, 'materials');
|
||||||
@@ -481,7 +497,7 @@ private function createMaterial(array $data, int $tenantId, int $userId, string
|
|||||||
'tenant_id' => $tenantId,
|
'tenant_id' => $tenantId,
|
||||||
'created_by' => $userId,
|
'created_by' => $userId,
|
||||||
'material_type' => $materialType,
|
'material_type' => $materialType,
|
||||||
'material_code' => $data['material_code'],
|
'material_code' => $code,
|
||||||
'name' => $data['name'],
|
'name' => $data['name'],
|
||||||
'unit' => $data['unit'],
|
'unit' => $data['unit'],
|
||||||
'category_id' => $data['category_id'] ?? null,
|
'category_id' => $data['category_id'] ?? null,
|
||||||
@@ -498,104 +514,6 @@ private function createMaterial(array $data, int $tenantId, int $userId, string
|
|||||||
return Material::create($payload);
|
return Material::create($payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Material 코드 중복 체크 및 고유 코드 생성
|
|
||||||
*/
|
|
||||||
private function resolveUniqueMaterialCode(string $code, int $tenantId): string
|
|
||||||
{
|
|
||||||
$exists = Material::withTrashed()
|
|
||||||
->where('tenant_id', $tenantId)
|
|
||||||
->where('material_code', $code)
|
|
||||||
->exists();
|
|
||||||
|
|
||||||
if (! $exists) {
|
|
||||||
return $code;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 마지막이 숫자인지 확인
|
|
||||||
if (preg_match('/^(.+?)(\d+)$/', $code, $matches)) {
|
|
||||||
$prefix = $matches[1];
|
|
||||||
$number = (int) $matches[2];
|
|
||||||
$digits = strlen($matches[2]);
|
|
||||||
|
|
||||||
do {
|
|
||||||
$number++;
|
|
||||||
$newCode = $prefix.str_pad($number, $digits, '0', STR_PAD_LEFT);
|
|
||||||
$exists = Material::withTrashed()
|
|
||||||
->where('tenant_id', $tenantId)
|
|
||||||
->where('material_code', $newCode)
|
|
||||||
->exists();
|
|
||||||
} while ($exists);
|
|
||||||
|
|
||||||
return $newCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 마지막이 문자면 -001 추가
|
|
||||||
$suffix = 1;
|
|
||||||
do {
|
|
||||||
$newCode = $code.'-'.str_pad($suffix, 3, '0', STR_PAD_LEFT);
|
|
||||||
$exists = Material::withTrashed()
|
|
||||||
->where('tenant_id', $tenantId)
|
|
||||||
->where('material_code', $newCode)
|
|
||||||
->exists();
|
|
||||||
$suffix++;
|
|
||||||
} while ($exists);
|
|
||||||
|
|
||||||
return $newCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 중복되지 않는 고유 코드 생성
|
|
||||||
*
|
|
||||||
* - 중복 없으면 원본 반환
|
|
||||||
* - 마지막이 숫자면 숫자 증가 (P-001 → P-002)
|
|
||||||
* - 마지막이 문자면 -001 추가 (ABC → ABC-001)
|
|
||||||
*/
|
|
||||||
private function resolveUniqueCode(string $code, int $tenantId): string
|
|
||||||
{
|
|
||||||
// 삭제된 항목 포함해서 중복 체크
|
|
||||||
$exists = Product::withTrashed()
|
|
||||||
->where('tenant_id', $tenantId)
|
|
||||||
->where('code', $code)
|
|
||||||
->exists();
|
|
||||||
|
|
||||||
if (! $exists) {
|
|
||||||
return $code;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 마지막이 숫자인지 확인 (예: P-001, ITEM123)
|
|
||||||
if (preg_match('/^(.+?)(\d+)$/', $code, $matches)) {
|
|
||||||
$prefix = $matches[1];
|
|
||||||
$number = (int) $matches[2];
|
|
||||||
$digits = strlen($matches[2]);
|
|
||||||
|
|
||||||
// 숫자 증가하며 고유 코드 찾기
|
|
||||||
do {
|
|
||||||
$number++;
|
|
||||||
$newCode = $prefix.str_pad($number, $digits, '0', STR_PAD_LEFT);
|
|
||||||
$exists = Product::withTrashed()
|
|
||||||
->where('tenant_id', $tenantId)
|
|
||||||
->where('code', $newCode)
|
|
||||||
->exists();
|
|
||||||
} while ($exists);
|
|
||||||
|
|
||||||
return $newCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 마지막이 문자면 -001 추가
|
|
||||||
$suffix = 1;
|
|
||||||
do {
|
|
||||||
$newCode = $code.'-'.str_pad($suffix, 3, '0', STR_PAD_LEFT);
|
|
||||||
$exists = Product::withTrashed()
|
|
||||||
->where('tenant_id', $tenantId)
|
|
||||||
->where('code', $newCode)
|
|
||||||
->exists();
|
|
||||||
$suffix++;
|
|
||||||
} while ($exists);
|
|
||||||
|
|
||||||
return $newCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 품목 수정 (Product/Material 통합)
|
* 품목 수정 (Product/Material 통합)
|
||||||
*
|
*
|
||||||
@@ -632,14 +550,14 @@ private function updateProduct(int $id, array $data): Product
|
|||||||
|
|
||||||
// 코드 변경 시 중복 체크
|
// 코드 변경 시 중복 체크
|
||||||
if (isset($data['code']) && $data['code'] !== $product->code) {
|
if (isset($data['code']) && $data['code'] !== $product->code) {
|
||||||
$exists = Product::query()
|
$existingProduct = Product::query()
|
||||||
->where('tenant_id', $tenantId)
|
->where('tenant_id', $tenantId)
|
||||||
->where('code', $data['code'])
|
->where('code', $data['code'])
|
||||||
->where('id', '!=', $product->id)
|
->where('id', '!=', $product->id)
|
||||||
->exists();
|
->first();
|
||||||
|
|
||||||
if ($exists) {
|
if ($existingProduct) {
|
||||||
throw new BadRequestHttpException(__('error.duplicate_code'));
|
throw new DuplicateCodeException($data['code'], $existingProduct->id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,14 +591,14 @@ private function updateMaterial(int $id, array $data): Material
|
|||||||
// 코드 변경 시 중복 체크
|
// 코드 변경 시 중복 체크
|
||||||
$newCode = $data['material_code'] ?? $data['code'] ?? null;
|
$newCode = $data['material_code'] ?? $data['code'] ?? null;
|
||||||
if ($newCode && $newCode !== $material->material_code) {
|
if ($newCode && $newCode !== $material->material_code) {
|
||||||
$exists = Material::query()
|
$existingMaterial = Material::query()
|
||||||
->where('tenant_id', $tenantId)
|
->where('tenant_id', $tenantId)
|
||||||
->where('material_code', $newCode)
|
->where('material_code', $newCode)
|
||||||
->where('id', '!=', $material->id)
|
->where('id', '!=', $material->id)
|
||||||
->exists();
|
->first();
|
||||||
|
|
||||||
if ($exists) {
|
if ($existingMaterial) {
|
||||||
throw new BadRequestHttpException(__('error.duplicate_code'));
|
throw new DuplicateCodeException($newCode, $existingMaterial->id);
|
||||||
}
|
}
|
||||||
$data['material_code'] = $newCode;
|
$data['material_code'] = $newCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,7 @@
|
|||||||
'already_deleted' => '이미 삭제된 품목입니다.',
|
'already_deleted' => '이미 삭제된 품목입니다.',
|
||||||
'in_use_as_bom_component' => '다른 제품의 BOM 구성품으로 사용 중이어서 삭제할 수 없습니다. (사용처: :count건)',
|
'in_use_as_bom_component' => '다른 제품의 BOM 구성품으로 사용 중이어서 삭제할 수 없습니다. (사용처: :count건)',
|
||||||
'invalid_item_type' => '유효하지 않은 품목 유형입니다.',
|
'invalid_item_type' => '유효하지 않은 품목 유형입니다.',
|
||||||
|
'duplicate_code' => '중복된 품목 코드입니다.',
|
||||||
],
|
],
|
||||||
|
|
||||||
// 잠금 관련
|
// 잠금 관련
|
||||||
|
|||||||
Reference in New Issue
Block a user