From 3fce54b7d48dc90c5d4e79b002f67e9ef24d2d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Thu, 29 Jan 2026 01:10:42 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B2=BD=EB=8F=99=EA=B8=B0=EC=97=85=20?= =?UTF-8?q?=EC=A0=84=EC=9A=A9=20=EA=B2=AC=EC=A0=81=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20(Phase=204=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KdPriceTable 모델: 경동기업 단가 테이블 (motor, shaft, pipe, angle, raw_material, bdmodels) - KyungdongFormulaHandler: 모터 용량, 브라켓 크기, 절곡품(10종), 부자재(3종) 계산 - FormulaEvaluatorService: tenant_id=287 라우팅 추가 - kd_price_tables 마이그레이션 및 시더 (47건 단가 데이터) 테스트 결과: W0=3000, H0=2500 입력 시 16개 항목, 합계 751,200원 정상 계산 Co-Authored-By: Claude Opus 4.5 --- CURRENT_WORKS.md | 46 ++ app/Models/Kyungdong/KdPriceTable.php | 341 ++++++++ .../Quote/FormulaEvaluatorService.php | 226 +++++ .../Handlers/KyungdongFormulaHandler.php | 773 ++++++++++++++++++ ...29_004736_create_kd_price_tables_table.php | 62 ++ .../seeders/Kyungdong/KdPriceTableSeeder.php | 565 +++++++++++++ 6 files changed, 2013 insertions(+) create mode 100644 app/Models/Kyungdong/KdPriceTable.php create mode 100644 app/Services/Quote/Handlers/KyungdongFormulaHandler.php create mode 100644 database/migrations/2026_01_29_004736_create_kd_price_tables_table.php create mode 100644 database/seeders/Kyungdong/KdPriceTableSeeder.php diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index c95d053..d5bc318 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1,3 +1,49 @@ +## 2026-01-29 (수) - 경동기업 견적 로직 Phase 4 완료 + +### 작업 목표 +- 경동기업(tenant_id=287) 전용 견적 계산 로직 구현 +- 5130 레거시 시스템의 BOM/견적 로직을 SAM에 이식 +- 동적 BOM 계산: 모터, 제어기, 절곡품(10종), 부자재(3종) + +### 생성된 파일 +| 파일명 | 설명 | +|--------|------| +| `app/Models/Kyungdong/KdPriceTable.php` | 경동기업 전용 단가 테이블 모델 | +| `app/Services/Quote/Handlers/KyungdongFormulaHandler.php` | 경동기업 견적 계산 핸들러 | +| `database/migrations/2026_01_29_004736_create_kd_price_tables_table.php` | kd_price_tables 마이그레이션 | +| `database/seeders/Kyungdong/KdPriceTableSeeder.php` | 단가 데이터 시더 (47건) | + +### 수정된 파일 +| 파일명 | 설명 | +|--------|------| +| `app/Services/Quote/FormulaEvaluatorService.php` | tenant_id=287 라우팅 추가 | + +### 구현된 기능 +| 기능 | 설명 | +|------|------| +| 모터 용량 계산 | 제품타입 × 인치 × 중량 3차원 조건 | +| 브라켓 크기 결정 | 중량 기반 530*320, 600*350, 690*390 | +| 주자재 계산 | W × (H + 550) / 1,000,000 × 단가 | +| 절곡품 계산 (10종) | 케이스, 마구리, 가이드레일, 하장바, L바, 평철, 환봉 등 | +| 부자재 계산 (3종) | 감기샤프트, 각파이프, 앵글 | + +### 테스트 결과 +``` +입력: W0=3000, H0=2500, 철재형, 5인치, KSS01 SUS +출력: 16개 항목, 합계 751,200원 ✅ +``` + +### 검증 완료 +- [x] Pint 코드 스타일 통과 +- [x] 마이그레이션 실행 완료 (kd_price_tables) +- [x] 시더 실행 완료 (47건 단가 데이터) +- [x] tinker 테스트 통과 (16개 항목 정상 계산) + +### 계획 문서 +- `docs/plans/kd-quote-logic-plan.md` - Phase 0~4 완료 (100%) + +--- + ## 2026-01-21 (화) - TodayIssue 헤더 알림 API (Phase 3 완료) ### 작업 목표 diff --git a/app/Models/Kyungdong/KdPriceTable.php b/app/Models/Kyungdong/KdPriceTable.php new file mode 100644 index 0000000..fa5c682 --- /dev/null +++ b/app/Models/Kyungdong/KdPriceTable.php @@ -0,0 +1,341 @@ + 'decimal:2', + 'raw_data' => 'array', + 'is_active' => 'boolean', + ]; + + // ========================================================================= + // Scopes + // ========================================================================= + + /** + * 테이블 유형으로 필터링 + */ + public function scopeOfType(Builder $query, string $type): Builder + { + return $query->where('table_type', $type); + } + + /** + * 활성 데이터만 + */ + public function scopeActive(Builder $query): Builder + { + return $query->where('is_active', true); + } + + /** + * 모터 단가 조회 + */ + public function scopeMotor(Builder $query): Builder + { + return $query->ofType(self::TYPE_MOTOR)->active(); + } + + /** + * 샤프트 단가 조회 + */ + public function scopeShaft(Builder $query): Builder + { + return $query->ofType(self::TYPE_SHAFT)->active(); + } + + /** + * 파이프 단가 조회 + */ + public function scopePipeType(Builder $query): Builder + { + return $query->ofType(self::TYPE_PIPE)->active(); + } + + /** + * 앵글 단가 조회 + */ + public function scopeAngle(Builder $query): Builder + { + return $query->ofType(self::TYPE_ANGLE)->active(); + } + + // ========================================================================= + // Static Query Methods + // ========================================================================= + + /** + * 모터 단가 조회 + * + * @param string $motorCapacity 모터 용량 (150K, 300K, 400K, 500K, 600K, 800K, 1000K) + */ + public static function getMotorPrice(string $motorCapacity): float + { + $record = self::motor() + ->where('category', $motorCapacity) + ->first(); + + return (float) ($record?->unit_price ?? 0); + } + + /** + * 제어기 단가 조회 + * + * @param string $controllerType 제어기 타입 (매립형, 노출형, 뒷박스) + */ + public static function getControllerPrice(string $controllerType): float + { + $record = self::motor() + ->where('category', $controllerType) + ->first(); + + return (float) ($record?->unit_price ?? 0); + } + + /** + * 샤프트 단가 조회 + * + * @param string $size 사이즈 (3, 4, 5인치) + * @param float $length 길이 (m 단위) + */ + public static function getShaftPrice(string $size, float $length): float + { + // 길이를 소수점 1자리 문자열로 변환 (DB 저장 형식: '3.0', '4.0') + $lengthStr = number_format($length, 1, '.', ''); + + $record = self::shaft() + ->where('spec1', $size) + ->where('spec2', $lengthStr) + ->first(); + + return (float) ($record?->unit_price ?? 0); + } + + /** + * 파이프 단가 조회 + * + * @param string $thickness 두께 (1.4 등) + * @param int $length 길이 (3000, 6000) + */ + public static function getPipePrice(string $thickness, int $length): float + { + $record = self::pipeType() + ->where('spec1', $thickness) + ->where('spec2', (string) $length) + ->first(); + + return (float) ($record?->unit_price ?? 0); + } + + /** + * 앵글 단가 조회 + * + * @param string $type 타입 (스크린용, 철재용) + * @param string $bracketSize 브라켓크기 (530*320, 600*350, 690*390) + * @param string $angleType 앵글타입 (앵글3T, 앵글4T) + */ + public static function getAnglePrice(string $type, string $bracketSize, string $angleType): float + { + $record = self::angle() + ->where('category', $type) + ->where('spec1', $bracketSize) + ->where('spec2', $angleType) + ->first(); + + return (float) ($record?->unit_price ?? 0); + } + + /** + * 원자재 단가 조회 + * + * @param string $materialName 원자재명 (실리카, 스크린 등) + */ + public static function getRawMaterialPrice(string $materialName): float + { + $record = self::ofType(self::TYPE_RAW_MATERIAL) + ->active() + ->where('item_name', $materialName) + ->first(); + + return (float) ($record?->unit_price ?? 0); + } + + // ========================================================================= + // BDmodels 단가 조회 (절곡품) + // ========================================================================= + + /** + * BDmodels 스코프 + */ + public function scopeBdmodels(Builder $query): Builder + { + return $query->ofType(self::TYPE_BDMODELS)->active(); + } + + /** + * BDmodels 단가 조회 (케이스, 가이드레일, 하단마감재 등) + * + * @param string $secondItem 부품분류 (케이스, 가이드레일, 하단마감재, L-BAR 등) + * @param string|null $modelName 모델코드 (KSS01, KWS01 등) + * @param string|null $finishingType 마감재질 (SUS, EGI) + * @param string|null $spec 규격 (120*70, 650*550 등) + */ + public static function getBDModelPrice( + string $secondItem, + ?string $modelName = null, + ?string $finishingType = null, + ?string $spec = null + ): float { + $query = self::bdmodels()->where('category', $secondItem); + + if ($modelName) { + $query->where('item_code', $modelName); + } + + if ($finishingType) { + $query->where('spec1', $finishingType); + } + + if ($spec) { + $query->where('spec2', $spec); + } + + $record = $query->first(); + + return (float) ($record?->unit_price ?? 0); + } + + /** + * 케이스 단가 조회 + * + * @param string $spec 케이스 규격 (500*380, 650*550 등) + */ + public static function getCasePrice(string $spec): float + { + return self::getBDModelPrice('케이스', null, null, $spec); + } + + /** + * 가이드레일 단가 조회 + * + * @param string $modelName 모델코드 (KSS01 등) + * @param string $finishingType 마감재질 (SUS, EGI) + * @param string $spec 규격 (120*70, 120*100) + */ + public static function getGuideRailPrice(string $modelName, string $finishingType, string $spec): float + { + return self::getBDModelPrice('가이드레일', $modelName, $finishingType, $spec); + } + + /** + * 하단마감재(하장바) 단가 조회 + * + * @param string $modelName 모델코드 + * @param string $finishingType 마감재질 + */ + public static function getBottomBarPrice(string $modelName, string $finishingType): float + { + return self::getBDModelPrice('하단마감재', $modelName, $finishingType); + } + + /** + * L-BAR 단가 조회 + * + * @param string $modelName 모델코드 + */ + public static function getLBarPrice(string $modelName): float + { + return self::getBDModelPrice('L-BAR', $modelName); + } + + /** + * 보강평철 단가 조회 + */ + public static function getFlatBarPrice(): float + { + return self::getBDModelPrice('보강평철'); + } + + /** + * 케이스 마구리 단가 조회 + * + * @param string $spec 규격 + */ + public static function getCaseCapPrice(string $spec): float + { + return self::getBDModelPrice('마구리', null, null, $spec); + } + + /** + * 케이스용 연기차단재 단가 조회 + */ + public static function getCaseSmokeBlockPrice(): float + { + return self::getBDModelPrice('케이스용 연기차단재'); + } + + /** + * 가이드레일용 연기차단재 단가 조회 + */ + public static function getRailSmokeBlockPrice(): float + { + return self::getBDModelPrice('가이드레일용 연기차단재'); + } +} diff --git a/app/Services/Quote/FormulaEvaluatorService.php b/app/Services/Quote/FormulaEvaluatorService.php index 2b30b1b..03eca59 100644 --- a/app/Services/Quote/FormulaEvaluatorService.php +++ b/app/Services/Quote/FormulaEvaluatorService.php @@ -6,6 +6,7 @@ use App\Models\Items\Item; use App\Models\Products\Price; use App\Models\Quote\QuoteFormula; +use App\Services\Quote\Handlers\KyungdongFormulaHandler; use App\Services\Service; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; @@ -28,6 +29,11 @@ class FormulaEvaluatorService extends Service 'SUM', 'ROUND', 'CEIL', 'FLOOR', 'ABS', 'MIN', 'MAX', 'IF', 'AND', 'OR', 'NOT', ]; + /** + * 경동기업 테넌트 ID + */ + private const KYUNGDONG_TENANT_ID = 287; + private array $variables = []; private array $errors = []; @@ -599,6 +605,11 @@ public function calculateBomWithDebug( ]; } + // 경동기업(tenant_id=287) 전용 계산 로직 분기 + if ($tenantId === self::KYUNGDONG_TENANT_ID) { + return $this->calculateKyungdongBom($finishedGoodsCode, $inputVariables, $tenantId); + } + // Step 1: 입력값 수집 (React 동기화 변수 포함) $this->addDebugStep(1, '입력값수집', [ 'W0' => $inputVariables['W0'] ?? null, @@ -1538,4 +1549,219 @@ private function mergeLeafMaterials(array $leafMaterials, int $tenantId): array return array_values($result); } + + // ========================================================================= + // 경동기업 전용 계산 (tenant_id = 287) + // ========================================================================= + + /** + * 경동기업 전용 BOM 계산 + * + * 5130 레거시 시스템의 견적 로직을 구현한 KyungdongFormulaHandler 사용 + * - 3차원 조건 모터 용량 계산 (제품타입 × 인치 × 중량) + * - 브라켓 크기 결정 + * - 10종 절곡품 계산 + * - 3종 부자재 계산 + * + * @param string $finishedGoodsCode 완제품 코드 + * @param array $inputVariables 입력 변수 (W0, H0, QTY 등) + * @param int $tenantId 테넌트 ID + * @return array 계산 결과 + */ + private function calculateKyungdongBom( + string $finishedGoodsCode, + array $inputVariables, + int $tenantId + ): array { + $this->addDebugStep(0, '경동전용계산', [ + 'tenant_id' => $tenantId, + 'handler' => 'KyungdongFormulaHandler', + 'finished_goods' => $finishedGoodsCode, + ]); + + // Step 1: 입력값 수집 + $this->addDebugStep(1, '입력값수집', [ + 'W0' => $inputVariables['W0'] ?? null, + 'H0' => $inputVariables['H0'] ?? null, + 'QTY' => $inputVariables['QTY'] ?? 1, + 'bracket_inch' => $inputVariables['bracket_inch'] ?? '5', + 'product_type' => $inputVariables['product_type'] ?? 'screen', + 'finishing_type' => $inputVariables['finishing_type'] ?? 'SUS', + 'finished_goods' => $finishedGoodsCode, + ]); + + // Step 2: 완제품 조회 + $finishedGoods = $this->getItemDetails($finishedGoodsCode, $tenantId); + + if (! $finishedGoods) { + $this->addDebugStep(2, '완제품선택', [ + 'code' => $finishedGoodsCode, + 'error' => '완제품을 찾을 수 없습니다.', + ]); + + return [ + 'success' => false, + 'error' => __('error.finished_goods_not_found', ['code' => $finishedGoodsCode]), + 'debug_steps' => $this->debugSteps, + ]; + } + + $this->addDebugStep(2, '완제품선택', [ + 'code' => $finishedGoods['code'], + 'name' => $finishedGoods['name'], + 'item_category' => $finishedGoods['item_category'] ?? 'N/A', + ]); + + // KyungdongFormulaHandler 인스턴스 생성 + $handler = new KyungdongFormulaHandler; + + // Step 3: 경동 전용 변수 계산 + $W0 = (float) ($inputVariables['W0'] ?? 0); + $H0 = (float) ($inputVariables['H0'] ?? 0); + $QTY = (int) ($inputVariables['QTY'] ?? 1); + $bracketInch = $inputVariables['bracket_inch'] ?? '5'; + $productType = $inputVariables['product_type'] ?? 'screen'; + + // 중량 계산 (5130 로직) + $area = ($W0 * ($H0 + 550)) / 1000000; + $weight = $area * ($productType === 'steel' ? 25 : 2) + ($W0 / 1000) * 14.17; + + // 모터 용량 결정 + $motorCapacity = $handler->calculateMotorCapacity($productType, $weight, $bracketInch); + + // 브라켓 크기 결정 + $bracketSize = $handler->calculateBracketSize($weight, $bracketInch); + + $calculatedVariables = array_merge($inputVariables, [ + 'W0' => $W0, + 'H0' => $H0, + 'QTY' => $QTY, + 'W1' => $W0 + 140, + 'H1' => $H0 + 350, + 'AREA' => round($area, 4), + 'WEIGHT' => round($weight, 2), + 'MOTOR_CAPACITY' => $motorCapacity, + 'BRACKET_SIZE' => $bracketSize, + 'bracket_inch' => $bracketInch, + 'product_type' => $productType, + ]); + + $this->addDebugStep(3, '변수계산', [ + 'W0' => $W0, + 'H0' => $H0, + 'area' => round($area, 4), + 'weight' => round($weight, 2), + 'motor_capacity' => $motorCapacity, + 'bracket_size' => $bracketSize, + 'calculation_type' => '경동기업 전용 공식', + ]); + + // Step 4-7: 동적 항목 계산 (KyungdongFormulaHandler 사용) + $dynamicItems = $handler->calculateDynamicItems($calculatedVariables); + + $this->addDebugStep(4, 'BOM전개', [ + 'total_items' => count($dynamicItems), + 'item_categories' => array_unique(array_column($dynamicItems, 'category')), + ]); + + // Step 5-7: 단가 계산 (각 항목별) + $calculatedItems = []; + foreach ($dynamicItems as $item) { + $this->addDebugStep(6, '수량계산', [ + 'item_name' => $item['item_name'], + 'quantity' => $item['quantity'], + ]); + + $this->addDebugStep(7, '금액계산', [ + 'item_name' => $item['item_name'], + 'quantity' => $item['quantity'], + 'unit_price' => $item['unit_price'], + 'total_price' => $item['total_price'], + ]); + + $calculatedItems[] = [ + 'item_code' => $item['item_code'] ?? '', + 'item_name' => $item['item_name'], + 'item_category' => $item['category'], + 'specification' => $item['specification'] ?? '', + 'unit' => $item['unit'], + 'quantity' => $item['quantity'], + 'unit_price' => $item['unit_price'], + 'total_price' => $item['total_price'], + 'category_group' => $item['category'], + 'calculation_note' => '경동기업 전용 계산', + ]; + } + + // Step 8: 카테고리별 그룹화 + $groupedItems = []; + foreach ($calculatedItems as $item) { + $category = $item['category_group']; + if (! isset($groupedItems[$category])) { + $groupedItems[$category] = [ + 'name' => $this->getKyungdongCategoryName($category), + 'items' => [], + 'subtotal' => 0, + ]; + } + $groupedItems[$category]['items'][] = $item; + $groupedItems[$category]['subtotal'] += $item['total_price']; + } + + $this->addDebugStep(8, '카테고리그룹화', [ + 'groups' => array_map(fn ($g) => [ + 'name' => $g['name'], + 'count' => count($g['items']), + 'subtotal' => $g['subtotal'], + ], $groupedItems), + ]); + + // Step 9: 소계 계산 + $subtotals = []; + foreach ($groupedItems as $category => $group) { + $subtotals[$category] = [ + 'name' => $group['name'], + 'count' => count($group['items']), + 'subtotal' => $group['subtotal'], + ]; + } + + $this->addDebugStep(9, '소계계산', $subtotals); + + // Step 10: 최종 합계 + $grandTotal = array_sum(array_column($calculatedItems, 'total_price')); + + $this->addDebugStep(10, '최종합계', [ + 'item_count' => count($calculatedItems), + 'grand_total' => $grandTotal, + 'formatted' => number_format($grandTotal).'원', + ]); + + return [ + 'success' => true, + 'finished_goods' => $finishedGoods, + 'variables' => $calculatedVariables, + 'items' => $calculatedItems, + 'grouped_items' => $groupedItems, + 'subtotals' => $subtotals, + 'grand_total' => $grandTotal, + 'debug_steps' => $this->debugSteps, + 'calculation_type' => 'kyungdong', + ]; + } + + /** + * 경동기업 카테고리명 반환 + */ + private function getKyungdongCategoryName(string $category): string + { + return match ($category) { + 'material' => '주자재', + 'motor' => '모터', + 'controller' => '제어기', + 'steel' => '절곡품', + 'parts' => '부자재', + default => $category, + }; + } } diff --git a/app/Services/Quote/Handlers/KyungdongFormulaHandler.php b/app/Services/Quote/Handlers/KyungdongFormulaHandler.php new file mode 100644 index 0000000..6e27858 --- /dev/null +++ b/app/Services/Quote/Handlers/KyungdongFormulaHandler.php @@ -0,0 +1,773 @@ +calculateScreenMotor($weight, $inch); + } + + return $this->calculateSteelMotor($weight, $inch); + } + + /** + * 스크린 모터 용량 계산 + */ + private function calculateScreenMotor(float $weight, int $inch): string + { + if ($inch === 4) { + if ($weight <= 150) { + return '150K'; + } + if ($weight <= 300) { + return '300K'; + } + + return '400K'; + } + + if ($inch === 5) { + if ($weight <= 123) { + return '150K'; + } + if ($weight <= 246) { + return '300K'; + } + if ($weight <= 327) { + return '400K'; + } + if ($weight <= 500) { + return '500K'; + } + + return '600K'; + } + + if ($inch === 6) { + if ($weight <= 104) { + return '150K'; + } + if ($weight <= 208) { + return '300K'; + } + if ($weight <= 300) { + return '400K'; + } + if ($weight <= 424) { + return '500K'; + } + + return '600K'; + } + + // 기본값 + return '300K'; + } + + /** + * 철재 모터 용량 계산 + */ + private function calculateSteelMotor(float $weight, int $inch): string + { + if ($inch === 4) { + if ($weight <= 300) { + return '300K'; + } + + return '400K'; + } + + if ($inch === 5) { + if ($weight <= 246) { + return '300K'; + } + if ($weight <= 327) { + return '400K'; + } + if ($weight <= 500) { + return '500K'; + } + + return '600K'; + } + + if ($inch === 6) { + if ($weight <= 208) { + return '300K'; + } + if ($weight <= 277) { + return '400K'; + } + if ($weight <= 424) { + return '500K'; + } + if ($weight <= 508) { + return '600K'; + } + if ($weight <= 800) { + return '800K'; + } + + return '1000K'; + } + + if ($inch === 8) { + if ($weight <= 324) { + return '500K'; + } + if ($weight <= 388) { + return '600K'; + } + if ($weight <= 611) { + return '800K'; + } + + return '1000K'; + } + + // 기본값 + return '300K'; + } + + // ========================================================================= + // 브라켓 크기 계산 + // ========================================================================= + + /** + * 브라켓 크기 결정 + * + * @param float $weight 중량 (kg) + * @param string|null $bracketInch 브라켓 인치 (선택) + * @return string 브라켓 크기 (530*320, 600*350, 690*390) + */ + public function calculateBracketSize(float $weight, ?string $bracketInch = null): string + { + $motorCapacity = $this->getMotorCapacityByWeight($weight, $bracketInch); + + return match ($motorCapacity) { + '300K', '400K' => '530*320', + '500K', '600K' => '600*350', + '800K', '1000K' => '690*390', + default => '530*320', + }; + } + + /** + * 중량으로 모터 용량 판단 (인치 없을 때) + */ + private function getMotorCapacityByWeight(float $weight, ?string $bracketInch = null): string + { + if ($bracketInch) { + // 인치가 있으면 철재 기준으로 계산 + return $this->calculateSteelMotor($weight, (int) $bracketInch); + } + + // 인치 없으면 중량만으로 판단 + if ($weight <= 300) { + return '300K'; + } + if ($weight <= 400) { + return '400K'; + } + if ($weight <= 500) { + return '500K'; + } + if ($weight <= 600) { + return '600K'; + } + if ($weight <= 800) { + return '800K'; + } + + return '1000K'; + } + + // ========================================================================= + // 주자재(스크린) 계산 + // ========================================================================= + + /** + * 스크린 주자재 가격 계산 + * + * @param float $width 폭 (mm) + * @param float $height 높이 (mm) + * @return array [unit_price, area, total_price] + */ + public function calculateScreenPrice(float $width, float $height): array + { + // 면적 계산: W × (H + 550) / 1,000,000 + $calculateHeight = $height + 550; + $area = ($width * $calculateHeight) / 1000000; + + // 원자재 단가 조회 (실리카/스크린) + $unitPrice = $this->getRawMaterialPrice('실리카'); + + return [ + 'unit_price' => $unitPrice, + 'area' => round($area, 2), + 'total_price' => round($unitPrice * $area), + ]; + } + + // ========================================================================= + // 단가 조회 메서드 (KdPriceTable 사용) + // ========================================================================= + + /** + * BDmodels 테이블에서 단가 조회 + * + * @param string $modelName 모델코드 (KSS01, KWS01 등) + * @param string $secondItem 부품분류 (케이스, 가이드레일, 하단마감재, L-BAR 등) + * @param string|null $finishingType 마감재질 (SUS, EGI) + * @param string|null $spec 규격 (120*70, 650*550 등) + * @return float 단가 + */ + public function getBDModelPrice( + string $modelName, + string $secondItem, + ?string $finishingType = null, + ?string $spec = null + ): float { + // BDmodels는 복잡한 구조이므로 items 테이블의 기존 데이터 활용 + // TODO: 필요시 kd_price_tables TYPE_BDMODELS 추가 + return 0.0; + } + + /** + * price_* 테이블에서 단가 조회 (모터, 샤프트, 파이프, 앵글) + * + * @param string $tableName 테이블명 (motor, shaft, pipe, angle) + * @param array $conditions 조회 조건 + * @return float 단가 + */ + public function getPriceFromTable(string $tableName, array $conditions): float + { + $query = KdPriceTable::where('table_type', $tableName)->active(); + + foreach ($conditions as $field => $value) { + $query->where($field, $value); + } + + $record = $query->first(); + + return (float) ($record?->unit_price ?? 0); + } + + /** + * 원자재 단가 조회 + * + * @param string $materialName 원자재명 (실리카, 스크린 등) + * @return float 단가 + */ + public function getRawMaterialPrice(string $materialName): float + { + return KdPriceTable::getRawMaterialPrice($materialName); + } + + /** + * 모터 단가 조회 + * + * @param string $motorCapacity 모터 용량 (150K, 300K 등) + * @return float 단가 + */ + public function getMotorPrice(string $motorCapacity): float + { + return KdPriceTable::getMotorPrice($motorCapacity); + } + + /** + * 제어기 단가 조회 + * + * @param string $controllerType 제어기 타입 (매립형, 노출형, 뒷박스) + * @return float 단가 + */ + public function getControllerPrice(string $controllerType): float + { + return KdPriceTable::getControllerPrice($controllerType); + } + + /** + * 샤프트 단가 조회 + * + * @param string $size 사이즈 (3, 4, 5인치) + * @param float $length 길이 (m 단위) + * @return float 단가 + */ + public function getShaftPrice(string $size, float $length): float + { + return KdPriceTable::getShaftPrice($size, $length); + } + + /** + * 파이프 단가 조회 + * + * @param string $thickness 두께 (1.4 등) + * @param int $length 길이 (3000, 6000) + * @return float 단가 + */ + public function getPipePrice(string $thickness, int $length): float + { + return KdPriceTable::getPipePrice($thickness, $length); + } + + /** + * 앵글 단가 조회 + * + * @param string $type 타입 (스크린용, 철재용) + * @param string $bracketSize 브라켓크기 (530*320, 600*350, 690*390) + * @param string $angleType 앵글타입 (앵글3T, 앵글4T) + * @return float 단가 + */ + public function getAnglePrice(string $type, string $bracketSize, string $angleType): float + { + return KdPriceTable::getAnglePrice($type, $bracketSize, $angleType); + } + + // ========================================================================= + // 절곡품 계산 (10종) + // ========================================================================= + + /** + * 절곡품 항목 계산 (10종) + * + * 케이스, 케이스용 연기차단재, 케이스 마구리, 가이드레일, + * 레일용 연기차단재, 하장바, L바, 보강평철, 무게평철12T, 환봉 + * + * @param array $params 입력 파라미터 + * @return array 절곡품 항목 배열 + */ + public function calculateSteelItems(array $params): array + { + $items = []; + + // 기본 파라미터 + $width = (float) ($params['W0'] ?? 0); + $height = (float) ($params['H0'] ?? 0); + $quantity = (int) ($params['QTY'] ?? 1); + $modelName = $params['model_name'] ?? 'KSS01'; + $finishingType = $params['finishing_type'] ?? 'SUS'; + + // 절곡품 관련 파라미터 + $caseSpec = $params['case_spec'] ?? '500*380'; + $caseLength = (float) ($params['case_length'] ?? $width); // mm 단위 + $guideType = $params['guide_type'] ?? '벽면형'; // 벽면형, 측면형, 혼합형 + $guideSpec = $params['guide_spec'] ?? '120*70'; // 120*70, 120*100 + $guideLength = (float) ($params['guide_length'] ?? ($height + 550)) / 1000; // m 단위 + $bottomBarLength = (float) ($params['bottombar_length'] ?? $width) / 1000; // m 단위 + $lbarLength = (float) ($params['lbar_length'] ?? $width) / 1000; // m 단위 + $flatBarLength = (float) ($params['flatbar_length'] ?? $width) / 1000; // m 단위 + $weightPlateQty = (int) ($params['weight_plate_qty'] ?? 0); // 무게평철 수량 + $roundBarQty = (int) ($params['round_bar_qty'] ?? 0); // 환봉 수량 + + // 1. 케이스 (단가/1000 × 길이mm × 수량) + $casePrice = KdPriceTable::getCasePrice($caseSpec); + if ($casePrice > 0 && $caseLength > 0) { + $totalPrice = ($casePrice / 1000) * $caseLength * $quantity; + $items[] = [ + 'category' => 'steel', + 'item_name' => '케이스', + 'specification' => "{$caseSpec} {$caseLength}mm", + 'unit' => 'm', + 'quantity' => $caseLength / 1000 * $quantity, + 'unit_price' => $casePrice, + 'total_price' => round($totalPrice), + ]; + } + + // 2. 케이스용 연기차단재 (단가 × 길이m × 수량) + $caseSmokePrice = KdPriceTable::getCaseSmokeBlockPrice(); + if ($caseSmokePrice > 0 && $caseLength > 0) { + $lengthM = $caseLength / 1000; + $items[] = [ + 'category' => 'steel', + 'item_name' => '케이스용 연기차단재', + 'specification' => "{$lengthM}m", + 'unit' => 'm', + 'quantity' => $lengthM * $quantity, + 'unit_price' => $caseSmokePrice, + 'total_price' => round($caseSmokePrice * $lengthM * $quantity), + ]; + } + + // 3. 케이스 마구리 (단가 × 수량) + $caseCapPrice = KdPriceTable::getCaseCapPrice($caseSpec); + if ($caseCapPrice > 0) { + $capQty = 2 * $quantity; // 좌우 2개 + $items[] = [ + 'category' => 'steel', + 'item_name' => '케이스 마구리', + 'specification' => $caseSpec, + 'unit' => 'EA', + 'quantity' => $capQty, + 'unit_price' => $caseCapPrice, + 'total_price' => round($caseCapPrice * $capQty), + ]; + } + + // 4. 가이드레일 (단가 × 길이m × 수량) - 타입별 처리 + $guideItems = $this->calculateGuideRails($modelName, $finishingType, $guideType, $guideSpec, $guideLength, $quantity); + $items = array_merge($items, $guideItems); + + // 5. 레일용 연기차단재 (단가 × 길이m × 2 × 수량) + $railSmokePrice = KdPriceTable::getRailSmokeBlockPrice(); + if ($railSmokePrice > 0 && $guideLength > 0) { + $railSmokeQty = 2 * $quantity; // 좌우 2개 + $items[] = [ + 'category' => 'steel', + 'item_name' => '레일용 연기차단재', + 'specification' => "{$guideLength}m × 2", + 'unit' => 'm', + 'quantity' => $guideLength * $railSmokeQty, + 'unit_price' => $railSmokePrice, + 'total_price' => round($railSmokePrice * $guideLength * $railSmokeQty), + ]; + } + + // 6. 하장바 (단가 × 길이m × 수량) + $bottomBarPrice = KdPriceTable::getBottomBarPrice($modelName, $finishingType); + if ($bottomBarPrice > 0 && $bottomBarLength > 0) { + $items[] = [ + 'category' => 'steel', + 'item_name' => '하장바', + 'specification' => "{$modelName} {$finishingType} {$bottomBarLength}m", + 'unit' => 'm', + 'quantity' => $bottomBarLength * $quantity, + 'unit_price' => $bottomBarPrice, + 'total_price' => round($bottomBarPrice * $bottomBarLength * $quantity), + ]; + } + + // 7. L바 (단가 × 길이m × 수량) + $lbarPrice = KdPriceTable::getLBarPrice($modelName); + if ($lbarPrice > 0 && $lbarLength > 0) { + $items[] = [ + 'category' => 'steel', + 'item_name' => 'L바', + 'specification' => "{$modelName} {$lbarLength}m", + 'unit' => 'm', + 'quantity' => $lbarLength * $quantity, + 'unit_price' => $lbarPrice, + 'total_price' => round($lbarPrice * $lbarLength * $quantity), + ]; + } + + // 8. 보강평철 (단가 × 길이m × 수량) + $flatBarPrice = KdPriceTable::getFlatBarPrice(); + if ($flatBarPrice > 0 && $flatBarLength > 0) { + $items[] = [ + 'category' => 'steel', + 'item_name' => '보강평철', + 'specification' => "{$flatBarLength}m", + 'unit' => 'm', + 'quantity' => $flatBarLength * $quantity, + 'unit_price' => $flatBarPrice, + 'total_price' => round($flatBarPrice * $flatBarLength * $quantity), + ]; + } + + // 9. 무게평철12T (고정 12,000원 × 수량) + if ($weightPlateQty > 0) { + $weightPlatePrice = 12000; + $items[] = [ + 'category' => 'steel', + 'item_name' => '무게평철12T', + 'specification' => '12T', + 'unit' => 'EA', + 'quantity' => $weightPlateQty * $quantity, + 'unit_price' => $weightPlatePrice, + 'total_price' => $weightPlatePrice * $weightPlateQty * $quantity, + ]; + } + + // 10. 환봉 (고정 2,000원 × 수량) + if ($roundBarQty > 0) { + $roundBarPrice = 2000; + $items[] = [ + 'category' => 'steel', + 'item_name' => '환봉', + 'specification' => '', + 'unit' => 'EA', + 'quantity' => $roundBarQty * $quantity, + 'unit_price' => $roundBarPrice, + 'total_price' => $roundBarPrice * $roundBarQty * $quantity, + ]; + } + + return $items; + } + + /** + * 가이드레일 계산 (타입별 처리) + * + * @param string $modelName 모델코드 + * @param string $finishingType 마감재질 + * @param string $guideType 가이드레일 타입 (벽면형, 측면형, 혼합형) + * @param string $guideSpec 가이드레일 규격 (120*70, 120*100) + * @param float $guideLength 가이드레일 길이 (m) + * @param int $quantity 수량 + * @return array 가이드레일 항목 배열 + */ + private function calculateGuideRails( + string $modelName, + string $finishingType, + string $guideType, + string $guideSpec, + float $guideLength, + int $quantity + ): array { + $items = []; + + if ($guideLength <= 0) { + return $items; + } + + switch ($guideType) { + case '벽면형': + // 120*70 × 2개 + $price = KdPriceTable::getGuideRailPrice($modelName, $finishingType, '120*70'); + if ($price > 0) { + $guideQty = 2 * $quantity; + $items[] = [ + 'category' => 'steel', + 'item_name' => '가이드레일', + 'specification' => "{$modelName} {$finishingType} 120*70 {$guideLength}m × 2", + 'unit' => 'm', + 'quantity' => $guideLength * $guideQty, + 'unit_price' => $price, + 'total_price' => round($price * $guideLength * $guideQty), + ]; + } + break; + + case '측면형': + // 120*100 × 2개 + $price = KdPriceTable::getGuideRailPrice($modelName, $finishingType, '120*100'); + if ($price > 0) { + $guideQty = 2 * $quantity; + $items[] = [ + 'category' => 'steel', + 'item_name' => '가이드레일', + 'specification' => "{$modelName} {$finishingType} 120*100 {$guideLength}m × 2", + 'unit' => 'm', + 'quantity' => $guideLength * $guideQty, + 'unit_price' => $price, + 'total_price' => round($price * $guideLength * $guideQty), + ]; + } + break; + + case '혼합형': + // 120*70 × 1개 + 120*100 × 1개 + $price70 = KdPriceTable::getGuideRailPrice($modelName, $finishingType, '120*70'); + $price100 = KdPriceTable::getGuideRailPrice($modelName, $finishingType, '120*100'); + + if ($price70 > 0) { + $items[] = [ + 'category' => 'steel', + 'item_name' => '가이드레일', + 'specification' => "{$modelName} {$finishingType} 120*70 {$guideLength}m", + 'unit' => 'm', + 'quantity' => $guideLength * $quantity, + 'unit_price' => $price70, + 'total_price' => round($price70 * $guideLength * $quantity), + ]; + } + if ($price100 > 0) { + $items[] = [ + 'category' => 'steel', + 'item_name' => '가이드레일', + 'specification' => "{$modelName} {$finishingType} 120*100 {$guideLength}m", + 'unit' => 'm', + 'quantity' => $guideLength * $quantity, + 'unit_price' => $price100, + 'total_price' => round($price100 * $guideLength * $quantity), + ]; + } + break; + } + + return $items; + } + + // ========================================================================= + // 부자재 계산 (3종) + // ========================================================================= + + /** + * 부자재 항목 계산 + * + * @param array $params 입력 파라미터 + * @return array 부자재 항목 배열 + */ + public function calculatePartItems(array $params): array + { + $items = []; + + $width = (float) ($params['W0'] ?? 0); + $bracketInch = $params['bracket_inch'] ?? '5'; + $bracketSize = $params['BRACKET_SIZE'] ?? $this->calculateBracketSize(100, $bracketInch); + $productType = $params['product_type'] ?? 'screen'; + $quantity = (int) ($params['QTY'] ?? 1); + + // 1. 감기샤프트 + $shaftSize = $bracketInch; + $shaftLength = ceil($width / 1000); // mm → m 변환 후 올림 + $shaftPrice = $this->getShaftPrice($shaftSize, $shaftLength); + if ($shaftPrice > 0) { + $items[] = [ + 'category' => 'parts', + 'item_name' => "감기샤프트 {$shaftSize}인치", + 'specification' => "{$shaftLength}m", + 'unit' => 'EA', + 'quantity' => $quantity, + 'unit_price' => $shaftPrice, + 'total_price' => $shaftPrice * $quantity, + ]; + } + + // 2. 각파이프 + $pipeThickness = '1.4'; + $pipeLength = $width > 3000 ? 6000 : 3000; + $pipePrice = $this->getPipePrice($pipeThickness, $pipeLength); + if ($pipePrice > 0) { + $items[] = [ + 'category' => 'parts', + 'item_name' => '각파이프', + 'specification' => "{$pipeThickness}T {$pipeLength}mm", + 'unit' => 'EA', + 'quantity' => $quantity, + 'unit_price' => $pipePrice, + 'total_price' => $pipePrice * $quantity, + ]; + } + + // 3. 앵글 + $angleType = $productType === 'steel' ? '철재용' : '스크린용'; + $angleSpec = $bracketSize === '690*390' ? '앵글4T' : '앵글3T'; + $anglePrice = $this->getAnglePrice($angleType, $bracketSize, $angleSpec); + if ($anglePrice > 0) { + $items[] = [ + 'category' => 'parts', + 'item_name' => "앵글 {$angleSpec}", + 'specification' => "{$angleType} {$bracketSize}", + 'unit' => 'EA', + 'quantity' => 2 * $quantity, // 좌우 2개 + 'unit_price' => $anglePrice, + 'total_price' => $anglePrice * 2 * $quantity, + ]; + } + + return $items; + } + + // ========================================================================= + // 전체 동적 항목 계산 + // ========================================================================= + + /** + * 동적 항목 전체 계산 + * + * @param array $inputs 입력 파라미터 + * @return array 계산된 항목 배열 + */ + public function calculateDynamicItems(array $inputs): array + { + $items = []; + + $width = (float) ($inputs['W0'] ?? 0); + $height = (float) ($inputs['H0'] ?? 0); + $quantity = (int) ($inputs['QTY'] ?? 1); + $bracketInch = $inputs['bracket_inch'] ?? '5'; + $productType = $inputs['product_type'] ?? 'screen'; + + // 중량 계산 (5130 로직) + $area = ($width * ($height + 550)) / 1000000; + $weight = $area * ($productType === 'steel' ? 25 : 2) + ($width / 1000) * 14.17; + + // 모터 용량/브라켓 크기 계산 + $motorCapacity = $this->calculateMotorCapacity($productType, $weight, $bracketInch); + $bracketSize = $this->calculateBracketSize($weight, $bracketInch); + + // 입력값에 계산된 값 추가 (부자재 계산용) + $inputs['WEIGHT'] = $weight; + $inputs['MOTOR_CAPACITY'] = $motorCapacity; + $inputs['BRACKET_SIZE'] = $bracketSize; + + // 1. 주자재 (스크린) + $screenResult = $this->calculateScreenPrice($width, $height); + $items[] = [ + 'category' => 'material', + 'item_code' => 'KD-SCREEN', + 'item_name' => '주자재(스크린)', + 'specification' => "면적 {$screenResult['area']}㎡", + 'unit' => '㎡', + 'quantity' => $screenResult['area'] * $quantity, + 'unit_price' => $screenResult['unit_price'], + 'total_price' => $screenResult['total_price'] * $quantity, + ]; + + // 2. 모터 + $motorPrice = $this->getMotorPrice($motorCapacity); + $items[] = [ + 'category' => 'motor', + 'item_code' => "KD-MOTOR-{$motorCapacity}", + 'item_name' => "모터 {$motorCapacity}", + 'specification' => $motorCapacity, + 'unit' => 'EA', + 'quantity' => $quantity, + 'unit_price' => $motorPrice, + 'total_price' => $motorPrice * $quantity, + ]; + + // 3. 제어기 + $controllerType = $inputs['controller_type'] ?? '매립형'; + $controllerPrice = $this->getControllerPrice($controllerType); + $items[] = [ + 'category' => 'controller', + 'item_code' => 'KD-CTRL-'.strtoupper($controllerType), + 'item_name' => "제어기 {$controllerType}", + 'specification' => $controllerType, + 'unit' => 'EA', + 'quantity' => $quantity, + 'unit_price' => $controllerPrice, + 'total_price' => $controllerPrice * $quantity, + ]; + + // 4. 절곡품 + $steelItems = $this->calculateSteelItems($inputs); + $items = array_merge($items, $steelItems); + + // 5. 부자재 + $partItems = $this->calculatePartItems($inputs); + $items = array_merge($items, $partItems); + + return $items; + } +} diff --git a/database/migrations/2026_01_29_004736_create_kd_price_tables_table.php b/database/migrations/2026_01_29_004736_create_kd_price_tables_table.php new file mode 100644 index 0000000..b1d5691 --- /dev/null +++ b/database/migrations/2026_01_29_004736_create_kd_price_tables_table.php @@ -0,0 +1,62 @@ +id(); + $table->unsignedBigInteger('tenant_id')->default(287)->comment('경동기업 테넌트 ID'); + $table->string('table_type', 50)->comment('테이블 유형: motor, shaft, pipe, angle, raw_material, bdmodels'); + $table->string('item_code', 100)->nullable()->comment('품목 코드 (연동용)'); + $table->string('item_name', 200)->nullable()->comment('품목명'); + + // 조회 조건 필드들 + $table->string('category', 100)->nullable()->comment('분류 (모터용량, 재질, 타입 등)'); + $table->string('spec1', 100)->nullable()->comment('규격1 (사이즈, 두께 등)'); + $table->string('spec2', 100)->nullable()->comment('규격2 (길이, 브라켓크기 등)'); + $table->string('spec3', 100)->nullable()->comment('규격3 (추가 조건)'); + + // 단가 정보 + $table->decimal('unit_price', 15, 2)->default(0)->comment('단가'); + $table->string('unit', 20)->default('EA')->comment('단위'); + + // 원본 JSON 데이터 (레거시 호환용) + $table->json('raw_data')->nullable()->comment('원본 JSON 데이터'); + + // 메타 정보 + $table->boolean('is_active')->default(true)->comment('활성 여부'); + $table->timestamps(); + + // 인덱스 + $table->index(['tenant_id', 'table_type']); + $table->index(['table_type', 'category']); + $table->index(['table_type', 'spec1', 'spec2']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('kd_price_tables'); + } +}; diff --git a/database/seeders/Kyungdong/KdPriceTableSeeder.php b/database/seeders/Kyungdong/KdPriceTableSeeder.php new file mode 100644 index 0000000..490553a --- /dev/null +++ b/database/seeders/Kyungdong/KdPriceTableSeeder.php @@ -0,0 +1,565 @@ +command->info(''); + $this->command->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + $this->command->info('🔧 경동기업 단가 테이블 마이그레이션 (kd_price_tables)'); + $this->command->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + // 기존 데이터 삭제 + DB::table('kd_price_tables')->where('tenant_id', self::TENANT_ID)->delete(); + $this->command->info(' → 기존 데이터 삭제 완료'); + + // chandj 연결 가능 여부 확인 + $chandjAvailable = $this->checkChandjConnection(); + + if ($chandjAvailable) { + $this->command->info(' → chandj 데이터베이스 연결됨'); + $this->migrateFromChandj(); + } else { + $this->command->warn(' → chandj 데이터베이스 연결 불가 - 샘플 데이터 사용'); + $this->insertSampleData(); + } + + $count = DB::table('kd_price_tables')->where('tenant_id', self::TENANT_ID)->count(); + $this->command->info(''); + $this->command->info("✅ 완료: kd_price_tables {$count}건"); + } + + /** + * chandj 데이터베이스 연결 확인 + */ + private function checkChandjConnection(): bool + { + try { + DB::connection('chandj')->getPdo(); + + return true; + } catch (\Exception $e) { + return false; + } + } + + /** + * chandj 데이터베이스에서 마이그레이션 + */ + private function migrateFromChandj(): void + { + $this->migrateMotorPrices(); + $this->migrateShaftPrices(); + $this->migratePipePrices(); + $this->migrateAnglePrices(); + $this->migrateRawMaterialPrices(); + } + + /** + * price_motor → kd_price_tables + */ + private function migrateMotorPrices(): void + { + $this->command->info(''); + $this->command->info('📦 [1/5] 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; + } + + $itemList = json_decode($priceMotor->itemList, true); + if (! is_array($itemList)) { + $this->command->info(' → JSON 파싱 실패'); + + return; + } + + $count = 0; + $now = now(); + + foreach ($itemList as $item) { + $col1 = $item['col1'] ?? ''; // 전압/카테고리 (220, 380, 제어기 등) + $col2 = $item['col2'] ?? ''; // 용량/품목명 + $salesPrice = (float) str_replace(',', '', $item['col13'] ?? '0'); + + if (empty($col2) || $salesPrice <= 0) { + continue; + } + + // 카테고리 결정 + $category = match ($col1) { + '220', '380' => $col2, // 모터 용량 (150K, 300K 등) + default => $col1, // 제어기, 방화, 방범 등 + }; + + KdPriceTable::create([ + 'tenant_id' => self::TENANT_ID, + 'table_type' => KdPriceTable::TYPE_MOTOR, + 'item_name' => trim("{$col1} {$col2}"), + 'category' => $category, + 'spec1' => $col1, + 'spec2' => $col2, + 'unit_price' => $salesPrice, + 'unit' => 'EA', + 'raw_data' => $item, + 'is_active' => true, + ]); + $count++; + } + + $this->command->info(" → {$count}건 완료"); + } + + /** + * price_shaft → kd_price_tables + */ + private function migrateShaftPrices(): void + { + $this->command->info(''); + $this->command->info('📦 [2/5] price_shaft 마이그레이션...'); + + $priceShaft = DB::connection('chandj') + ->table('price_shaft') + ->where(function ($q) { + $q->where('is_deleted', 0)->orWhereNull('is_deleted'); + }) + ->orderByDesc('registedate') + ->first(); + + if (! $priceShaft || empty($priceShaft->itemList)) { + $this->command->info(' → 데이터 없음'); + + return; + } + + $itemList = json_decode($priceShaft->itemList, true); + if (! is_array($itemList)) { + $this->command->info(' → JSON 파싱 실패'); + + return; + } + + $count = 0; + + foreach ($itemList as $item) { + $size = $item['col4'] ?? ''; // 사이즈 (3, 4, 5인치) + $length = $item['col10'] ?? ''; // 길이 (m 단위) + $salesPrice = (float) str_replace(',', '', $item['col19'] ?? '0'); + + if (empty($size) || $salesPrice <= 0) { + continue; + } + + KdPriceTable::create([ + 'tenant_id' => self::TENANT_ID, + 'table_type' => KdPriceTable::TYPE_SHAFT, + 'item_name' => "감기샤프트 {$size}인치 {$length}m", + 'category' => '감기샤프트', + 'spec1' => $size, + 'spec2' => $length, + 'unit_price' => $salesPrice, + 'unit' => 'EA', + 'raw_data' => $item, + 'is_active' => true, + ]); + $count++; + } + + $this->command->info(" → {$count}건 완료"); + } + + /** + * price_pipe → kd_price_tables + */ + private function migratePipePrices(): void + { + $this->command->info(''); + $this->command->info('📦 [3/5] price_pipe 마이그레이션...'); + + $pricePipe = DB::connection('chandj') + ->table('price_pipe') + ->where(function ($q) { + $q->where('is_deleted', 0)->orWhereNull('is_deleted'); + }) + ->orderByDesc('registedate') + ->first(); + + if (! $pricePipe || empty($pricePipe->itemList)) { + $this->command->info(' → 데이터 없음'); + + return; + } + + $itemList = json_decode($pricePipe->itemList, true); + if (! is_array($itemList)) { + $this->command->info(' → JSON 파싱 실패'); + + return; + } + + $count = 0; + + foreach ($itemList as $item) { + $length = $item['col2'] ?? ''; // 길이 (3000, 6000) + $thickness = $item['col4'] ?? ''; // 두께 (1.4) + $salesPrice = (float) str_replace(',', '', $item['col8'] ?? '0'); + + if (empty($thickness) || $salesPrice <= 0) { + continue; + } + + KdPriceTable::create([ + 'tenant_id' => self::TENANT_ID, + 'table_type' => KdPriceTable::TYPE_PIPE, + 'item_name' => "각파이프 {$thickness}T {$length}mm", + 'category' => '각파이프', + 'spec1' => $thickness, + 'spec2' => $length, + 'unit_price' => $salesPrice, + 'unit' => 'EA', + 'raw_data' => $item, + 'is_active' => true, + ]); + $count++; + } + + $this->command->info(" → {$count}건 완료"); + } + + /** + * price_angle → kd_price_tables + */ + private function migrateAnglePrices(): void + { + $this->command->info(''); + $this->command->info('📦 [4/5] price_angle 마이그레이션...'); + + $priceAngle = DB::connection('chandj') + ->table('price_angle') + ->where(function ($q) { + $q->where('is_deleted', 0)->orWhereNull('is_deleted'); + }) + ->orderByDesc('registedate') + ->first(); + + if (! $priceAngle || empty($priceAngle->itemList)) { + $this->command->info(' → 데이터 없음'); + + return; + } + + $itemList = json_decode($priceAngle->itemList, true); + if (! is_array($itemList)) { + $this->command->info(' → JSON 파싱 실패'); + + return; + } + + $count = 0; + + foreach ($itemList as $item) { + $type = $item['col2'] ?? ''; // 타입 (스크린용, 철재용) + $bracketSize = $item['col3'] ?? ''; // 브라켓크기 + $angleType = $item['col4'] ?? ''; // 앵글타입 (앵글3T, 앵글4T) + $thickness = $item['col10'] ?? ''; // 두께 + $salesPrice = (float) str_replace(',', '', $item['col19'] ?? '0'); + + if (empty($type) || $salesPrice <= 0) { + continue; + } + + KdPriceTable::create([ + 'tenant_id' => self::TENANT_ID, + 'table_type' => KdPriceTable::TYPE_ANGLE, + 'item_name' => "앵글 {$type} {$bracketSize} {$angleType}", + 'category' => $type, + 'spec1' => $bracketSize, + 'spec2' => $angleType, + 'spec3' => $thickness, + 'unit_price' => $salesPrice, + 'unit' => 'EA', + 'raw_data' => $item, + 'is_active' => true, + ]); + $count++; + } + + $this->command->info(" → {$count}건 완료"); + } + + /** + * price_raw_materials → kd_price_tables + */ + private function migrateRawMaterialPrices(): void + { + $this->command->info(''); + $this->command->info('📦 [5/5] 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; + } + + $itemList = json_decode($priceRaw->itemList, true); + if (! is_array($itemList)) { + $this->command->info(' → JSON 파싱 실패'); + + return; + } + + $count = 0; + + foreach ($itemList as $item) { + $name = $item['col2'] ?? ''; + $spec = $item['col3'] ?? ''; + $salesPrice = (float) str_replace(',', '', $item['col19'] ?? $item['col13'] ?? '0'); + + if (empty($name) || $salesPrice <= 0) { + continue; + } + + KdPriceTable::create([ + 'tenant_id' => self::TENANT_ID, + 'table_type' => KdPriceTable::TYPE_RAW_MATERIAL, + 'item_name' => $name, + 'category' => '원자재', + 'spec1' => $spec, + 'unit_price' => $salesPrice, + 'unit' => '㎡', + 'raw_data' => $item, + 'is_active' => true, + ]); + $count++; + } + + $this->command->info(" → {$count}건 완료"); + } + + /** + * chandj 연결 불가 시 샘플 데이터 삽입 + */ + private function insertSampleData(): void + { + $this->command->info(''); + $this->command->info('📦 샘플 데이터 삽입 중...'); + + // 모터 샘플 데이터 (5130 분석 결과 기반) + $motorData = [ + ['category' => '150K', 'spec1' => '220', 'spec2' => '150K', 'unit_price' => 85000], + ['category' => '300K', 'spec1' => '220', 'spec2' => '300K', 'unit_price' => 120000], + ['category' => '400K', 'spec1' => '220', 'spec2' => '400K', 'unit_price' => 150000], + ['category' => '500K', 'spec1' => '220', 'spec2' => '500K', 'unit_price' => 180000], + ['category' => '600K', 'spec1' => '220', 'spec2' => '600K', 'unit_price' => 220000], + ['category' => '800K', 'spec1' => '220', 'spec2' => '800K', 'unit_price' => 280000], + ['category' => '1000K', 'spec1' => '220', 'spec2' => '1000K', 'unit_price' => 350000], + ['category' => '매립형', 'spec1' => '제어기', 'spec2' => '매립형', 'unit_price' => 45000], + ['category' => '노출형', 'spec1' => '제어기', 'spec2' => '노출형', 'unit_price' => 55000], + ['category' => '뒷박스', 'spec1' => '제어기', 'spec2' => '뒷박스', 'unit_price' => 35000], + ]; + + foreach ($motorData as $data) { + KdPriceTable::create(array_merge($data, [ + 'tenant_id' => self::TENANT_ID, + 'table_type' => KdPriceTable::TYPE_MOTOR, + 'item_name' => "모터/제어기 {$data['category']}", + 'unit' => 'EA', + 'is_active' => true, + ])); + } + $this->command->info(' → 모터/제어기 '.count($motorData).'건'); + + // 샤프트 샘플 데이터 + $shaftData = [ + ['spec1' => '3', 'spec2' => '3.0', 'unit_price' => 45000], + ['spec1' => '4', 'spec2' => '3.0', 'unit_price' => 55000], + ['spec1' => '5', 'spec2' => '3.0', 'unit_price' => 65000], + ['spec1' => '3', 'spec2' => '4.0', 'unit_price' => 60000], + ['spec1' => '4', 'spec2' => '4.0', 'unit_price' => 75000], + ['spec1' => '5', 'spec2' => '4.0', 'unit_price' => 90000], + ]; + + foreach ($shaftData as $data) { + KdPriceTable::create(array_merge($data, [ + 'tenant_id' => self::TENANT_ID, + 'table_type' => KdPriceTable::TYPE_SHAFT, + 'item_name' => "감기샤프트 {$data['spec1']}인치 {$data['spec2']}m", + 'category' => '감기샤프트', + 'unit' => 'EA', + 'is_active' => true, + ])); + } + $this->command->info(' → 샤프트 '.count($shaftData).'건'); + + // 파이프 샘플 데이터 + $pipeData = [ + ['spec1' => '1.4', 'spec2' => '3000', 'unit_price' => 12000], + ['spec1' => '1.4', 'spec2' => '6000', 'unit_price' => 24000], + ]; + + foreach ($pipeData as $data) { + KdPriceTable::create(array_merge($data, [ + 'tenant_id' => self::TENANT_ID, + 'table_type' => KdPriceTable::TYPE_PIPE, + 'item_name' => "각파이프 {$data['spec1']}T {$data['spec2']}mm", + 'category' => '각파이프', + 'unit' => 'EA', + 'is_active' => true, + ])); + } + $this->command->info(' → 파이프 '.count($pipeData).'건'); + + // 앵글 샘플 데이터 + $angleData = [ + ['category' => '스크린용', 'spec1' => '530*320', 'spec2' => '앵글3T', 'unit_price' => 8000], + ['category' => '스크린용', 'spec1' => '600*350', 'spec2' => '앵글3T', 'unit_price' => 10000], + ['category' => '스크린용', 'spec1' => '690*390', 'spec2' => '앵글4T', 'unit_price' => 12000], + ['category' => '철재용', 'spec1' => '530*320', 'spec2' => '앵글3T', 'unit_price' => 9000], + ['category' => '철재용', 'spec1' => '600*350', 'spec2' => '앵글3T', 'unit_price' => 11000], + ['category' => '철재용', 'spec1' => '690*390', 'spec2' => '앵글4T', 'unit_price' => 14000], + ]; + + foreach ($angleData as $data) { + KdPriceTable::create(array_merge($data, [ + 'tenant_id' => self::TENANT_ID, + 'table_type' => KdPriceTable::TYPE_ANGLE, + 'item_name' => "앵글 {$data['category']} {$data['spec1']} {$data['spec2']}", + 'unit' => 'EA', + 'is_active' => true, + ])); + } + $this->command->info(' → 앵글 '.count($angleData).'건'); + + // 원자재 샘플 데이터 + $rawData = [ + ['item_name' => '실리카', 'spec1' => '스크린용', 'unit_price' => 25000], + ['item_name' => '불투명', 'spec1' => '스크린용', 'unit_price' => 22000], + ['item_name' => '화이바원단', 'spec1' => '스크린용', 'unit_price' => 28000], + ]; + + foreach ($rawData as $data) { + KdPriceTable::create(array_merge($data, [ + 'tenant_id' => self::TENANT_ID, + 'table_type' => KdPriceTable::TYPE_RAW_MATERIAL, + 'category' => '원자재', + 'unit' => '㎡', + 'is_active' => true, + ])); + } + $this->command->info(' → 원자재 '.count($rawData).'건'); + + // BDmodels 샘플 데이터 (절곡품) + $this->insertBDModelsSampleData(); + } + + /** + * BDmodels 샘플 데이터 삽입 (절곡품) + */ + private function insertBDModelsSampleData(): void + { + $bdmodelsData = [ + // 케이스 (규격별 단가 - 원/m) + ['category' => '케이스', 'spec2' => '500*380', 'unit_price' => 15000, 'unit' => 'm'], + ['category' => '케이스', 'spec2' => '550*430', 'unit_price' => 18000, 'unit' => 'm'], + ['category' => '케이스', 'spec2' => '650*550', 'unit_price' => 22000, 'unit' => 'm'], + + // 케이스 마구리 (규격별 단가 - 원/개) + ['category' => '마구리', 'spec2' => '500*380', 'unit_price' => 5000, 'unit' => 'EA'], + ['category' => '마구리', 'spec2' => '550*430', 'unit_price' => 6000, 'unit' => 'EA'], + ['category' => '마구리', 'spec2' => '650*550', 'unit_price' => 7500, 'unit' => 'EA'], + + // 케이스용 연기차단재 (공통 단가 - 원/m) + ['category' => '케이스용 연기차단재', 'unit_price' => 3500, 'unit' => 'm'], + + // 가이드레일 (모델+마감+규격별 단가 - 원/m) + ['category' => '가이드레일', 'item_code' => 'KSS01', 'spec1' => 'SUS', 'spec2' => '120*70', 'unit_price' => 12000, 'unit' => 'm'], + ['category' => '가이드레일', 'item_code' => 'KSS01', 'spec1' => 'SUS', 'spec2' => '120*100', 'unit_price' => 15000, 'unit' => 'm'], + ['category' => '가이드레일', 'item_code' => 'KSS01', 'spec1' => 'EGI', 'spec2' => '120*70', 'unit_price' => 10000, 'unit' => 'm'], + ['category' => '가이드레일', 'item_code' => 'KSS01', 'spec1' => 'EGI', 'spec2' => '120*100', 'unit_price' => 13000, 'unit' => 'm'], + ['category' => '가이드레일', 'item_code' => 'KWS01', 'spec1' => 'SUS', 'spec2' => '120*70', 'unit_price' => 13000, 'unit' => 'm'], + ['category' => '가이드레일', 'item_code' => 'KWS01', 'spec1' => 'SUS', 'spec2' => '120*100', 'unit_price' => 16000, 'unit' => 'm'], + + // 가이드레일용 연기차단재 (공통 단가 - 원/m) + ['category' => '가이드레일용 연기차단재', 'unit_price' => 2500, 'unit' => 'm'], + + // 하단마감재/하장바 (모델+마감별 단가 - 원/m) + ['category' => '하단마감재', 'item_code' => 'KSS01', 'spec1' => 'SUS', 'unit_price' => 8000, 'unit' => 'm'], + ['category' => '하단마감재', 'item_code' => 'KSS01', 'spec1' => 'EGI', 'unit_price' => 6500, 'unit' => 'm'], + ['category' => '하단마감재', 'item_code' => 'KWS01', 'spec1' => 'SUS', 'unit_price' => 8500, 'unit' => 'm'], + + // L-BAR (모델별 단가 - 원/m) + ['category' => 'L-BAR', 'item_code' => 'KSS01', 'unit_price' => 4500, 'unit' => 'm'], + ['category' => 'L-BAR', 'item_code' => 'KWS01', 'unit_price' => 5000, 'unit' => 'm'], + + // 보강평철 (공통 단가 - 원/m) + ['category' => '보강평철', 'unit_price' => 3000, 'unit' => 'm'], + ]; + + foreach ($bdmodelsData as $data) { + $itemCode = $data['item_code'] ?? null; + $spec1 = $data['spec1'] ?? null; + $spec2 = $data['spec2'] ?? null; + + $itemName = $data['category']; + if ($itemCode) { + $itemName .= " {$itemCode}"; + } + if ($spec1) { + $itemName .= " {$spec1}"; + } + if ($spec2) { + $itemName .= " {$spec2}"; + } + + KdPriceTable::create([ + 'tenant_id' => self::TENANT_ID, + 'table_type' => KdPriceTable::TYPE_BDMODELS, + 'item_code' => $itemCode, + 'item_name' => $itemName, + 'category' => $data['category'], + 'spec1' => $spec1, + 'spec2' => $spec2, + 'unit_price' => $data['unit_price'], + 'unit' => $data['unit'], + 'is_active' => true, + ]); + } + $this->command->info(' → BDmodels(절곡품) '.count($bdmodelsData).'건'); + } +}