From c983d7414850193a688df6ce740f308edf814fbc Mon Sep 17 00:00:00 2001 From: hskwon Date: Wed, 24 Dec 2025 16:11:44 +0900 Subject: [PATCH] =?UTF-8?q?Phase=205:=20=EC=8B=9C=EB=AE=AC=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=8F=99=EA=B8=B0=ED=99=94=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EA=B2=B0=EA=B3=BC=20=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테스트 케이스: FG-SCR-001 (W0=2000, H0=2500) - 변수 계산 검증: W1=2140, H1=2850, M=6.099 - 10단계 디버깅 검증 완료 - 공정별 그룹화 검증 완료 (screen, assembly) - 단가 우선순위 검증 완료 (prices → items.salesPrice) - 성공 기준 5가지 모두 달성 --- plans/simulator-calculation-logic-mapping.md | 870 +++++++++++++++---- 1 file changed, 716 insertions(+), 154 deletions(-) diff --git a/plans/simulator-calculation-logic-mapping.md b/plans/simulator-calculation-logic-mapping.md index 76a1426..77defda 100644 --- a/plans/simulator-calculation-logic-mapping.md +++ b/plans/simulator-calculation-logic-mapping.md @@ -1,187 +1,450 @@ -# 견적 시뮬레이터 계산 로직 매핑 +# 견적 시뮬레이터 완전 동기화 계획 -> **작성일**: 2025-12-23 -> **목표**: design.sam.kr 계산 로직 → mng 시뮬레이터 동기화 +> **작성일**: 2025-12-23 (업데이트: 2025-12-24) +> **목표**: design.sam.kr 시뮬레이터와 mng 시뮬레이터가 **동일한 결과**를 출력하도록 완전 동기화 --- -## 1. Design 계산 로직 분석 +## 1. Design 시스템 전체 분석 -### 1.1 핵심 계산 변수 (AutoCalculationSimulator.tsx:429-444) +### 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 -const calculationVariables = { - W0: quote.w0, // 오픈사이즈 폭 (입력) - H0: quote.h0, // 오픈사이즈 높이 (입력) - W1: quote.category === '스크린' ? W0 + 140 : W0 + 110, // 제작폭 - H1: H0 + 350, // 제작높이 - W: W1, // W = W1 (매핑) - H: H1, // H = H1 (매핑) - M: (W1 * H1) / 1000000, // 면적 (㎡) - K: 0 // 중량 (미구현) -}; +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 목록 + // ... 기타 필드 +} ``` -### 1.2 수식 평가 함수 (formulaEvaluator.ts) +#### 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(value, decimals)` | 반올림 | `ROUND(M, 2)` | -| `CEIL(value)` | 올림 | `CEIL(H1 / 1000)` | -| `FLOOR(value)` | 내림 | `FLOOR(W1 / 500)` | -| `ABS(value)` | 절대값 | `ABS(W0 - 2000)` | -| `IF(cond, true, false)` | 조건문 | `IF(W0 > 3000, 2, 1)` | -| `SQRT(value)` | 제곱근 | `SQRT(M)` | +| `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. 변수 치환 -let expr = formula; -Object.keys(vars).forEach(key => { - const regex = new RegExp(`\\b${key}\\b`, 'g'); - expr = expr.replace(regex, vars[key].toString()); +// 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 등) -expr = processFunctions(expr); +formula = processFunctions(formula); // 3. 최종 계산 -return new Function(`return (${expr})`)(); +return new Function(`return (${formula})`)(); ``` -### 1.3 BOM 계산 과정 (bomCalculatorWithDebug.ts) +### 1.5 BOM 계산 10단계 프로세스 -**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원)` | +| 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.4 단가 계산 방식 +### 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 = ['원단', '패널', '도장', '표면처리']; -if (isAreaBased && M > 0) { - finalUnitPrice = unitPrice * M; // 면적 단가 +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: '고정용' }, + ] } ``` --- -## 2. MNG 현재 구현 상태 +## 3. MNG 현재 상태 분석 -### 2.1 FormulaEvaluatorService.php +### 3.1 테이블 구조 -**✅ 구현됨:** -- `evaluate()` - 수식 평가 -- `validateFormula()` - 수식 검증 -- `executeAll()` - 전체 수식 실행 -- `getItemPrice()` - 단가 조회 (Price 모델 연동) -- `getBomTree()` - BOM 트리 조회 -- `enrichItemsWithDetails()` - 품목 상세 정보 추가 +| 테이블 | 현재 상태 | 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 추가 필요 | -**지원 함수:** -- SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT +### 3.2 quote_formulas 현재 데이터 (샘플) -**❌ 미구현:** -- 제품 카테고리별 변수 계산 규칙 (W1, H1, M) -- 면적/중량 기반 단가 계산 -- 공정별 분류 +``` +[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 +``` -### 2.2 simulator.blade.php +### 3.3 누락 항목 -**✅ 구현됨:** -- 입력 변수 폼 (W0, H0, PC, GT, MP, CT 등) -- API 호출 (`/api/admin/quote-formulas/formulas/simulate`) -- 계산된 변수 표시 -- 품목 목록 표시 (BOM 트리 포함) - -**❌ 미구현:** -- 공정별 그룹화 표시 -- 단가/금액 계산 결과 표시 -- 총합계 표시 +| 항목 | 설명 | 우선순위 | +|------|------|---------| +| `items.process_type` | 공정유형 (스크린/절곡/전기) | 높음 | +| `items.item_category` | 품목분류 (원단/패널/도장 등) | 높음 | +| `category_groups` 테이블 | 면적/중량 기반 분류 | 높음 | +| Design 샘플 품목 데이터 | 100개 품목 Seeder | 높음 | +| BOM 구성 데이터 | 제품별 BOM Seeder | 높음 | +| 단가 데이터 | 품목별 단가 Seeder | 중간 | --- -## 3. 매핑 계획 +## 4. 완전 동기화 구현 계획 -### 3.1 변수 계산 규칙 추가 - -**quote_formulas 테이블에 추가할 수식:** +### Phase 1: DB 스키마 확장 (1일) +#### 1.1 items 테이블 필드 추가 ```sql --- 제작폭 (W1) - 스크린 -INSERT INTO quote_formulas (category_id, variable, name, type, formula, output_type, sort_order) -VALUES (1, 'W1', '제작폭', 'calculation', 'IF(PC == "screen", W0 + 140, W0 + 110)', 'variable', 10); +ALTER TABLE items ADD COLUMN process_type VARCHAR(20) DEFAULT NULL + COMMENT '공정유형: screen(스크린), bending(절곡), electric(전기), steel(철재)'; --- 제작높이 (H1) -INSERT INTO quote_formulas (category_id, variable, name, type, formula, output_type, sort_order) -VALUES (1, 'H1', '제작높이', 'calculation', 'H0 + 350', 'variable', 20); +ALTER TABLE items ADD COLUMN item_category VARCHAR(50) DEFAULT NULL + COMMENT '품목분류: 원단, 패널, 도장, 표면처리, 가이드레일, 케이스, 모터, 제어반 등'; --- 면적 (M) -INSERT INTO quote_formulas (category_id, variable, name, type, formula, output_type, sort_order) -VALUES (1, 'M', '면적', 'calculation', '(W1 * H1) / 1000000', 'variable', 30); - --- W, H 매핑 -INSERT INTO quote_formulas (category_id, variable, name, type, formula, output_type, sort_order) -VALUES (1, 'W', '제작폭', 'calculation', 'W1', 'variable', 40); -INSERT INTO quote_formulas (category_id, variable, name, type, formula, output_type, sort_order) -VALUES (1, 'H', '제작높이', 'calculation', 'H1', 'variable', 50); +CREATE INDEX idx_items_process_type ON items(process_type); +CREATE INDEX idx_items_item_category ON items(item_category); ``` -### 3.2 FormulaEvaluatorService 확장 +#### 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 { - $areaBasedCategories = ['원단', '패널', '도장', '표면처리']; - $itemCategory = $item['item_category'] ?? ''; + $categoryGroup = CategoryGroup::query() + ->whereJsonContains('categories', $item['item_category'] ?? '') + ->first(); - if (in_array($itemCategory, $areaBasedCategories) && isset($variables['M'])) { + if (!$categoryGroup || !$categoryGroup->multiplier_variable) { return [ - 'final_price' => $basePrice * $variables['M'], - 'calculation_note' => "면적단가 ({$basePrice}원/㎡ × {$variables['M']}㎡)" + 'final_price' => $basePrice, + 'calculation_note' => '수량단가', + 'multiplier' => 1, ]; } + $multiplierVar = $categoryGroup->multiplier_variable; + $multiplierValue = $variables[$multiplierVar] ?? 1; + return [ - 'final_price' => $basePrice, - 'calculation_note' => '수량단가' + 'final_price' => $basePrice * $multiplierValue, + 'calculation_note' => "{$categoryGroup->name} ({$basePrice}원/{$this->getUnit($multiplierVar)} × {$multiplierValue})", + 'multiplier' => $multiplierValue, ]; } @@ -190,104 +453,403 @@ private function calculateCategoryPrice( */ private function groupItemsByProcess(array $items): array { - $processOrder = ['screen' => '스크린 공정', 'bending' => '절곡 공정', 'electric' => '전기 공정']; - $grouped = []; - - foreach ($processOrder as $code => $label) { - $grouped[$code] = [ - 'label' => $label, - 'items' => [], - 'subtotal' => 0 - ]; - } + $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($grouped[$process])) { - $grouped[$process]['items'][] = $item; - $grouped[$process]['subtotal'] += $item['total_price'] ?? 0; + 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 $grouped; + 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.3 items 테이블 확장 +#### 3.2 executeAll() 반환 구조 확장 -```sql --- 공정 유형 필드 추가 -ALTER TABLE items ADD COLUMN process_type VARCHAR(20) DEFAULT NULL - COMMENT '공정유형: screen(스크린), bending(절곡), electric(전기)'; +```php +public function executeAll(array $inputVariables): array +{ + // 1. 변수 계산 + $calculatedVariables = $this->calculateVariables($inputVariables); --- 인덱스 추가 -CREATE INDEX idx_items_process_type ON items(process_type); + // 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. 구현 순서 +#### 4.1 simulator.blade.php 결과 표시 개선 -### Phase 1: DB 스키마 및 데이터 (1일) +```blade +{{-- 공정별 그룹화 결과 --}} +@if(isset($result['grouped_by_process'])) +
+ @foreach($result['grouped_by_process'] as $processCode => $group) +
+
+

