# 견적 시뮬레이터 완전 동기화 계획 > **작성일**: 2025-12-23 (업데이트: 2025-12-30) > **목표**: design.sam.kr 시뮬레이터와 mng 시뮬레이터가 **동일한 결과**를 출력하도록 완전 동기화 --- ## 1. Design 시스템 전체 분석 ### 1.1 핵심 파일 구조 | 파일 | 줄 수 | 역할 | |------|-------|------| | `AutoCalculationSimulator.tsx` | 1,068 | 메인 시뮬레이터 UI + 계산 로직 | | `formulaEvaluator.ts` | 312 | 수식 평가 엔진 | | `bomCalculatorWithDebug.ts` | 232 | BOM 계산 + 10단계 디버깅 | | `DataContext.tsx` | 9,859 | 마스터 데이터 타입 + 상태 관리 | | `sampleQuoteData_Complete.ts` | 600+ | 샘플 품목 데이터 | | `addProductBoms.ts` | 298 | 완제품 BOM 구성 | ### 1.2 데이터 구조 (TypeScript 인터페이스) #### 품목 마스터 (ItemMaster) ```typescript interface ItemMaster { id: string; itemCode: string; // 품목코드 itemName: string; // 품목명 itemType: 'FG' | 'SF' | 'PT' | 'SM' | 'RM' | 'CS'; productCategory?: 'SCREEN' | 'STEEL'; // 제품 카테고리 partType?: 'ASSEMBLY' | 'BENDING' | 'PURCHASED'; unit: string; salesPrice?: number; // 판매단가 purchasePrice?: number; // 매입단가 marginRate?: number; // 마진율 bom?: BOMLine[]; // 하위 BOM 목록 // ... 기타 필드 } ``` #### BOM 라인 (BOMLine) ```typescript interface BOMLine { childItemCode: string; // 자식 품목 코드 childItemName: string; // 자식 품목명 quantity: number; // 기준 수량 unit: string; // 단위 quantityFormula?: string; // 수량 수식 (예: "W*H/1000000", "H/1000") note?: string; // 비고 } ``` #### 단가 관리 (PricingData) ```typescript interface PricingData { id: string; itemId: string; itemCode: string; purchasePrice?: number; // 매입단가 processingCost?: number; // 가공비 loss?: number; // LOSS(%) marginRate?: number; // 마진율 salesPrice?: number; // 판매단가 effectiveDate: string; // 적용일 status: 'draft' | 'active' | 'inactive' | 'finalized'; } ``` #### 카테고리 그룹 (CategoryGroup) - MNG에 누락 ```typescript interface CategoryGroup { id: string; name: string; // "면적기반", "중량기반", "수량기반" categories: string[]; // 소속 카테고리들 multiplierVariable?: string; // 곱할 변수 (M, K 등) } ``` ### 1.3 계산 변수 체계 | 변수 | 설명 | 계산식 | |------|------|--------| | `W0` | 오픈사이즈 폭 | 사용자 입력 | | `H0` | 오픈사이즈 높이 | 사용자 입력 | | `PC` | 제품 카테고리 | "스크린" / "철재" | | `W1` | 제작폭 | PC=="스크린" ? W0+140 : W0+110 | | `H1` | 제작높이 | H0 + 350 | | `W` | 제작폭 (별칭) | = W1 | | `H` | 제작높이 (별칭) | = H1 | | `M` | 면적 (㎡) | (W1 × H1) / 1,000,000 | | `K` | 중량 (kg) | 스크린: M×2 + W0/1000×14.17, 철재: M×25 | | `GT` | 가이드레일 설치유형 | "벽면형" / "측면형" | | `MP` | 모터 전원 | "220V" / "380V" | | `CT` | 연동제어기 | "단독" / "연동" | | `QTY` | 수량 | 사용자 입력 | ### 1.4 수식 평가 함수 **지원 함수 목록:** | 함수 | 설명 | 예시 | |------|------|------| | `SUM(a, b, ...)` | 합계 | `SUM(W0, H0, 100)` | | `AVERAGE(a, b, ...)` | 평균 | `AVERAGE(W0, H0)` | | `MAX(a, b, ...)` | 최대값 | `MAX(W0, 1000)` | | `MIN(a, b, ...)` | 최소값 | `MIN(H0, 3000)` | | `ROUND(val, dec)` | 반올림 | `ROUND(M, 2)` | | `CEIL(val)` | 올림 | `CEIL(H1 / 1000)` | | `FLOOR(val)` | 내림 | `FLOOR(W1 / 500)` | | `ABS(val)` | 절대값 | `ABS(W0 - 2000)` | | `IF(cond, t, f)` | 조건문 | `IF(W0 > 3000, 2, 1)` | | `SQRT(val)` | 제곱근 | `SQRT(M)` | | `POWER(base, exp)` | 거듭제곱 | `POWER(W1, 2)` | **평가 과정:** ```typescript // 1. 변수 치환 (긴 변수명부터) const sortedVars = Object.keys(vars).sort((a, b) => b.length - a.length); sortedVars.forEach(varName => { const regex = new RegExp(`\\b${varName}\\b`, 'g'); formula = formula.replace(regex, String(vars[varName])); }); // 2. 함수 처리 (CEIL, FLOOR, ROUND 등) formula = processFunctions(formula); // 3. 최종 계산 return new Function(`return (${formula})`)(); ``` ### 1.5 BOM 계산 10단계 프로세스 | 단계 | 항목 | 예시 | |------|------|------| | Step 1 | 수량 공식 확인 | `H/1000` | | Step 2 | 변수 값 확인 | `{W0:2000, H0:2500, W1:2140, H1:2850, M:6.099}` | | Step 3 | 수량 계산 과정 | `H/1000 = 2850/1000 = 2.85` | | Step 4 | 계산된 수량 | `2.85` | | Step 5 | 단가 소스 | `단가관리 (15,000원)` 또는 `품목마스터 (15,000원)` | | Step 6 | 기본 단가 | `15,000` | | Step 7 | 카테고리 승수 | `면적단가 (15,000원/㎡ × 6.099㎡)` | | Step 8 | 최종 단가 | `91,485` | | Step 9 | 금액 계산 | `2.85 × 91,485 = 260,732` | | Step 10 | 최종 금액 | `260,732` | ### 1.6 단가 계산 로직 ```typescript // 1. 단가 조회 우선순위 let unitPrice = 0; let priceSource = '단가 없음'; // 1순위: pricing 테이블에서 조회 const itemPricing = pricings.find(p => p.itemCode === bomEntry.childItemCode); if (itemPricing && itemPricing.salesPrice) { unitPrice = itemPricing.salesPrice; priceSource = `단가관리 (${unitPrice.toLocaleString()}원)`; } // 2순위: 품목마스터에서 조회 else if (childItem.salesPrice) { unitPrice = childItem.salesPrice; priceSource = `품목마스터 (${unitPrice.toLocaleString()}원)`; } // 2. 면적 기반 품목 판단 const areaBasedCategories = ['원단', '패널', '도장', '표면처리']; const isAreaBased = areaBasedCategories.some(cat => itemCategory.includes(cat) || childItem.itemName.includes(cat) ); // 3. 최종 단가 계산 let finalUnitPrice = unitPrice; if (isAreaBased && calculationVariables.M > 0) { finalUnitPrice = unitPrice * calculationVariables.M; // 면적 단가 priceCalculationNote = `면적단가 (${unitPrice}원/㎡ × ${M}㎡)`; } else { priceCalculationNote = '수량단가'; } // 4. 최종 금액 const totalPrice = calculatedQuantity * finalUnitPrice; ``` --- ## 2. Design 샘플 데이터 분석 ### 2.1 품목 구성 (약 100개) | 유형 | 코드 접두사 | 수량 | 설명 | |------|------------|------|------| | 원자재 (RM) | RM-* | 20 | 강판, 알루미늄, 원단, 패킹 등 | | 부자재 (SM) | SM-* | 25 | 볼트, 너트, 전선, 실리콘 등 | | 스크린 반제품 (SF) | SF-SCR-* | 20 | 원단, 가이드레일, 케이스, 모터 등 | | 철재 반제품 (SF) | SF-STL-*, SF-BND-* | 20 | 도어, 프레임, 패널, 절곡 부품 등 | | 스크린 완제품 (FG) | FG-SCR-* | 5 | 소형/중형/대형/특대/맞춤형 | | 철재 완제품 (FG) | FG-STL-* | 5 | 소형/중형/대형/양개문/특수 | | 절곡 완제품 (FG) | FG-BND-* | 4 | L형/U형/Z형/ㄷ형 | ### 2.2 주요 BOM 수식 패턴 | 품목 유형 | 수식 | 설명 | |----------|------|------| | 스크린 원단 | `W*H/1000000` | 면적 계산 | | 가이드레일 | `H/1000` | 높이(m) 기준 | | 엣지윙 | `H/1000` | 높이(m) 기준 | | 철재 프레임 | `(W+H)*2/1000` | 둘레(m) 기준 | | 철재 패널 | `W*H/1000000` | 면적 계산 | | 실링재 | `(W+H)*2/1000` | 둘레(m) 기준 | | 파우더 도장 | `W*H/1000000` | 면적 계산 | ### 2.3 완제품 BOM 예시 (FG-SCR-002 중형 스크린) ```typescript { itemCode: 'FG-SCR-002', itemName: '방화스크린 중형 (2000x3000)', bom: [ { childItemCode: 'SF-SCR-F01', quantity: 6.0, unit: 'M2', quantityFormula: 'W*H/1000000' }, { childItemCode: 'SF-SCR-F02', quantity: 3.0, unit: 'M', quantityFormula: 'H/1000' }, { childItemCode: 'SF-SCR-F03', quantity: 3.0, unit: 'M', quantityFormula: 'H/1000' }, { childItemCode: 'SF-SCR-F04', quantity: 1, unit: 'EA' }, { childItemCode: 'SF-SCR-F05', quantity: 1, unit: 'EA' }, { childItemCode: 'SF-SCR-M02', quantity: 1, unit: 'EA', note: '중형용' }, { childItemCode: 'SF-SCR-C01', quantity: 1, unit: 'EA' }, { childItemCode: 'SF-SCR-S01', quantity: 1, unit: 'SET' }, { childItemCode: 'SF-SCR-W01', quantity: 1, unit: 'EA' }, { childItemCode: 'SF-SCR-B01', quantity: 2, unit: 'SET', note: '중형용 2세트' }, { childItemCode: 'SF-SCR-E01', quantity: 3.0, unit: 'M', quantityFormula: 'H/1000' }, { childItemCode: 'SF-SCR-E02', quantity: 3.0, unit: 'M', quantityFormula: 'H/1000' }, { childItemCode: 'SF-SCR-REM01', quantity: 1, unit: 'EA' }, { childItemCode: 'SM-B002', quantity: 30, unit: 'EA', note: '조립용' }, { childItemCode: 'SM-N002', quantity: 30, unit: 'EA' }, { childItemCode: 'SM-A001', quantity: 8, unit: 'EA', note: '고정용' }, ] } ``` --- ## 3. MNG 현재 상태 분석 ### 3.1 테이블 구조 | 테이블 | 현재 상태 | Design 대응 | |--------|----------|-------------| | `items` | 364개 (RM:133, SM:217, PT:6, FG:3, CS:5) | ItemMaster | | `item_details` | 품목 상세 정보 | ItemMaster 확장 필드 | | `prices` | 3개 (거의 없음) | PricingData | | `quote_formulas` | 57개 (기본 변수 있음) | FormulaRule, CalculationFormula | | `quote_formula_ranges` | 범위 규칙 | FormulaRule.ranges | | `quote_formula_items` | 수식 품목 매핑 | BOM 연동 | | `common_codes` | 코드 그룹 | CategoryGroup (부분) | | `category_groups` | ❌ 없음 | CategoryGroup 추가 필요 | ### 3.2 quote_formulas 현재 데이터 (샘플) ``` [PC] 제품카테고리 (input) => variable [W0] 가로 (W0) (input) => variable [H0] 세로 (H0) (input) => variable [W1_SCREEN] 제작사이즈 W1 (스크린): W0 + 140 => variable [H1_SCREEN] 제작사이즈 H1 (스크린): H0 + 350 => variable [W1_STEEL] 제작사이즈 W1 (철재): W0 + 110 => variable [H1_STEEL] 제작사이즈 H1 (철재): H0 + 350 => variable [M] 면적 계산: W1 * H1 / 1000000 => variable [K_SCREEN] 중량 계산 (스크린): M * 2 + W0 / 1000 * 14.17 => variable [K_STEEL] 중량 계산 (철재): M * 25 => variable ``` ### 3.3 누락 항목 | 항목 | 설명 | 우선순위 | |------|------|---------| | `items.process_type` | 공정유형 (스크린/절곡/전기) | 높음 | | `items.item_category` | 품목분류 (원단/패널/도장 등) | 높음 | | `category_groups` 테이블 | 면적/중량 기반 분류 | 높음 | | Design 샘플 품목 데이터 | 100개 품목 Seeder | 높음 | | BOM 구성 데이터 | 제품별 BOM Seeder | 높음 | | 단가 데이터 | 품목별 단가 Seeder | 중간 | --- ## 4. 완전 동기화 구현 계획 ### Phase 1: DB 스키마 확장 (1일) #### 1.1 items 테이블 필드 추가 ```sql ALTER TABLE items ADD COLUMN process_type VARCHAR(20) DEFAULT NULL COMMENT '공정유형: screen(스크린), bending(절곡), electric(전기), steel(철재)'; ALTER TABLE items ADD COLUMN item_category VARCHAR(50) DEFAULT NULL COMMENT '품목분류: 원단, 패널, 도장, 표면처리, 가이드레일, 케이스, 모터, 제어반 등'; CREATE INDEX idx_items_process_type ON items(process_type); CREATE INDEX idx_items_item_category ON items(item_category); ``` #### 1.2 category_groups 테이블 생성 ```sql CREATE TABLE category_groups ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, code VARCHAR(50) NOT NULL COMMENT '코드: area_based, weight_based, quantity_based', name VARCHAR(100) NOT NULL COMMENT '이름: 면적기반, 중량기반, 수량기반', multiplier_variable VARCHAR(20) COMMENT '곱셈 변수: M, K, null', categories JSON COMMENT '소속 카테고리 목록', description TEXT, sort_order INT DEFAULT 0, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_tenant (tenant_id), INDEX idx_code (code) ); ``` ### Phase 2: Seeder 작성 (2일) #### 2.1 품목 마스터 Seeder **파일**: `database/seeders/DesignItemSeeder.php` ```php class DesignItemSeeder extends Seeder { public function run(): void { // 원자재 (20개) $rawMaterials = [ ['code' => 'RM-S001', 'name' => '강판 1.2T', 'unit' => 'KG', 'price' => 3500, 'category' => '강판'], ['code' => 'RM-F001', 'name' => '방화원단 A급', 'unit' => 'M2', 'price' => 28000, 'category' => '원단'], // ... 18개 더 ]; // 부자재 (25개) $subMaterials = [ ['code' => 'SM-B001', 'name' => '볼트 M8x30', 'unit' => 'EA', 'price' => 150, 'category' => '볼트'], // ... 24개 더 ]; // 스크린 반제품 (20개) $screenSemiProducts = [ ['code' => 'SF-SCR-F01', 'name' => '스크린 원단', 'unit' => 'M2', 'price' => 35000, 'category' => '원단', 'process' => 'screen'], ['code' => 'SF-SCR-F02', 'name' => '가이드레일 (좌)', 'unit' => 'M', 'price' => 42000, 'category' => '가이드레일', 'process' => 'screen'], // ... 18개 더 ]; // 완제품 (14개) $finishedProducts = [ ['code' => 'FG-SCR-001', 'name' => '방화스크린 소형', 'category' => 'SCREEN'], ['code' => 'FG-SCR-002', 'name' => '방화스크린 중형', 'category' => 'SCREEN'], // ... 12개 더 ]; } } ``` #### 2.2 BOM 구성 Seeder **파일**: `database/seeders/DesignBomSeeder.php` ```php class DesignBomSeeder extends Seeder { public function run(): void { $bomData = [ 'FG-SCR-002' => [ ['child' => 'SF-SCR-F01', 'qty' => 1, 'formula' => 'W*H/1000000', 'unit' => 'M2'], ['child' => 'SF-SCR-F02', 'qty' => 1, 'formula' => 'H/1000', 'unit' => 'M'], ['child' => 'SF-SCR-F03', 'qty' => 1, 'formula' => 'H/1000', 'unit' => 'M'], ['child' => 'SF-SCR-F04', 'qty' => 1, 'formula' => '', 'unit' => 'EA'], // ... 더 많은 BOM 라인 ], // ... 다른 제품들 ]; } } ``` #### 2.3 CategoryGroup Seeder ```php class CategoryGroupSeeder extends Seeder { public function run(): void { $groups = [ [ 'code' => 'area_based', 'name' => '면적기반', 'multiplier_variable' => 'M', 'categories' => json_encode(['원단', '패널', '도장', '표면처리']), ], [ 'code' => 'weight_based', 'name' => '중량기반', 'multiplier_variable' => 'K', 'categories' => json_encode(['강판', '알루미늄']), ], [ 'code' => 'quantity_based', 'name' => '수량기반', 'multiplier_variable' => null, 'categories' => json_encode(['볼트', '너트', '모터', '제어반']), ], ]; } } ``` ### Phase 3: 백엔드 로직 확장 (2일) #### 3.1 FormulaEvaluatorService 확장 **추가할 메서드:** ```php /** * 카테고리 기반 단가 계산 */ private function calculateCategoryPrice( array $item, float $basePrice, array $variables ): array { $categoryGroup = CategoryGroup::query() ->whereJsonContains('categories', $item['item_category'] ?? '') ->first(); if (!$categoryGroup || !$categoryGroup->multiplier_variable) { return [ 'final_price' => $basePrice, 'calculation_note' => '수량단가', 'multiplier' => 1, ]; } $multiplierVar = $categoryGroup->multiplier_variable; $multiplierValue = $variables[$multiplierVar] ?? 1; return [ 'final_price' => $basePrice * $multiplierValue, 'calculation_note' => "{$categoryGroup->name} ({$basePrice}원/{$this->getUnit($multiplierVar)} × {$multiplierValue})", 'multiplier' => $multiplierValue, ]; } /** * 공정별 품목 그룹화 */ private function groupItemsByProcess(array $items): array { $processOrder = [ 'screen' => ['label' => '스크린 공정', 'items' => [], 'subtotal' => 0], 'bending' => ['label' => '절곡 공정', 'items' => [], 'subtotal' => 0], 'electric' => ['label' => '전기 공정', 'items' => [], 'subtotal' => 0], 'assembly' => ['label' => '조립 공정', 'items' => [], 'subtotal' => 0], 'etc' => ['label' => '기타', 'items' => [], 'subtotal' => 0], ]; foreach ($items as $item) { $process = $item['process_type'] ?? 'etc'; if (isset($processOrder[$process])) { $processOrder[$process]['items'][] = $item; $processOrder[$process]['subtotal'] += $item['total_price'] ?? 0; } else { $processOrder['etc']['items'][] = $item; $processOrder['etc']['subtotal'] += $item['total_price'] ?? 0; } } return array_filter($processOrder, fn($g) => count($g['items']) > 0); } /** * 10단계 디버깅 정보 생성 */ private function generateDebugInfo( array $bomLine, array $variables, float $calculatedQty, float $basePrice, float $finalPrice, float $totalPrice, string $priceSource, string $calcNote ): array { return [ 'step1_formula' => $bomLine['quantity_formula'] ?? '수식 없음', 'step2_variables' => $variables, 'step3_quantity_calc' => $this->buildQuantityCalcString($bomLine, $variables, $calculatedQty), 'step4_quantity' => $calculatedQty, 'step5_price_source' => $priceSource, 'step6_base_price' => $basePrice, 'step7_category_multiplier' => $calcNote, 'step8_final_price' => $finalPrice, 'step9_total_calc' => sprintf('%.2f × %s = %s', $calculatedQty, number_format($finalPrice), number_format($totalPrice)), 'step10_total' => $totalPrice, ]; } ``` #### 3.2 executeAll() 반환 구조 확장 ```php public function executeAll(array $inputVariables): array { // 1. 변수 계산 $calculatedVariables = $this->calculateVariables($inputVariables); // 2. 제품 BOM 조회 $product = Item::where('code', $inputVariables['PRODUCT_ID'])->first(); $bomTree = $this->getBomTree($product); // 3. BOM 항목별 계산 $bomItems = []; foreach ($bomTree as $bomLine) { $result = $this->calculateBomItem($bomLine, $calculatedVariables); $bomItems[] = $result; } // 4. 공정별 그룹화 $groupedByProcess = $this->groupItemsByProcess($bomItems); // 5. 총합계 $totalAmount = array_sum(array_column($bomItems, 'total_price')); return [ 'input_variables' => $inputVariables, 'calculated_variables' => $calculatedVariables, 'product' => [ 'code' => $product->code, 'name' => $product->name, 'category' => $product->item_details->product_category ?? null, ], 'bom_items' => $bomItems, 'grouped_by_process' => $groupedByProcess, 'summary' => [ 'total_items' => count($bomItems), 'total_amount' => $totalAmount, ], ]; } ``` ### Phase 4: 프론트엔드 확장 (1일) #### 4.1 simulator.blade.php 결과 표시 개선 ```blade {{-- 공정별 그룹화 결과 --}} @if(isset($result['grouped_by_process']))
| 품목코드 | 품목명 | 수량 | 단위 | 단가 | 금액 |
|---|---|---|---|---|---|
| {{ $item['item_code'] }} | {{ $item['item_name'] }} | {{ number_format($item['calculated_quantity'], 2) }} | {{ $item['unit'] }} | {{ number_format($item['final_price']) }} | {{ number_format($item['total_price']) }} |