feat: 견적 단가를 items+item_details+prices 통합 구조로 전환
- EstimatePriceService 생성: items+item_details+prices JOIN 기반 단가 조회 - item_details.product_category/part_type/specification 컬럼 매핑 - items.attributes JSON으로 model_name/finishing_type 추가 차원 처리 - 세션 내 캐시로 중복 조회 방지 - MigrateBDModelsPrices 커맨드: 레거시 BDmodels + kd_price_tables → 85건 마이그레이션 - KyungdongFormulaHandler: KdPriceTable 의존 제거 → EstimatePriceService 사용 - FormulaEvaluatorService: W1 마진 140→160, 면적 공식 W1×(H1+550) 수정 - 가이드레일 H0+250, 케이스/L바/평철 W0+220 (레거시 일치) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
363
app/Console/Commands/MigrateBDModelsPrices.php
Normal file
363
app/Console/Commands/MigrateBDModelsPrices.php
Normal file
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* BDmodels + kd_price_tables → items + item_details + prices 마이그레이션
|
||||
*
|
||||
* 레거시 chandj.BDmodels 데이터와 kd_price_tables 데이터를
|
||||
* items + item_details + prices 통합 구조로 마이그레이션
|
||||
*/
|
||||
class MigrateBDModelsPrices extends Command
|
||||
{
|
||||
protected $signature = 'kd:migrate-prices {--dry-run : 실제 DB 변경 없이 미리보기}';
|
||||
|
||||
protected $description = '경동 견적 단가를 items+item_details+prices로 마이그레이션';
|
||||
|
||||
private const TENANT_ID = 287;
|
||||
|
||||
private int $created = 0;
|
||||
|
||||
private int $skipped = 0;
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
$this->info('=== 경동 견적 단가 마이그레이션 ===');
|
||||
$this->info($dryRun ? '[DRY RUN] 실제 변경 없음' : '[LIVE] DB에 반영합니다');
|
||||
$this->newLine();
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
// 1. 레거시 BDmodels (chandj DB)
|
||||
$this->migrateBDModels($dryRun);
|
||||
|
||||
// 2. kd_price_tables (motor, shaft, pipe, angle, raw_material)
|
||||
$this->migrateKdPriceTables($dryRun);
|
||||
|
||||
if ($dryRun) {
|
||||
DB::rollBack();
|
||||
$this->warn('[DRY RUN] 롤백 완료');
|
||||
} else {
|
||||
DB::commit();
|
||||
$this->info('커밋 완료');
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("생성: {$this->created}건, 스킵: {$this->skipped}건");
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
$this->error("오류: {$e->getMessage()}");
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 레거시 chandj.BDmodels → items + item_details + prices
|
||||
*/
|
||||
private function migrateBDModels(bool $dryRun): void
|
||||
{
|
||||
$this->info('--- BDmodels (레거시) ---');
|
||||
|
||||
// chandj DB에서 BDmodels 조회 (chandj connection 사용)
|
||||
$rows = DB::connection('chandj')->select("
|
||||
SELECT model_name, seconditem, finishing_type, spec, unitprice, description
|
||||
FROM BDmodels
|
||||
WHERE is_deleted = 0
|
||||
ORDER BY model_name, seconditem, finishing_type, spec
|
||||
");
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$modelName = trim($row->model_name ?? '');
|
||||
$secondItem = trim($row->seconditem ?? '');
|
||||
$finishingType = trim($row->finishing_type ?? '');
|
||||
$spec = trim($row->spec ?? '');
|
||||
$unitPrice = (float) str_replace(',', '', $row->unitprice ?? '0');
|
||||
|
||||
// finishing_type 정규화: 'SUS마감' → 'SUS', 'EGI마감' → 'EGI'
|
||||
$finishingType = str_replace('마감', '', $finishingType);
|
||||
|
||||
if (empty($secondItem) || $unitPrice <= 0) {
|
||||
$this->skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// 코드 생성
|
||||
$codeParts = ['BD', $secondItem];
|
||||
if ($modelName) {
|
||||
$codeParts[] = $modelName;
|
||||
}
|
||||
if ($finishingType) {
|
||||
$codeParts[] = $finishingType;
|
||||
}
|
||||
if ($spec) {
|
||||
$codeParts[] = $spec;
|
||||
}
|
||||
$code = implode('-', $codeParts);
|
||||
|
||||
// 이름 생성
|
||||
$nameParts = [$secondItem];
|
||||
if ($modelName) {
|
||||
$nameParts[] = $modelName;
|
||||
}
|
||||
if ($finishingType) {
|
||||
$nameParts[] = $finishingType;
|
||||
}
|
||||
if ($spec) {
|
||||
$nameParts[] = $spec;
|
||||
}
|
||||
$name = implode(' ', $nameParts);
|
||||
|
||||
$this->createEstimateItem(
|
||||
code: $code,
|
||||
name: $name,
|
||||
productCategory: 'bdmodels',
|
||||
partType: $secondItem,
|
||||
specification: $spec ?: null,
|
||||
attributes: array_filter([
|
||||
'model_name' => $modelName ?: null,
|
||||
'finishing_type' => $finishingType ?: null,
|
||||
'bdmodel_source' => 'BDmodels',
|
||||
'description' => $row->description ?: null,
|
||||
]),
|
||||
salesPrice: $unitPrice,
|
||||
note: 'BDmodels 마이그레이션',
|
||||
dryRun: $dryRun
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* kd_price_tables → items + item_details + prices
|
||||
*/
|
||||
private function migrateKdPriceTables(bool $dryRun): void
|
||||
{
|
||||
$this->info('--- kd_price_tables ---');
|
||||
|
||||
$rows = DB::table('kd_price_tables')
|
||||
->where('tenant_id', self::TENANT_ID)
|
||||
->where('is_active', true)
|
||||
->where('table_type', '!=', 'bdmodels') // BDmodels는 위에서 처리
|
||||
->orderBy('table_type')
|
||||
->orderBy('item_code')
|
||||
->get();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$tableType = $row->table_type;
|
||||
$unitPrice = (float) $row->unit_price;
|
||||
|
||||
if ($unitPrice <= 0) {
|
||||
$this->skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($tableType) {
|
||||
case 'motor':
|
||||
$this->migrateMotor($row, $dryRun);
|
||||
break;
|
||||
case 'shaft':
|
||||
$this->migrateShaft($row, $dryRun);
|
||||
break;
|
||||
case 'pipe':
|
||||
$this->migratePipe($row, $dryRun);
|
||||
break;
|
||||
case 'angle':
|
||||
$this->migrateAngle($row, $dryRun);
|
||||
break;
|
||||
case 'raw_material':
|
||||
$this->migrateRawMaterial($row, $dryRun);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function migrateMotor(object $row, bool $dryRun): void
|
||||
{
|
||||
$category = $row->category; // 150K, 300K, 매립형, 노출형 등
|
||||
$code = "EST-MOTOR-{$category}";
|
||||
$name = "모터/제어기 {$category}";
|
||||
|
||||
$this->createEstimateItem(
|
||||
code: $code,
|
||||
name: $name,
|
||||
productCategory: 'motor',
|
||||
partType: $category,
|
||||
specification: $row->spec2 ?? null,
|
||||
attributes: ['price_unit' => $row->unit ?? 'EA'],
|
||||
salesPrice: (float) $row->unit_price,
|
||||
note: 'kd_price_tables motor 마이그레이션',
|
||||
dryRun: $dryRun
|
||||
);
|
||||
}
|
||||
|
||||
private function migrateShaft(object $row, bool $dryRun): void
|
||||
{
|
||||
$size = $row->spec1; // 인치
|
||||
$length = $row->spec2; // 길이
|
||||
$code = "EST-SHAFT-{$size}-{$length}";
|
||||
$name = "감기샤프트 {$size}인치 {$length}m";
|
||||
|
||||
$this->createEstimateItem(
|
||||
code: $code,
|
||||
name: $name,
|
||||
productCategory: 'shaft',
|
||||
partType: $size,
|
||||
specification: $length,
|
||||
attributes: ['price_unit' => $row->unit ?? 'EA'],
|
||||
salesPrice: (float) $row->unit_price,
|
||||
note: 'kd_price_tables shaft 마이그레이션',
|
||||
dryRun: $dryRun
|
||||
);
|
||||
}
|
||||
|
||||
private function migratePipe(object $row, bool $dryRun): void
|
||||
{
|
||||
$thickness = $row->spec1;
|
||||
$length = $row->spec2;
|
||||
$code = "EST-PIPE-{$thickness}-{$length}";
|
||||
$name = "각파이프 {$thickness}T {$length}mm";
|
||||
|
||||
$this->createEstimateItem(
|
||||
code: $code,
|
||||
name: $name,
|
||||
productCategory: 'pipe',
|
||||
partType: $thickness,
|
||||
specification: $length,
|
||||
attributes: ['price_unit' => $row->unit ?? 'EA'],
|
||||
salesPrice: (float) $row->unit_price,
|
||||
note: 'kd_price_tables pipe 마이그레이션',
|
||||
dryRun: $dryRun
|
||||
);
|
||||
}
|
||||
|
||||
private function migrateAngle(object $row, bool $dryRun): void
|
||||
{
|
||||
$category = $row->category; // 스크린용, 철재용
|
||||
$bracketSize = $row->spec1; // 530*320, 600*350, 690*390
|
||||
$angleType = $row->spec2; // 앵글3T, 앵글4T
|
||||
$code = "EST-ANGLE-{$category}-{$bracketSize}-{$angleType}";
|
||||
$name = "앵글 {$category} {$bracketSize} {$angleType}";
|
||||
|
||||
$this->createEstimateItem(
|
||||
code: $code,
|
||||
name: $name,
|
||||
productCategory: 'angle',
|
||||
partType: $category,
|
||||
specification: $bracketSize,
|
||||
attributes: [
|
||||
'angle_type' => $angleType,
|
||||
'price_unit' => $row->unit ?? 'EA',
|
||||
],
|
||||
salesPrice: (float) $row->unit_price,
|
||||
note: 'kd_price_tables angle 마이그레이션',
|
||||
dryRun: $dryRun
|
||||
);
|
||||
}
|
||||
|
||||
private function migrateRawMaterial(object $row, bool $dryRun): void
|
||||
{
|
||||
$name = $row->item_name;
|
||||
$code = 'EST-RAW-'.preg_replace('/[^A-Za-z0-9가-힣]/', '', $name);
|
||||
|
||||
$this->createEstimateItem(
|
||||
code: $code,
|
||||
name: $name,
|
||||
productCategory: 'raw_material',
|
||||
partType: $name,
|
||||
specification: $row->spec1 ?? null,
|
||||
attributes: ['price_unit' => $row->unit ?? 'EA'],
|
||||
salesPrice: (float) $row->unit_price,
|
||||
note: 'kd_price_tables raw_material 마이그레이션',
|
||||
dryRun: $dryRun
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 품목 생성 (items + item_details + prices)
|
||||
*/
|
||||
private function createEstimateItem(
|
||||
string $code,
|
||||
string $name,
|
||||
string $productCategory,
|
||||
string $partType,
|
||||
?string $specification,
|
||||
array $attributes,
|
||||
float $salesPrice,
|
||||
string $note,
|
||||
bool $dryRun
|
||||
): void {
|
||||
// 중복 체크 (code 기준)
|
||||
$existing = DB::table('items')
|
||||
->where('tenant_id', self::TENANT_ID)
|
||||
->where('code', $code)
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$this->line(" [스킵] {$code} - 이미 존재");
|
||||
$this->skipped++;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->line(" [생성] {$code} ({$name}) = {$salesPrice}");
|
||||
|
||||
if ($dryRun) {
|
||||
$this->created++;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$now = now();
|
||||
|
||||
// 1. items
|
||||
$itemId = DB::table('items')->insertGetId([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'item_type' => 'PT',
|
||||
'code' => $code,
|
||||
'name' => $name,
|
||||
'unit' => 'EA',
|
||||
'attributes' => json_encode($attributes, JSON_UNESCAPED_UNICODE),
|
||||
'is_active' => true,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
// 2. item_details
|
||||
DB::table('item_details')->insert([
|
||||
'item_id' => $itemId,
|
||||
'product_category' => $productCategory,
|
||||
'part_type' => $partType,
|
||||
'specification' => $specification,
|
||||
'item_name' => $name,
|
||||
'is_purchasable' => true,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
// 3. prices
|
||||
DB::table('prices')->insert([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'item_type_code' => 'PT',
|
||||
'item_id' => $itemId,
|
||||
'sales_price' => $salesPrice,
|
||||
'effective_from' => $now->toDateString(),
|
||||
'status' => 'active',
|
||||
'note' => $note,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
$this->created++;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user