{{ $group['label'] }}

+ + 소계: {{ number_format($group['subtotal']) }}원 + +
+ + + + + + + + + + + + + @foreach($group['items'] as $item) + + + + + + + + + @endforeach + +
품목코드품목명수량단위단가금액
{{ $item['item_code'] }}{{ $item['item_name'] }}{{ number_format($item['calculated_quantity'], 2) }}{{ $item['unit'] }}{{ number_format($item['final_price']) }}{{ number_format($item['total_price']) }}
+
+ @endforeach +
-1. ✅ items 테이블에 `process_type` 필드 추가 -2. ✅ 기존 품목에 공정 유형 매핑 -3. ✅ quote_formulas에 기본 변수 계산 수식 추가 +{{-- 총합계 --}} +
+
+ 총 합계 + + {{ number_format($result['summary']['total_amount']) }}원 + +
+
+``` -### Phase 2: 백엔드 로직 (2일) +### Phase 5: 검증 및 동기화 (1일) -1. ✅ FormulaEvaluatorService에 카테고리별 단가 계산 추가 -2. ✅ 공정별 그룹화 메서드 추가 -3. ✅ executeAll() 반환 구조 확장 +#### 5.1 테스트 케이스 -### Phase 3: 프론트엔드 (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원 | 동일 | -1. ✅ simulator.blade.php에 공정별 결과 표시 -2. ✅ 단가/금액 표시 추가 -3. ✅ 총합계 표시 +#### 5.2 검증 스크립트 -### Phase 4: 검증 (1일) +```php +// php artisan tinker -1. ✅ design.sam.kr과 결과 비교 -2. ✅ 금액 차이 분석 및 조정 +// 동일 입력으로 계산 비교 +$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 (참조용) +### Design (참조용 - 수정 금지) ``` -/SAM/design/src/components/ -├── AutoCalculationSimulator.tsx # 메인 시뮬레이터 (1068줄) -├── utils/ -│ ├── formulaEvaluator.ts # 수식 평가 (312줄) -│ └── bomCalculatorWithDebug.ts # BOM 계산 (232줄) -└── contexts/ - └── DataContext.tsx # 마스터 데이터 (9859줄) +/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 # 핵심 서비스 (575줄) -├── resources/views/quote-formulas/ -│ └── simulator.blade.php # 시뮬레이터 UI (867줄) -└── app/Models/ - ├── Price.php # 단가 모델 (135줄) - └── Item.php # 품목 모델 +│ └── 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. 테스트 케이스 +## 6. 작업 일정 요약 -| 입력 | Design 결과 | MNG 목표 | -|------|------------|----------| -| W0=2000, H0=2500, PC=스크린 | W1=2140, H1=2850, M=6.099 | 동일 | -| 스크린 원단 | 수량공식: W*H/1000000 = 6.099㎡ | 동일 | -| 가이드레일 | 수량공식: H/1000 = 2.85m | 동일 | +| 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일** | --- -*이 문서는 design.sam.kr 분석 결과를 바탕으로 mng 시뮬레이터 구현 계획을 정리합니다.* \ No newline at end of file +## 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 데이터 없음 → items.attributes.salesPrice에서 조회 + +### 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 (스크린 기준) + +--- + +*이 문서는 design.sam.kr 완전 분석을 바탕으로 mng 시뮬레이터 완전 동기화 계획을 상세히 기술합니다.* \ No newline at end of file