- TenantSetting CRUD API 추가 - Calendar, Entertainment, VAT 서비스 개선 - 5130 BOM 계산 로직 수정 - quote_items에 item_type 컬럼 추가 - tenant_settings 테이블 마이그레이션 - Swagger 문서 업데이트
236 lines
7.8 KiB
PHP
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';
|
|
}
|
|
}
|
|
}
|