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:
2026-02-06 15:16:10 +09:00
parent 5c63f4c093
commit aedec2a2ae

View File

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