- BendingItemController: CRUD + filters 엔드포인트 (pagination 메타 보존) - BendingItemService: items 테이블 item_category=BENDING 필터 기반 - BendingItemResource: options → 최상위 필드 노출 + 계산값(width_sum, bend_count) - FormRequest: Index/Store/Update 유효성 검증 (unique:items,code 포함) - BendingFillOptions: BD-* prefix/분류 속성 자동 보강 커맨드 - BendingImportLegacy: chandj 레거시 전개도(bendingData) 임포트 커맨드 (125/170건 매칭) - ensureContext: Bearer 토큰 없이 X-TENANT-ID 헤더로 컨텍스트 설정
354 lines
15 KiB
PHP
354 lines
15 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Attributes\AsCommand;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* BD-* 품목의 options 속성 보강
|
|
*
|
|
* 1단계: BD-PREFIX-LEN 패턴(112건)에서 prefix/length 자동 추출
|
|
* 2단계: BD-한글 패턴(58건)에 item_sep/item_bending 등 분류 속성 추가
|
|
*
|
|
* 실행: php artisan bending:fill-options [--dry-run] [--tenant_id=287]
|
|
*/
|
|
#[AsCommand(name: 'bending:fill-options', description: 'BD-* 품목 options 속성 보강 (prefix/length + 분류 속성)')]
|
|
class BendingFillOptions extends Command
|
|
{
|
|
protected $signature = 'bending:fill-options
|
|
{--tenant_id=287 : Target tenant ID}
|
|
{--dry-run : 실제 저장 없이 미리보기}';
|
|
|
|
// PREFIX → 분류 속성 매핑
|
|
private const PREFIX_META = [
|
|
// 가이드레일 (벽면)
|
|
'RS' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'SUS마감재', 'material' => 'SUS 1.2T'],
|
|
'RE' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'EGI마감재', 'material' => 'EGI 1.55T'],
|
|
'RM' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => '본체', 'material' => 'EGI 1.55T'],
|
|
'RC' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'C형', 'material' => 'EGI 1.55T'],
|
|
'RD' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'D형', 'material' => 'EGI 1.55T'],
|
|
'RT' => ['item_sep' => '철재', 'item_bending' => '가이드레일', 'item_name' => '본체(철재)', 'material' => 'EGI 1.55T'],
|
|
// 가이드레일 (측면)
|
|
'SS' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'SUS마감재', 'material' => 'SUS 1.2T'],
|
|
'SE' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'EGI마감재', 'material' => 'EGI 1.55T'],
|
|
'SM' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => '본체', 'material' => 'EGI 1.55T'],
|
|
'SC' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'C형', 'material' => 'EGI 1.55T'],
|
|
'SD' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'D형', 'material' => 'EGI 1.55T'],
|
|
'ST' => ['item_sep' => '철재', 'item_bending' => '가이드레일', 'item_name' => '본체(철재)', 'material' => 'EGI 1.55T'],
|
|
'SU' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'SUS마감재2', 'material' => 'SUS 1.2T'],
|
|
// 하단마감재
|
|
'BE' => ['item_sep' => '스크린', 'item_bending' => '하단마감재', 'item_name' => '하단마감재', 'material' => 'EGI 1.55T'],
|
|
'BS' => ['item_sep' => '스크린', 'item_bending' => '하단마감재', 'item_name' => '하단마감재', 'material' => 'SUS 1.5T'],
|
|
'TS' => ['item_sep' => '철재', 'item_bending' => '하단마감재', 'item_name' => '하단마감재(철재)', 'material' => 'SUS 1.2T'],
|
|
'LA' => ['item_sep' => '스크린', 'item_bending' => 'L-BAR', 'item_name' => 'L-Bar', 'material' => 'EGI 1.55T'],
|
|
'HH' => ['item_sep' => '스크린', 'item_bending' => '보강평철', 'item_name' => '보강평철', 'material' => 'EGI 1.55T'],
|
|
// 셔터박스
|
|
'CF' => ['item_sep' => '스크린', 'item_bending' => '케이스', 'item_name' => '전면부', 'material' => 'EGI 1.55T'],
|
|
'CL' => ['item_sep' => '스크린', 'item_bending' => '케이스', 'item_name' => '린텔부', 'material' => 'EGI 1.55T'],
|
|
'CP' => ['item_sep' => '스크린', 'item_bending' => '케이스', 'item_name' => '점검구', 'material' => 'EGI 1.55T'],
|
|
'CB' => ['item_sep' => '스크린', 'item_bending' => '케이스', 'item_name' => '후면코너부', 'material' => 'EGI 1.55T'],
|
|
// 연기차단재
|
|
'GI' => ['item_sep' => '스크린', 'item_bending' => '연기차단재', 'item_name' => '연기차단재', 'material' => '화이바원단'],
|
|
// 공용
|
|
'XX' => ['item_sep' => '스크린', 'item_bending' => '공용', 'item_name' => '하부BASE/상부덮개/마구리', 'material' => 'EGI 1.55T'],
|
|
'YY' => ['item_sep' => '스크린', 'item_bending' => '별도마감', 'item_name' => '별도SUS마감', 'material' => 'SUS 1.2T'],
|
|
];
|
|
|
|
// 한글 패턴 → 분류 매핑
|
|
private const KOREAN_PATTERN_META = [
|
|
'BD-가이드레일' => ['item_sep' => null, 'item_bending' => '가이드레일'],
|
|
'BD-케이스' => ['item_sep' => null, 'item_bending' => '케이스'],
|
|
'BD-마구리' => ['item_sep' => null, 'item_bending' => '마구리'],
|
|
'BD-하단마감재' => ['item_sep' => null, 'item_bending' => '하단마감재'],
|
|
'BD-L-BAR' => ['item_sep' => '스크린', 'item_bending' => 'L-BAR'],
|
|
'BD-보강평철' => ['item_sep' => '스크린', 'item_bending' => '보강평철'],
|
|
];
|
|
|
|
private const LENGTH_MAP = [
|
|
'02' => 200, '12' => 1219, '24' => 2438, '30' => 3000,
|
|
'35' => 3500, '40' => 4000, '41' => 4150, '42' => 4200,
|
|
'43' => 4300, '53' => 3000, '54' => 4000, '83' => 3000, '84' => 4000,
|
|
];
|
|
|
|
private array $stats = [
|
|
'total' => 0,
|
|
'prefix_len_filled' => 0,
|
|
'korean_filled' => 0,
|
|
'already_complete' => 0,
|
|
'unknown_pattern' => 0,
|
|
];
|
|
|
|
public function handle(): int
|
|
{
|
|
$tenantId = (int) $this->option('tenant_id');
|
|
$dryRun = $this->option('dry-run');
|
|
|
|
$this->info('=== BD-* 품목 options 보강 ===');
|
|
$this->info('Tenant: '.$tenantId.' | Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE'));
|
|
$this->newLine();
|
|
|
|
// BD-* 전체 품목 조회
|
|
$items = DB::table('items')
|
|
->where('tenant_id', $tenantId)
|
|
->where('code', 'like', 'BD-%')
|
|
->whereNull('deleted_at')
|
|
->select('id', 'code', 'name', 'options')
|
|
->orderBy('code')
|
|
->get();
|
|
|
|
$this->stats['total'] = $items->count();
|
|
$this->info("BD-* 품목: {$items->count()}건");
|
|
$this->newLine();
|
|
|
|
foreach ($items as $item) {
|
|
$options = json_decode($item->options ?? '{}', true) ?: [];
|
|
$code = $item->code;
|
|
$newOptions = $this->resolveOptions($code, $item->name, $options);
|
|
|
|
if ($newOptions === null) {
|
|
$this->stats['unknown_pattern']++;
|
|
$this->warn(" ❓ 미인식 패턴: {$code}");
|
|
|
|
continue;
|
|
}
|
|
|
|
// 변경 필요 여부 확인
|
|
$merged = array_merge($options, $newOptions);
|
|
if ($merged == $options) {
|
|
$this->stats['already_complete']++;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (! $dryRun) {
|
|
$encoded = json_encode($merged, JSON_UNESCAPED_UNICODE);
|
|
if ($encoded === false) {
|
|
$this->error(" ❌ JSON 인코딩 실패: {$code} — ".json_last_error_msg());
|
|
|
|
continue;
|
|
}
|
|
DB::table('items')
|
|
->where('id', $item->id)
|
|
->update([
|
|
'options' => $encoded,
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
$pattern = $this->detectPattern($code);
|
|
if ($pattern === 'prefix_len') {
|
|
$this->stats['prefix_len_filled']++;
|
|
} else {
|
|
$this->stats['korean_filled']++;
|
|
}
|
|
|
|
$this->line(" ✅ {$code}: +".implode(', ', array_keys($newOptions)));
|
|
}
|
|
|
|
$this->showStats($dryRun);
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* 코드에서 options 속성 추출
|
|
*/
|
|
private function resolveOptions(string $code, string $name, array $existing): ?array
|
|
{
|
|
$new = [];
|
|
|
|
// item_category 보장
|
|
if (empty($existing['item_category'])) {
|
|
// item_category는 items 테이블 컬럼이므로 여기서는 skip
|
|
}
|
|
|
|
// 패턴 A: BD-PREFIX-LEN (예: BD-RS-30)
|
|
if (preg_match('/^BD-([A-Z]{2})-(\d{2})$/', $code, $m)) {
|
|
$prefix = $m[1];
|
|
$lengthCode = $m[2];
|
|
|
|
// prefix/length 기본값
|
|
if (empty($existing['prefix'])) {
|
|
$new['prefix'] = $prefix;
|
|
}
|
|
if (empty($existing['length_code'])) {
|
|
$new['length_code'] = $lengthCode;
|
|
}
|
|
if (empty($existing['length_mm']) && isset(self::LENGTH_MAP[$lengthCode])) {
|
|
$new['length_mm'] = self::LENGTH_MAP[$lengthCode];
|
|
}
|
|
|
|
// PREFIX 기반 분류 속성
|
|
$meta = self::PREFIX_META[$prefix] ?? null;
|
|
if ($meta) {
|
|
foreach ($meta as $key => $value) {
|
|
if (empty($existing[$key])) {
|
|
$new[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $new;
|
|
}
|
|
|
|
// 특수 코드 (패턴 미준수)
|
|
$specialCodes = [
|
|
'BD-가이드레일용 연기차단재' => ['item_bending' => '연기차단재'],
|
|
'BD-케이스용 연기차단재' => ['item_bending' => '연기차단재'],
|
|
];
|
|
if (isset($specialCodes[$code])) {
|
|
foreach ($specialCodes[$code] as $key => $value) {
|
|
if (empty($existing[$key])) {
|
|
$new[$key] = $value;
|
|
}
|
|
}
|
|
|
|
return $new;
|
|
}
|
|
|
|
// 패턴 B~G: 한글 패턴
|
|
foreach (self::KOREAN_PATTERN_META as $patternPrefix => $meta) {
|
|
// 정확한 접두사+구분자 매칭 (BD-케이스-xxx는 O, BD-케이스용xxx는 X)
|
|
if ($code === $patternPrefix || str_starts_with($code, $patternPrefix.'-')) {
|
|
// 분류 속성
|
|
foreach ($meta as $key => $value) {
|
|
if ($value !== null && empty($existing[$key])) {
|
|
$new[$key] = $value;
|
|
}
|
|
}
|
|
|
|
// 한글 패턴별 추가 파싱
|
|
$this->parseKoreanPattern($code, $patternPrefix, $existing, $new);
|
|
|
|
return $new;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 한글 패턴에서 모델/재질/규격 추출
|
|
*/
|
|
private function parseKoreanPattern(string $code, string $patternPrefix, array $existing, array &$new): void
|
|
{
|
|
$suffix = substr($code, strlen($patternPrefix) + 1); // "-" 제거
|
|
$parts = explode('-', $suffix);
|
|
|
|
switch ($patternPrefix) {
|
|
case 'BD-가이드레일':
|
|
// BD-가이드레일-KSS01-SUS-120*70
|
|
if (count($parts) >= 3) {
|
|
if (empty($existing['model_name'])) {
|
|
$new['model_name'] = $parts[0];
|
|
}
|
|
if (empty($existing['material'])) {
|
|
$material = $parts[1];
|
|
$new['material'] = str_contains($material, 'SUS') ? 'SUS 1.2T' : 'EGI 1.55T';
|
|
}
|
|
if (empty($existing['item_spec'])) {
|
|
$new['item_spec'] = $parts[2];
|
|
}
|
|
// item_sep 추론 (KTE → 철재)
|
|
if (empty($existing['item_sep'])) {
|
|
$new['item_sep'] = str_starts_with($parts[0], 'KTE') ? '철재' : '스크린';
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'BD-하단마감재':
|
|
// BD-하단마감재-KSS01-SUS-60*40
|
|
if (count($parts) >= 3) {
|
|
if (empty($existing['model_name'])) {
|
|
$new['model_name'] = $parts[0];
|
|
}
|
|
if (empty($existing['material'])) {
|
|
$material = $parts[1];
|
|
$new['material'] = str_contains($material, 'SUS') ? 'SUS 1.5T' : 'EGI 1.55T';
|
|
}
|
|
if (empty($existing['item_spec'])) {
|
|
$new['item_spec'] = $parts[2];
|
|
}
|
|
if (empty($existing['item_sep'])) {
|
|
$new['item_sep'] = str_starts_with($parts[0], 'KTE') ? '철재' : '스크린';
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'BD-케이스':
|
|
// BD-케이스-650*550
|
|
if (count($parts) >= 1 && ! empty($parts[0])) {
|
|
if (empty($existing['item_spec'])) {
|
|
$new['item_spec'] = $parts[0];
|
|
}
|
|
// 케이스는 대부분 철재
|
|
if (empty($existing['item_sep'])) {
|
|
$new['item_sep'] = '철재';
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'BD-마구리':
|
|
// BD-마구리-655*505
|
|
if (count($parts) >= 1 && ! empty($parts[0])) {
|
|
if (empty($existing['item_spec'])) {
|
|
$new['item_spec'] = $parts[0];
|
|
}
|
|
if (empty($existing['item_sep'])) {
|
|
$new['item_sep'] = '철재';
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'BD-L-BAR':
|
|
// BD-L-BAR-KSS01-17*60
|
|
if (count($parts) >= 2) {
|
|
if (empty($existing['model_name'])) {
|
|
$new['model_name'] = $parts[0];
|
|
}
|
|
if (empty($existing['item_spec'])) {
|
|
$new['item_spec'] = $parts[1];
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'BD-보강평철':
|
|
// BD-보강평철-50
|
|
if (count($parts) >= 1 && ! empty($parts[0])) {
|
|
if (empty($existing['item_spec'])) {
|
|
$new['item_spec'] = $parts[0];
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private function detectPattern(string $code): string
|
|
{
|
|
return preg_match('/^BD-[A-Z]{2}-\d{2}$/', $code) ? 'prefix_len' : 'korean';
|
|
}
|
|
|
|
private function showStats(bool $dryRun): void
|
|
{
|
|
$this->newLine();
|
|
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
$this->info('📊 결과'.($dryRun ? ' (DRY-RUN)' : ''));
|
|
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
$this->info(" 전체 BD-* 품목: {$this->stats['total']}건");
|
|
$this->info(" PREFIX-LEN 업데이트: {$this->stats['prefix_len_filled']}건");
|
|
$this->info(" 한글 패턴 업데이트: {$this->stats['korean_filled']}건");
|
|
$this->info(" 이미 완료: {$this->stats['already_complete']}건");
|
|
if ($this->stats['unknown_pattern'] > 0) {
|
|
$this->warn(" 미인식 패턴: {$this->stats['unknown_pattern']}건");
|
|
}
|
|
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
|
|
if ($dryRun) {
|
|
$this->newLine();
|
|
$this->info('🔍 DRY-RUN 완료. --dry-run 제거하면 실제 반영됩니다.');
|
|
}
|
|
}
|
|
}
|