diff --git a/app/Constants/SystemFieldDefinitions.php b/app/Constants/SystemFieldDefinitions.php new file mode 100644 index 00000000..060c5dd2 --- /dev/null +++ b/app/Constants/SystemFieldDefinitions.php @@ -0,0 +1,139 @@ + 'code', 'field_name' => '품목코드', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 1], + ['field_key' => 'name', 'field_name' => '품목명', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 2], + ['field_key' => 'unit', 'field_name' => '단위', 'field_type' => 'dropdown', 'is_required' => true, 'order_no' => 3], + ['field_key' => 'product_type', 'field_name' => '제품유형', 'field_type' => 'dropdown', 'order_no' => 4, 'options' => [ + ['label' => '완제품', 'value' => 'FG'], + ['label' => '부품', 'value' => 'PT'], + ]], + ['field_key' => 'category_id', 'field_name' => '카테고리', 'field_type' => 'dropdown', 'order_no' => 5], + ['field_key' => 'is_sellable', 'field_name' => '판매가능', 'field_type' => 'checkbox', 'order_no' => 6, 'default_value' => 'true'], + ['field_key' => 'is_purchasable', 'field_name' => '구매가능', 'field_type' => 'checkbox', 'order_no' => 7], + ['field_key' => 'is_producible', 'field_name' => '생산가능', 'field_type' => 'checkbox', 'order_no' => 8, 'default_value' => 'true'], + ['field_key' => 'is_active', 'field_name' => '활성화', 'field_type' => 'checkbox', 'order_no' => 9, 'default_value' => 'true'], + ['field_key' => 'certification_number', 'field_name' => '인증번호', 'field_type' => 'textbox', 'order_no' => 10], + ['field_key' => 'certification_start_date', 'field_name' => '인증시작일', 'field_type' => 'date', 'order_no' => 11], + ['field_key' => 'certification_end_date', 'field_name' => '인증만료일', 'field_type' => 'date', 'order_no' => 12], + ]; + + /** + * materials 테이블 시스템 필드 정의 + */ + public const MATERIALS = [ + ['field_key' => 'material_code', 'field_name' => '자재코드', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 1], + ['field_key' => 'name', 'field_name' => '자재명', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 2], + ['field_key' => 'item_name', 'field_name' => '품목명', 'field_type' => 'textbox', 'order_no' => 3], + ['field_key' => 'specification', 'field_name' => '규격', 'field_type' => 'textbox', 'order_no' => 4], + ['field_key' => 'unit', 'field_name' => '단위', 'field_type' => 'dropdown', 'is_required' => true, 'order_no' => 5], + ['field_key' => 'category_id', 'field_name' => '카테고리', 'field_type' => 'dropdown', 'order_no' => 6], + ['field_key' => 'is_inspection', 'field_name' => '검수필요', 'field_type' => 'checkbox', 'order_no' => 7], + ['field_key' => 'search_tag', 'field_name' => '검색태그', 'field_type' => 'textarea', 'order_no' => 8], + ]; + + /** + * product_components 테이블 (BOM) 시스템 필드 정의 + */ + public const PRODUCT_COMPONENTS = [ + ['field_key' => 'ref_type', 'field_name' => '참조유형', 'field_type' => 'dropdown', 'order_no' => 1, 'options' => [ + ['label' => '제품', 'value' => 'product'], + ['label' => '자재', 'value' => 'material'], + ]], + ['field_key' => 'ref_id', 'field_name' => '참조품목', 'field_type' => 'dropdown', 'order_no' => 2], + ['field_key' => 'quantity', 'field_name' => '수량', 'field_type' => 'number', 'is_required' => true, 'order_no' => 3], + ['field_key' => 'formula', 'field_name' => '계산공식', 'field_type' => 'textbox', 'order_no' => 4], + ['field_key' => 'note', 'field_name' => '비고', 'field_type' => 'textarea', 'order_no' => 5], + ]; + + /** + * material_inspections 테이블 시스템 필드 정의 + */ + public const MATERIAL_INSPECTIONS = [ + ['field_key' => 'inspection_date', 'field_name' => '검수일', 'field_type' => 'date', 'is_required' => true, 'order_no' => 1], + ['field_key' => 'inspector_id', 'field_name' => '검수자', 'field_type' => 'dropdown', 'order_no' => 2], + ['field_key' => 'status', 'field_name' => '검수상태', 'field_type' => 'dropdown', 'order_no' => 3, 'options' => [ + ['label' => '대기', 'value' => 'pending'], + ['label' => '진행중', 'value' => 'in_progress'], + ['label' => '완료', 'value' => 'completed'], + ['label' => '불합격', 'value' => 'rejected'], + ]], + ['field_key' => 'lot_no', 'field_name' => 'LOT번호', 'field_type' => 'textbox', 'order_no' => 4], + ['field_key' => 'quantity', 'field_name' => '검수수량', 'field_type' => 'number', 'order_no' => 5], + ['field_key' => 'passed_quantity', 'field_name' => '합격수량', 'field_type' => 'number', 'order_no' => 6], + ['field_key' => 'rejected_quantity', 'field_name' => '불합격수량', 'field_type' => 'number', 'order_no' => 7], + ['field_key' => 'note', 'field_name' => '비고', 'field_type' => 'textarea', 'order_no' => 8], + ]; + + /** + * material_receipts 테이블 시스템 필드 정의 + */ + public const MATERIAL_RECEIPTS = [ + ['field_key' => 'receipt_date', 'field_name' => '입고일', 'field_type' => 'date', 'is_required' => true, 'order_no' => 1], + ['field_key' => 'lot_no', 'field_name' => 'LOT번호', 'field_type' => 'textbox', 'order_no' => 2], + ['field_key' => 'quantity', 'field_name' => '입고수량', 'field_type' => 'number', 'is_required' => true, 'order_no' => 3], + ['field_key' => 'unit_price', 'field_name' => '단가', 'field_type' => 'number', 'order_no' => 4], + ['field_key' => 'total_price', 'field_name' => '금액', 'field_type' => 'number', 'order_no' => 5], + ['field_key' => 'supplier_id', 'field_name' => '공급업체', 'field_type' => 'dropdown', 'order_no' => 6], + ['field_key' => 'warehouse_id', 'field_name' => '입고창고', 'field_type' => 'dropdown', 'order_no' => 7], + ['field_key' => 'po_number', 'field_name' => '발주번호', 'field_type' => 'textbox', 'order_no' => 8], + ['field_key' => 'invoice_number', 'field_name' => '송장번호', 'field_type' => 'textbox', 'order_no' => 9], + ['field_key' => 'note', 'field_name' => '비고', 'field_type' => 'textarea', 'order_no' => 10], + ]; + + /** + * 소스 테이블 목록 + */ + public const SOURCE_TABLES = [ + 'products' => '제품', + 'materials' => '자재', + 'product_components' => 'BOM', + 'material_inspections' => '자재검수', + 'material_receipts' => '자재입고', + ]; + + /** + * 소스 테이블별 필드 정의 가져오기 + */ + public static function getFieldsFor(string $sourceTable): array + { + return match ($sourceTable) { + 'products' => self::PRODUCTS, + 'materials' => self::MATERIALS, + 'product_components' => self::PRODUCT_COMPONENTS, + 'material_inspections' => self::MATERIAL_INSPECTIONS, + 'material_receipts' => self::MATERIAL_RECEIPTS, + default => [], + }; + } + + /** + * 전체 테이블 필드 수 조회 + */ + public static function getTotalFieldCount(string $sourceTable): int + { + return count(self::getFieldsFor($sourceTable)); + } + + /** + * 모든 소스 테이블의 시스템 필드 키 목록 + */ + public static function getAllSystemFieldKeys(string $sourceTable): array + { + $fields = self::getFieldsFor($sourceTable); + + return array_column($fields, 'field_key'); + } +} diff --git a/app/Http/Controllers/Api/Admin/ItemFieldController.php b/app/Http/Controllers/Api/Admin/ItemFieldController.php new file mode 100644 index 00000000..2bce199b --- /dev/null +++ b/app/Http/Controllers/Api/Admin/ItemFieldController.php @@ -0,0 +1,366 @@ + $tenantId, + 'session' => session()->all(), + ]); + + if (! $tenantId || $tenantId === 'all') { + return view('item-fields.partials.seeding-status', [ + 'statuses' => [], + 'error' => '테넌트를 선택해주세요.', + ]); + } + + $statuses = $this->service->getSeedingStatus($tenantId); + + Log::info('seedingStatus result', [ + 'statuses_count' => count($statuses), + 'statuses' => $statuses, + ]); + + return view('item-fields.partials.seeding-status', compact('statuses')); + } + + /** + * 단일 테이블 시딩 + */ + public function seed(Request $request): JsonResponse + { + $tenantId = session('selected_tenant_id'); + + if (! $tenantId || $tenantId === 'all') { + return response()->json([ + 'success' => false, + 'message' => '테넌트를 선택해주세요.', + ], 400); + } + + $sourceTable = $request->input('source_table'); + + if (! $sourceTable) { + return response()->json([ + 'success' => false, + 'message' => '소스 테이블을 지정해주세요.', + ], 400); + } + + $result = $this->service->seedTable($tenantId, $sourceTable); + + return response()->json($result); + } + + /** + * 전체 테이블 시딩 + */ + public function seedAll(Request $request): JsonResponse + { + $tenantId = session('selected_tenant_id'); + + if (! $tenantId || $tenantId === 'all') { + return response()->json([ + 'success' => false, + 'message' => '테넌트를 선택해주세요.', + ], 400); + } + + $result = $this->service->seedAll($tenantId); + + return response()->json($result); + } + + /** + * 단일 테이블 초기화 + */ + public function reset(Request $request): JsonResponse + { + $tenantId = session('selected_tenant_id'); + + if (! $tenantId || $tenantId === 'all') { + return response()->json([ + 'success' => false, + 'message' => '테넌트를 선택해주세요.', + ], 400); + } + + $sourceTable = $request->input('source_table'); + + if (! $sourceTable) { + return response()->json([ + 'success' => false, + 'message' => '소스 테이블을 지정해주세요.', + ], 400); + } + + $result = $this->service->resetTable($tenantId, $sourceTable); + + return response()->json($result); + } + + /** + * 전체 테이블 초기화 + */ + public function resetAll(Request $request): JsonResponse + { + $tenantId = session('selected_tenant_id'); + + if (! $tenantId || $tenantId === 'all') { + return response()->json([ + 'success' => false, + 'message' => '테넌트를 선택해주세요.', + ], 400); + } + + $result = $this->service->resetAll($tenantId); + + return response()->json($result); + } + + /** + * 커스텀 필드 목록 (HTMX partial) + */ + public function customFields(Request $request): View + { + $tenantId = session('selected_tenant_id'); + + if (! $tenantId || $tenantId === 'all') { + return view('item-fields.partials.custom-fields', [ + 'fields' => collect([]), + 'sourceTables' => SystemFieldDefinitions::SOURCE_TABLES, + 'error' => '테넌트를 선택해주세요.', + ]); + } + + $fields = $this->service->getCustomFields($tenantId, $request->all()); + + return view('item-fields.partials.custom-fields', [ + 'fields' => $fields, + 'sourceTables' => SystemFieldDefinitions::SOURCE_TABLES, + ]); + } + + /** + * 커스텀 필드 추가 + */ + public function storeCustomField(Request $request): JsonResponse + { + $tenantId = session('selected_tenant_id'); + + if (! $tenantId || $tenantId === 'all') { + return response()->json([ + 'success' => false, + 'message' => '테넌트를 선택해주세요.', + ], 400); + } + + $validated = $request->validate([ + 'source_table' => 'required|string', + 'field_key' => 'required|string|max:100|regex:/^[a-zA-Z0-9_]+$/', + 'field_name' => 'required|string|max:255', + 'field_type' => 'required|string|in:textbox,number,dropdown,checkbox,date,textarea', + 'is_required' => 'nullable|boolean', + 'default_value' => 'nullable|string', + 'options' => 'nullable|array', + ]); + + $result = $this->service->createCustomField($tenantId, $validated); + + return response()->json($result, $result['success'] ? 200 : 400); + } + + /** + * 커스텀 필드 수정 + */ + public function updateCustomField(Request $request, int $id): JsonResponse + { + $tenantId = session('selected_tenant_id'); + + if (! $tenantId || $tenantId === 'all') { + return response()->json([ + 'success' => false, + 'message' => '테넌트를 선택해주세요.', + ], 400); + } + + $validated = $request->validate([ + 'source_table' => 'nullable|string', + 'field_key' => 'nullable|string|max:100|regex:/^[a-zA-Z0-9_]+$/', + 'field_name' => 'required|string|max:255', + 'field_type' => 'required|string|in:textbox,number,dropdown,checkbox,date,textarea', + 'is_required' => 'nullable|boolean', + 'default_value' => 'nullable|string', + 'options' => 'nullable|array', + ]); + + $result = $this->service->updateCustomField($tenantId, $id, $validated); + + return response()->json($result, $result['success'] ? 200 : 400); + } + + /** + * 커스텀 필드 삭제 + */ + public function destroyCustomField(int $id): JsonResponse + { + $tenantId = session('selected_tenant_id'); + + if (! $tenantId || $tenantId === 'all') { + return response()->json([ + 'success' => false, + 'message' => '테넌트를 선택해주세요.', + ], 400); + } + + $result = $this->service->deleteCustomField($tenantId, $id); + + return response()->json($result, $result['success'] ? 200 : 400); + } + + /** + * 커스텀 필드 일괄 삭제 + */ + public function destroyCustomFields(Request $request): JsonResponse + { + $tenantId = session('selected_tenant_id'); + + if (! $tenantId || $tenantId === 'all') { + return response()->json([ + 'success' => false, + 'message' => '테넌트를 선택해주세요.', + ], 400); + } + + $validated = $request->validate([ + 'ids' => 'required|array', + 'ids.*' => 'required|integer', + ]); + + $result = $this->service->deleteCustomFields($tenantId, $validated['ids']); + + return response()->json($result); + } + + /** + * 소스 테이블 목록 (JSON) + */ + public function sourceTables(): JsonResponse + { + return response()->json([ + 'success' => true, + 'data' => SystemFieldDefinitions::SOURCE_TABLES, + ]); + } + + /** + * 오류 로그 조회 (HTMX partial) + */ + public function errorLogs(): View + { + $errorLogs = $this->service->getErrorLogs(); + + return view('item-fields.partials.error-logs', compact('errorLogs')); + } + + /** + * 오류 로그 초기화 + */ + public function clearErrorLogs(): JsonResponse + { + $this->service->clearErrorLogs(); + + return response()->json([ + 'success' => true, + 'message' => '오류 로그가 초기화되었습니다.', + ]); + } + + /** + * AI 문의용 오류 보고서 생성 + */ + public function generateErrorReport(): JsonResponse + { + $tenantId = session('selected_tenant_id'); + $errorLogs = $this->service->getErrorLogs(); + + if (empty($errorLogs)) { + return response()->json([ + 'success' => false, + 'message' => '저장된 오류 로그가 없습니다.', + ]); + } + + $latestError = $errorLogs[0]; + + // 보고서 생성 + $report = $this->buildErrorReport($latestError, $tenantId); + + return response()->json([ + 'success' => true, + 'report' => $report, + 'error_count' => count($latestError['errors'] ?? []), + ]); + } + + /** + * 오류 보고서 텍스트 생성 + */ + protected function buildErrorReport(array $errorLog, ?int $tenantId): string + { + $errors = $errorLog['errors'] ?? []; + $sourceTable = $errorLog['source_table'] ?? 'unknown'; + $timestamp = $errorLog['timestamp'] ?? now()->toDateTimeString(); + + $report = "## 품목기준 필드 시딩 오류 보고서\n\n"; + $report .= "### 기본 정보\n"; + $report .= "- **발생 시각**: {$timestamp}\n"; + $report .= "- **테넌트 ID**: {$tenantId}\n"; + $report .= "- **소스 테이블**: {$sourceTable}\n"; + $report .= '- **오류 수**: '.count($errors)."건\n\n"; + + $report .= "### 오류 상세\n"; + foreach ($errors as $index => $error) { + $num = $index + 1; + $report .= "\n#### 오류 #{$num}\n"; + $report .= "- **필드 키**: `{$error['field_key']}`\n"; + $report .= "- **필드명**: {$error['field_name']}\n"; + $report .= "- **오류 코드**: {$error['error_code']}\n"; + $report .= "- **오류 메시지**:\n```\n{$error['error_message']}\n```\n"; + } + + $report .= "\n### 환경 정보\n"; + $report .= "- **애플리케이션**: MNG (품목기준 필드 관리)\n"; + $report .= "- **DB 테이블**: item_fields\n"; + $report .= "- **관련 파일**: `app/Services/ItemFieldSeedingService.php`\n"; + + $report .= "\n### 요청 사항\n"; + $report .= "위 오류의 원인을 분석하고 해결 방법을 제시해 주세요.\n"; + + return $report; + } +} diff --git a/app/Http/Controllers/ItemFieldController.php b/app/Http/Controllers/ItemFieldController.php new file mode 100644 index 00000000..681b3e41 --- /dev/null +++ b/app/Http/Controllers/ItemFieldController.php @@ -0,0 +1,19 @@ + 'integer', + 'order_no' => 'integer', + 'is_required' => 'boolean', + 'is_common' => 'boolean', + 'is_active' => 'boolean', + 'is_locked' => 'boolean', + 'display_condition' => 'array', + 'validation_rules' => 'array', + 'options' => 'array', + 'properties' => 'array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + 'locked_at' => 'datetime', + ]; + + /** + * 시스템 필드 여부 확인 (DB 컬럼과 매핑된 필드) + */ + public function isSystemField(): bool + { + return $this->storage_type === 'column' && ! is_null($this->source_column); + } + + /** + * 컬럼 저장 방식 여부 확인 + */ + public function isColumnStorage(): bool + { + return $this->storage_type === 'column'; + } + + /** + * JSON 저장 방식 여부 확인 + */ + public function isJsonStorage(): bool + { + return $this->storage_type === 'json'; + } +} diff --git a/app/Services/ItemFieldSeedingService.php b/app/Services/ItemFieldSeedingService.php new file mode 100644 index 00000000..976ff3b3 --- /dev/null +++ b/app/Services/ItemFieldSeedingService.php @@ -0,0 +1,509 @@ + $label) { + $totalCount = SystemFieldDefinitions::getTotalFieldCount($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; + } + + /** + * 단일 테이블 시스템 필드 시딩 + */ + public function seedTable(int $tenantId, string $sourceTable): array + { + $fields = SystemFieldDefinitions::getFieldsFor($sourceTable); + + if (empty($fields)) { + return [ + 'success' => false, + 'message' => '알 수 없는 소스 테이블입니다.', + 'seeded_count' => 0, + ]; + } + + $seededCount = 0; + $skippedCount = 0; + $errorCount = 0; + $this->errors = []; + + foreach ($fields as $fieldData) { + try { + // 이미 존재하는지 확인 (field_key + source_table 조합) + $exists = ItemField::where('tenant_id', $tenantId) + ->where('source_table', $sourceTable) + ->where('field_key', $fieldData['field_key']) + ->exists(); + + if ($exists) { + $skippedCount++; + + continue; + } + + // DB 유니크 제약조건 충돌 방지: tenant_id + field_key 조합 체크 + // (DB에 source_table 없이 유니크 키가 있는 경우를 대비) + $existsGlobal = ItemField::where('tenant_id', $tenantId) + ->where('field_key', $fieldData['field_key']) + ->exists(); + + if ($existsGlobal) { + // 다른 source_table에 같은 field_key가 있으면 고유한 키로 변환 + $fieldKey = "{$sourceTable}_{$fieldData['field_key']}"; + + // 변환된 키도 이미 존재하는지 확인 + $existsTransformed = ItemField::where('tenant_id', $tenantId) + ->where('field_key', $fieldKey) + ->exists(); + + if ($existsTransformed) { + $skippedCount++; + + continue; + } + } else { + $fieldKey = $fieldData['field_key']; + } + + // 필드 생성 + ItemField::create([ + 'tenant_id' => $tenantId, + 'group_id' => 1, // 품목관리 그룹 + 'field_key' => $fieldKey, + 'field_name' => $fieldData['field_name'], + 'field_type' => $fieldData['field_type'], + 'order_no' => $fieldData['order_no'] ?? 0, + 'is_required' => $fieldData['is_required'] ?? false, + 'default_value' => $fieldData['default_value'] ?? null, + 'options' => $fieldData['options'] ?? null, + 'source_table' => $sourceTable, + 'source_column' => $fieldData['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' => $fieldData['field_key'], + 'field_name' => $fieldData['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' => $fieldData['field_key'], + 'error' => $e->getMessage(), + ]); + } catch (\Exception $e) { + $errorCount++; + $this->errors[] = [ + 'field_key' => $fieldData['field_key'], + 'field_name' => $fieldData['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' => $fieldData['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; + + foreach (array_keys(SystemFieldDefinitions::SOURCE_TABLES) 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; + + foreach (array_keys(SystemFieldDefinitions::SOURCE_TABLES) 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, + ]; + } + + /** + * 커스텀 필드 목록 조회 + */ + public function getCustomFields(int $tenantId, array $filters = []): Collection + { + $query = ItemField::where('tenant_id', $tenantId) + ->where('storage_type', 'json'); + + // 소스 테이블 필터 + 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}%"); + }); + } + + return $query->orderBy('source_table') + ->orderBy('order_no') + ->get(); + } + + /** + * 커스텀 필드 추가 + */ + public function createCustomField(int $tenantId, array $data): array + { + $sourceTable = $data['source_table']; + $fieldKey = $data['field_key']; + + // 시스템 예약어 체크 + $systemFieldKeys = SystemFieldDefinitions::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' => '필드를 찾을 수 없습니다.', + ]; + } + + // 시스템 필드는 삭제 불가 + if ($field->storage_type === 'column') { + return [ + 'success' => false, + 'message' => '시스템 필드는 삭제할 수 없습니다. 초기화 기능을 사용하세요.', + ]; + } + + $field->update(['deleted_by' => auth()->id()]); + $field->delete(); + + 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, + ]; + } +} diff --git a/resources/views/item-fields/index.blade.php b/resources/views/item-fields/index.blade.php new file mode 100644 index 00000000..baefa7b0 --- /dev/null +++ b/resources/views/item-fields/index.blade.php @@ -0,0 +1,784 @@ +@extends('layouts.app') + +@section('title', '품목기준 필드 관리') + +@section('content') +
테넌트별 품목관련 테이블의 필드를 관리합니다. 시스템 필드 시딩 및 커스텀 필드를 추가할 수 있습니다.
+{{ $error }}
+커스텀 필드가 없습니다.
+새 커스텀 필드를 추가해보세요.
+| 소스 테이블 | +필드 키 | +필드명 | +타입 | +필수 | +기본값 | +액션 | +
|---|---|---|---|---|---|---|
| + @if(empty($field->source_table)) + + 미지정 + + @else + + {{ $sourceTables[$field->source_table] ?? $field->source_table }} + + @endif + | +
+ @if(empty($field->field_key))
+ 미지정
+ @else
+ {{ $field->field_key }}
+ @endif
+ |
+ + {{ $field->field_name }} + | ++ @php + $typeLabels = [ + 'textbox' => '텍스트', + 'number' => '숫자', + 'dropdown' => '드롭다운', + 'checkbox' => '체크박스', + 'date' => '날짜', + 'textarea' => '텍스트영역', + ]; + @endphp + + {{ $typeLabels[$field->field_type] ?? $field->field_type }} + + | ++ @if($field->is_required) + + + + @else + - + @endif + | ++ {{ $field->default_value ?? '-' }} + | +
+
+
+
+
+ |
+
오류 없음
+최근 시딩 작업에서 오류가 발생하지 않았습니다.
+{{ $error['field_key'] }}
+ {{ $error }}
+| 소스 테이블 | +시스템 필드 수 | +현재 등록 수 | +상태 | +액션 | +
|---|---|---|---|---|
|
+ {{ $status['label'] }}
+ {{ $sourceTable }}
+ |
+ + {{ $status['total_count'] }}개 + | ++ {{ $status['current_count'] }}개 + | ++ @if($status['status'] === 'complete') + + 완료 + + @elseif($status['status'] === 'partial') + + 일부 + + @else + + 미시딩 + + @endif + | +
+
+ @if($status['status'] !== 'complete')
+
+ @endif
+ @if($status['current_count'] > 0)
+
+ @endif
+
+ |
+