From a332e0cee4b38a4901fa8c4e2b2a59c38225ffcf Mon Sep 17 00:00:00 2001 From: hskwon Date: Mon, 22 Dec 2025 22:48:03 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Price=20=EB=AA=A8=EB=8D=B8=20items=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=B0=B8=EC=A1=B0=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B2=AC=EC=A0=81=20=EC=8B=9C=EB=8D=94=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Price.getSalesPriceByItemCode() products/materials → items 테이블 수정 - SeedQuoteFormulasCommand INPUT 카테고리 및 Design 사이트 동기화 - PC(제품카테고리), PRODUCT_ID(제품명), GT(설치유형) - MP(모터전원), CT(제어기), QTY(수량) 필드 추가 - metadata에 field_type, options 정보 추가 --- CURRENT_WORKS.md | 49 ++ .../Commands/SeedQuoteFormulasCommand.php | 823 ++++++++++++++++++ app/Models/Price.php | 25 +- 3 files changed, 878 insertions(+), 19 deletions(-) create mode 100644 app/Console/Commands/SeedQuoteFormulasCommand.php diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index 95cd349e..3490595b 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1709,3 +1709,52 @@ ### Git 커밋: - ✅ `85cbe23` "feat: [users] 사용자 등록 시 비밀번호 자동 생성 및 이메일 발송" --- + +## 2025-12-22 (일) - 견적 자동산출 시더 및 Price 모델 수정 + +### 주요 작업 + +**1. Price 모델 수정 (products → items 테이블)** +- `getSalesPriceByItemCode()` 메서드가 삭제된 `products`, `materials` 테이블 대신 통합된 `items` 테이블 조회하도록 수정 +- 원인: products/materials 테이블이 items로 통합되었으나 MNG의 Price 모델은 미반영 + +**2. 견적수식 시더 입력 필드 추가** +- Design 사이트 견적 입력 폼과 동기화 +- INPUT 카테고리 추가 (sort_order: 0) +- 새 입력 필드 6개 추가: + +| 변수명 | 필드명 | 타입 | 옵션 | +|------------|-------------|--------|------| +| PC | 제품카테고리 | select | 스크린/철재 | +| PRODUCT_ID | 제품명 | select | PC에 따라 변경 | +| GT | 설치유형 | select | 벽면/천장/바닥 | +| MP | 모터전원 | select | 220V/380V | +| CT | 제어기 | select | 단독제어/연동제어 | +| QTY | 수량 | number | min: 1 | + +- 기존 W0, H0 필드 metadata 보강 + +### 수정된 파일 + +**Models** +- `app/Models/Price.php` - getSalesPriceByItemCode() items 테이블 사용 + +**Console Commands** +- `app/Console/Commands/SeedQuoteFormulasCommand.php` + - INPUT 카테고리 추가 + - Design 사이트 입력 필드 6개 추가 (PC, PRODUCT_ID, GT, MP, CT, QTY) + - metadata에 field_type, options 정보 추가 + +### 시더 실행 결과 (tenant: 287) +``` +카테고리: 12개 (INPUT 추가) +수식: 24개 (이전 18개 + 새 input 6개) +범위: 18개 +품목: 4개 +``` + +### 시뮬레이터 접근 +- URL: https://mng.sam.kr/quote-formulas/simulator +- 새 입력 필드 표시 (UI가 metadata 지원 시) + +--- diff --git a/app/Console/Commands/SeedQuoteFormulasCommand.php b/app/Console/Commands/SeedQuoteFormulasCommand.php new file mode 100644 index 00000000..0ffb625a --- /dev/null +++ b/app/Console/Commands/SeedQuoteFormulasCommand.php @@ -0,0 +1,823 @@ +option('tenant'); + $only = $this->option('only'); + $fresh = $this->option('fresh'); + + $this->info("🚀 견적수식 시드 시작 (tenant_id: {$tenantId})"); + + if ($fresh) { + if ($this->confirm('⚠️ 기존 데이터를 모두 삭제하시겠습니까?')) { + $this->truncateTables($tenantId); + } else { + $this->warn('작업이 취소되었습니다.'); + + return Command::FAILURE; + } + } + + $stats = ['categories' => 0, 'formulas' => 0, 'ranges' => 0, 'items' => 0]; + + // 1. 카테고리 시드 + if (! $only || $only === 'categories') { + $stats['categories'] = $this->seedCategories($tenantId); + } + + // 2. 수식 시드 + if (! $only || $only === 'formulas') { + $stats['formulas'] = $this->seedFormulas($tenantId); + } + + // 3. 범위 시드 (수식에 포함된 ranges) + if (! $only || $only === 'ranges') { + $stats['ranges'] = $this->seedRanges($tenantId); + } + + // 4. 품목 시드 (quote_formula_items) + if (! $only || $only === 'items') { + $stats['items'] = $this->seedItems($tenantId); + } + + $this->newLine(); + $this->info('✅ 견적수식 시드 완료!'); + $this->table( + ['항목', '개수'], + [ + ['카테고리', $stats['categories']], + ['수식', $stats['formulas']], + ['범위', $stats['ranges']], + ['품목', $stats['items']], + ] + ); + + return Command::SUCCESS; + } + + /** + * 카테고리 시드 + */ + private function seedCategories(int $tenantId): int + { + $this->info('📁 카테고리 생성 중...'); + + $categories = [ + ['code' => 'INPUT', 'name' => '입력값', 'description' => '견적 산출을 위한 사용자 입력값', 'sort_order' => 0], + ['code' => 'OPEN_SIZE', 'name' => '오픈사이즈', 'description' => '자동 견적 산출의 기본 입력값', 'sort_order' => 1], + ['code' => 'MAKE_SIZE', 'name' => '제작사이즈', 'description' => '오픈사이즈 기반 제작 치수 계산', 'sort_order' => 2], + ['code' => 'AREA', 'name' => '면적', 'description' => '제작사이즈 기반 면적 계산', 'sort_order' => 3], + ['code' => 'WEIGHT', 'name' => '중량', 'description' => '면적 기반 중량 계산', 'sort_order' => 4], + ['code' => 'GUIDE_RAIL', 'name' => '가이드레일', 'description' => '가이드레일 자재 및 수량 산출', 'sort_order' => 5], + ['code' => 'CASE', 'name' => '케이스', 'description' => '케이스 자재 및 수량 산출', 'sort_order' => 6], + ['code' => 'MOTOR', 'name' => '모터', 'description' => '모터 규격 자동 선택', 'sort_order' => 7], + ['code' => 'CONTROLLER', 'name' => '제어기', 'description' => '제어기 유형 및 자동 선택', 'sort_order' => 8], + ['code' => 'EDGE_WING', 'name' => '마구리', 'description' => '마구리 날개 수량 계산', 'sort_order' => 9], + ['code' => 'INSPECTION', 'name' => '검사', 'description' => '검사비 계산', 'sort_order' => 10], + ['code' => 'PRICE_FORMULA', 'name' => '단가수식', 'description' => '품목별 단가 계산 수식', 'sort_order' => 11], + ]; + + $count = 0; + foreach ($categories as $cat) { + DB::table('quote_formula_categories')->updateOrInsert( + ['tenant_id' => $tenantId, 'code' => $cat['code']], + array_merge($cat, [ + 'tenant_id' => $tenantId, + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now(), + ]) + ); + $count++; + } + + $this->info(" → {$count}개 카테고리 생성됨"); + + return $count; + } + + /** + * 수식 시드 + */ + private function seedFormulas(int $tenantId): int + { + $this->info('📐 수식 생성 중...'); + + // 카테고리 코드 → ID 매핑 + $categoryMap = DB::table('quote_formula_categories') + ->where('tenant_id', $tenantId) + ->pluck('id', 'code') + ->toArray(); + + if (empty($categoryMap)) { + $this->error(' ❌ 카테고리가 없습니다. --only=categories를 먼저 실행하세요.'); + + return 0; + } + + $formulas = $this->getFormulaData(); + $count = 0; + + foreach ($formulas as $formula) { + $categoryId = $categoryMap[$formula['category_code']] ?? null; + if (! $categoryId) { + $this->warn(" ⚠️ 카테고리 '{$formula['category_code']}'를 찾을 수 없음: {$formula['variable']}"); + + continue; + } + + DB::table('quote_formulas')->updateOrInsert( + ['tenant_id' => $tenantId, 'variable' => $formula['variable']], + [ + 'tenant_id' => $tenantId, + 'category_id' => $categoryId, + 'variable' => $formula['variable'], + 'name' => $formula['name'], + 'type' => $formula['type'], + 'formula' => $formula['formula'] ?? null, + 'output_type' => 'variable', + 'description' => $formula['description'] ?? null, + 'sort_order' => $formula['sort_order'] ?? 0, + 'is_active' => $formula['is_active'] ?? true, + 'created_at' => now(), + 'updated_at' => now(), + ] + ); + $count++; + } + + $this->info(" → {$count}개 수식 생성됨"); + + return $count; + } + + /** + * 범위 시드 (수식에 포함된 ranges) + */ + private function seedRanges(int $tenantId): int + { + $this->info('📊 범위 데이터 생성 중...'); + + $formulas = $this->getFormulaData(); + $count = 0; + + foreach ($formulas as $formula) { + if (empty($formula['ranges'])) { + continue; + } + + // 수식 ID 조회 + $formulaRecord = DB::table('quote_formulas') + ->where('tenant_id', $tenantId) + ->where('variable', $formula['variable']) + ->first(); + + if (! $formulaRecord) { + continue; + } + + // 조건 변수 추출 + $conditionVariable = $formula['metadata']['input_variable'] ?? $formula['variable']; + + foreach ($formula['ranges'] as $rangeOrder => $range) { + $resultData = [ + 'value' => $range['result'], + 'item_code' => $range['item_code'] ?? null, + 'quantity' => $range['quantity'] ?? 1, + 'note' => $range['description'] ?? null, + ]; + + DB::table('quote_formula_ranges')->updateOrInsert( + [ + 'formula_id' => $formulaRecord->id, + 'min_value' => $range['min'], + 'max_value' => $range['max'], + ], + [ + 'formula_id' => $formulaRecord->id, + 'min_value' => $range['min'], + 'max_value' => $range['max'], + 'condition_variable' => $conditionVariable, + 'result_value' => json_encode($resultData), + 'result_type' => 'fixed', + 'sort_order' => $rangeOrder + 1, + 'created_at' => now(), + 'updated_at' => now(), + ] + ); + $count++; + } + } + + $this->info(" → {$count}개 범위 생성됨"); + + return $count; + } + + /** + * 기존 데이터 삭제 + */ + private function truncateTables(int $tenantId): void + { + $this->warn('🗑️ 기존 데이터 삭제 중...'); + + // 순서 중요: FK 제약조건 때문에 역순으로 삭제 + $deleted = DB::table('quote_formula_ranges') + ->whereIn('formula_id', function ($query) use ($tenantId) { + $query->select('id') + ->from('quote_formulas') + ->where('tenant_id', $tenantId); + }) + ->delete(); + $this->info(" → quote_formula_ranges: {$deleted}개 삭제"); + + $deleted = DB::table('quote_formula_mappings') + ->whereIn('formula_id', function ($query) use ($tenantId) { + $query->select('id') + ->from('quote_formulas') + ->where('tenant_id', $tenantId); + }) + ->delete(); + $this->info(" → quote_formula_mappings: {$deleted}개 삭제"); + + $deleted = DB::table('quote_formula_items') + ->whereIn('formula_id', function ($query) use ($tenantId) { + $query->select('id') + ->from('quote_formulas') + ->where('tenant_id', $tenantId); + }) + ->delete(); + $this->info(" → quote_formula_items: {$deleted}개 삭제"); + + $deleted = DB::table('quote_formulas') + ->where('tenant_id', $tenantId) + ->delete(); + $this->info(" → quote_formulas: {$deleted}개 삭제"); + + $deleted = DB::table('quote_formula_categories') + ->where('tenant_id', $tenantId) + ->delete(); + $this->info(" → quote_formula_categories: {$deleted}개 삭제"); + } + + /** + * 수식 데이터 정의 (API 시더와 동일) + */ + private function getFormulaData(): array + { + return [ + // ============================== + // 0. 입력값 (INPUT) - Design 사이트 필드와 동일 + // ============================== + [ + 'category_code' => 'INPUT', + 'variable' => 'PC', + 'name' => '제품카테고리', + 'type' => 'input', + 'formula' => null, + 'description' => '제품 카테고리 선택 (스크린/철재 등)', + 'metadata' => [ + 'field_type' => 'select', + 'options' => [ + ['value' => 'screen', 'label' => '스크린'], + ['value' => 'steel', 'label' => '철재'], + ], + ], + 'sort_order' => 1, + ], + [ + 'category_code' => 'INPUT', + 'variable' => 'PRODUCT_ID', + 'name' => '제품명', + 'type' => 'input', + 'formula' => null, + 'description' => '제품 선택 (제품카테고리에 따라 목록 변경)', + 'metadata' => ['field_type' => 'select', 'depends_on' => 'PC'], + 'sort_order' => 2, + ], + [ + 'category_code' => 'INPUT', + 'variable' => 'GT', + 'name' => '설치유형', + 'type' => 'input', + 'formula' => null, + 'description' => '설치 유형 선택 (벽면/천장/바닥)', + 'metadata' => [ + 'field_type' => 'select', + 'options' => [ + ['value' => 'wall', 'label' => '벽면'], + ['value' => 'ceiling', 'label' => '천장'], + ['value' => 'floor', 'label' => '바닥'], + ], + ], + 'sort_order' => 3, + ], + [ + 'category_code' => 'INPUT', + 'variable' => 'MP', + 'name' => '모터전원', + 'type' => 'input', + 'formula' => null, + 'description' => '모터 전원 선택', + 'metadata' => [ + 'field_type' => 'select', + 'options' => [ + ['value' => '220V', 'label' => '220V'], + ['value' => '380V', 'label' => '380V'], + ], + ], + 'sort_order' => 4, + ], + [ + 'category_code' => 'INPUT', + 'variable' => 'CT', + 'name' => '제어기', + 'type' => 'input', + 'formula' => null, + 'description' => '제어기 유형 선택 (단독제어/연동제어)', + 'metadata' => [ + 'field_type' => 'select', + 'options' => [ + ['value' => 'single', 'label' => '단독제어'], + ['value' => 'linked', 'label' => '연동제어'], + ], + ], + 'sort_order' => 5, + ], + [ + 'category_code' => 'INPUT', + 'variable' => 'QTY', + 'name' => '수량', + 'type' => 'input', + 'formula' => null, + 'description' => '견적 수량', + 'metadata' => ['field_type' => 'number', 'min' => 1, 'default' => 1], + 'sort_order' => 6, + ], + + // ============================== + // 1. 오픈사이즈 (OPEN_SIZE) - 2개 + // ============================== + [ + 'category_code' => 'OPEN_SIZE', + 'variable' => 'W0', + 'name' => '가로 (W0)', + 'type' => 'input', + 'formula' => null, + 'description' => '오픈사이즈 가로 (mm)', + 'metadata' => ['field_type' => 'number', 'unit' => 'mm', 'min' => 100, 'max' => 10000], + 'sort_order' => 1, + ], + [ + 'category_code' => 'OPEN_SIZE', + 'variable' => 'H0', + 'name' => '세로 (H0)', + 'type' => 'input', + 'formula' => null, + 'description' => '오픈사이즈 세로 (mm)', + 'metadata' => ['field_type' => 'number', 'unit' => 'mm', 'min' => 100, 'max' => 10000], + 'sort_order' => 2, + ], + + // ============================== + // 2. 제작사이즈 (MAKE_SIZE) - 4개 + // ============================== + [ + 'category_code' => 'MAKE_SIZE', + 'variable' => 'W1_SCREEN', + 'name' => '제작사이즈 W1 (스크린)', + 'type' => 'calculation', + 'formula' => 'W0 + 140', + 'description' => '스크린 제작 가로 = 오픈 가로 + 140', + 'metadata' => ['unit' => 'mm', 'product_type' => 'screen'], + 'sort_order' => 1, + ], + [ + 'category_code' => 'MAKE_SIZE', + 'variable' => 'H1_SCREEN', + 'name' => '제작사이즈 H1 (스크린)', + 'type' => 'calculation', + 'formula' => 'H0 + 350', + 'description' => '스크린 제작 세로 = 오픈 세로 + 350', + 'metadata' => ['unit' => 'mm', 'product_type' => 'screen'], + 'sort_order' => 2, + ], + [ + 'category_code' => 'MAKE_SIZE', + 'variable' => 'W1_STEEL', + 'name' => '제작사이즈 W1 (철재)', + 'type' => 'calculation', + 'formula' => 'W0 + 110', + 'description' => '철재 제작 가로 = 오픈 가로 + 110', + 'metadata' => ['unit' => 'mm', 'product_type' => 'steel'], + 'sort_order' => 3, + ], + [ + 'category_code' => 'MAKE_SIZE', + 'variable' => 'H1_STEEL', + 'name' => '제작사이즈 H1 (철재)', + 'type' => 'calculation', + 'formula' => 'H0 + 350', + 'description' => '철재 제작 세로 = 오픈 세로 + 350', + 'metadata' => ['unit' => 'mm', 'product_type' => 'steel'], + 'sort_order' => 4, + ], + + // ============================== + // 3. 면적 (AREA) - 1개 + // ============================== + [ + 'category_code' => 'AREA', + 'variable' => 'M', + 'name' => '면적 계산', + 'type' => 'calculation', + 'formula' => 'W1 * H1 / 1000000', + 'description' => '면적(㎡) = 제작가로(W1) × 제작세로(H1) ÷ 1,000,000', + 'metadata' => ['unit' => '㎡'], + 'sort_order' => 1, + ], + + // ============================== + // 4. 중량 (WEIGHT) - 2개 + // ============================== + [ + 'category_code' => 'WEIGHT', + 'variable' => 'K_SCREEN', + 'name' => '중량 계산 (스크린)', + 'type' => 'calculation', + 'formula' => 'M * 2 + W0 / 1000 * 14.17', + 'description' => '스크린 중량(kg) = 면적 × 2 + (오픈가로 ÷ 1000 × 14.17)', + 'metadata' => ['unit' => 'kg', 'product_type' => 'screen'], + 'sort_order' => 1, + ], + [ + 'category_code' => 'WEIGHT', + 'variable' => 'K_STEEL', + 'name' => '중량 계산 (철재)', + 'type' => 'calculation', + 'formula' => 'M * 25', + 'description' => '철재 중량(kg) = 면적 × 25', + 'metadata' => ['unit' => 'kg', 'product_type' => 'steel'], + 'sort_order' => 2, + ], + + // ============================== + // 5. 가이드레일 (GUIDE_RAIL) - 2개 (활성) + // ============================== + [ + 'category_code' => 'GUIDE_RAIL', + 'variable' => 'G', + 'name' => '가이드레일 제작길이', + 'type' => 'calculation', + 'formula' => 'H0 + 250', + 'description' => '가이드레일 제작길이(G) = 오픈세로(H0) + 250', + 'metadata' => ['unit' => 'mm'], + 'sort_order' => 1, + ], + [ + 'category_code' => 'GUIDE_RAIL', + 'variable' => 'GR_AUTO_SELECT', + 'name' => '가이드레일 자재 자동 선택', + 'type' => 'range', + 'formula' => null, + 'description' => '가이드레일 길이 및 수량 자동 산출 (기본 2개)', + 'metadata' => ['unit' => 'EA', 'input_variable' => 'G'], + 'sort_order' => 2, + 'ranges' => [ + ['min' => 0, 'max' => 1219, 'result' => '1219 2개', 'item_code' => 'PT-GR-1219', 'quantity' => 2, 'description' => '0 < G ≤ 1219'], + ['min' => 1219, 'max' => 2438, 'result' => '2438 2개', 'item_code' => 'PT-GR-2438', 'quantity' => 2, 'description' => '1219 < G ≤ 2438'], + ['min' => 2438, 'max' => 3000, 'result' => '3000 2개', 'item_code' => 'PT-GR-3000', 'quantity' => 2, 'description' => '2438 < G ≤ 3000'], + ['min' => 3000, 'max' => 3600, 'result' => '3600 2개', 'item_code' => 'PT-GR-3600', 'quantity' => 2, 'description' => '3000 < G ≤ 3600'], + ], + ], + + // ============================== + // 6. 케이스 (CASE) - 3개 + // ============================== + [ + 'category_code' => 'CASE', + 'variable' => 'S_SCREEN', + 'name' => '케이스 사이즈 (스크린)', + 'type' => 'calculation', + 'formula' => 'W0 + 220', + 'description' => '스크린 케이스 사이즈(S) = 오픈가로(W0) + 220', + 'metadata' => ['unit' => 'mm', 'product_type' => 'screen'], + 'sort_order' => 1, + ], + [ + 'category_code' => 'CASE', + 'variable' => 'S_STEEL', + 'name' => '케이스 사이즈 (철재)', + 'type' => 'calculation', + 'formula' => 'W0 + 240', + 'description' => '철재 케이스 사이즈(S) = 오픈가로(W0) + 240', + 'metadata' => ['unit' => 'mm', 'product_type' => 'steel'], + 'sort_order' => 2, + ], + [ + 'category_code' => 'CASE', + 'variable' => 'CASE_AUTO_SELECT', + 'name' => '케이스 자재 자동 선택', + 'type' => 'range', + 'formula' => null, + 'description' => '케이스 자재 길이 및 수량 자동 산출', + 'metadata' => ['unit' => 'EA', 'input_variable' => 'S'], + 'sort_order' => 3, + 'ranges' => [ + ['min' => 0, 'max' => 1219, 'result' => '1219 1개', 'item_code' => 'PT-CASE-1219', 'quantity' => 1, 'description' => '0 < S ≤ 1219'], + ['min' => 1219, 'max' => 2438, 'result' => '2438 1개', 'item_code' => 'PT-CASE-2438', 'quantity' => 1, 'description' => '1219 < S ≤ 2438'], + ['min' => 2438, 'max' => 3000, 'result' => '3000 1개', 'item_code' => 'PT-CASE-3000', 'quantity' => 1, 'description' => '2438 < S ≤ 3000'], + ['min' => 3000, 'max' => 3600, 'result' => '3600 1개', 'item_code' => 'PT-CASE-3600', 'quantity' => 1, 'description' => '3000 < S ≤ 3600'], + ['min' => 3600, 'max' => 6000, 'result' => '6000 1개', 'item_code' => 'PT-CASE-6000', 'quantity' => 1, 'description' => '3600 < S ≤ 6000'], + ], + ], + + // ============================== + // 7. 모터 (MOTOR) - 1개 + // ============================== + [ + 'category_code' => 'MOTOR', + 'variable' => 'MOTOR_AUTO_SELECT', + 'name' => '모터 자동 선택', + 'type' => 'range', + 'formula' => null, + 'description' => '모터 중량 기반 자동 선택 (5130 실제 규격 기준)', + 'metadata' => ['unit' => 'EA', 'input_variable' => 'K'], + 'sort_order' => 1, + 'ranges' => [ + ['min' => 0, 'max' => 150, 'result' => '150K', 'item_code' => 'PT-MOTOR-150', 'quantity' => 1, 'description' => '0 < K ≤ 150kg'], + ['min' => 150, 'max' => 300, 'result' => '300K', 'item_code' => 'PT-MOTOR-300', 'quantity' => 1, 'description' => '150 < K ≤ 300kg'], + ['min' => 300, 'max' => 400, 'result' => '400K', 'item_code' => 'PT-MOTOR-400', 'quantity' => 1, 'description' => '300 < K ≤ 400kg'], + ['min' => 400, 'max' => 500, 'result' => '500K', 'item_code' => 'PT-MOTOR-500', 'quantity' => 1, 'description' => '400 < K ≤ 500kg'], + ['min' => 500, 'max' => 600, 'result' => '600K', 'item_code' => 'PT-MOTOR-600', 'quantity' => 1, 'description' => '500 < K ≤ 600kg'], + ['min' => 600, 'max' => 800, 'result' => '800K', 'item_code' => 'PT-MOTOR-800', 'quantity' => 1, 'description' => '600 < K ≤ 800kg'], + ['min' => 800, 'max' => 1000, 'result' => '1000K', 'item_code' => 'PT-MOTOR-1000', 'quantity' => 1, 'description' => '800 < K ≤ 1000kg'], + ['min' => 1000, 'max' => 1500, 'result' => '1500K', 'item_code' => 'PT-MOTOR-1500', 'quantity' => 1, 'description' => '1000 < K ≤ 1500kg'], + ['min' => 1500, 'max' => 2000, 'result' => '2000K', 'item_code' => 'PT-MOTOR-2000', 'quantity' => 1, 'description' => '1500 < K ≤ 2000kg'], + ], + ], + + // ============================== + // 8. 제어기 (CONTROLLER) - 2개 + // ============================== + [ + 'category_code' => 'CONTROLLER', + 'variable' => 'CONTROLLER_TYPE', + 'name' => '제어기 유형', + 'type' => 'input', + 'formula' => null, + 'description' => '제어기 설치 유형 선택 (매립형/노출형/일체형)', + 'sort_order' => 0, + ], + [ + 'category_code' => 'CONTROLLER', + 'variable' => 'CTRL_AUTO_SELECT', + 'name' => '제어기 자동 선택', + 'type' => 'mapping', + 'formula' => null, + 'description' => '연동제어기 설치 유형(매립/노출)에 따라 자동 선택', + 'sort_order' => 1, + ], + + // ============================== + // 9. 검사 (INSPECTION) - 1개 + // ============================== + [ + 'category_code' => 'INSPECTION', + 'variable' => 'INSP_FEE', + 'name' => '검사비', + 'type' => 'calculation', + 'formula' => '1', + 'description' => '검사비 고정 1EA (단가는 검사비 설정값 적용)', + 'metadata' => ['unit' => 'EA'], + 'sort_order' => 1, + ], + ]; + } + + /** + * 품목 시드 (quote_formula_items) + * output_type='item' 수식과 연결된 품목 정의 + */ + private function seedItems(int $tenantId): int + { + $this->info('📦 품목 데이터 생성 중...'); + + // 카테고리 코드 → ID 매핑 + $categoryMap = DB::table('quote_formula_categories') + ->where('tenant_id', $tenantId) + ->pluck('id', 'code') + ->toArray(); + + if (empty($categoryMap)) { + $this->error(' ❌ 카테고리가 없습니다.'); + + return 0; + } + + // 품목 출력용 수식 생성 (output_type = 'item') + $itemFormulas = $this->getItemFormulaData(); + $formulaMap = []; + + foreach ($itemFormulas as $formula) { + $categoryId = $categoryMap[$formula['category_code']] ?? null; + if (! $categoryId) { + continue; + } + + DB::table('quote_formulas')->updateOrInsert( + ['tenant_id' => $tenantId, 'variable' => $formula['variable']], + [ + 'tenant_id' => $tenantId, + 'category_id' => $categoryId, + 'variable' => $formula['variable'], + 'name' => $formula['name'], + 'type' => $formula['type'], + 'formula' => $formula['formula'] ?? null, + 'output_type' => 'item', // 품목 출력 + 'description' => $formula['description'] ?? null, + 'sort_order' => $formula['sort_order'] ?? 0, + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now(), + ] + ); + + // 수식 ID 조회 + $formulaRecord = DB::table('quote_formulas') + ->where('tenant_id', $tenantId) + ->where('variable', $formula['variable']) + ->first(); + + if ($formulaRecord) { + $formulaMap[$formula['variable']] = $formulaRecord->id; + } + } + + // 품목 데이터 생성 + $items = $this->getItemData(); + $count = 0; + + foreach ($items as $item) { + $formulaId = $formulaMap[$item['formula_variable']] ?? null; + if (! $formulaId) { + $this->warn(" ⚠️ 수식 '{$item['formula_variable']}'를 찾을 수 없음"); + + continue; + } + + DB::table('quote_formula_items')->updateOrInsert( + [ + 'formula_id' => $formulaId, + 'item_code' => $item['item_code'], + ], + [ + 'formula_id' => $formulaId, + 'item_code' => $item['item_code'], + 'item_name' => $item['item_name'], + 'specification' => $item['specification'] ?? null, + 'unit' => $item['unit'] ?? 'EA', + 'quantity_formula' => $item['quantity_formula'], + 'unit_price_formula' => $item['unit_price_formula'] ?? null, + 'sort_order' => $item['sort_order'] ?? 0, + 'created_at' => now(), + 'updated_at' => now(), + ] + ); + $count++; + } + + $this->info(" → {$count}개 품목 생성됨"); + + return $count; + } + + /** + * 품목 출력용 수식 데이터 + */ + private function getItemFormulaData(): array + { + return [ + // 가이드레일 품목 출력 + [ + 'category_code' => 'GUIDE_RAIL', + 'variable' => 'ITEM_GUIDE_RAIL', + 'name' => '가이드레일 품목', + 'type' => 'calculation', + 'formula' => '1', // 항상 출력 + 'description' => '가이드레일 품목 출력용', + 'sort_order' => 10, + ], + // 케이스 품목 출력 + [ + 'category_code' => 'CASE', + 'variable' => 'ITEM_CASE', + 'name' => '케이스 품목', + 'type' => 'calculation', + 'formula' => '1', + 'description' => '케이스 품목 출력용', + 'sort_order' => 10, + ], + // 모터 품목 출력 + [ + 'category_code' => 'MOTOR', + 'variable' => 'ITEM_MOTOR', + 'name' => '모터 품목', + 'type' => 'calculation', + 'formula' => '1', + 'description' => '모터 품목 출력용', + 'sort_order' => 10, + ], + // 검사비 품목 출력 + [ + 'category_code' => 'INSPECTION', + 'variable' => 'ITEM_INSPECTION', + 'name' => '검사비 품목', + 'type' => 'calculation', + 'formula' => '1', + 'description' => '검사비 품목 출력용', + 'sort_order' => 10, + ], + ]; + } + + /** + * 품목 데이터 정의 + */ + private function getItemData(): array + { + return [ + // ========== 가이드레일 품목 ========== + [ + 'formula_variable' => 'ITEM_GUIDE_RAIL', + 'item_code' => 'PT-GR-3000', + 'item_name' => '가이드레일 3000', + 'specification' => '3000mm', + 'unit' => 'EA', + 'quantity_formula' => '2', // 기본 2개 + 'unit_price_formula' => null, // DB에서 조회 + 'sort_order' => 1, + ], + + // ========== 케이스 품목 ========== + [ + 'formula_variable' => 'ITEM_CASE', + 'item_code' => 'PT-CASE-3600', + 'item_name' => '케이스 3600', + 'specification' => '3600mm', + 'unit' => 'EA', + 'quantity_formula' => '1', + 'unit_price_formula' => null, + 'sort_order' => 1, + ], + + // ========== 모터 품목 ========== + [ + 'formula_variable' => 'ITEM_MOTOR', + 'item_code' => 'PT-MOTOR-150', + 'item_name' => '모터 150K', + 'specification' => '150kg', + 'unit' => 'EA', + 'quantity_formula' => '1', + 'unit_price_formula' => null, + 'sort_order' => 1, + ], + + // ========== 검사비 품목 ========== + [ + 'formula_variable' => 'ITEM_INSPECTION', + 'item_code' => 'SVC-INSP', + 'item_name' => '검사비', + 'specification' => null, + 'unit' => '식', + 'quantity_formula' => 'INSP_FEE', // 검사비 수량 수식 참조 + 'unit_price_formula' => '50000', // 고정 단가 (테스트용) + 'sort_order' => 1, + ], + ]; + } +} \ No newline at end of file diff --git a/app/Models/Price.php b/app/Models/Price.php index 842eb1d7..c80df3d2 100644 --- a/app/Models/Price.php +++ b/app/Models/Price.php @@ -117,32 +117,19 @@ public static function getCurrentPrice( */ public static function getSalesPriceByItemCode(int $tenantId, string $itemCode): float { - // products 테이블에서 품목 코드로 검색 - $product = DB::table('products') + // items 테이블에서 품목 코드로 검색 (products + materials 통합 테이블) + $item = DB::table('items') ->where('tenant_id', $tenantId) ->where('code', $itemCode) ->whereNull('deleted_at') ->first(); - if ($product) { - $price = static::getCurrentPrice($tenantId, self::ITEM_TYPE_PRODUCT, $product->id); - - return (float) ($price?->sales_price ?? 0); + if (! $item) { + return 0; } - // materials 테이블에서도 검색 - $material = DB::table('materials') - ->where('tenant_id', $tenantId) - ->where('code', $itemCode) - ->whereNull('deleted_at') - ->first(); + $price = static::getCurrentPrice($tenantId, $item->item_type, $item->id); - if ($material) { - $price = static::getCurrentPrice($tenantId, self::ITEM_TYPE_MATERIAL, $material->id); - - return (float) ($price?->sales_price ?? 0); - } - - return 0; + return (float) ($price?->sales_price ?? 0); } }