fix: Price 모델 items 테이블 참조 및 견적 시더 입력필드 추가

- Price.getSalesPriceByItemCode() products/materials → items 테이블 수정
- SeedQuoteFormulasCommand INPUT 카테고리 및 Design 사이트 동기화
  - PC(제품카테고리), PRODUCT_ID(제품명), GT(설치유형)
  - MP(모터전원), CT(제어기), QTY(수량) 필드 추가
- metadata에 field_type, options 정보 추가
This commit is contained in:
2025-12-22 22:48:03 +09:00
parent 811a04347f
commit a332e0cee4
3 changed files with 878 additions and 19 deletions

View File

@@ -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 지원 )
---

View File

@@ -0,0 +1,823 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
/**
* 견적수식 시드 데이터 생성 명령어
*
* API 시더(api/database/seeders/QuoteFormulaSeeder.php)와 동일한 데이터를
* MNG 관리자 패널에서 사용할 수 있도록 생성합니다.
*
* @example php artisan quote:seed-formulas --tenant=1
* @example php artisan quote:seed-formulas --tenant=1 --fresh
* @example php artisan quote:seed-formulas --only=categories
*/
class SeedQuoteFormulasCommand extends Command
{
protected $signature = 'quote:seed-formulas
{--tenant=1 : 테넌트 ID}
{--only= : categories|formulas|ranges (특정 타입만 생성)}
{--fresh : 기존 데이터 삭제 후 재생성}';
protected $description = '견적수식 시드 데이터를 생성합니다 (카테고리, 수식, 범위)';
public function handle(): int
{
$tenantId = (int) $this->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,
],
];
}
}

View File

@@ -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);
}
}