refactor: 절곡 재고 마이그레이션 커맨드 리팩토링 및 검증/시더 추가

- Migrate5130BendingStock: BD-* 품목 초기 재고 셋팅으로 목적 변경, --min-stock 옵션 추가
- ValidateBendingItems: BD-* 품목 존재 여부 검증 커맨드 신규
- BendingItemSeeder: 경동 절곡 품목 시딩 신규

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 03:04:47 +09:00
parent b00fa0502a
commit 855e806e42
3 changed files with 642 additions and 496 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,137 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Attributes\AsCommand;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
#[AsCommand(name: 'bending:validate-items', description: 'BD-* 절곡 세부품목 마스터 데이터 검증 (prefix × lengthCode 전 조합)')]
class ValidateBendingItems extends Command
{
protected $signature = 'bending:validate-items
{--tenant_id=287 : Target tenant ID (default: 287 경동기업)}';
/**
* prefix별 유효 길이코드 정의
*
* 가이드레일: 30, 35, 40, 43 (벽면/측면 공통)
* 하단마감재: 30, 40
* 셔터박스: 12, 24, 30, 35, 40, 41
* 연기차단재: 53, 54, 83, 84 (W50/W80 전용 코드)
* XX: 12, 24, 30, 35, 40, 41, 43 (하부BASE + 셔터 상부/마구리)
* YY: 30, 35, 40, 43 (별도 SUS 마감)
* HH: 30, 40 (보강평철)
*/
private function getPrefixLengthCodes(): array
{
$guideRailCodes = ['30', '35', '40', '43'];
$guideRailCodesWithExtra = ['24', '30', '35', '40', '43']; // RT/ST는 적은 종류
$bottomBarCodes = ['30', '40'];
$shutterBoxCodes = ['12', '24', '30', '35', '40', '41'];
return [
// 가이드레일 벽면형
'RS' => $guideRailCodes, // 벽면 SUS 마감재
'RM' => ['24', '30', '35', '40', '42', '43'], // 벽면 본체 (EGI)
'RC' => ['24', '30', '35', '40', '42', '43'], // 벽면 C형
'RD' => ['24', '30', '35', '40', '42', '43'], // 벽면 D형
'RT' => ['30', '43'], // 벽면 본체 (철재)
// 가이드레일 측면형
'SS' => ['30', '35', '40'], // 측면 SUS 마감재
'SM' => ['24', '30', '35', '40', '43'], // 측면 본체 (EGI)
'SC' => ['24', '30', '35', '40', '43'], // 측면 C형
'SD' => ['24', '30', '35', '40', '43'], // 측면 D형
'ST' => ['43'], // 측면 본체 (철재)
'SU' => ['30', '35', '40', '43'], // 측면 SUS (SUS2)
// 하단마감재
'BE' => $bottomBarCodes, // EGI 마감
'BS' => ['24', '30', '35', '40', '43'], // SUS 마감
'TS' => ['43'], // 철재 SUS
'LA' => $bottomBarCodes, // L-Bar
// 셔터박스
'CF' => $shutterBoxCodes, // 전면부
'CL' => $shutterBoxCodes, // 린텔부
'CP' => $shutterBoxCodes, // 점검구
'CB' => $shutterBoxCodes, // 후면코너부
// 연기차단재
'GI' => ['53', '54', '83', '84', '30', '35', '40'], // W50/W80 + 일반
// 공용/기타
'XX' => ['12', '24', '30', '35', '40', '41', '43'], // 하부BASE/셔터 상부/마구리
'YY' => ['30', '35', '40', '43'], // 별도 SUS 마감
'HH' => ['30', '40'], // 보강평철
];
}
public function handle(): int
{
$tenantId = (int) $this->option('tenant_id');
$this->info("=== BD-* 절곡 세부품목 마스터 검증 (tenant: {$tenantId}) ===");
$this->newLine();
// DB에서 전체 BD-* 품목 조회
$existingItems = DB::table('items')
->where('tenant_id', $tenantId)
->where('code', 'like', 'BD-%')
->whereNull('deleted_at')
->pluck('code')
->toArray();
$existingSet = array_flip($existingItems);
$this->info('현재 등록된 BD-* 품목: '.count($existingItems).'개');
$this->newLine();
$prefixMap = $this->getPrefixLengthCodes();
$totalExpected = 0;
$missing = [];
$found = 0;
foreach ($prefixMap as $prefix => $codes) {
$prefixMissing = [];
foreach ($codes as $code) {
$itemCode = "BD-{$prefix}-{$code}";
$totalExpected++;
if (isset($existingSet[$itemCode])) {
$found++;
} else {
$prefixMissing[] = $itemCode;
$missing[] = $itemCode;
}
}
$status = empty($prefixMissing) ? '✅' : '❌';
$countStr = count($codes) - count($prefixMissing).'/'.count($codes);
$this->line(" {$status} BD-{$prefix}: {$countStr}");
if (! empty($prefixMissing)) {
foreach ($prefixMissing as $m) {
$this->line(" ⚠️ 누락: {$m}");
}
}
}
$this->newLine();
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
$this->info("검증 결과: {$found}/{$totalExpected} 등록 완료");
if (empty($missing)) {
$this->info('✅ All items registered — 누락 0건');
return self::SUCCESS;
}
$this->warn('❌ 누락 항목: '.count($missing).'건');
$this->newLine();
$this->table(['누락 품목코드'], array_map(fn ($m) => [$m], $missing));
return self::FAILURE;
}
}