Files
sam-api/app/Console/Commands/BendingImportLegacy.php

358 lines
14 KiB
PHP
Raw Normal View History

<?php
namespace App\Console\Commands;
use Illuminate\Console\Attributes\AsCommand;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
/**
* 3단계: chandj.bending SAM items.options 전개도(bendingData) + 속성 임포트
*
* chandj bending 265 SAM items (item_category=BENDING) 170
*
* 매핑 방식:
* A) 한글 패턴 (58): code 파싱으로 item_spec/material 매칭
* B) PREFIX-LEN (112): PREFIX 부품 유형 chandj item_bending+itemName+material 매칭
*
* 실행: php artisan bending:import-legacy [--dry-run] [--tenant_id=287]
*/
#[AsCommand(name: 'bending:import-legacy', description: 'chandj 레거시 전개도(bendingData) + 속성 → SAM items.options 임포트')]
class BendingImportLegacy extends Command
{
protected $signature = 'bending:import-legacy
{--tenant_id=287 : Target tenant ID}
{--dry-run : 실제 저장 없이 미리보기}
{--force : 기존 bendingData 덮어쓰기}';
// PREFIX → chandj 매칭 조건 (item_bending + itemName 패턴 + material)
private const PREFIX_TO_CHANDJ = [
// 가이드레일 (벽면) — item_spec=120*70 (KSS01/02 기준)
'RS' => ['item_bending' => '가이드레일', 'itemName_like' => '%마감%', 'material' => 'SUS 1.2T', 'item_spec' => '120*70'],
'RE' => ['item_bending' => '가이드레일', 'itemName_like' => '%마감%', 'material' => 'EGI 1.55T', 'item_spec' => '120*70'],
'RM' => ['item_bending' => '가이드레일', 'itemName_like' => '%본체%', 'material' => 'EGI 1.55T', 'item_spec' => '120*70'],
'RC' => ['item_bending' => '가이드레일', 'itemName_like' => '%C형%', 'material' => 'EGI 1.55T', 'item_spec' => '120*70'],
'RD' => ['item_bending' => '가이드레일', 'itemName_like' => '%D형%', 'material' => 'EGI 1.55T', 'item_spec' => '120*70'],
'RT' => ['item_bending' => '가이드레일', 'itemName_like' => '%본체%', 'material' => 'EGI 1.55T', 'item_spec' => '130*75'],
// 가이드레일 (측면) — 벽면과 같은 전개도
'SS' => ['same_as' => 'RS'],
'SU' => ['same_as' => 'RS'],
'SM' => ['same_as' => 'RM'],
'SC' => ['same_as' => 'RC'],
'SD' => ['same_as' => 'RD'],
'ST' => ['same_as' => 'RT'],
'SE' => ['same_as' => 'RE'],
// 하단마감재
'BS' => ['item_bending' => '하단마감재', 'itemName_like' => '%하장바%', 'material_like' => '%SUS%', 'item_sep' => '스크린'],
'BE' => ['item_bending' => '하단마감재', 'itemName_like' => '%하장바%', 'material_like' => '%EGI%', 'item_sep' => '스크린'],
'TS' => ['item_bending' => '하단마감재', 'itemName_like' => '%하장바%', 'material_like' => '%SUS%', 'item_sep' => '철재'],
'LA' => ['item_bending' => 'L-BAR', 'itemName_like' => '%L-BAR%', 'material' => 'EGI 1.55T'],
'HH' => ['item_bending' => '하단마감재', 'itemName_like' => '%보강평철%', 'material_like' => '%EGI%'],
// 케이스 — spec 없이 itemName으로 구분
'CF' => ['item_bending' => '케이스', 'itemName_like' => '%전면%', 'item_sep' => '스크린'],
'CL' => ['item_bending' => '케이스', 'itemName_like' => '%린텔%', 'item_sep' => '스크린'],
'CP' => ['item_bending' => '케이스', 'itemName_like' => '%점검%', 'item_sep' => '스크린'],
'CB' => ['item_bending' => '케이스', 'itemName_like' => '%후면%', 'item_sep' => '스크린'],
// 연기차단재
'GI' => ['item_bending' => '연기차단재', 'itemName_like' => '%연기%'],
// 공용
'XX' => null, // 여러 부품이 섞여 있어 자동 매핑 불가
'YY' => null, // 별도 마감 — 자동 매핑 불가
];
private array $stats = [
'total_sam' => 0,
'matched' => 0,
'updated' => 0,
'already_has' => 0,
'no_match' => 0,
'no_bending_data' => 0,
];
private array $unmatchedItems = [];
public function handle(): int
{
$tenantId = (int) $this->option('tenant_id');
$dryRun = $this->option('dry-run');
$force = $this->option('force');
$this->info('=== 3단계: chandj 전개도 → SAM options 임포트 ===');
$this->info('Tenant: '.$tenantId.' | Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE').($force ? ' (FORCE)' : ''));
$this->newLine();
// 1. chandj bending 전체 로드
$chandjRows = DB::connection('chandj')->table('bending')
->whereNull('is_deleted')
->get();
$this->info("chandj bending 활성: {$chandjRows->count()}");
// 2. SAM BENDING items 전체 로드
$samItems = DB::table('items')
->where('tenant_id', $tenantId)
->where('item_category', 'BENDING')
->whereNull('deleted_at')
->orderBy('code')
->get(['id', 'code', 'name', 'options']);
$this->stats['total_sam'] = $samItems->count();
$this->info("SAM BENDING items: {$samItems->count()}");
$this->newLine();
// 3. 매칭 + 임포트
foreach ($samItems as $item) {
$options = json_decode($item->options ?? '{}', true) ?: [];
// 이미 bendingData가 있으면 skip (--force 아닌 경우)
if (! empty($options['bendingData']) && ! $force) {
$this->stats['already_has']++;
continue;
}
// chandj 매칭
$chandjRow = $this->findChandjMatch($item->code, $options, $chandjRows);
if (! $chandjRow) {
$this->stats['no_match']++;
$this->unmatchedItems[] = $item->code;
continue;
}
// bendingData 변환
$bendingData = $this->convertBendingData($chandjRow);
if (empty($bendingData)) {
$this->stats['no_bending_data']++;
continue;
}
// options 업데이트
$updates = ['bendingData' => $bendingData];
// 추가 속성 (비어있으면 채우기)
$optionalFields = [
'memo' => $chandjRow->memo,
'author' => $chandjRow->author,
'search_keyword' => $chandjRow->search_keyword,
'registration_date' => $chandjRow->registration_date,
'model_UA' => $chandjRow->model_UA,
'exit_direction' => $chandjRow->exit_direction,
'front_bottom_width' => $chandjRow->front_bottom_width,
'rail_width' => $chandjRow->rail_width,
'box_width' => $chandjRow->box_width,
'box_height' => $chandjRow->box_height,
'item_spec' => $chandjRow->item_spec,
'legacy_bending_num' => $chandjRow->num,
];
foreach ($optionalFields as $key => $value) {
if (! empty($value) && empty($options[$key])) {
$updates[$key] = $value;
}
}
$merged = array_merge($options, $updates);
if (! $dryRun) {
DB::table('items')->where('id', $item->id)->update([
'options' => json_encode($merged, JSON_UNESCAPED_UNICODE),
'updated_at' => now(),
]);
}
$this->stats['matched']++;
$this->stats['updated']++;
$colCount = count($bendingData);
$this->line("{$item->code} ← chandj#{$chandjRow->num} (전개도 {$colCount}열, +".implode(',', array_keys($updates)).')');
}
$this->showStats($dryRun);
return self::SUCCESS;
}
/**
* SAM item code chandj bending 매칭
*/
private function findChandjMatch(string $code, array $options, $chandjRows): ?object
{
// A) 한글 패턴 — code에서 속성 추출하여 매칭
if (! preg_match('/^BD-[A-Z]{2}-\d{2}$/', $code)) {
return $this->matchKoreanPattern($code, $chandjRows);
}
// B) PREFIX-LEN — PREFIX로 chandj 조건 결정
preg_match('/^BD-([A-Z]{2})-\d{2}$/', $code, $m);
$prefix = $m[1];
$mapping = self::PREFIX_TO_CHANDJ[$prefix] ?? null;
if (! $mapping) {
return null;
}
// same_as 참조
if (isset($mapping['same_as'])) {
$mapping = self::PREFIX_TO_CHANDJ[$mapping['same_as']] ?? null;
if (! $mapping) {
return null;
}
}
return $this->queryChangj($chandjRows, $mapping);
}
/**
* 한글 패턴 매칭
*/
private function matchKoreanPattern(string $code, $chandjRows): ?object
{
// BD-가이드레일-KSS01-SUS-120*70
if (preg_match('/^BD-가이드레일-(\w+)-(\w+)-(.+)$/', $code, $m)) {
$material = str_contains($m[2], 'SUS') ? 'SUS 1.2T' : 'EGI 1.55T';
return $this->queryChangj($chandjRows, [
'item_bending' => '가이드레일',
'material' => $material,
'item_spec' => $m[3],
]);
}
// BD-하단마감재-KSS01-SUS-60*40
if (preg_match('/^BD-하단마감재-(\w+)-(\w+)-(.+)$/', $code, $m)) {
$material = str_contains($m[2], 'SUS') ? 'SUS' : 'EGI';
return $this->queryChangj($chandjRows, [
'item_bending' => '하단마감재',
'material_like' => "%{$material}%",
'item_spec_like' => "%{$m[3]}%",
]);
}
// BD-케이스-650*550
if (preg_match('/^BD-케이스-(\d+)\*(\d+)$/', $code, $m)) {
$spec = $m[1].'*'.$m[2];
return $chandjRows->first(function ($r) use ($spec) {
return $r->item_bending === '케이스'
&& (str_contains($r->itemName, $spec) || $r->item_spec === $spec);
});
}
// BD-마구리-655*505
if (preg_match('/^BD-마구리-(.+)$/', $code, $m)) {
return $chandjRows->first(fn ($r) => $r->item_bending === '마구리' && $r->item_spec === $m[1]);
}
// BD-L-BAR-KSS01-17*60
if (preg_match('/^BD-L-BAR-\w+-(.+)$/', $code, $m)) {
return $chandjRows->first(fn ($r) => $r->item_bending === 'L-BAR' && $r->item_spec === $m[1]);
}
// BD-보강평철-50
if (preg_match('/^BD-보강평철-(.+)$/', $code, $m)) {
return $chandjRows->first(fn ($r) => str_contains($r->itemName, '보강평철') && $r->item_spec === $m[1]);
}
return null;
}
/**
* chandj 컬렉션에서 조건으로 검색
*/
private function queryChangj($rows, array $cond): ?object
{
return $rows->first(function ($r) use ($cond) {
if (isset($cond['item_sep']) && $r->item_sep !== $cond['item_sep']) {
return false;
}
if (isset($cond['item_bending']) && $r->item_bending !== $cond['item_bending']) {
return false;
}
if (isset($cond['material']) && $r->material !== $cond['material']) {
return false;
}
if (isset($cond['material_like']) && ! str_contains($r->material, str_replace('%', '', $cond['material_like']))) {
return false;
}
if (isset($cond['itemName_like']) && ! str_contains($r->itemName, str_replace('%', '', $cond['itemName_like']))) {
return false;
}
if (isset($cond['item_spec']) && $r->item_spec !== $cond['item_spec']) {
return false;
}
if (isset($cond['item_spec_like']) && ! str_contains($r->item_spec ?? '', str_replace('%', '', $cond['item_spec_like']))) {
return false;
}
return true;
});
}
/**
* chandj bending row bendingData JSON 배열 변환
*
* 레거시: inputList=["10","11","110"], bendingrateList=["","-1",""], sumList=["10","21","131"], colorList=[false,false,true], AList=[false,false,false]
* SAM: [{"no":1,"input":10,"rate":"","sum":10,"color":false,"aAngle":false}, ...]
*/
private function convertBendingData(object $row): array
{
$inputs = json_decode($row->inputList ?? '[]', true) ?: [];
$rates = json_decode($row->bendingrateList ?? '[]', true) ?: [];
$sums = json_decode($row->sumList ?? '[]', true) ?: [];
$colors = json_decode($row->colorList ?? '[]', true) ?: [];
$angles = json_decode($row->AList ?? '[]', true) ?: [];
if (empty($inputs)) {
return [];
}
$data = [];
$count = count($inputs);
for ($i = 0; $i < $count; $i++) {
$data[] = [
'no' => $i + 1,
'input' => (float) ($inputs[$i] ?? 0),
'rate' => (string) ($rates[$i] ?? ''),
'sum' => (float) ($sums[$i] ?? 0),
'color' => (bool) ($colors[$i] ?? false),
'aAngle' => (bool) ($angles[$i] ?? false),
];
}
return $data;
}
private function showStats(bool $dryRun): void
{
$this->newLine();
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
$this->info('📊 결과'.($dryRun ? ' (DRY-RUN)' : ''));
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
$this->info(" SAM BENDING 전체: {$this->stats['total_sam']}");
$this->info(" 매칭 성공 (업데이트): {$this->stats['updated']}");
$this->info(" 이미 bendingData 있음 (skip): {$this->stats['already_has']}");
$this->info(" 매칭 실패: {$this->stats['no_match']}");
if ($this->stats['no_bending_data'] > 0) {
$this->warn(" 전개도 데이터 없음: {$this->stats['no_bending_data']}");
}
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
if (! empty($this->unmatchedItems)) {
$this->newLine();
$this->warn('⚠️ 매칭 실패 항목:');
foreach ($this->unmatchedItems as $code) {
$this->line(" - {$code}");
}
}
if ($dryRun) {
$this->newLine();
$this->info('🔍 DRY-RUN 완료. --dry-run 제거하면 실제 반영됩니다.');
}
}
}