feat: 품목 코드 중복 시 에러 반환 및 중복 ID 제공

- DuplicateCodeException 커스텀 예외 추가
- 등록/수정 시 자동 코드 증가 기능 제거
- 중복 발견 시 duplicate_id, duplicate_code 함께 반환
- resolveUniqueCode(), resolveUniqueMaterialCode() 메서드 제거
This commit is contained in:
2025-12-11 11:07:33 +09:00
parent d5ab522902
commit 07f0db17a7
3 changed files with 81 additions and 111 deletions

View 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);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Services;
use App\Constants\SystemFields;
use App\Exceptions\DuplicateCodeException;
use App\Helpers\ItemTypeHelper;
use App\Models\ItemMaster\ItemField;
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
{
// 품목 코드 중복 시 자동 증가
$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에 병합
$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
{
// 품목 코드 중복 시 자동 증가 (Materials용)
// 품목 코드 중복 체크
$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에 병합
$this->processDynamicOptions($data, 'materials');
@@ -481,7 +497,7 @@ private function createMaterial(array $data, int $tenantId, int $userId, string
'tenant_id' => $tenantId,
'created_by' => $userId,
'material_type' => $materialType,
'material_code' => $data['material_code'],
'material_code' => $code,
'name' => $data['name'],
'unit' => $data['unit'],
'category_id' => $data['category_id'] ?? null,
@@ -498,104 +514,6 @@ private function createMaterial(array $data, int $tenantId, int $userId, string
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 통합)
*
@@ -632,14 +550,14 @@ private function updateProduct(int $id, array $data): Product
// 코드 변경 시 중복 체크
if (isset($data['code']) && $data['code'] !== $product->code) {
$exists = Product::query()
$existingProduct = Product::query()
->where('tenant_id', $tenantId)
->where('code', $data['code'])
->where('id', '!=', $product->id)
->exists();
->first();
if ($exists) {
throw new BadRequestHttpException(__('error.duplicate_code'));
if ($existingProduct) {
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;
if ($newCode && $newCode !== $material->material_code) {
$exists = Material::query()
$existingMaterial = Material::query()
->where('tenant_id', $tenantId)
->where('material_code', $newCode)
->where('id', '!=', $material->id)
->exists();
->first();
if ($exists) {
throw new BadRequestHttpException(__('error.duplicate_code'));
if ($existingMaterial) {
throw new DuplicateCodeException($newCode, $existingMaterial->id);
}
$data['material_code'] = $newCode;
}

View File

@@ -118,6 +118,7 @@
'already_deleted' => '이미 삭제된 품목입니다.',
'in_use_as_bom_component' => '다른 제품의 BOM 구성품으로 사용 중이어서 삭제할 수 없습니다. (사용처: :count건)',
'invalid_item_type' => '유효하지 않은 품목 유형입니다.',
'duplicate_code' => '중복된 품목 코드입니다.',
],
// 잠금 관련