Files
sam-manage/app/Console/Commands/SeedQuoteFormulasCommand.php
hskwon 60618ddd04 feat: 견적 시뮬레이터 개선 및 FlowTester 조건 평가기 추가
- 견적 시뮬레이터 UI 레이아웃 개선 (가로 배치, 반응형)
- FlowTester ConditionEvaluator 클래스 추가 (조건부 실행 지원)
- FormulaEvaluatorService 기능 확장
- DependencyResolver 의존성 해결 로직 개선
- PushDeviceToken 모델 확장 (FCM 토큰 관리)
- QuoteFormula API 엔드포인트 추가
- FlowTester 가이드 모달 업데이트
2025-12-23 23:41:37 +09:00

803 lines
33 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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,
],
// 제작사이즈 통합 변수 (제품 카테고리 기반)
[
'category_code' => 'MAKE_SIZE',
'variable' => 'W1',
'name' => '제작가로 통합',
'type' => 'calculation',
'formula' => 'W1_SCREEN', // TODO: PC 기반 분기 로직 필요
'description' => '제품 카테고리에 따른 제작 가로 (기본: 스크린)',
'metadata' => ['unit' => 'mm'],
'sort_order' => 5,
],
[
'category_code' => 'MAKE_SIZE',
'variable' => 'H1',
'name' => '제작세로 통합',
'type' => 'calculation',
'formula' => 'H1_SCREEN', // TODO: PC 기반 분기 로직 필요
'description' => '제품 카테고리에 따른 제작 세로 (기본: 스크린)',
'metadata' => ['unit' => 'mm'],
'sort_order' => 6,
],
// ==============================
// 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) - 3개
// ==============================
[
'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,
],
// 중량 통합 변수 (모터 선택용)
[
'category_code' => 'WEIGHT',
'variable' => 'K',
'name' => '중량 통합',
'type' => 'calculation',
'formula' => 'K_SCREEN', // TODO: PC 기반 분기 로직 필요
'description' => '제품 카테고리에 따른 중량 (기본: 스크린)',
'metadata' => ['unit' => 'kg'],
'sort_order' => 3,
],
// ==============================
// 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) - 4개
// ==============================
[
'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' => 'S',
'name' => '케이스 사이즈 통합',
'type' => 'calculation',
'formula' => 'S_SCREEN', // TODO: PC 기반 분기 로직 필요
'description' => '제품 카테고리에 따른 케이스 사이즈 (기본: 스크린)',
'metadata' => ['unit' => 'mm'],
'sort_order' => 3,
],
[
'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;
}
/**
* 품목 출력용 수식 데이터
* Note: 가이드레일, 케이스, 모터는 range 수식에서 자동 선택되므로 별도 품목 수식 불필요
*/
private function getItemFormulaData(): array
{
return [
// 검사비 품목 출력 (range가 아닌 고정 품목)
[
'category_code' => 'INSPECTION',
'variable' => 'ITEM_INSPECTION',
'name' => '검사비 품목',
'type' => 'calculation',
'formula' => '1',
'description' => '검사비 품목 출력용',
'sort_order' => 10,
],
];
}
/**
* 품목 데이터 정의
* Note: 가이드레일, 케이스, 모터는 range 수식의 result_value에서 자동 추출됨
*/
private function getItemData(): array
{
return [
// ========== 검사비 품목 ==========
[
'formula_variable' => 'ITEM_INSPECTION',
'item_code' => 'SVC-INSP',
'item_name' => '검사비',
'specification' => null,
'unit' => '식',
'quantity_formula' => 'INSP_FEE', // 검사비 수량 수식 참조
'unit_price_formula' => '50000', // 고정 단가 (테스트용)
'sort_order' => 1,
],
];
}
}