2026-03-16 20:49:20 +09:00
|
|
|
<?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'],
|
2026-03-17 12:50:26 +09:00
|
|
|
'RD' => ['item_bending' => '가이드레일', 'itemName_like' => '%벽면형-D%', 'material' => 'EGI 1.55T', 'item_spec' => '120*70'],
|
2026-03-16 20:49:20 +09:00
|
|
|
'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'],
|
2026-03-17 12:50:26 +09:00
|
|
|
'SD' => ['item_bending' => '가이드레일', 'itemName_like' => '%측면형-D%', 'material' => 'EGI 1.55T', 'item_spec' => '120*120'],
|
2026-03-16 20:49:20 +09:00
|
|
|
'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]}%",
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 12:50:26 +09:00
|
|
|
// BD-케이스-650*550 → chandj에서 itemName에 "650*550" 포함된 전면판 매칭
|
2026-03-16 20:49:20 +09:00
|
|
|
if (preg_match('/^BD-케이스-(\d+)\*(\d+)$/', $code, $m)) {
|
|
|
|
|
$spec = $m[1].'*'.$m[2];
|
|
|
|
|
|
|
|
|
|
return $chandjRows->first(function ($r) use ($spec) {
|
2026-03-17 12:50:26 +09:00
|
|
|
return (str_contains($r->itemName, $spec) || $r->item_spec === $spec)
|
|
|
|
|
&& str_contains($r->itemName, '전면');
|
2026-03-16 20:49:20 +09:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 제거하면 실제 반영됩니다.');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|