feat: [bending] 절곡품 전용 테이블 분리 API
- bending_items 전용 테이블 생성 (items.options → 정규 컬럼 승격) - bending_models 전용 테이블 생성 (가이드레일/케이스/하단마감재 통합) - bending_data JSON 통합 (별도 테이블 → bending_items.bending_data 컬럼) - bending_item_mappings 테이블 DROP (bending_items.code에 흡수) - BendingItemService/BendingCodeService → BendingItem 모델 전환 - GuiderailModelService component 이미지 자동 복사 - ItemsFileController bending_items/bending_models 폴백 지원 - Swagger 스키마 업데이트
This commit is contained in:
199
app/Console/Commands/MigrateBendingItemsToNewTable.php
Normal file
199
app/Console/Commands/MigrateBendingItemsToNewTable.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\BendingItem;
|
||||
use App\Models\Items\Item;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* items(BENDING) + options JSON → bending_items + bending_data 이관
|
||||
*
|
||||
* 실행: php artisan bending:migrate-to-new-table
|
||||
* 롤백: php artisan bending:migrate-to-new-table --rollback
|
||||
*/
|
||||
class MigrateBendingItemsToNewTable extends Command
|
||||
{
|
||||
protected $signature = 'bending:migrate-to-new-table
|
||||
{--tenant_id=287 : 테넌트 ID}
|
||||
{--dry-run : 실행하지 않고 미리보기만}
|
||||
{--rollback : bending_items/bending_data 전체 삭제}';
|
||||
|
||||
protected $description = 'items(BENDING) → bending_items + bending_data 테이블 이관';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$tenantId = (int) $this->option('tenant_id');
|
||||
$dryRun = $this->option('dry-run');
|
||||
$rollback = $this->option('rollback');
|
||||
|
||||
if ($rollback) {
|
||||
return $this->rollback($tenantId);
|
||||
}
|
||||
|
||||
// 이미 이관된 데이터 확인
|
||||
$existingCount = BendingItem::where('tenant_id', $tenantId)->count();
|
||||
if ($existingCount > 0) {
|
||||
$this->warn("이미 bending_items에 {$existingCount}건 존재합니다.");
|
||||
if (! $this->confirm('기존 데이터 삭제 후 재이관하시겠습니까?')) {
|
||||
return 0;
|
||||
}
|
||||
$this->rollback($tenantId);
|
||||
}
|
||||
|
||||
// items(BENDING) 조회
|
||||
$items = Item::where('item_category', 'BENDING')
|
||||
->where('tenant_id', $tenantId)
|
||||
->get();
|
||||
|
||||
$this->info("이관 대상: {$items->count()}건");
|
||||
|
||||
if ($dryRun) {
|
||||
$this->previewItems($items);
|
||||
return 0;
|
||||
}
|
||||
|
||||
$success = 0;
|
||||
$errors = 0;
|
||||
$bdCount = 0;
|
||||
|
||||
DB::transaction(function () use ($items, $tenantId, &$success, &$errors, &$bdCount) {
|
||||
foreach ($items as $item) {
|
||||
try {
|
||||
$bi = $this->migrateItem($item, $tenantId);
|
||||
$bdRows = $this->migrateBendingData($bi, $item);
|
||||
$bdCount += $bdRows;
|
||||
$success++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error(" ❌ {$item->code}: {$e->getMessage()}");
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$this->newLine();
|
||||
$this->info("완료: {$success}건 이관, {$bdCount}건 전개도 행, {$errors}건 오류");
|
||||
|
||||
return $errors > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
private function migrateItem(Item $item, int $tenantId): BendingItem
|
||||
{
|
||||
$opts = $item->options ?? [];
|
||||
|
||||
// item_name: options.item_name → name 폴백
|
||||
$itemName = $opts['item_name'] ?? null;
|
||||
if (empty($itemName) || $itemName === 'null') {
|
||||
$itemName = $item->name;
|
||||
}
|
||||
|
||||
$bi = BendingItem::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'code' => $item->code,
|
||||
'legacy_code' => $item->code,
|
||||
'legacy_bending_id' => $opts['legacy_bending_num'] ?? null,
|
||||
// 정규 컬럼 (options에서 승격)
|
||||
'item_name' => $itemName,
|
||||
'item_sep' => $this->cleanNull($opts['item_sep'] ?? null),
|
||||
'item_bending' => $this->cleanNull($opts['item_bending'] ?? null),
|
||||
'material' => $this->cleanNull($opts['material'] ?? null),
|
||||
'item_spec' => $this->cleanNull($opts['item_spec'] ?? null),
|
||||
'model_name' => $this->cleanNull($opts['model_name'] ?? null),
|
||||
'model_UA' => $this->cleanNull($opts['model_UA'] ?? null),
|
||||
'rail_width' => $this->toDecimal($opts['rail_width'] ?? null),
|
||||
'exit_direction' => $this->cleanNull($opts['exit_direction'] ?? null),
|
||||
'box_width' => $this->toDecimal($opts['box_width'] ?? null),
|
||||
'box_height' => $this->toDecimal($opts['box_height'] ?? null),
|
||||
'front_bottom' => $this->toDecimal($opts['front_bottom_width'] ?? $opts['front_bottom'] ?? null),
|
||||
'inspection_door' => $this->cleanNull($opts['inspection_door'] ?? null),
|
||||
// 비정형 속성
|
||||
'options' => $this->buildMetaOptions($opts),
|
||||
'is_active' => $item->is_active,
|
||||
'created_by' => $item->created_by,
|
||||
'updated_by' => $item->updated_by,
|
||||
]);
|
||||
|
||||
$this->line(" ✅ {$item->code} → bending_items#{$bi->id} ({$itemName})");
|
||||
|
||||
return $bi;
|
||||
}
|
||||
|
||||
private function migrateBendingData(BendingItem $bi, Item $item): int
|
||||
{
|
||||
$opts = $item->options ?? [];
|
||||
$bendingData = $opts['bendingData'] ?? [];
|
||||
|
||||
if (empty($bendingData) || ! is_array($bendingData)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// bending_items.bending_data JSON 컬럼에 저장
|
||||
$bi->update(['bending_data' => $bendingData]);
|
||||
|
||||
return count($bendingData);
|
||||
}
|
||||
|
||||
private function rollback(int $tenantId): int
|
||||
{
|
||||
$biCount = BendingItem::where('tenant_id', $tenantId)->count();
|
||||
BendingItem::where('tenant_id', $tenantId)->forceDelete();
|
||||
$this->info("롤백 완료: bending_items {$biCount}건 삭제");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function previewItems($items): void
|
||||
{
|
||||
$headers = ['code', 'name', 'item_name(opts)', 'item_sep', 'material', 'has_bd'];
|
||||
$rows = $items->take(20)->map(function ($item) {
|
||||
$opts = $item->options ?? [];
|
||||
return [
|
||||
$item->code,
|
||||
mb_substr($item->name, 0, 20),
|
||||
mb_substr($opts['item_name'] ?? '(NULL)', 0, 20),
|
||||
$opts['item_sep'] ?? '-',
|
||||
$opts['material'] ?? '-',
|
||||
! empty($opts['bendingData']) ? '✅' : '❌',
|
||||
];
|
||||
});
|
||||
$this->table($headers, $rows);
|
||||
|
||||
$nullNameCount = $items->filter(fn ($i) => empty(($i->options ?? [])['item_name']))->count();
|
||||
$hasBdCount = $items->filter(fn ($i) => ! empty(($i->options ?? [])['bendingData']))->count();
|
||||
$this->info("item_name NULL: {$nullNameCount}건 (name 필드로 폴백)");
|
||||
$this->info("bendingData 있음: {$hasBdCount}건");
|
||||
}
|
||||
|
||||
private function cleanNull(?string $value): ?string
|
||||
{
|
||||
if ($value === null || $value === 'null' || $value === '') {
|
||||
return null;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function toDecimal(mixed $value): ?float
|
||||
{
|
||||
if ($value === null || $value === 'null' || $value === '') {
|
||||
return null;
|
||||
}
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* options에 남길 비정형 속성만 추출
|
||||
*/
|
||||
private function buildMetaOptions(array $opts): ?array
|
||||
{
|
||||
$metaKeys = ['search_keyword', 'registration_date', 'author', 'memo', 'parent_num', 'modified_by'];
|
||||
$meta = [];
|
||||
foreach ($metaKeys as $key) {
|
||||
$val = $opts[$key] ?? null;
|
||||
if ($val !== null && $val !== 'null' && $val !== '') {
|
||||
$meta[$key] = $val;
|
||||
}
|
||||
}
|
||||
return empty($meta) ? null : $meta;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user