withCount(['sections', 'columns']); // 선택된 테넌트 필터 $tenantId = session('selected_tenant_id'); if ($tenantId) { $query->where('tenant_id', $tenantId); } // 슈퍼관리자 휴지통 조회 $showTrashed = $request->filled('trashed') && auth()->user()?->is_super_admin; if ($showTrashed) { $query->onlyTrashed(); } // 검색 (양식명, 제목, 분류, 연결 품목) if ($search = $request->input('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); } }); } // 카테고리 필터 if ($category = $request->input('category')) { $query->where('category', $category); } // 활성 상태 필터 if ($request->filled('is_active') && ! $showTrashed) { $query->where('is_active', $request->boolean('is_active')); } $templates = $query->orderBy('updated_at', 'desc') ->paginate($request->input('per_page', 10)); // 연결된 품목 ID 수집 및 품목명 조회 $allItemIds = $templates->pluck('linked_item_ids')->flatten()->filter()->unique()->values()->toArray(); $itemNames = []; if (! empty($allItemIds)) { $itemNames = \App\Models\Items\Item::whereIn('id', $allItemIds) ->pluck('name', 'id') ->toArray(); } // 분류 코드 → 이름 매핑 $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')); } /** * 단일 조회 */ public function show(int $id): JsonResponse { $template = DocumentTemplate::with([ 'approvalLines', 'basicFields', 'sections.items', 'columns', 'sectionFields', 'links.linkValues', ])->findOrFail($id); return response()->json([ 'success' => true, 'data' => $template, ]); } /** * 생성 */ public function store(Request $request): JsonResponse { $validated = $request->validate([ 'name' => 'required|string|max:100', 'category' => 'nullable|string|max:50', 'builder_type' => 'nullable|string|in:legacy,block', 'title' => 'nullable|string|max:200', 'company_name' => 'nullable|string|max:100', 'company_address' => 'nullable|string|max:255', 'company_contact' => 'nullable|string|max:100', 'footer_remark_label' => 'nullable|string|max:50', 'footer_judgement_label' => 'nullable|string|max:50', 'footer_judgement_options' => 'nullable|array', 'schema' => 'nullable|array', 'page_config' => 'nullable|array', 'is_active' => 'boolean', 'linked_item_ids' => 'nullable|array', 'linked_item_ids.*' => 'integer', 'linked_process_id' => 'nullable|integer', // 관계 데이터 'approval_lines' => 'nullable|array', 'basic_fields' => 'nullable|array', 'sections' => 'nullable|array', 'columns' => 'nullable|array', 'section_fields' => 'nullable|array', 'template_links' => 'nullable|array', ]); // 동일 분류 내 연결품목 중복 검증 $category = $validated['category'] ?? null; $newItemIds = $this->extractLinkedItemIds($validated); $duplicateError = $this->checkLinkedItemDuplicates($category, $newItemIds); if ($duplicateError) { return $duplicateError; } try { DB::beginTransaction(); $template = DocumentTemplate::create([ 'tenant_id' => session('selected_tenant_id'), 'name' => $validated['name'], 'category' => $validated['category'] ?? null, 'builder_type' => $validated['builder_type'] ?? 'legacy', 'title' => $validated['title'] ?? null, 'company_name' => $validated['company_name'] ?? '경동기업', 'company_address' => $validated['company_address'] ?? null, 'company_contact' => $validated['company_contact'] ?? null, 'footer_remark_label' => $validated['footer_remark_label'] ?? '부적합 내용', 'footer_judgement_label' => $validated['footer_judgement_label'] ?? '종합판정', 'footer_judgement_options' => $validated['footer_judgement_options'] ?? ['적합', '부적합'], 'schema' => $validated['schema'] ?? null, 'page_config' => $validated['page_config'] ?? null, 'is_active' => $validated['is_active'] ?? true, 'linked_item_ids' => $validated['linked_item_ids'] ?? null, 'linked_process_id' => $validated['linked_process_id'] ?? null, ]); // 관계 데이터 저장 $this->saveRelations($template, $validated); DB::commit(); return response()->json([ 'success' => true, 'message' => '문서양식이 생성되었습니다.', 'data' => $template->load(['approvalLines', 'basicFields', 'sections.items', 'columns', 'sectionFields', 'links.linkValues']), ]); } catch (\Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => '생성 중 오류가 발생했습니다: '.$e->getMessage(), ], 500); } } /** * 수정 */ public function update(Request $request, int $id): JsonResponse { $template = DocumentTemplate::findOrFail($id); $validated = $request->validate([ 'name' => 'required|string|max:100', 'category' => 'nullable|string|max:50', 'builder_type' => 'nullable|string|in:legacy,block', 'title' => 'nullable|string|max:200', 'company_name' => 'nullable|string|max:100', 'company_address' => 'nullable|string|max:255', 'company_contact' => 'nullable|string|max:100', 'footer_remark_label' => 'nullable|string|max:50', 'footer_judgement_label' => 'nullable|string|max:50', 'footer_judgement_options' => 'nullable|array', 'schema' => 'nullable|array', 'page_config' => 'nullable|array', 'is_active' => 'boolean', 'linked_item_ids' => 'nullable|array', 'linked_item_ids.*' => 'integer', 'linked_process_id' => 'nullable|integer', // 관계 데이터 'approval_lines' => 'nullable|array', 'basic_fields' => 'nullable|array', 'sections' => 'nullable|array', 'columns' => 'nullable|array', 'section_fields' => 'nullable|array', '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(); $updateData = [ 'name' => $validated['name'], 'category' => $validated['category'] ?? null, 'title' => $validated['title'] ?? null, 'company_name' => $validated['company_name'] ?? null, 'company_address' => $validated['company_address'] ?? null, 'company_contact' => $validated['company_contact'] ?? null, 'footer_remark_label' => $validated['footer_remark_label'] ?? '부적합 내용', 'footer_judgement_label' => $validated['footer_judgement_label'] ?? '종합판정', 'footer_judgement_options' => $validated['footer_judgement_options'] ?? ['적합', '부적합'], 'is_active' => $validated['is_active'] ?? true, 'linked_item_ids' => $validated['linked_item_ids'] ?? null, 'linked_process_id' => $validated['linked_process_id'] ?? null, ]; // 블록 빌더 전용 필드 if (isset($validated['builder_type'])) { $updateData['builder_type'] = $validated['builder_type']; } if (array_key_exists('schema', $validated)) { $updateData['schema'] = $validated['schema']; } if (array_key_exists('page_config', $validated)) { $updateData['page_config'] = $validated['page_config']; } $template->update($updateData); // 관계 데이터 저장 (기존 데이터 삭제 후 재생성) $this->saveRelations($template, $validated, true); DB::commit(); return response()->json([ 'success' => true, 'message' => '문서양식이 수정되었습니다.', 'data' => $template->fresh(['approvalLines', 'basicFields', 'sections.items', 'columns', 'sectionFields', 'links.linkValues']), ]); } catch (\Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => '수정 중 오류가 발생했습니다: '.$e->getMessage(), ], 500); } } /** * 삭제 (소프트 삭제) */ public function destroy(int $id): JsonResponse { $template = DocumentTemplate::findOrFail($id); $template->update(['deleted_by' => auth()->id()]); $template->delete(); return response()->json([ 'success' => true, 'message' => '문서양식이 삭제되었습니다.', ]); } /** * 영구삭제 (슈퍼관리자 전용) */ public function forceDestroy(int $id): JsonResponse { if (! auth()->user()?->is_super_admin) { return response()->json([ 'success' => false, 'message' => '슈퍼관리자만 영구 삭제할 수 있습니다.', ], 403); } $tenantId = session('selected_tenant_id'); $template = DocumentTemplate::withTrashed() ->where('tenant_id', $tenantId) ->findOrFail($id); // 이 양식을 참조하는 문서가 있는지 확인 (소프트삭제 포함) $documentCount = \App\Models\Documents\Document::withTrashed() ->where('template_id', $template->id) ->count(); if ($documentCount > 0) { return response()->json([ 'success' => false, 'message' => "이 양식을 사용한 문서가 {$documentCount}건 있어 영구 삭제할 수 없습니다. 문서를 먼저 삭제해주세요.", ], 422); } // 관련 데이터도 영구삭제 $template->approvalLines()->delete(); $template->basicFields()->delete(); $template->sections()->each(function ($section) { $section->items()->delete(); $section->delete(); }); $template->columns()->delete(); $template->sectionFields()->delete(); $template->links()->delete(); // cascade로 linkValues도 삭제 $template->forceDelete(); return response()->json([ 'success' => true, 'message' => '문서양식이 영구 삭제되었습니다.', ]); } /** * 삭제된 문서양식 복원 (슈퍼관리자 전용) */ public function restore(int $id): JsonResponse { if (! auth()->user()?->is_super_admin) { return response()->json([ 'success' => false, 'message' => '슈퍼관리자만 복원할 수 있습니다.', ], 403); } $tenantId = session('selected_tenant_id'); $template = DocumentTemplate::onlyTrashed() ->where('tenant_id', $tenantId) ->findOrFail($id); $template->update(['deleted_by' => null]); $template->restore(); return response()->json([ 'success' => true, 'message' => '문서양식이 복원되었습니다.', ]); } /** * 활성 상태 토글 */ public function toggleActive(int $id): JsonResponse { $template = DocumentTemplate::findOrFail($id); $template->update(['is_active' => ! $template->is_active]); return response()->json([ 'success' => true, 'message' => $template->is_active ? '활성화되었습니다.' : '비활성화되었습니다.', 'is_active' => $template->is_active, ]); } /** * 양식 복제 */ public function duplicate(Request $request, int $id): JsonResponse { $source = DocumentTemplate::with([ 'approvalLines', 'basicFields', 'sections.items', 'columns', 'sectionFields', 'links.linkValues', ])->findOrFail($id); $newName = $request->input('name', $source->name.' (복사)'); try { DB::beginTransaction(); $newTemplate = DocumentTemplate::create([ 'tenant_id' => $source->tenant_id, 'name' => $newName, 'category' => $source->category, 'builder_type' => $source->builder_type ?? 'legacy', 'title' => $source->title, 'company_name' => $source->company_name, 'company_address' => $source->company_address, 'company_contact' => $source->company_contact, 'footer_remark_label' => $source->footer_remark_label, 'footer_judgement_label' => $source->footer_judgement_label, 'footer_judgement_options' => $source->footer_judgement_options, 'schema' => $source->schema, 'page_config' => $source->page_config, 'is_active' => false, 'linked_item_ids' => null, // 연결품목은 복사하지 않음 (중복 방지) 'linked_process_id' => null, ]); foreach ($source->approvalLines as $line) { DocumentTemplateApprovalLine::create([ 'template_id' => $newTemplate->id, 'name' => $line->name, 'dept' => $line->dept, 'role' => $line->role, 'user_id' => $line->user_id, 'sort_order' => $line->sort_order, ]); } foreach ($source->basicFields as $field) { DocumentTemplateBasicField::create([ 'template_id' => $newTemplate->id, 'label' => $field->label, 'field_type' => $field->field_type, 'default_value' => $field->default_value, 'sort_order' => $field->sort_order, ]); } foreach ($source->sections as $section) { $newSection = DocumentTemplateSection::create([ 'template_id' => $newTemplate->id, 'title' => $section->title, 'image_path' => $section->image_path, 'sort_order' => $section->sort_order, ]); foreach ($section->items as $item) { DocumentTemplateSectionItem::create([ 'section_id' => $newSection->id, 'category' => $item->category, 'item' => $item->item, 'standard' => $item->standard, 'tolerance' => $item->tolerance, 'standard_criteria' => $item->standard_criteria, 'method' => $item->method, 'measurement_type' => $item->measurement_type, 'frequency_n' => $item->frequency_n, 'frequency_c' => $item->frequency_c, 'frequency' => $item->frequency, 'regulation' => $item->regulation, 'sort_order' => $item->sort_order, ]); } } foreach ($source->columns as $col) { DocumentTemplateColumn::create([ 'template_id' => $newTemplate->id, 'label' => $col->label, 'width' => $col->width, 'column_type' => $col->column_type, 'group_name' => $col->group_name, 'sub_labels' => $col->sub_labels, 'sort_order' => $col->sort_order, ]); } // 검사 기준서 동적 필드 복제 foreach ($source->sectionFields as $field) { DocumentTemplateSectionField::create([ 'template_id' => $newTemplate->id, 'field_key' => $field->field_key, 'label' => $field->label, 'field_type' => $field->field_type, 'options' => $field->options, 'width' => $field->width, 'is_required' => $field->is_required, 'sort_order' => $field->sort_order, ]); } // 외부 키 매핑 복제 (구조만 복사, 연결 값은 제외 - 중복 방지) foreach ($source->links as $link) { DocumentTemplateLink::create([ 'template_id' => $newTemplate->id, 'link_key' => $link->link_key, 'label' => $link->label, 'link_type' => $link->link_type, 'source_table' => $link->source_table, 'search_params' => $link->search_params, 'display_fields' => $link->display_fields, 'is_required' => $link->is_required, 'sort_order' => $link->sort_order, ]); // linkValues는 복사하지 않음 (동일 분류 내 연결품목 중복 방지) } DB::commit(); return response()->json([ 'success' => true, 'message' => "'{$newName}' 양식이 복제되었습니다.", 'data' => $newTemplate->load(['approvalLines', 'basicFields', 'sections.items', 'columns']), ]); } catch (\Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => '복제 중 오류가 발생했습니다: '.$e->getMessage(), ], 500); } } /** * 이미지 업로드 (API 파일 저장소 연동) */ public function uploadImage(Request $request): JsonResponse { $request->validate([ 'image' => 'required|image|max:5120', // 5MB ]); $apiBaseUrl = config('services.api.base_url'); $apiKey = config('services.api.key'); if (empty($apiBaseUrl)) { return response()->json([ 'success' => false, 'message' => 'API 서버 URL이 설정되지 않았습니다.', ], 500); } // API 토큰 교환 $tokenService = new \App\Services\ApiTokenService; $userId = auth()->id(); $tenantId = session('selected_tenant_id', 1); $tokenResult = $tokenService->exchangeToken($userId, $tenantId); if (! $tokenResult['success']) { Log::error('[DocumentTemplate] API 토큰 교환 실패', [ 'error' => $tokenResult['error'] ?? '', ]); return response()->json([ 'success' => false, 'message' => 'API 인증 실패', ], 500); } // API 파일 업로드 호출 $image = $request->file('image'); try { $response = Http::withToken($tokenResult['data']['access_token']) ->withHeaders(['X-API-KEY' => $apiKey]) ->attach('file', file_get_contents($image->getRealPath()), $image->getClientOriginalName()) ->post("{$apiBaseUrl}/api/v1/files/upload"); if ($response->successful() && $response->json('success')) { $filePath = $response->json('data.file_path'); return response()->json([ 'success' => true, 'path' => $filePath, 'url' => "{$apiBaseUrl}/storage/tenants/{$filePath}", ]); } Log::error('[DocumentTemplate] API 파일 업로드 실패', [ 'status' => $response->status(), 'body' => $response->json(), ]); return response()->json([ 'success' => false, 'message' => 'API 파일 업로드 실패', ], 500); } catch (\Exception $e) { Log::error('[DocumentTemplate] API 파일 업로드 예외', [ 'exception' => $e->getMessage(), ]); return response()->json([ 'success' => false, 'message' => '파일 업로드 중 오류가 발생했습니다.', ], 500); } } /** * 공통코드 그룹 조회 (JSON) */ public function getCommonCodes(string $group): JsonResponse { $tenantId = session('selected_tenant_id'); $codes = \App\Models\Products\CommonCode::query() ->where('code_group', $group) ->where('is_active', true) ->where(function ($q) use ($tenantId) { $q->whereNull('tenant_id'); if ($tenantId) { $q->orWhere('tenant_id', $tenantId); } }) ->orderBy('sort_order') ->get(['code', 'name']); return response()->json([ 'success' => true, 'data' => $codes, ]); } /** * 관계 데이터 저장 (ID 보존 upsert 방식) * * sections/columns는 기존 ID를 보존하여 document_data 참조가 깨지지 않도록 함. * 나머지(approval_lines, basic_fields 등)는 단순 관계라 삭제→재생성. */ private function saveRelations(DocumentTemplate $template, array $data, bool $deleteExisting = false): void { // 단순 관계: 삭제 → 재생성 (document_data가 참조하지 않는 테이블) if ($deleteExisting) { $template->approvalLines()->delete(); $template->basicFields()->delete(); $template->sectionFields()->delete(); $template->links()->delete(); } // 결재라인 if (! empty($data['approval_lines'])) { foreach ($data['approval_lines'] as $index => $line) { DocumentTemplateApprovalLine::create([ 'template_id' => $template->id, 'name' => $line['name'] ?? '', 'dept' => $line['dept'] ?? '', 'role' => $line['role'] ?? '', 'user_id' => $line['user_id'] ?? null, 'sort_order' => $index, ]); } } // 기본 필드 if (! empty($data['basic_fields'])) { foreach ($data['basic_fields'] as $index => $field) { DocumentTemplateBasicField::create([ 'template_id' => $template->id, 'label' => $field['label'] ?? '', 'field_key' => $field['field_key'] ?? null, 'field_type' => $field['field_type'] ?? 'text', 'default_value' => $field['default_value'] ?? '', 'sort_order' => $index, ]); } } // 섹션 및 항목: ID 보존 upsert (document_data.section_id 참조 보호) if (isset($data['sections'])) { $incomingIds = collect($data['sections'])->pluck('id')->filter()->toArray(); // 요청에 없는 섹션만 삭제 (cascade로 items도 삭제) if ($deleteExisting) { $template->sections()->whereNotIn('id', $incomingIds)->each(function ($section) { $section->items()->delete(); $section->delete(); }); } foreach ($data['sections'] as $sIndex => $section) { $sectionData = [ 'title' => $section['title'] ?? '', 'image_path' => $section['image_path'] ?? null, 'sort_order' => $sIndex, ]; if (! empty($section['id']) && $template->sections()->where('id', $section['id'])->exists()) { $existing = $template->sections()->where('id', $section['id'])->first(); $existing->update($sectionData); $savedSection = $existing; } else { $savedSection = DocumentTemplateSection::create(array_merge( ['template_id' => $template->id], $sectionData )); } // 섹션 항목 upsert if (isset($section['items'])) { $itemIncomingIds = collect($section['items'])->pluck('id')->filter()->toArray(); $savedSection->items()->whereNotIn('id', $itemIncomingIds)->delete(); foreach ($section['items'] as $iIndex => $item) { $itemData = [ 'category' => $item['category'] ?? '', 'item' => $item['item'] ?? '', 'standard' => $item['standard'] ?? '', 'tolerance' => $item['tolerance'] ?? null, 'standard_criteria' => $item['standard_criteria'] ?? null, 'method' => $item['method'] ?? '', 'measurement_type' => $item['measurement_type'] ?? null, 'frequency_n' => $item['frequency_n'] ?? null, 'frequency_c' => $item['frequency_c'] ?? null, 'frequency' => $item['frequency'] ?? '', 'regulation' => $item['regulation'] ?? '', 'field_values' => $item['field_values'] ?? null, 'sort_order' => $iIndex, ]; if (! empty($item['id']) && $savedSection->items()->where('id', $item['id'])->exists()) { $savedSection->items()->where('id', $item['id'])->update($itemData); } else { DocumentTemplateSectionItem::create(array_merge( ['section_id' => $savedSection->id], $itemData )); } } } } } // 컬럼: ID 보존 upsert (document_data.column_id 참조 보호) if (isset($data['columns'])) { $incomingIds = collect($data['columns'])->pluck('id')->filter()->toArray(); if ($deleteExisting) { $template->columns()->whereNotIn('id', $incomingIds)->delete(); } foreach ($data['columns'] as $index => $column) { $colData = [ 'label' => $column['label'] ?? '', 'width' => $column['width'] ?? '100px', 'column_type' => $column['column_type'] ?? 'text', 'group_name' => $column['group_name'] ?? null, 'sub_labels' => $column['sub_labels'] ?? null, 'sort_order' => $index, ]; if (! empty($column['id']) && $template->columns()->where('id', $column['id'])->exists()) { $template->columns()->where('id', $column['id'])->update($colData); } else { DocumentTemplateColumn::create(array_merge( ['template_id' => $template->id], $colData )); } } } // 검사 기준서 동적 필드 정의 if (! empty($data['section_fields'])) { foreach ($data['section_fields'] as $index => $field) { DocumentTemplateSectionField::create([ 'template_id' => $template->id, 'field_key' => $field['field_key'] ?? '', 'label' => $field['label'] ?? '', 'field_type' => $field['field_type'] ?? 'text', 'options' => $field['options'] ?? null, 'width' => $field['width'] ?? '100px', 'is_required' => $field['is_required'] ?? false, 'sort_order' => $index, ]); } } // 외부 키 매핑 + 연결 값 if (! empty($data['template_links'])) { foreach ($data['template_links'] as $index => $link) { $newLink = DocumentTemplateLink::create([ 'template_id' => $template->id, 'link_key' => $link['link_key'] ?? '', 'label' => $link['label'] ?? '', 'link_type' => $link['link_type'] ?? 'single', 'source_table' => $link['source_table'] ?? '', 'search_params' => $link['search_params'] ?? null, 'display_fields' => $link['display_fields'] ?? null, 'is_required' => $link['is_required'] ?? false, 'sort_order' => $index, ]); // 연결 값 저장 if (! empty($link['values'])) { foreach ($link['values'] as $vIndex => $value) { DocumentTemplateLinkValue::create([ 'template_id' => $template->id, 'link_id' => $newLink->id, 'linkable_id' => $value['linkable_id'] ?? $value['id'] ?? $value, 'sort_order' => $vIndex, 'created_at' => now(), ]); } } } } } /** * 요청 데이터에서 연결 품목 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); } }