Files
sam-api/database/seeders/Kyungdong/KyungdongItemSeeder.php
권혁성 4208ca3010 feat: [HR/기타] 캘린더/배차/설비/재고 + DB 마이그레이션
- 캘린더 CRUD API, 배차차량 관리 API (CRUD + options)
- 배차정보 다중 행 시스템 (shipment_vehicle_dispatches)
- 설비 다중점검주기 + 부 담당자 스키마 추가
- TodayIssue 날짜 기반 조회, Stock/Client 날짜 필터
- i18n 메시지 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 02:59:30 +09:00

912 lines
32 KiB
PHP

<?php
namespace Database\Seeders\Kyungdong;
use Database\Seeders\DummyDataSeeder;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
/**
* 경동기업 품목/단가 마이그레이션 Seeder
*
* Phase 1.0: chandj.KDunitprice (601건) → items, prices
* Phase 1.1: chandj.models (18건) → items (FG), prices
* Phase 1.2: chandj.item_list (9건) → items (PT), prices
* Phase 2.1: chandj.BDmodels.seconditem → items (PT) 누락 부품 추가
* Phase 2.2: chandj.BDmodels → items.bom JSON (FG ↔ PT 연결)
* Phase 3.1: chandj.price_motor → items (SM) + prices 누락 품목
* Phase 3.2: chandj.price_raw_materials → items (RM) + prices 누락 품목
*
* @see docs/dev_plans/kd-items-migration-plan.md
*/
class KyungdongItemSeeder extends Seeder
{
/**
* item_div → item_type 매핑
*/
private const ITEM_TYPE_MAP = [
'[제품]' => 'FG',
'[상품]' => 'FG',
'[반제품]' => 'PT',
'[부재료]' => 'SM',
'[원재료]' => 'RM',
'[무형상품]' => 'CS',
];
/**
* finishing_type 약어 매핑
*/
private const FINISHING_MAP = [
'SUS마감' => 'SUS',
'EGI마감' => 'EGI',
];
/**
* 경동기업 품목/단가 마이그레이션 실행
*/
public function run(): void
{
$tenantId = DummyDataSeeder::TENANT_ID;
$userId = DummyDataSeeder::USER_ID;
$this->command->info('🚀 경동기업 품목/단가 마이그레이션 시작...');
$this->command->info(" 대상 테넌트: ID {$tenantId}");
// 1. 기존 데이터 삭제
$this->cleanupExistingData($tenantId);
// Phase 1.0: KDunitprice → items
$itemCount = $this->migrateItems($tenantId, $userId);
// Phase 1.1: models → items (FG)
$modelCount = $this->migrateModels($tenantId, $userId);
// Phase 1.2: item_list → items (PT)
$itemListCount = $this->migrateItemList($tenantId, $userId);
// Phase 2.1: BDmodels.seconditem → items (PT) 누락 부품
$bdPartsCount = $this->migrateBDmodelsParts($tenantId, $userId);
// prices 생성 (모든 items 기반)
$priceCount = $this->migratePrices($tenantId, $userId);
// Phase 2.2: BDmodels → items.bom JSON
$bomCount = $this->migrateBom($tenantId);
// Phase 3.1: price_motor → items (SM) + prices
$motorResult = $this->migratePriceMotor($tenantId, $userId);
// Phase 3.2: price_raw_materials → items (RM) + prices
$rawMatResult = $this->migratePriceRawMaterials($tenantId, $userId);
$totalItems = $itemCount + $modelCount + $itemListCount + $bdPartsCount + $motorResult['items'] + $rawMatResult['items'];
$totalPrices = $priceCount + $motorResult['prices'] + $rawMatResult['prices'];
$this->command->info('');
$this->command->info('✅ 마이그레이션 완료:');
$this->command->info(" → items: {$totalItems}");
$this->command->info(" - KDunitprice: {$itemCount}");
$this->command->info(" - models: {$modelCount}");
$this->command->info(" - item_list: {$itemListCount}");
$this->command->info(" - BDmodels부품: {$bdPartsCount}");
$this->command->info(" - price_motor: {$motorResult['items']}");
$this->command->info(" - price_raw_materials: {$rawMatResult['items']}");
$this->command->info(" → prices: {$totalPrices}");
$this->command->info(" → BOM 연결: {$bomCount}");
}
/**
* 기존 데이터 삭제 (tenant_id 기준)
*/
private function cleanupExistingData(int $tenantId): void
{
$this->command->info('');
$this->command->info('🧹 기존 데이터 삭제 중...');
// prices 먼저 삭제 (FK 관계)
$priceCount = DB::table('prices')->where('tenant_id', $tenantId)->count();
DB::table('prices')->where('tenant_id', $tenantId)->delete();
$this->command->info(" → prices: {$priceCount}건 삭제");
// items 삭제
$itemCount = DB::table('items')->where('tenant_id', $tenantId)->count();
DB::table('items')->where('tenant_id', $tenantId)->delete();
$this->command->info(" → items: {$itemCount}건 삭제");
}
/**
* KDunitprice → items 마이그레이션
*/
private function migrateItems(int $tenantId, int $userId): int
{
$this->command->info('');
$this->command->info('📦 KDunitprice → items 마이그레이션...');
// chandj.KDunitprice에서 데이터 조회 (is_deleted=NULL이 활성 상태)
$kdItems = DB::connection('chandj')
->table('KDunitprice')
->whereNull('is_deleted')
->whereNotNull('prodcode')
->where('prodcode', '!=', '')
->get();
$this->command->info(" → 소스 데이터: {$kdItems->count()}");
$items = [];
$now = now();
$batchCount = 0;
foreach ($kdItems as $kd) {
$items[] = [
'tenant_id' => $tenantId,
'item_type' => $this->mapItemType($kd->item_div),
'code' => $kd->prodcode,
'name' => $kd->item_name,
'unit' => $kd->unit,
'category_id' => null,
'process_type' => null,
'item_category' => null,
'bom' => null,
'attributes' => json_encode([
'spec' => $kd->spec,
'item_div' => $kd->item_div,
'legacy_source' => 'KDunitprice',
'legacy_num' => $kd->num,
]),
'attributes_archive' => null,
'options' => null,
'description' => null,
'is_active' => true,
'created_by' => $userId,
'updated_by' => $userId,
'created_at' => $now,
'updated_at' => $now,
];
// 500건씩 배치 INSERT
if (count($items) >= 500) {
DB::table('items')->insert($items);
$batchCount += count($items);
$this->command->info("{$batchCount}건 완료...");
$items = [];
}
}
// 남은 데이터 INSERT
if (! empty($items)) {
DB::table('items')->insert($items);
$batchCount += count($items);
}
$this->command->info(" ✓ items: {$batchCount}건 생성 완료");
return $batchCount;
}
/**
* items 기반 → prices 마이그레이션
*/
private function migratePrices(int $tenantId, int $userId): int
{
$this->command->info('');
$this->command->info('💰 items → prices 마이그레이션...');
// 생성된 items 조회
$items = DB::table('items')
->where('tenant_id', $tenantId)
->get(['id', 'code', 'item_type', 'attributes']);
// KDunitprice 단가 (code → unitprice)
$kdPrices = DB::connection('chandj')
->table('KDunitprice')
->whereNull('is_deleted')
->whereNotNull('prodcode')
->where('prodcode', '!=', '')
->pluck('unitprice', 'prodcode');
// item_list 단가 (item_name → col13)
$itemListPrices = DB::connection('chandj')
->table('item_list')
->pluck('col13', 'item_name');
$prices = [];
$now = now();
$batchCount = 0;
foreach ($items as $item) {
$attributes = json_decode($item->attributes, true) ?? [];
$legacySource = $attributes['legacy_source'] ?? '';
// 소스별 단가 결정
$unitPrice = match ($legacySource) {
'KDunitprice' => $kdPrices[$item->code] ?? 0,
'item_list' => $itemListPrices[$attributes['legacy_num'] ? $this->getItemListName($item->code) : ''] ?? $attributes['base_price'] ?? 0,
'models' => 0, // models는 단가 없음
default => 0,
};
// item_list의 경우 attributes에 저장된 base_price 사용
if ($legacySource === 'item_list' && isset($attributes['base_price'])) {
$unitPrice = $attributes['base_price'];
}
$prices[] = [
'tenant_id' => $tenantId,
'item_type_code' => $item->item_type,
'item_id' => $item->id,
'client_group_id' => null,
'purchase_price' => 0,
'processing_cost' => null,
'loss_rate' => null,
'margin_rate' => null,
'sales_price' => $unitPrice,
'rounding_rule' => 'round',
'rounding_unit' => 1,
'supplier' => null,
'effective_from' => now()->toDateString(),
'effective_to' => null,
'note' => "{$legacySource} 마이그레이션",
'status' => 'active',
'is_final' => false,
'created_by' => $userId,
'updated_by' => $userId,
'created_at' => $now,
'updated_at' => $now,
];
// 500건씩 배치 INSERT
if (count($prices) >= 500) {
DB::table('prices')->insert($prices);
$batchCount += count($prices);
$this->command->info("{$batchCount}건 완료...");
$prices = [];
}
}
// 남은 데이터 INSERT
if (! empty($prices)) {
DB::table('prices')->insert($prices);
$batchCount += count($prices);
}
$this->command->info(" ✓ prices: {$batchCount}건 생성 완료");
return $batchCount;
}
/**
* PT-{name} 코드에서 name 추출
*/
private function getItemListName(string $code): string
{
return str_starts_with($code, 'PT-') ? substr($code, 3) : '';
}
/**
* Phase 1.1: models → items (FG) 마이그레이션
*/
private function migrateModels(int $tenantId, int $userId): int
{
$this->command->info('');
$this->command->info('📦 [Phase 1.1] models → items (FG) 마이그레이션...');
$models = DB::connection('chandj')
->table('models')
->where(function ($q) {
$q->where('is_deleted', 0)->orWhereNull('is_deleted');
})
->get();
$this->command->info(" → 소스 데이터: {$models->count()}");
$items = [];
$now = now();
foreach ($models as $model) {
$finishingShort = self::FINISHING_MAP[$model->finishing_type] ?? 'STD';
$code = "FG-{$model->model_name}-{$model->guiderail_type}-{$finishingShort}";
$name = "{$model->model_name} {$model->major_category} {$model->finishing_type} {$model->guiderail_type}";
$items[] = [
'tenant_id' => $tenantId,
'item_type' => 'FG',
'code' => $code,
'name' => trim($name),
'unit' => 'EA',
'category_id' => null,
'process_type' => null,
'item_category' => $model->major_category,
'bom' => null,
'attributes' => json_encode([
'model_name' => $model->model_name,
'major_category' => $model->major_category,
'finishing_type' => $model->finishing_type,
'guiderail_type' => $model->guiderail_type,
'legacy_source' => 'models',
'legacy_model_id' => $model->model_id,
]),
'attributes_archive' => null,
'options' => null,
'description' => null,
'is_active' => true,
'created_by' => $userId,
'updated_by' => $userId,
'created_at' => $now,
'updated_at' => $now,
];
}
if (! empty($items)) {
DB::table('items')->insert($items);
}
$this->command->info(" ✓ items (FG): {$models->count()}건 생성 완료");
return $models->count();
}
/**
* Phase 1.2: item_list → items (PT) 마이그레이션
*/
private function migrateItemList(int $tenantId, int $userId): int
{
$this->command->info('');
$this->command->info('📦 [Phase 1.2] item_list → items (PT) 마이그레이션...');
$itemList = DB::connection('chandj')
->table('item_list')
->get();
$this->command->info(" → 소스 데이터: {$itemList->count()}");
$items = [];
$now = now();
foreach ($itemList as $item) {
$code = "PT-{$item->item_name}";
$items[] = [
'tenant_id' => $tenantId,
'item_type' => 'PT',
'code' => $code,
'name' => $item->item_name,
'unit' => 'EA',
'category_id' => null,
'process_type' => null,
'item_category' => null,
'bom' => null,
'attributes' => json_encode([
'base_price' => $item->col13,
'legacy_source' => 'item_list',
'legacy_num' => $item->num,
]),
'attributes_archive' => null,
'options' => null,
'description' => null,
'is_active' => true,
'created_by' => $userId,
'updated_by' => $userId,
'created_at' => $now,
'updated_at' => $now,
];
}
if (! empty($items)) {
DB::table('items')->insert($items);
}
$this->command->info(" ✓ items (PT): {$itemList->count()}건 생성 완료");
return $itemList->count();
}
/**
* item_div → item_type 매핑
*/
private function mapItemType(?string $itemDiv): string
{
return self::ITEM_TYPE_MAP[$itemDiv] ?? 'SM';
}
/**
* Phase 2.1: BDmodels.seconditem → items (PT) 누락 부품 추가
*
* item_list에 없는 BDmodels.seconditem을 PT items로 생성
*/
private function migrateBDmodelsParts(int $tenantId, int $userId): int
{
$this->command->info('');
$this->command->info('📦 [Phase 2.1] BDmodels.seconditem → items (PT) 누락 부품...');
// BDmodels에서 고유한 seconditem 목록 조회
$bdSecondItems = DB::connection('chandj')
->table('BDmodels')
->where(function ($q) {
$q->where('is_deleted', 0)->orWhereNull('is_deleted');
})
->whereNotNull('seconditem')
->where('seconditem', '!=', '')
->distinct()
->pluck('seconditem');
// 이미 존재하는 PT items 코드 조회
$existingPtCodes = DB::table('items')
->where('tenant_id', $tenantId)
->where('item_type', 'PT')
->pluck('code')
->map(fn ($code) => str_starts_with($code, 'PT-') ? substr($code, 3) : $code)
->toArray();
$items = [];
$now = now();
foreach ($bdSecondItems as $secondItem) {
// 이미 PT items에 있으면 스킵
if (in_array($secondItem, $existingPtCodes)) {
continue;
}
$code = "PT-{$secondItem}";
$items[] = [
'tenant_id' => $tenantId,
'item_type' => 'PT',
'code' => $code,
'name' => $secondItem,
'unit' => 'EA',
'category_id' => null,
'process_type' => null,
'item_category' => null,
'bom' => null,
'attributes' => json_encode([
'legacy_source' => 'BDmodels_seconditem',
]),
'attributes_archive' => null,
'options' => null,
'description' => null,
'is_active' => true,
'created_by' => $userId,
'updated_by' => $userId,
'created_at' => $now,
'updated_at' => $now,
];
}
if (! empty($items)) {
DB::table('items')->insert($items);
}
$this->command->info(" → 소스 데이터: {$bdSecondItems->count()}건 (중복 제외 ".count($items).'건 신규)');
$this->command->info(' ✓ items (PT): '.count($items).'건 생성 완료');
return count($items);
}
/**
* Phase 2.2: BDmodels → items.bom JSON (FG ↔ PT 연결)
*
* models 기반 FG items에 BOM 연결
* bom: [{child_item_id: X, quantity: Y}, ...]
*/
private function migrateBom(int $tenantId): int
{
$this->command->info('');
$this->command->info('🔗 [Phase 2.2] BDmodels → items.bom JSON 연결...');
// PT items 조회 (code → id 매핑)
$ptItems = DB::table('items')
->where('tenant_id', $tenantId)
->where('item_type', 'PT')
->pluck('id', 'code')
->toArray();
// PT- prefix 없는 버전도 매핑 추가
$ptItemsByName = [];
foreach ($ptItems as $code => $id) {
$name = str_starts_with($code, 'PT-') ? substr($code, 3) : $code;
$ptItemsByName[$name] = $id;
}
// FG items 조회 (models 기반)
$fgItems = DB::table('items')
->where('tenant_id', $tenantId)
->where('item_type', 'FG')
->whereNotNull('attributes')
->get(['id', 'code', 'attributes']);
// BDmodels 데이터 조회
$bdModels = DB::connection('chandj')
->table('BDmodels')
->where(function ($q) {
$q->where('is_deleted', 0)->orWhereNull('is_deleted');
})
->whereNotNull('model_name')
->where('model_name', '!=', '')
->get(['model_name', 'seconditem', 'savejson']);
// model_name → seconditems 그룹핑
$modelBomMap = [];
foreach ($bdModels as $bd) {
if (empty($bd->seconditem)) {
continue;
}
$modelName = $bd->model_name;
if (! isset($modelBomMap[$modelName])) {
$modelBomMap[$modelName] = [];
}
// savejson에서 수량 파싱 (col8이 수량)
$quantity = 1;
if (! empty($bd->savejson)) {
$json = json_decode($bd->savejson, true);
if (is_array($json) && ! empty($json)) {
// 첫 번째 항목의 col8(수량) 사용
$quantity = (int) ($json[0]['col8'] ?? 1);
}
}
// 중복 체크 후 추가
$found = false;
foreach ($modelBomMap[$modelName] as &$existing) {
if ($existing['seconditem'] === $bd->seconditem) {
$found = true;
break;
}
}
if (! $found) {
$modelBomMap[$modelName][] = [
'seconditem' => $bd->seconditem,
'quantity' => $quantity,
];
}
}
$updatedCount = 0;
foreach ($fgItems as $fgItem) {
$attributes = json_decode($fgItem->attributes, true) ?? [];
$modelName = $attributes['model_name'] ?? null;
if (empty($modelName) || ! isset($modelBomMap[$modelName])) {
continue;
}
$bomArray = [];
foreach ($modelBomMap[$modelName] as $bomItem) {
$childItemId = $ptItemsByName[$bomItem['seconditem']] ?? null;
if ($childItemId) {
$bomArray[] = [
'child_item_id' => $childItemId,
'quantity' => $bomItem['quantity'],
];
}
}
if (! empty($bomArray)) {
DB::table('items')
->where('id', $fgItem->id)
->update(['bom' => json_encode($bomArray)]);
$updatedCount++;
}
}
$this->command->info(' → BDmodels 모델: '.count($modelBomMap).'개');
$this->command->info(" ✓ items.bom 연결: {$updatedCount}건 완료");
return $updatedCount;
}
/**
* Phase 3.1: price_motor → items (SM) + prices
*
* price_motor JSON에서 누락된 품목만 추가
* - 제어기, 방화/방범 콘트롤박스, 스위치, 리모콘 등
*
* @return array{items: int, prices: int}
*/
private function migratePriceMotor(int $tenantId, int $userId): array
{
$this->command->info('');
$this->command->info('📦 [Phase 3.1] price_motor → items (SM) 누락 품목...');
// 최신 price_motor 데이터 조회
$priceMotor = DB::connection('chandj')
->table('price_motor')
->where(function ($q) {
$q->where('is_deleted', 0)->orWhereNull('is_deleted');
})
->orderByDesc('registedate')
->first();
if (! $priceMotor || empty($priceMotor->itemList)) {
$this->command->info(' → 소스 데이터 없음');
return ['items' => 0, 'prices' => 0];
}
$itemList = json_decode($priceMotor->itemList, true);
if (! is_array($itemList)) {
$this->command->info(' → JSON 파싱 실패');
return ['items' => 0, 'prices' => 0];
}
// 기존 items 이름 조회 (중복 체크용)
$existingNames = DB::table('items')
->where('tenant_id', $tenantId)
->pluck('name')
->map(fn ($n) => mb_strtolower($n))
->toArray();
$items = [];
$now = now();
$newItemCodes = [];
foreach ($itemList as $idx => $item) {
$col1 = $item['col1'] ?? ''; // 전압/카테고리 (220, 380, 제어기, 방화, 방범)
$col2 = $item['col2'] ?? ''; // 용량/품목명
$salesPrice = (float) str_replace(',', '', $item['col13'] ?? '0');
// 모터 품목은 KDunitprice에 이미 있으므로 스킵
if (in_array($col1, ['220', '380'])) {
continue;
}
// 품목명 생성
$name = trim("{$col1} {$col2}");
if (empty($name) || $name === ' ') {
continue;
}
// 이미 존재하는 품목 스킵 (유사 이름 체크)
$nameLower = mb_strtolower($name);
$exists = false;
foreach ($existingNames as $existingName) {
if (str_contains($existingName, $nameLower) || str_contains($nameLower, $existingName)) {
$exists = true;
break;
}
}
if ($exists) {
continue;
}
// 코드 생성
$code = 'PM-'.str_pad($idx + 1, 3, '0', STR_PAD_LEFT);
$items[] = [
'tenant_id' => $tenantId,
'item_type' => 'SM',
'code' => $code,
'name' => $name,
'unit' => 'EA',
'category_id' => null,
'process_type' => null,
'item_category' => null,
'bom' => null,
'attributes' => json_encode([
'price_category' => $col1,
'price_spec' => $col2,
'legacy_source' => 'price_motor',
]),
'attributes_archive' => null,
'options' => null,
'description' => null,
'is_active' => true,
'created_by' => $userId,
'updated_by' => $userId,
'created_at' => $now,
'updated_at' => $now,
];
$newItemCodes[$code] = $salesPrice;
$existingNames[] = $nameLower; // 중복 방지
}
if (! empty($items)) {
DB::table('items')->insert($items);
}
// prices 생성
$priceCount = 0;
if (! empty($newItemCodes)) {
$newItems = DB::table('items')
->where('tenant_id', $tenantId)
->whereIn('code', array_keys($newItemCodes))
->get(['id', 'code', 'item_type']);
$prices = [];
foreach ($newItems as $item) {
$prices[] = [
'tenant_id' => $tenantId,
'item_type_code' => $item->item_type,
'item_id' => $item->id,
'client_group_id' => null,
'purchase_price' => 0,
'processing_cost' => null,
'loss_rate' => null,
'margin_rate' => null,
'sales_price' => $newItemCodes[$item->code],
'rounding_rule' => 'round',
'rounding_unit' => 1,
'supplier' => null,
'effective_from' => $priceMotor->registedate ?? now()->toDateString(),
'effective_to' => null,
'note' => 'price_motor 마이그레이션',
'status' => 'active',
'is_final' => false,
'created_by' => $userId,
'updated_by' => $userId,
'created_at' => $now,
'updated_at' => $now,
];
}
if (! empty($prices)) {
DB::table('prices')->insert($prices);
$priceCount = count($prices);
}
}
$this->command->info(' → 소스 데이터: '.count($itemList).'건 (누락 '.count($items).'건 추가)');
$this->command->info(' ✓ items: '.count($items).'건, prices: '.$priceCount.'건 생성 완료');
return ['items' => count($items), 'prices' => $priceCount];
}
/**
* Phase 3.2: price_raw_materials → items (RM) + prices
*
* price_raw_materials JSON에서 누락된 원자재 품목 추가
*
* @return array{items: int, prices: int}
*/
private function migratePriceRawMaterials(int $tenantId, int $userId): array
{
$this->command->info('');
$this->command->info('📦 [Phase 3.2] price_raw_materials → items (RM) 누락 품목...');
// 최신 price_raw_materials 데이터 조회
$priceRaw = DB::connection('chandj')
->table('price_raw_materials')
->where(function ($q) {
$q->where('is_deleted', 0)->orWhereNull('is_deleted');
})
->orderByDesc('registedate')
->first();
if (! $priceRaw || empty($priceRaw->itemList)) {
$this->command->info(' → 소스 데이터 없음');
return ['items' => 0, 'prices' => 0];
}
$itemList = json_decode($priceRaw->itemList, true);
if (! is_array($itemList)) {
$this->command->info(' → JSON 파싱 실패');
return ['items' => 0, 'prices' => 0];
}
// 기존 items 이름 조회 (중복 체크용)
$existingNames = DB::table('items')
->where('tenant_id', $tenantId)
->pluck('name')
->map(fn ($n) => mb_strtolower($n))
->toArray();
$items = [];
$now = now();
$newItemCodes = [];
foreach ($itemList as $idx => $item) {
$col1 = $item['col1'] ?? ''; // 카테고리 (슬랫, 스크린)
$col2 = $item['col2'] ?? ''; // 품목명 (방화, 실리카, 화이바)
$salesPrice = (float) str_replace(',', '', $item['col13'] ?? '0');
// 품목명 생성
$name = trim("{$col1} {$col2}");
if (empty($name) || $name === ' ') {
continue;
}
// 이미 존재하는 품목 스킵
$nameLower = mb_strtolower($name);
$exists = false;
foreach ($existingNames as $existingName) {
// 정확히 일치하거나 유사한 이름 체크
$col2Lower = mb_strtolower($col2);
if (str_contains($existingName, $col2Lower) || $existingName === $nameLower) {
$exists = true;
break;
}
}
if ($exists) {
continue;
}
// 코드 생성
$code = 'RM-'.str_pad($idx + 1, 3, '0', STR_PAD_LEFT);
$items[] = [
'tenant_id' => $tenantId,
'item_type' => 'RM',
'code' => $code,
'name' => $name,
'unit' => 'EA',
'category_id' => null,
'process_type' => null,
'item_category' => $col1,
'bom' => null,
'attributes' => json_encode([
'raw_category' => $col1,
'raw_name' => $col2,
'legacy_source' => 'price_raw_materials',
]),
'attributes_archive' => null,
'options' => null,
'description' => null,
'is_active' => true,
'created_by' => $userId,
'updated_by' => $userId,
'created_at' => $now,
'updated_at' => $now,
];
$newItemCodes[$code] = $salesPrice;
$existingNames[] = $nameLower;
}
if (! empty($items)) {
DB::table('items')->insert($items);
}
// prices 생성
$priceCount = 0;
if (! empty($newItemCodes)) {
$newItems = DB::table('items')
->where('tenant_id', $tenantId)
->whereIn('code', array_keys($newItemCodes))
->get(['id', 'code', 'item_type']);
$prices = [];
foreach ($newItems as $item) {
$prices[] = [
'tenant_id' => $tenantId,
'item_type_code' => $item->item_type,
'item_id' => $item->id,
'client_group_id' => null,
'purchase_price' => 0,
'processing_cost' => null,
'loss_rate' => null,
'margin_rate' => null,
'sales_price' => $newItemCodes[$item->code],
'rounding_rule' => 'round',
'rounding_unit' => 1,
'supplier' => null,
'effective_from' => $priceRaw->registedate ?? now()->toDateString(),
'effective_to' => null,
'note' => 'price_raw_materials 마이그레이션',
'status' => 'active',
'is_final' => false,
'created_by' => $userId,
'updated_by' => $userId,
'created_at' => $now,
'updated_at' => $now,
];
}
if (! empty($prices)) {
DB::table('prices')->insert($prices);
$priceCount = count($prices);
}
}
$this->command->info(' → 소스 데이터: '.count($itemList).'건 (누락 '.count($items).'건 추가)');
$this->command->info(' ✓ items: '.count($items).'건, prices: '.$priceCount.'건 생성 완료');
return ['items' => count($items), 'prices' => $priceCount];
}
}