fix:동일 분류 내 연결품목 중복 방지 및 복사 시 연결품목 제외
- store/update 시 같은 category의 다른 템플릿에 이미 연결된 품목이 있으면 422 에러 반환 - duplicate 시 linked_item_ids, linked_process_id, linkValues 복사하지 않음 - 레거시(linked_item_ids)와 신규(template_link_values) 양쪽 경로 모두 검증 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,12 +39,22 @@ public function index(Request $request): View
|
||||
$query->onlyTrashed();
|
||||
}
|
||||
|
||||
// 검색
|
||||
// 검색 (양식명, 제목, 분류, 연결 품목)
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
// 연결 품목 검색을 위해 품목명으로 ID 조회
|
||||
$matchingItemIds = \App\Models\Items\Item::where('name', 'like', "%{$search}%")
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
$query->where(function ($q) use ($search, $matchingItemIds) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('title', 'like', "%{$search}%")
|
||||
->orWhere('category', 'like', "%{$search}%");
|
||||
|
||||
// 연결 품목 검색 (JSON 배열에서 매칭)
|
||||
foreach ($matchingItemIds as $itemId) {
|
||||
$q->orWhereJsonContains('linked_item_ids', $itemId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,7 +80,21 @@ public function index(Request $request): View
|
||||
->toArray();
|
||||
}
|
||||
|
||||
return view('document-templates.partials.table', compact('templates', 'showTrashed', 'itemNames'));
|
||||
// 분류 코드 → 이름 매핑
|
||||
$tenantId = session('selected_tenant_id');
|
||||
$categoryNames = DB::table('common_codes')
|
||||
->where('code_group', 'document_category')
|
||||
->where('is_active', true)
|
||||
->where(function ($q) use ($tenantId) {
|
||||
$q->whereNull('tenant_id');
|
||||
if ($tenantId) {
|
||||
$q->orWhere('tenant_id', $tenantId);
|
||||
}
|
||||
})
|
||||
->pluck('name', 'code')
|
||||
->toArray();
|
||||
|
||||
return view('document-templates.partials.table', compact('templates', 'showTrashed', 'itemNames', 'categoryNames'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,6 +145,14 @@ public function store(Request $request): JsonResponse
|
||||
'template_links' => 'nullable|array',
|
||||
]);
|
||||
|
||||
// 동일 분류 내 연결품목 중복 검증
|
||||
$category = $validated['category'] ?? null;
|
||||
$newItemIds = $this->extractLinkedItemIds($validated);
|
||||
$duplicateError = $this->checkLinkedItemDuplicates($category, $newItemIds);
|
||||
if ($duplicateError) {
|
||||
return $duplicateError;
|
||||
}
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
@@ -190,6 +222,14 @@ public function update(Request $request, int $id): JsonResponse
|
||||
'template_links' => 'nullable|array',
|
||||
]);
|
||||
|
||||
// 동일 분류 내 연결품목 중복 검증
|
||||
$category = $validated['category'] ?? null;
|
||||
$newItemIds = $this->extractLinkedItemIds($validated);
|
||||
$duplicateError = $this->checkLinkedItemDuplicates($category, $newItemIds, $template->id);
|
||||
if ($duplicateError) {
|
||||
return $duplicateError;
|
||||
}
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
@@ -362,8 +402,8 @@ public function duplicate(Request $request, int $id): JsonResponse
|
||||
'footer_judgement_label' => $source->footer_judgement_label,
|
||||
'footer_judgement_options' => $source->footer_judgement_options,
|
||||
'is_active' => false,
|
||||
'linked_item_ids' => $source->linked_item_ids,
|
||||
'linked_process_id' => $source->linked_process_id,
|
||||
'linked_item_ids' => null, // 연결품목은 복사하지 않음 (중복 방지)
|
||||
'linked_process_id' => null,
|
||||
]);
|
||||
|
||||
foreach ($source->approvalLines as $line) {
|
||||
@@ -440,9 +480,9 @@ public function duplicate(Request $request, int $id): JsonResponse
|
||||
]);
|
||||
}
|
||||
|
||||
// 외부 키 매핑 복제
|
||||
// 외부 키 매핑 복제 (구조만 복사, 연결 값은 제외 - 중복 방지)
|
||||
foreach ($source->links as $link) {
|
||||
$newLink = DocumentTemplateLink::create([
|
||||
DocumentTemplateLink::create([
|
||||
'template_id' => $newTemplate->id,
|
||||
'link_key' => $link->link_key,
|
||||
'label' => $link->label,
|
||||
@@ -453,16 +493,7 @@ public function duplicate(Request $request, int $id): JsonResponse
|
||||
'is_required' => $link->is_required,
|
||||
'sort_order' => $link->sort_order,
|
||||
]);
|
||||
|
||||
foreach ($link->linkValues as $value) {
|
||||
DocumentTemplateLinkValue::create([
|
||||
'template_id' => $newTemplate->id,
|
||||
'link_id' => $newLink->id,
|
||||
'linkable_id' => $value->linkable_id,
|
||||
'sort_order' => $value->sort_order,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
// linkValues는 복사하지 않음 (동일 분류 내 연결품목 중복 방지)
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
@@ -662,4 +693,95 @@ private function saveRelations(DocumentTemplate $template, array $data, bool $de
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청 데이터에서 연결 품목 ID 추출 (레거시 + 신규 방식)
|
||||
*/
|
||||
private function extractLinkedItemIds(array $data): array
|
||||
{
|
||||
$itemIds = collect($data['linked_item_ids'] ?? []);
|
||||
|
||||
if (! empty($data['template_links'])) {
|
||||
foreach ($data['template_links'] as $link) {
|
||||
if (($link['source_table'] ?? '') === 'items' && ! empty($link['values'])) {
|
||||
foreach ($link['values'] as $value) {
|
||||
$id = $value['linkable_id'] ?? $value['id'] ?? (is_numeric($value) ? $value : null);
|
||||
if ($id) {
|
||||
$itemIds->push((int) $id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $itemIds->filter()->unique()->values()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 동일 분류 내 연결품목 중복 검증 → 중복 시 JsonResponse 반환
|
||||
*/
|
||||
private function checkLinkedItemDuplicates(?string $category, array $newItemIds, ?int $excludeTemplateId = null): ?JsonResponse
|
||||
{
|
||||
if (empty($newItemIds) || empty($category)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
$query = DocumentTemplate::where('tenant_id', $tenantId)
|
||||
->where('category', $category);
|
||||
|
||||
if ($excludeTemplateId) {
|
||||
$query->where('id', '!=', $excludeTemplateId);
|
||||
}
|
||||
|
||||
$otherTemplates = $query->get(['id', 'name', 'linked_item_ids']);
|
||||
|
||||
$duplicates = [];
|
||||
|
||||
foreach ($otherTemplates as $template) {
|
||||
// 레거시 linked_item_ids
|
||||
$otherItemIds = collect($template->linked_item_ids ?? []);
|
||||
|
||||
// 신규 link_values (source_table = 'items')
|
||||
$linkValueItemIds = DocumentTemplateLinkValue::where('template_id', $template->id)
|
||||
->whereIn('linkable_id', $newItemIds)
|
||||
->whereHas('link', fn ($q) => $q->where('source_table', 'items'))
|
||||
->pluck('linkable_id');
|
||||
|
||||
$otherItemIds = $otherItemIds->merge($linkValueItemIds)->unique();
|
||||
$overlap = $otherItemIds->intersect($newItemIds);
|
||||
|
||||
if ($overlap->isNotEmpty()) {
|
||||
$duplicates[] = [
|
||||
'template_name' => $template->name,
|
||||
'item_ids' => $overlap->values()->toArray(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($duplicates)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 품목명 조회
|
||||
$allDupItemIds = collect($duplicates)->pluck('item_ids')->flatten()->unique();
|
||||
$itemNames = \App\Models\Items\Item::whereIn('id', $allDupItemIds)
|
||||
->pluck('name', 'id')
|
||||
->toArray();
|
||||
|
||||
$messages = [];
|
||||
foreach ($duplicates as $dup) {
|
||||
$names = collect($dup['item_ids'])
|
||||
->map(fn ($id) => $itemNames[$id] ?? "ID:{$id}")
|
||||
->join(', ');
|
||||
$messages[] = "'{$dup['template_name']}' 양식에 이미 연결된 품목: {$names}";
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '동일 분류 내 연결품목이 중복됩니다.',
|
||||
'errors' => ['linked_items' => $messages],
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user