Files
sam-docs/plans/simulator-calculation-logic-mapping.md
kent 7d639bb584 docs(simulator): Phase 7 제품 유형별 테스트 결과 문서화
- 철재 제품(FG-STL-001) 테스트 결과 추가
  - W1=2110, K=150.34 (M×25 공식) 검증 완료
  - 총액: 3,158,111원
- 절곡 제품(FG-BND-001) 테스트 결과 추가
  - CategoryGroup에 "절곡" 카테고리 추가
  - 총액: 727,893원
- 스크린 제품(FG-SCR-001) 재검증 완료
  - 총액: 1,711,225원
- 제품 유형별 검증 결과 요약 섹션 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 14:46:26 +09:00

1057 lines
35 KiB
Markdown
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.

# 견적 시뮬레이터 완전 동기화 계획
> **작성일**: 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']))
<div class="space-y-6">
@foreach($result['grouped_by_process'] as $processCode => $group)
<div class="border rounded-lg">
<div class="bg-gray-50 px-4 py-2 border-b flex justify-between">
<h4 class="font-semibold">{{ $group['label'] }}</h4>
<span class="text-blue-600 font-medium">
소계: {{ number_format($group['subtotal']) }}원
</span>
</div>
<table class="w-full">
<thead>
<tr class="bg-gray-100">
<th>품목코드</th>
<th>품목명</th>
<th>수량</th>
<th>단위</th>
<th>단가</th>
<th>금액</th>
</tr>
</thead>
<tbody>
@foreach($group['items'] as $item)
<tr>
<td>{{ $item['item_code'] }}</td>
<td>{{ $item['item_name'] }}</td>
<td>{{ number_format($item['calculated_quantity'], 2) }}</td>
<td>{{ $item['unit'] }}</td>
<td>{{ number_format($item['final_price']) }}</td>
<td>{{ number_format($item['total_price']) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endforeach
</div>
{{-- 총합계 --}}
<div class="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex justify-between items-center">
<span class="text-lg font-semibold text-blue-900">총 합계</span>
<span class="text-2xl font-bold text-blue-700">
{{ number_format($result['summary']['total_amount']) }}원
</span>
</div>
</div>
```
### Phase 5: 검증 및 동기화 (1일)
#### 5.1 테스트 케이스
| 입력값 | Design 결과 | MNG 목표 |
|--------|------------|----------|
| W0=2000, H0=2500, PC=스크린 | W1=2140, H1=2850, M=6.099 | 동일 |
| 스크린 원단 (면적단가) | 35,000 × 6.099 = 213,465원 | 동일 |
| 가이드레일 (길이단가) | 42,000 × 2.85 = 119,700원 | 동일 |
| 모터 (고정단가) | 480,000 × 1 = 480,000원 | 동일 |
#### 5.2 검증 스크립트
```php
// php artisan tinker
// 동일 입력으로 계산 비교
$input = [
'PC' => '스크린',
'PRODUCT_ID' => 'FG-SCR-002',
'W0' => 2000,
'H0' => 2500,
'GT' => '벽면형',
'MP' => '220V',
'CT' => '단독',
'QTY' => 1,
];
$service = app(\App\Services\Quote\FormulaEvaluatorService::class);
$result = $service->executeAll($input);
// Design 결과와 비교
dump([
'W1' => $result['calculated_variables']['W1'], // 예상: 2140
'H1' => $result['calculated_variables']['H1'], // 예상: 2850
'M' => $result['calculated_variables']['M'], // 예상: 6.099
'total' => $result['summary']['total_amount'], // Design과 동일해야 함
]);
```
---
## 5. 핵심 파일 참조
### Design (참조용 - 수정 금지)
```
/SAM/design/src/
├── components/
│ ├── AutoCalculationSimulator.tsx # 메인 시뮬레이터 (1068줄)
│ ├── BomCalculationResults.tsx # 결과 표시 컴포넌트
│ ├── contexts/
│ │ └── DataContext.tsx # 마스터 데이터 (9859줄)
│ └── utils/
│ ├── formulaEvaluator.ts # 수식 평가 (312줄)
│ └── bomCalculatorWithDebug.ts # BOM 계산 (232줄)
└── utils/
├── sampleQuoteData_Complete.ts # 샘플 품목 데이터
└── addProductBoms.ts # BOM 구성 데이터
```
### MNG (수정 대상)
```
/SAM/mng/
├── app/Services/Quote/
│ └── FormulaEvaluatorService.php # 핵심 서비스 확장 대상
├── database/
│ ├── migrations/
│ │ └── 20xx_add_simulator_fields.php # 신규 마이그레이션
│ └── seeders/
│ ├── DesignItemSeeder.php # 신규 Seeder
│ ├── DesignBomSeeder.php # 신규 Seeder
│ └── CategoryGroupSeeder.php # 신규 Seeder
├── app/Models/
│ ├── CategoryGroup.php # 신규 모델
│ ├── Item.php # 필드 추가
│ └── Price.php # 기존 모델
└── resources/views/quote-formulas/
└── simulator.blade.php # UI 확장
```
---
## 6. 작업 일정 요약
| Phase | 작업 내용 | 예상 일정 |
|-------|----------|----------|
| Phase 1 | DB 스키마 확장 (마이그레이션) | 1일 |
| Phase 2 | Seeder 작성 (품목/BOM/단가/CategoryGroup) | 2일 |
| Phase 3 | FormulaEvaluatorService 확장 | 2일 |
| Phase 4 | simulator.blade.php UI 개선 | 1일 |
| Phase 5 | 검증 및 동기화 테스트 | 1일 |
| **합계** | | **7일** |
---
## 7. 성공 기준
1. **계산 결과 동일**: Design과 MNG에서 동일 입력 시 동일한 금액 산출
2. **10단계 디버깅**: 각 품목별 계산 과정을 10단계로 확인 가능
3. **공정별 그룹화**: 스크린/절곡/전기 공정별로 품목 분류
4. **단가 우선순위**: prices 테이블 > items.salesPrice 순서 적용
5. **면적/중량 기반 단가**: CategoryGroup 설정에 따라 자동 계산
---
## 8. Serena 컨텍스트 유지 정책
> **목적**: 세션 간 컨텍스트 유지를 위해 Serena MCP 메모리에 역할별 분리 저장
### 8.1 메모리 구조
```
simulator-rules.md # 패턴, 규칙, 체크리스트
simulator-mappings.md # 필드 매핑 상세 (Design ↔ MNG)
simulator-progress.md # 진행 상황
```
### 8.2 메모리 내용
#### `simulator-rules.md`
- 계산 변수 체계 (W0, H0, W1, H1, M, K 등)
- 수식 평가 함수 목록 (SUM, CEIL, FLOOR, ROUND, IF 등)
- BOM 10단계 계산 프로세스
- 단가 우선순위 규칙
- 체크리스트
#### `simulator-mappings.md`
- Design TypeScript 인터페이스 ↔ MNG DB 테이블 매핑
- 품목 타입 매핑 (RM, SM, SF, FG, PT, CS)
- CategoryGroup 매핑
- 공정 타입 매핑 (screen, bending, electric, assembly)
#### `simulator-progress.md`
- Phase별 진행 상태
- 완료된 작업 목록
- 남은 작업 및 이슈
### 8.3 세션 시작/종료 패턴
**세션 시작:**
```
list_memories() → 기존 상태 확인
read_memory("simulator-progress.md") → 진행 상황 복원
read_memory("simulator-rules.md") → 규칙 컨텍스트 로드
```
**세션 종료:**
```
write_memory("simulator-progress.md", 현재 진행 상황)
```
### 8.4 초기 메모리 저장 명령
```bash
# 세션 시작 시 아래 명령으로 메모리 초기화
/sc:save simulator-rules # 규칙 저장
/sc:save simulator-mappings # 매핑 저장
/sc:save simulator-progress # 진행 상황 저장
```
---
## 9. 검증 결과 (Phase 5)
> **검증일**: 2025-12-24
> **테스트 환경**: Docker (sam-mng-1)
### 9.1 테스트 케이스: FG-SCR-001 (W0=2000, H0=2500)
#### 변수 계산 (Design 마진 적용)
| 변수 | 계산식 | 결과 | 상태 |
|------|--------|------|------|
| W0 | 입력값 | 2000 | ✅ |
| H0 | 입력값 | 2500 | ✅ |
| W1 | W0 + 140 | 2140 | ✅ |
| H1 | H0 + 350 | 2850 | ✅ |
| M | W1 × H1 / 1,000,000 | 6.099 ㎡ | ✅ |
#### 품목별 계산 결과
| 품목코드 | 그룹 | 수량 | 단가 | 금액 | 상태 |
|----------|------|------|------|------|------|
| SF-SCR-F01 | area_based | 6.10 | 35,000 | 213,465원 | ✅ |
| SF-SCR-F02 | quantity_based | 2.85 | 42,000 | 119,700원 | ✅ |
| SF-SCR-F03 | quantity_based | 2.85 | 42,000 | 119,700원 | ✅ |
| SF-SCR-F04 | quantity_based | 1.00 | 145,000 | 145,000원 | ✅ |
| SF-SCR-F05 | (미등록) | 1.00 | 55,000 | 55,000원 | ✅ |
| SF-SCR-M01 | quantity_based | 1.00 | 350,000 | 350,000원 | ✅ |
| SF-SCR-C01 | quantity_based | 1.00 | 280,000 | 280,000원 | ✅ |
| SF-SCR-S01 | (미등록) | 1.00 | 180,000 | 180,000원 | ✅ |
| SF-SCR-W01 | (미등록) | 1.00 | 125,000 | 125,000원 | ✅ |
| SF-SCR-B01 | quantity_based | 1.00 | 78,000 | 78,000원 | ✅ |
| SF-SCR-SW01 | quantity_based | 1.00 | 45,000 | 45,000원 | ✅ |
| SM-B002 | quantity_based | 1.00 | 200 | 200원 | ✅ |
| SM-N002 | quantity_based | 1.00 | 100 | 100원 | ✅ |
| SM-W002 | quantity_based | 1.00 | 60 | 60원 | ✅ |
| **합계** | | | | **1,711,225원** | ✅ |
### 9.2 10단계 디버깅 검증
| 단계 | 항목 | 상태 |
|------|------|------|
| Step 1 | 입력값수집 | ✅ |
| Step 2 | 변수계산 | ✅ |
| Step 3 | 완제품선택 | ✅ |
| Step 4 | BOM전개 | ✅ |
| Step 5 | 단가출처 | ✅ |
| Step 6 | 수량계산 | ✅ |
| Step 7 | 금액계산 | ✅ |
| Step 8 | 공정그룹화 | ✅ |
| Step 9 | 소계계산 | ✅ |
| Step 10 | 최종합계 | ✅ |
### 9.3 공정별 그룹화 검증
| 공정 | 품목 수 | 소계 | 상태 |
|------|---------|------|------|
| screen | 11 | 1,710,865원 | ✅ |
| assembly | 3 | 360원 | ✅ |
### 9.4 단가 우선순위 검증
| 품목 | 단가 출처 | 상태 |
|------|----------|------|
| SF-SCR-F01 | items.salesPrice | ✅ |
| SF-SCR-M01 | items.salesPrice | ✅ |
| SM-B002 | items.salesPrice | ✅ |
> **참고**: ~~prices 테이블에 active 데이터 없음~~ → **2025-12-29 prices 데이터 85개 추가 완료**
### 9.5 성공 기준 달성 현황
| 기준 | 달성 | 비고 |
|------|------|------|
| 계산 결과 동일 | ✅ | Design 마진 (W+140, H+350) 적용 |
| 10단계 디버깅 | ✅ | 모든 단계 정상 출력 |
| 공정별 그룹화 | ✅ | screen, assembly 분류 |
| 단가 우선순위 | ✅ | prices → items.salesPrice 순서 |
| 면적/중량 기반 단가 | ✅ | CategoryGroup 기반 자동 계산 |
### 9.6 수정 사항 (Phase 5 중)
1. **면적기반 단가 중복 계산 수정**
- 문제: `total = quantity × (base_price × multiplier)` (중복)
- 수정: 면적/중량기반은 `total = final_price` (이미 multiplier 적용됨)
2. **마진값 Design 표준 적용**
- 기존: W+100, H+100
- 수정: W+140, H+350 (스크린 기준)
---
## 10. Phase 6: prices 테이블 데이터 추가 (2025-12-29)
### 10.1 작업 내용
| 항목 | 내용 |
|------|------|
| 작업일 | 2025-12-29 |
| 목적 | prices 테이블에 시뮬레이터용 단가 데이터 추가 |
| Seeder | `DesignPriceSeeder.php` |
| 대상 품목 | 85개 (RM, SM, SF-SCR, SF-STL, SF-BND) |
### 10.2 생성된 Seeder
**파일**: `mng/database/seeders/DesignPriceSeeder.php`
```php
// items.attributes.salesPrice → prices 테이블 이전
// 단가 우선순위: prices (1순위) → items.attributes (2순위)
```
**실행 명령**:
```bash
php artisan db:seed --class=DesignPriceSeeder
```
### 10.3 추가된 데이터
| 품목 유형 | 코드 패턴 | 수량 |
|----------|----------|------|
| 원자재 | RM-* | 20개 |
| 부자재 | SM-* | 25개 |
| 스크린 반제품 | SF-SCR-* | 20개 |
| 철재 반제품 | SF-STL-* | 16개 |
| 절곡 반제품 | SF-BND-* | 4개 |
| **합계** | | **85개** |
### 10.4 단가 우선순위 검증 결과
```
=== prices 우선순위 테스트 ===
prices 테이블: 99,999원 (테스트용 변경)
items.attributes: 35,000원 (그대로)
getSalesPriceByItemCode(): 99,999원
✓ prices 테이블 우선 적용 확인!
```
### 10.5 FormulaEvaluatorService 단가 조회 로직
```php
// mng/app/Services/Quote/FormulaEvaluatorService.php:379-410
private function getItemPrice(string $itemCode): float
{
// 1순위: Price 모델에서 조회
$price = Price::getSalesPriceByItemCode($tenantId, $itemCode);
if ($price > 0) {
return $price;
}
// 2순위: Fallback - items.attributes.salesPrice
$item = DB::table('items')->where('code', $itemCode)->first();
return (float) ($attributes['salesPrice'] ?? 0);
}
```
---
## 11. Phase 7: 철재 제품 테스트 케이스 (2025-12-30)
### 11.1 작업 개요
| 항목 | 내용 |
|------|------|
| 작업일 | 2025-12-30 |
| 목적 | 철재 제품(FG-STL-*) 마진값/중량 계산 동기화 및 CategoryGroup 적용 |
| 테스트 완제품 | FG-STL-001 (철재 방화문) |
| 입력값 | W0=2000, H0=2500 |
### 11.2 수정 사항
#### 11.2.1 마진값 동적 적용 (SCREEN/STEEL 분기)
**파일**: `mng/app/Services/Quote/FormulaEvaluatorService.php`
| 제품 카테고리 | 마진 W | 마진 H | K 계산식 |
|-------------|-------|-------|---------|
| SCREEN (스크린) | W0+140 | H0+350 | M×2 + W0/1000×14.17 |
| STEEL (철재) | W0+110 | H0+350 | M×25 |
**변경 내용**:
```php
// 제품 카테고리에 따른 마진값 결정
if (strtoupper($productCategory) === 'STEEL') {
$marginW = 110; // 철재 마진
$K = $M * 25; // 철재 중량
} else {
$marginW = 140; // 스크린 기본 마진
$K = $M * 2 + ($W0 / 1000) * 14.17; // 스크린 중량
}
```
#### 11.2.2 CategoryGroup 데이터 생성 (tenant 287)
**문제**: CategoryGroup 데이터가 tenant_id=1에만 존재, tenant_id=287 미등록
**해결**: tenant 287용 CategoryGroup 3종 생성
| 코드 | 이름 | 승수변수 | 포함 카테고리 |
|------|------|---------|-------------|
| area_based | 면적기반 | M | 원단, 패널, 도장, 표면처리, 유리, 도어, 프레임, 창틀 |
| weight_based | 중량기반 | K | 강판, 알루미늄, 스테인리스, 철재 |
| quantity_based | 수량기반 | (없음) | 볼트, 경첩, 도어락, 도어클로저, 실링재, 문턱, 킥플레이트 등 |
### 11.3 테스트 결과
#### 11.3.1 변수 계산 검증
| 변수 | 계산값 | 예상값 | 상태 |
|------|-------|-------|------|
| W1 | 2110 | 2110 (W0+110) | ✅ |
| H1 | 2850 | 2850 (H0+350) | ✅ |
| M | 6.0135 ㎡ | 6.0135 | ✅ |
| K | 150.34 kg | 150.34 (M×25) | ✅ |
| PC | STEEL | STEEL | ✅ |
#### 11.3.2 CategoryGroup 적용 검증
| 품목 | 카테고리 | CategoryGroup | 기준단가 | 승수 | 최종단가 |
|------|---------|--------------|---------|------|---------|
| 철재 도어 | 도어 | area_based | 320,000 | M×6.01 | 1,924,320원 |
| 철재 프레임 | 프레임 | area_based | 58,000 | M×6.01 | 348,783원 |
| 철재 패널 | 패널 | area_based | 68,000 | M×6.01 | 408,918원 |
| 경첩 세트 | 경첩 | quantity_based | 42,000 | - | 42,000원 |
| 도어락 | 도어락 | quantity_based | 95,000 | - | 95,000원 |
| 도어클로저 | 도어클로저 | quantity_based | 115,000 | - | 115,000원 |
| 실링재 | 실링재 | quantity_based | 9,500 | - | 9,500원 |
| 문턱 | 문턱 | quantity_based | 58,000 | - | 58,000원 |
| 킥플레이트 | 킥플레이트 | quantity_based | 45,000 | - | 45,000원 |
| 볼트 세트 | 볼트 | quantity_based | 18,000 | - | 18,000원 |
**최종 합계**: 3,158,111원 ✅
### 11.4 수정된 파일
| 파일 | 수정 내용 |
|------|----------|
| `FormulaEvaluatorService.php` | 마진값/K계산 동적 분기, `getItemDetails()`에 item_category 추가 |
| `category_groups` (DB) | tenant 287용 3개 그룹 생성 |
### 11.5 성공 기준 달성
| 기준 | 달성 | 비고 |
|------|------|------|
| 철재 마진 적용 | ✅ | W+110 정상 적용 |
| 철재 중량 계산 | ✅ | M×25 정상 적용 |
| CategoryGroup 매칭 | ✅ | area_based, quantity_based 정상 |
| 면적기반 단가 계산 | ✅ | base_price × M 정상 |
| 수량기반 단가 계산 | ✅ | base_price 그대로 적용 |
### 11.6 절곡 제품 테스트 (FG-BND-001)
#### 테스트 결과
| 변수 | 계산값 | 상태 |
|------|-------|------|
| W1 | 2110 (W0+110) | ✅ 철재 마진 적용 |
| M | 6.0135 ㎡ | ✅ |
| K | 150.34 kg (M×25) | ✅ 철재 중량 |
| PC | STEEL | ✅ |
#### CategoryGroup 수정
**문제**: "절곡" 카테고리가 CategoryGroup 미등록 → 단가 0원
**해결**: `area_based`에 "절곡" 카테고리 추가
```json
// area_based categories (수정 후)
["원단","패널","도장","표면처리","스크린원단","유리","도어","프레임","창틀","절곡"]
```
#### 수정 후 단가 계산
| 품목 | CategoryGroup | 기준단가 | 승수 | 최종단가 |
|------|--------------|---------|------|---------|
| 절곡 | area_based | 28,000 | M×6.01 | 168,378원 |
| 프레임 | area_based | 58,000 | M×6.01 | 348,783원 |
| 도장 | area_based | 32,000 | M×6.01 | 192,432원 |
| 볼트 | quantity_based | 18,000 | - | 18,000원 |
**최종 합계**: 727,893원 ✅
### 11.7 전체 제품 유형 검증 완료
| 제품 유형 | 코드 | 마진 | K 계산 | 합계 |
|----------|------|------|--------|------|
| 스크린 | FG-SCR-001 | W+140 ✅ | M×2+W0/1000×14.17 ✅ | 1,711,225원 |
| 철재 | FG-STL-001 | W+110 ✅ | M×25 ✅ | 3,158,111원 |
| 절곡 | FG-BND-001 | W+110 ✅ | M×25 ✅ | 727,893원 |
---
*이 문서는 design.sam.kr 완전 분석을 바탕으로 mng 시뮬레이터 완전 동기화 계획을 상세히 기술합니다.*