Files
sam-api/app/Console/Commands/Migrate5130Bom.php
권혁성 6d05ab815f feat:테넌트설정 API 및 다수 서비스 개선
- TenantSetting CRUD API 추가
- Calendar, Entertainment, VAT 서비스 개선
- 5130 BOM 계산 로직 수정
- quote_items에 item_type 컬럼 추가
- tenant_settings 테이블 마이그레이션
- Swagger 문서 업데이트
2026-01-26 20:29:22 +09:00

236 lines
7.8 KiB
PHP

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
/**
* 5130 마이그레이션 완제품에 BOM 데이터 적용 커맨드
*
* 5130에서 마이그레이션된 완제품(FG) 중 BOM이 NULL인 항목에
* 기존 SAM BOM 템플릿을 적용합니다.
*
* Usage:
* php artisan migration:migrate-5130-bom --dry-run # 시뮬레이션
* php artisan migration:migrate-5130-bom # 실제 실행
* php artisan migration:migrate-5130-bom --code=S0001 # 특정 품목만
* php artisan migration:migrate-5130-bom --category=SCREEN # 특정 카테고리만
*/
class Migrate5130Bom extends Command
{
protected $signature = 'migration:migrate-5130-bom
{--dry-run : 실제 변경 없이 시뮬레이션}
{--code= : 특정 품목 코드만 처리}
{--category= : 특정 카테고리만 처리 (SCREEN|STEEL|BENDING)}
{--tenant-id=1 : 테넌트 ID}';
protected $description = '5130 마이그레이션 완제품에 BOM 템플릿 적용';
/**
* 카테고리별 BOM 템플릿 소스 (기존 SAM 완제품 코드)
*/
private array $templateSources = [
'SCREEN' => 'FG-SCR-001',
'STEEL' => 'FG-STL-001',
'BENDING' => 'FG-BND-001',
];
/**
* 로드된 BOM 템플릿 캐시
*/
private array $bomTemplates = [];
public function handle(): int
{
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
$this->info(' 5130 → SAM BOM 마이그레이션');
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
$this->newLine();
$dryRun = $this->option('dry-run');
$code = $this->option('code');
$category = $this->option('category');
$tenantId = (int) $this->option('tenant-id');
if ($dryRun) {
$this->warn('🔍 DRY-RUN 모드: 실제 변경 없이 시뮬레이션합니다.');
$this->newLine();
}
// 1. BOM 템플릿 로드
$this->info('📥 Step 1: BOM 템플릿 로드');
if (! $this->loadBomTemplates($tenantId)) {
$this->error('BOM 템플릿 로드 실패');
return Command::FAILURE;
}
// 2. 대상 품목 조회
$this->info('📥 Step 2: 대상 품목 조회');
$items = $this->getTargetItems($tenantId, $code, $category);
if ($items->isEmpty()) {
$this->info('✅ 처리할 대상 품목이 없습니다.');
return Command::SUCCESS;
}
$this->info(" 대상 품목 수: {$items->count()}");
$this->newLine();
// 3. BOM 적용
$this->info('🔄 Step 3: BOM 적용');
$stats = [
'total' => $items->count(),
'success' => 0,
'skipped' => 0,
'failed' => 0,
];
$this->output->progressStart($items->count());
foreach ($items as $item) {
$result = $this->applyBomToItem($item, $dryRun);
if ($result === 'success') {
$stats['success']++;
} elseif ($result === 'skipped') {
$stats['skipped']++;
} else {
$stats['failed']++;
}
$this->output->progressAdvance();
}
$this->output->progressFinish();
$this->newLine();
// 4. 결과 출력
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
$this->info('📊 처리 결과');
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
$this->table(
['항목', '건수'],
[
['전체 대상', $stats['total']],
['✅ 성공', $stats['success']],
['⏭️ 스킵 (템플릿 없음)', $stats['skipped']],
['❌ 실패', $stats['failed']],
]
);
if ($dryRun) {
$this->newLine();
$this->warn('🔍 DRY-RUN 모드였습니다. 실제 적용하려면 --dry-run 옵션을 제거하세요.');
}
return Command::SUCCESS;
}
/**
* BOM 템플릿 로드
*/
private function loadBomTemplates(int $tenantId): bool
{
foreach ($this->templateSources as $category => $sourceCode) {
$sourceItem = DB::table('items')
->where('tenant_id', $tenantId)
->where('code', $sourceCode)
->first(['bom']);
if ($sourceItem && $sourceItem->bom) {
$bom = json_decode($sourceItem->bom, true);
if (is_array($bom) && count($bom) > 0) {
$this->bomTemplates[$category] = $sourceItem->bom;
$this->info("{$category}: {$sourceCode} 템플릿 로드됨 (".count($bom).'개 항목)');
} else {
$this->warn(" ⚠️ {$category}: {$sourceCode} BOM이 비어있음");
}
} else {
$this->warn(" ⚠️ {$category}: {$sourceCode} 템플릿 없음");
}
}
$this->newLine();
return count($this->bomTemplates) > 0;
}
/**
* 대상 품목 조회 (BOM이 NULL인 FG 완제품)
*/
private function getTargetItems(int $tenantId, ?string $code, ?string $category)
{
$query = DB::table('items')
->where('tenant_id', $tenantId)
->where('item_type', 'FG')
->whereIn('item_category', array_keys($this->bomTemplates))
->where(function ($q) {
$q->whereNull('bom')
->orWhere('bom', '[]')
->orWhere('bom', 'null')
->orWhereRaw('JSON_LENGTH(bom) = 0');
});
if ($code) {
$query->where('code', $code);
}
if ($category) {
$query->where('item_category', strtoupper($category));
}
return $query->get(['id', 'code', 'name', 'item_category']);
}
/**
* 개별 품목에 BOM 적용
*/
private function applyBomToItem(object $item, bool $dryRun): string
{
$category = $item->item_category;
// 템플릿 확인
if (! isset($this->bomTemplates[$category])) {
if ($this->output->isVerbose()) {
$this->warn(" ⏭️ [{$item->code}] {$item->name} - {$category} 템플릿 없음");
}
return 'skipped';
}
$bomJson = $this->bomTemplates[$category];
if ($dryRun) {
if ($this->output->isVerbose()) {
$this->info(" 📋 [{$item->code}] {$item->name}{$category} 템플릿 적용 예정");
}
return 'success';
}
// 실제 업데이트
try {
DB::table('items')
->where('id', $item->id)
->update([
'bom' => $bomJson,
'updated_at' => now(),
]);
if ($this->output->isVerbose()) {
$this->info(" ✅ [{$item->code}] {$item->name} → BOM 적용 완료");
}
return 'success';
} catch (\Exception $e) {
$this->error(" ❌ [{$item->code}] 오류: ".$e->getMessage());
return 'failed';
}
}
}