$label) { $totalCount = SystemFieldDefinition::getFieldCountFor($sourceTable); // 현재 등록된 시스템 필드 수 $currentCount = ItemField::where('tenant_id', $tenantId) ->where('source_table', $sourceTable) ->where('storage_type', 'column') ->count(); $statuses[$sourceTable] = [ 'label' => $label, 'source_table' => $sourceTable, 'total_count' => $totalCount, 'current_count' => $currentCount, 'is_complete' => $currentCount >= $totalCount, 'status' => $currentCount >= $totalCount ? 'complete' : ($currentCount > 0 ? 'partial' : 'empty'), ]; } return $statuses; } /** * 특정 소스 테이블의 시딩 가능한 필드 목록 조회 * * @param bool $onlyDefault true면 기본 시딩 대상만, false면 전체 */ public function getAvailableFields(string $sourceTable, bool $onlyDefault = false): Collection { if ($onlyDefault) { return SystemFieldDefinition::getSeedDefaultFieldsFor($sourceTable); } return SystemFieldDefinition::getFieldsFor($sourceTable); } /** * 단일 테이블 시스템 필드 시딩 * * @param array|null $fieldIds 시딩할 필드 ID 목록 (null이면 is_seed_default=true인 것만) */ public function seedTable(int $tenantId, string $sourceTable, ?array $fieldIds = null): array { // 필드 ID가 지정되면 해당 필드만, 아니면 기본 시딩 대상만 if ($fieldIds !== null) { $fields = SystemFieldDefinition::whereIn('id', $fieldIds) ->where('source_table', $sourceTable) ->where('is_active', true) ->orderBy('order_no') ->get(); } else { $fields = SystemFieldDefinition::getSeedDefaultFieldsFor($sourceTable); } if ($fields->isEmpty()) { return [ 'success' => false, 'message' => '시딩할 필드가 없습니다.', 'seeded_count' => 0, ]; } $seededCount = 0; $skippedCount = 0; $errorCount = 0; $this->errors = []; foreach ($fields as $fieldDef) { try { // 이미 존재하는지 확인 (field_key + source_table 조합) $exists = ItemField::where('tenant_id', $tenantId) ->where('source_table', $sourceTable) ->where('field_key', $fieldDef->field_key) ->exists(); if ($exists) { $skippedCount++; continue; } // DB 유니크 제약조건 충돌 방지: tenant_id + field_key 조합 체크 $existsGlobal = ItemField::where('tenant_id', $tenantId) ->where('field_key', $fieldDef->field_key) ->exists(); if ($existsGlobal) { // 다른 source_table에 같은 field_key가 있으면 고유한 키로 변환 $fieldKey = "{$sourceTable}_{$fieldDef->field_key}"; // 변환된 키도 이미 존재하는지 확인 $existsTransformed = ItemField::where('tenant_id', $tenantId) ->where('field_key', $fieldKey) ->exists(); if ($existsTransformed) { $skippedCount++; continue; } } else { $fieldKey = $fieldDef->field_key; } // 필드 생성 ItemField::create([ 'tenant_id' => $tenantId, 'group_id' => 1, // 품목관리 그룹 'field_key' => $fieldKey, 'field_name' => $fieldDef->field_name, 'field_type' => $fieldDef->field_type, 'order_no' => $fieldDef->order_no ?? 0, 'is_required' => $fieldDef->is_required ?? false, 'default_value' => $fieldDef->default_value ?? null, 'options' => $fieldDef->options ?? null, 'source_table' => $sourceTable, 'source_column' => $fieldDef->field_key, // 원본 컬럼명 유지 'storage_type' => 'column', 'is_active' => true, 'is_common' => true, 'created_by' => auth()->id(), ]); $seededCount++; } catch (\Illuminate\Database\QueryException $e) { $errorCount++; $errorInfo = [ 'field_key' => $fieldDef->field_key, 'field_name' => $fieldDef->field_name, 'source_table' => $sourceTable, 'error_code' => $e->getCode(), 'error_message' => $e->getMessage(), 'sql_state' => $e->errorInfo[0] ?? null, 'timestamp' => now()->toDateTimeString(), ]; $this->errors[] = $errorInfo; Log::error('ItemField 시딩 오류', [ 'tenant_id' => $tenantId, 'source_table' => $sourceTable, 'field_key' => $fieldDef->field_key, 'error' => $e->getMessage(), ]); } catch (\Exception $e) { $errorCount++; $this->errors[] = [ 'field_key' => $fieldDef->field_key, 'field_name' => $fieldDef->field_name, 'source_table' => $sourceTable, 'error_code' => 'UNKNOWN', 'error_message' => $e->getMessage(), 'timestamp' => now()->toDateTimeString(), ]; Log::error('ItemField 시딩 예외', [ 'tenant_id' => $tenantId, 'source_table' => $sourceTable, 'field_key' => $fieldDef->field_key, 'error' => $e->getMessage(), ]); } } // 오류가 있으면 세션에 저장 if (! empty($this->errors)) { $this->saveErrorsToSession($tenantId, $sourceTable); } $success = $errorCount === 0; $message = $success ? "시딩 완료: {$seededCount}개 추가, {$skippedCount}개 건너뜀" : "시딩 부분 완료: {$seededCount}개 추가, {$skippedCount}개 건너뜀, {$errorCount}개 오류"; return [ 'success' => $success, 'message' => $message, 'seeded_count' => $seededCount, 'skipped_count' => $skippedCount, 'error_count' => $errorCount, 'errors' => $this->errors, 'has_errors' => $errorCount > 0, ]; } /** * 오류 정보를 세션에 저장 */ protected function saveErrorsToSession(int $tenantId, string $sourceTable): void { $errorLog = [ 'tenant_id' => $tenantId, 'source_table' => $sourceTable, 'errors' => $this->errors, 'timestamp' => now()->toDateTimeString(), 'user_id' => auth()->id(), 'user_name' => auth()->user()?->name, ]; // 기존 오류 로그 가져오기 $existingLogs = session('item_field_errors', []); // 최신 오류를 앞에 추가 (최대 10개 유지) array_unshift($existingLogs, $errorLog); $existingLogs = array_slice($existingLogs, 0, 10); session(['item_field_errors' => $existingLogs]); } /** * 저장된 오류 로그 조회 */ public function getErrorLogs(): array { return session('item_field_errors', []); } /** * 오류 로그 초기화 */ public function clearErrorLogs(): void { session()->forget('item_field_errors'); } /** * 전체 테이블 시스템 필드 시딩 */ public function seedAll(int $tenantId): array { $results = []; $totalSeeded = 0; $totalSkipped = 0; $sourceTables = array_keys(SystemFieldDefinition::getSourceTableOptions()); foreach ($sourceTables as $sourceTable) { $result = $this->seedTable($tenantId, $sourceTable); $results[$sourceTable] = $result; $totalSeeded += $result['seeded_count']; $totalSkipped += $result['skipped_count'] ?? 0; } return [ 'success' => true, 'message' => "전체 시딩 완료: {$totalSeeded}개 추가, {$totalSkipped}개 건너뜀", 'total_seeded' => $totalSeeded, 'total_skipped' => $totalSkipped, 'details' => $results, ]; } /** * 단일 테이블 시스템 필드 초기화 (삭제 후 재시딩) */ public function resetTable(int $tenantId, string $sourceTable): array { // 시스템 필드만 삭제 (커스텀 필드는 유지) $deletedCount = ItemField::where('tenant_id', $tenantId) ->where('source_table', $sourceTable) ->where('storage_type', 'column') ->forceDelete(); // 재시딩 $seedResult = $this->seedTable($tenantId, $sourceTable); return [ 'success' => true, 'message' => "초기화 완료: {$deletedCount}개 삭제, {$seedResult['seeded_count']}개 재등록", 'deleted_count' => $deletedCount, 'seeded_count' => $seedResult['seeded_count'], ]; } /** * 전체 테이블 시스템 필드 초기화 */ public function resetAll(int $tenantId): array { $results = []; $totalDeleted = 0; $totalSeeded = 0; $sourceTables = array_keys(SystemFieldDefinition::getSourceTableOptions()); foreach ($sourceTables as $sourceTable) { $result = $this->resetTable($tenantId, $sourceTable); $results[$sourceTable] = $result; $totalDeleted += $result['deleted_count']; $totalSeeded += $result['seeded_count']; } return [ 'success' => true, 'message' => "전체 초기화 완료: {$totalDeleted}개 삭제, {$totalSeeded}개 재등록", 'total_deleted' => $totalDeleted, 'total_seeded' => $totalSeeded, 'details' => $results, ]; } /** * 필드 목록 조회 (시스템 + 커스텀) * * @param array $filters source_table, field_type, field_category (system|custom), search, per_page */ public function getFields(int $tenantId, array $filters = []): \Illuminate\Contracts\Pagination\LengthAwarePaginator { $query = ItemField::withTrashed()->where('tenant_id', $tenantId); // 필드 유형 필터 (system=is_common:1, custom=is_common:0) if (! empty($filters['field_category'])) { if ($filters['field_category'] === 'system') { $query->where('is_common', true); } elseif ($filters['field_category'] === 'custom') { $query->where('is_common', false); } } // 소스 테이블 필터 if (! empty($filters['source_table'])) { $query->where('source_table', $filters['source_table']); } // 필드 타입 필터 if (! empty($filters['field_type'])) { $query->where('field_type', $filters['field_type']); } // 검색 if (! empty($filters['search'])) { $search = $filters['search']; $query->where(function ($q) use ($search) { $q->where('field_key', 'like', "%{$search}%") ->orWhere('field_name', 'like', "%{$search}%"); }); } // 페이지당 항목 수 (기본 20개) $perPage = ! empty($filters['per_page']) ? (int) $filters['per_page'] : 20; // 시스템 필드 우선 정렬 (is_common DESC), 그 다음 source_table, order_no return $query->orderByDesc('is_common') ->orderBy('source_table') ->orderBy('order_no') ->paginate($perPage) ->withQueryString(); } /** * 커스텀 필드 목록 조회 (기존 호환성 유지) * * @deprecated Use getFields() instead */ public function getCustomFields(int $tenantId, array $filters = []): Collection { // 기존 호환성 유지: 커스텀 필드만 반환 $filters['field_category'] = 'custom'; return $this->getFields($tenantId, $filters); } /** * 커스텀 필드 추가 */ public function createCustomField(int $tenantId, array $data): array { $sourceTable = $data['source_table']; $fieldKey = $data['field_key']; // 시스템 예약어 체크 $systemFieldKeys = SystemFieldDefinition::getAllSystemFieldKeys($sourceTable); if (in_array($fieldKey, $systemFieldKeys)) { return [ 'success' => false, 'message' => "\"{$fieldKey}\"은(는) 시스템 예약어로 사용할 수 없습니다.", ]; } // 기존 필드 키 중복 체크 $exists = ItemField::where('tenant_id', $tenantId) ->where('source_table', $sourceTable) ->where('field_key', $fieldKey) ->exists(); if ($exists) { return [ 'success' => false, 'message' => "\"{$fieldKey}\"는 이미 사용 중인 필드 키입니다.", ]; } // 커스텀 필드 생성 $field = ItemField::create([ 'tenant_id' => $tenantId, 'group_id' => 1, 'field_key' => $fieldKey, 'field_name' => $data['field_name'], 'field_type' => $data['field_type'], 'order_no' => $data['order_no'] ?? 0, 'is_required' => $data['is_required'] ?? false, 'default_value' => $data['default_value'] ?? null, 'options' => $data['options'] ?? null, 'source_table' => $sourceTable, 'source_column' => null, 'storage_type' => 'json', 'json_path' => "attributes.{$fieldKey}", 'is_active' => true, 'is_common' => false, 'created_by' => auth()->id(), ]); return [ 'success' => true, 'message' => '커스텀 필드가 추가되었습니다.', 'field' => $field, ]; } /** * 커스텀 필드 수정 */ public function updateCustomField(int $tenantId, int $fieldId, array $data): array { $field = ItemField::where('tenant_id', $tenantId) ->where('id', $fieldId) ->first(); if (! $field) { return [ 'success' => false, 'message' => '필드를 찾을 수 없습니다.', ]; } // 시스템 필드(storage_type = column)의 경우 일부 필드만 수정 가능 $isSystemField = $field->storage_type === 'column'; // field_key 변경 시 중복 체크 (시스템 필드는 field_key 변경 불가) if (! $isSystemField && ! empty($data['field_key']) && $data['field_key'] !== $field->field_key) { $exists = ItemField::where('tenant_id', $tenantId) ->where('field_key', $data['field_key']) ->where('id', '!=', $fieldId) ->exists(); if ($exists) { return [ 'success' => false, 'message' => "\"{$data['field_key']}\"는 이미 사용 중인 필드 키입니다.", ]; } } // 업데이트 데이터 준비 $updateData = [ 'field_name' => $data['field_name'], 'field_type' => $data['field_type'], 'is_required' => $data['is_required'] ?? false, 'default_value' => $data['default_value'] ?? null, 'options' => $data['options'] ?? null, 'updated_by' => auth()->id(), ]; // 커스텀 필드만 source_table, field_key 변경 가능 if (! $isSystemField) { if (isset($data['source_table'])) { $updateData['source_table'] = $data['source_table']; } if (isset($data['field_key'])) { $updateData['field_key'] = $data['field_key']; $updateData['json_path'] = "attributes.{$data['field_key']}"; } } $field->update($updateData); return [ 'success' => true, 'message' => '커스텀 필드가 수정되었습니다.', 'field' => $field->fresh(), ]; } /** * 커스텀 필드 삭제 */ public function deleteCustomField(int $tenantId, int $fieldId): array { $field = ItemField::where('tenant_id', $tenantId) ->where('id', $fieldId) ->first(); if (! $field) { return [ 'success' => false, 'message' => '필드를 찾을 수 없습니다.', ]; } $field->forceDelete(); return [ 'success' => true, 'message' => '필드가 삭제되었습니다.', ]; } /** * 커스텀 필드 일괄 삭제 */ public function deleteCustomFields(int $tenantId, array $fieldIds): array { $deletedCount = 0; foreach ($fieldIds as $fieldId) { $result = $this->deleteCustomField($tenantId, $fieldId); if ($result['success']) { $deletedCount++; } } return [ 'success' => true, 'message' => "{$deletedCount}개의 커스텀 필드가 삭제되었습니다.", 'deleted_count' => $deletedCount, ]; } /** * 소프트 삭제된 커스텀 필드 복원 */ public function restoreCustomField(int $tenantId, int $fieldId): array { $field = ItemField::withTrashed() ->where('tenant_id', $tenantId) ->where('id', $fieldId) ->first(); if (! $field) { return [ 'success' => false, 'message' => '필드를 찾을 수 없습니다.', ]; } if (is_null($field->deleted_at)) { return [ 'success' => false, 'message' => '이미 활성화된 필드입니다.', ]; } $field->restore(); return [ 'success' => true, 'message' => '필드가 복원되었습니다.', ]; } /** * 커스텀 필드 영구 삭제 */ public function forceDeleteCustomField(int $tenantId, int $fieldId): array { $field = ItemField::withTrashed() ->where('tenant_id', $tenantId) ->where('id', $fieldId) ->first(); if (! $field) { return [ 'success' => false, 'message' => '필드를 찾을 수 없습니다.', ]; } $field->forceDelete(); return [ 'success' => true, 'message' => '필드가 영구 삭제되었습니다.', ]; } /** * 삭제된 필드 일괄 영구 삭제 (휴지통 비우기) */ public function purgeDeletedFields(int $tenantId): array { // 삭제된 필드(soft deleted) 조회 $deletedFields = ItemField::onlyTrashed() ->where('tenant_id', $tenantId) ->get(); if ($deletedFields->isEmpty()) { return [ 'success' => true, 'message' => '삭제된 필드가 없습니다.', 'purged_count' => 0, ]; } $purgedCount = 0; foreach ($deletedFields as $field) { $field->forceDelete(); $purgedCount++; } return [ 'success' => true, 'message' => "{$purgedCount}개의 삭제된 필드가 영구 삭제되었습니다.", 'purged_count' => $purgedCount, ]; } }