tenantId = (int) $this->option('tenant_id'); $dryRun = $this->option('dry-run'); $existingNums = BendingItem::where('tenant_id', $this->tenantId) ->whereNotNull('legacy_bending_id') ->pluck('legacy_bending_id') ->toArray(); $missing = DB::connection('chandj')->table('bending') ->where(function ($q) { $q->whereNull('is_deleted')->orWhere('is_deleted', 0); }) ->whereNotIn('num', $existingNums) ->orderBy('num') ->get(); $this->info("누락분: {$missing->count()}건 (이미 매핑: " . count($existingNums) . "건)"); if ($dryRun) { $this->preview($missing); return 0; } $success = 0; $bdCount = 0; $errors = 0; DB::transaction(function () use ($missing, &$success, &$bdCount, &$errors) { foreach ($missing as $row) { try { $bi = $this->importItem($row); $bd = $this->importBendingData($bi, $row); $bdCount += $bd; $success++; } catch (\Throwable $e) { $this->error(" ❌ #{$row->num} {$row->itemName}: {$e->getMessage()}"); $errors++; } } }); $this->newLine(); $this->info("완료: {$success}건 이관, {$bdCount}건 전개도, {$errors}건 오류"); return $errors > 0 ? 1 : 0; } private function importItem(object $row): BendingItem { $code = $this->generateCode($row); $bi = BendingItem::create([ 'tenant_id' => $this->tenantId, 'code' => $code, 'legacy_code' => "CHANDJ-{$row->num}", 'legacy_bending_id' => $row->num, 'item_name' => $row->itemName ?: "부품#{$row->num}", 'item_sep' => $this->clean($row->item_sep), 'item_bending' => $this->clean($row->item_bending), 'material' => $this->clean($row->material), 'item_spec' => $this->clean($row->item_spec), 'model_name' => $this->clean($row->model_name ?? null), 'model_UA' => $this->clean($row->model_UA ?? null), 'rail_width' => $this->toNum($row->rail_width ?? null), 'exit_direction' => $this->clean($row->exit_direction ?? null), 'box_width' => $this->toNum($row->box_width ?? null), 'box_height' => $this->toNum($row->box_height ?? null), 'front_bottom' => $this->toNum($row->front_bottom_width ?? null), 'options' => $this->buildOptions($row), 'is_active' => true, 'created_by' => 1, ]); $this->line(" ✅ #{$row->num} → {$bi->id} ({$row->itemName}) [{$code}]"); return $bi; } private function importBendingData(BendingItem $bi, object $row): int { $inputs = json_decode($row->inputList ?? '[]', true) ?: []; if (empty($inputs)) { return 0; } $rates = json_decode($row->bendingrateList ?? '[]', true) ?: []; $sums = json_decode($row->sumList ?? '[]', true) ?: []; $colors = json_decode($row->colorList ?? '[]', true) ?: []; $angles = json_decode($row->AList ?? '[]', true) ?: []; $count = count($inputs); for ($i = 0; $i < $count; $i++) { $input = (float) ($inputs[$i] ?? 0); $rate = (string) ($rates[$i] ?? ''); $afterRate = ($rate !== '') ? $input + (float) $rate : $input; BendingDataRow::create([ 'bending_item_id' => $bi->id, 'sort_order' => $i + 1, 'input' => $input, 'rate' => $rate !== '' ? $rate : null, 'after_rate' => $afterRate, 'sum' => (float) ($sums[$i] ?? 0), 'color' => (bool) ($colors[$i] ?? false), 'a_angle' => (bool) ($angles[$i] ?? false), ]); } return $count; } private function generateCode(object $row): string { $bending = $row->item_bending ?? ''; $sep = $row->item_sep ?? ''; $material = $row->material ?? ''; $name = $row->itemName ?? ''; $prodCode = match (true) { $bending === '케이스' => 'C', $bending === '하단마감재' && str_contains($sep, '철재') => 'T', $bending === '하단마감재' => 'B', $bending === '가이드레일' && str_contains($sep, '철재') => 'R', $bending === '가이드레일' => 'R', $bending === '마구리' => 'X', $bending === 'L-BAR' => 'L', $bending === '연기차단재' => 'G', default => 'Z', }; $specCode = match (true) { str_contains($name, '전면') => 'F', str_contains($name, '린텔') => 'L', str_contains($name, '점검') => 'P', str_contains($name, '후면') => 'B', str_contains($name, '상부') || str_contains($name, '덮개') => 'X', str_contains($name, '본체') => 'M', str_contains($name, 'C형') || str_contains($name, '-C') => 'C', str_contains($name, 'D형') || str_contains($name, '-D') => 'D', str_contains($name, '마감') && str_contains($material, 'SUS') => 'S', str_contains($material, 'SUS') => 'S', str_contains($material, 'EGI') => 'E', default => 'Z', }; $date = $row->registration_date ?? now()->format('Y-m-d'); $dateCode = date('ymd', strtotime($date)); $base = "{$prodCode}{$specCode}{$dateCode}"; // 중복 방지 일련번호 $seq = 1; while (BendingItem::where('tenant_id', $this->tenantId) ->where('code', $seq === 1 ? $base : "{$base}-" . str_pad($seq, 2, '0', STR_PAD_LEFT)) ->whereNull('length_code') ->exists()) { $seq++; } return $seq === 1 ? $base : "{$base}-" . str_pad($seq, 2, '0', STR_PAD_LEFT); } private function buildOptions(object $row): ?array { $opts = []; if (! empty($row->memo)) $opts['memo'] = $row->memo; if (! empty($row->author)) $opts['author'] = $row->author; if (! empty($row->search_keyword)) $opts['search_keyword'] = $row->search_keyword; if (! empty($row->registration_date)) $opts['registration_date'] = (string) $row->registration_date; return empty($opts) ? null : $opts; } private function preview($missing): void { $grouped = $missing->groupBy(fn ($r) => ($r->item_bending ?: '미분류') . '/' . ($r->item_sep ?: '미분류')); $this->table(['분류', '건수'], $grouped->map(fn ($g, $k) => [$k, $g->count()])->values()); $this->newLine(); $headers = ['num', 'itemName', 'item_sep', 'item_bending', 'material', 'has_bd']; $rows = $missing->take(15)->map(fn ($r) => [ $r->num, mb_substr($r->itemName ?? '', 0, 25), $r->item_sep ?? '-', $r->item_bending ?? '-', mb_substr($r->material ?? '-', 0, 12), ! empty(json_decode($r->inputList ?? '[]', true)) ? '✅' : '❌', ]); $this->table($headers, $rows); } private function clean(?string $v): ?string { return ($v === null || $v === '' || $v === 'null') ? null : trim($v); } private function toNum(mixed $v): ?float { return ($v === null || $v === '' || $v === 'null') ? null : (float) $v; } }