fix: 견적 반올림 순서를 5130과 동일하게 수정 (단건→수량 곱셈)

- 케이스, 케이스용 연기차단재, 가이드레일, 레일용 연기차단재:
  round(단가 × 길이 × QTY) → round(단가 × 길이) × QTY
- 5130 레거시와 동일한 반올림 순서 적용
- 검증: 스크린 44건 + 슬랫 32건 + 가이드타입 21건 = 97건 ALL PASS
- 사이즈 범위: 3000×1500 ~ 12000×4000, QTY 1~5

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 23:52:02 +09:00
parent f4a902fceb
commit 9bd585bdf3
2 changed files with 80 additions and 15 deletions

View File

@@ -1,3 +1,63 @@
## 2026-01-30 (목) - 5130↔SAM 견적 교차 검증 완료 + 마이그레이션 검증
### 작업 목표
- SAM 견적 계산이 5130 레거시 시스템과 100% 일치하는지 교차 검증
- FormulaEvaluatorService 슬랫/스틸 지원 완성
- MigrateBDModelsPrices 커맨드 동작 검증
### 수정된 파일
| 파일명 | 설명 |
|--------|------|
| `app/Services/Quote/FormulaEvaluatorService.php` | 제품타입별 면적/중량 공식 분기, 모터/브라켓 입력값 오버라이드, 디버그 포뮬러 동적 표시 |
| `app/Services/Quote/Handlers/KyungdongFormulaHandler.php` | 제품타입별 면적/중량 공식, normalizeGuideType() 추가, guide_rail_spec 파라미터 별칭 |
### 핵심 수정 내용
#### 1. 제품타입별 면적/중량 공식 (FormulaEvaluatorService + Handler)
- **Screen**: area = (W1 × (H1+550)) / 1M, weight = area×2 + (W0/1000)×14.17
- **Slat**: area = (W0 × (H0+50)) / 1M, weight = area×25
- **Steel**: area = (W1 × (H1+550)) / 1M, weight = area×25
#### 2. 모터/브라켓 입력값 오버라이드
- 기존: 항상 자동 계산
- 수정: `MOTOR_CAPACITY`, `BRACKET_SIZE` 입력값이 있으면 우선 사용
#### 3. 가이드타입 정규화
- `normalizeGuideType()` 메서드 추가 (벽면↔벽면형, 측면↔측면형, 혼합↔혼합형)
- `guide_rail_spec` 파라미터 별칭 지원
### 검증 결과
#### 전 모델 교차 검증 (Task #6) ✅
```
16/16 ALL PASS
- 10개 스크린 조합 (KSS01, KSS02, KSE01, KWE01, KTE01, KQTS01, KDSS01 × SUS/EGI)
- 6개 슬랫 조합 (KSS02, KSE01, KTE01 × SUS × 2사이즈)
- 조건: 6800×2700, QTY=1, 300K 모터, 5인치 브라켓
```
#### 가이드타입 교차 검증 (Task #7) ✅
```
21/21 ALL PASS
- 벽면/측면/혼합 × 4모델(KSS02, KSE01, KTE01, KDSS01) × screen
- 벽면/측면/혼합 × 3모델(KSS02, KSE01, KTE01) × slat
- 혼합형: 5130은 col6에 "혼합 120*70/120*120" 두 규격 필요
```
#### MigrateBDModelsPrices 커맨드 검증 (Task #4, #5) ✅
```
커맨드 정상 동작 확인
- BD-* (절곡품): 58건 마이그레이션 완료
- EST-* (모터/제어기/원자재 등): 71건 마이그레이션 완료
- chandj 원본 가격 일치: 7/7 검증 통과
- --dry-run, --fresh 옵션 정상 동작
```
### Git 커밋
- `f4a902f` - fix: FormulaEvaluatorService 슬랫/스틸 제품타입별 면적/중량/모터/가이드 수정
---
## 2026-01-29 (수) - 경동기업 견적 로직 Phase 4 완료
### 작업 목표

View File

@@ -419,7 +419,8 @@ public function calculateSteelItems(array $params): array
// 1. 케이스 (단가/1000 × 길이mm × 수량)
$casePrice = $this->priceService->getCasePrice($caseSpec);
if ($casePrice > 0 && $caseLength > 0) {
$totalPrice = ($casePrice / 1000) * $caseLength * $quantity;
// 5130: round($shutter_price * $total_length * 1000) * $su → 단건 반올림 후 × QTY
$perUnitPrice = round(($casePrice / 1000) * $caseLength);
$items[] = [
'category' => 'steel',
'item_name' => '케이스',
@@ -427,14 +428,15 @@ public function calculateSteelItems(array $params): array
'unit' => 'm',
'quantity' => $caseLength / 1000 * $quantity,
'unit_price' => $casePrice,
'total_price' => round($totalPrice),
'total_price' => $perUnitPrice * $quantity,
];
}
// 2. 케이스용 연기차단재 (단가 × 길이m × 수량)
// 2. 케이스용 연기차단재 - 5130: round(단가 × 길이m) × QTY
$caseSmokePrice = $this->priceService->getCaseSmokeBlockPrice();
if ($caseSmokePrice > 0 && $caseLength > 0) {
$lengthM = $caseLength / 1000;
$perUnitSmoke = round($caseSmokePrice * $lengthM);
$items[] = [
'category' => 'steel',
'item_name' => '케이스용 연기차단재',
@@ -442,16 +444,15 @@ public function calculateSteelItems(array $params): array
'unit' => 'm',
'quantity' => $lengthM * $quantity,
'unit_price' => $caseSmokePrice,
'total_price' => round($caseSmokePrice * $lengthM * $quantity),
'total_price' => $perUnitSmoke * $quantity,
];
}
// 3. 케이스 마구리 (단가 × 수량)
// 마구리 규격 = 케이스 규격 각 치수 + 5mm (레거시 updateCol45 공식)
// 3. 케이스 마구리 - 5130: round(단가 × QTY)
$caseCapSpec = $this->convertToCaseCapSpec($caseSpec);
$caseCapPrice = $this->priceService->getCaseCapPrice($caseCapSpec);
if ($caseCapPrice > 0) {
$capQty = $quantity; // 5130: maguriPrices × $su (수량)
$capQty = $quantity;
$items[] = [
'category' => 'steel',
'item_name' => '케이스 마구리',
@@ -467,13 +468,12 @@ public function calculateSteelItems(array $params): array
$guideItems = $this->calculateGuideRails($modelName, $finishingType, $guideType, $guideSpec, $guideLength, $quantity);
$items = array_merge($items, $guideItems);
// 5. 레일용 연기차단재
// 스크린: 단가 × 길이m × 2 × 수량 (좌우)
// 슬랫: 단가 × 길이m × 수량 (×2 없음)
// 5. 레일용 연기차단재 - 5130: round(단가 × 길이m) × multiplier × QTY
$railSmokePrice = $this->priceService->getRailSmokeBlockPrice();
if ($railSmokePrice > 0 && $guideLength > 0) {
$railSmokeMultiplier = ($productType === 'slat') ? 1 : 2;
$railSmokeQty = $railSmokeMultiplier * $quantity;
$perUnitRailSmoke = round($railSmokePrice * $guideLength);
$items[] = [
'category' => 'steel',
'item_name' => '레일용 연기차단재',
@@ -481,7 +481,7 @@ public function calculateSteelItems(array $params): array
'unit' => 'm',
'quantity' => $guideLength * $railSmokeQty,
'unit_price' => $railSmokePrice,
'total_price' => round($railSmokePrice * $guideLength * $railSmokeQty),
'total_price' => $perUnitRailSmoke * $railSmokeQty,
];
}
@@ -604,11 +604,13 @@ private function calculateGuideRails(
$wallSpec = $specs['wall'];
$sideSpec = $specs['side'];
// 5130: round(단가 × 길이m) × QTY (단건 반올림 후 QTY 곱셈)
switch ($guideType) {
case '벽면형':
$price = $this->priceService->getGuideRailPrice($modelName, $finishingType, $wallSpec);
if ($price > 0) {
$guideQty = 2 * $quantity;
$perUnitGuide = round($price * $guideLength);
$items[] = [
'category' => 'steel',
'item_name' => '가이드레일',
@@ -616,7 +618,7 @@ private function calculateGuideRails(
'unit' => 'm',
'quantity' => $guideLength * $guideQty,
'unit_price' => $price,
'total_price' => round($price * $guideLength * $guideQty),
'total_price' => $perUnitGuide * $guideQty,
];
}
break;
@@ -625,6 +627,7 @@ private function calculateGuideRails(
$price = $this->priceService->getGuideRailPrice($modelName, $finishingType, $sideSpec);
if ($price > 0) {
$guideQty = 2 * $quantity;
$perUnitGuide = round($price * $guideLength);
$items[] = [
'category' => 'steel',
'item_name' => '가이드레일',
@@ -632,7 +635,7 @@ private function calculateGuideRails(
'unit' => 'm',
'quantity' => $guideLength * $guideQty,
'unit_price' => $price,
'total_price' => round($price * $guideLength * $guideQty),
'total_price' => $perUnitGuide * $guideQty,
];
}
break;
@@ -642,6 +645,7 @@ private function calculateGuideRails(
$priceSide = $this->priceService->getGuideRailPrice($modelName, $finishingType, $sideSpec);
if ($priceWall > 0) {
$perUnitWall = round($priceWall * $guideLength);
$items[] = [
'category' => 'steel',
'item_name' => '가이드레일',
@@ -649,10 +653,11 @@ private function calculateGuideRails(
'unit' => 'm',
'quantity' => $guideLength * $quantity,
'unit_price' => $priceWall,
'total_price' => round($priceWall * $guideLength * $quantity),
'total_price' => $perUnitWall * $quantity,
];
}
if ($priceSide > 0) {
$perUnitSide = round($priceSide * $guideLength);
$items[] = [
'category' => 'steel',
'item_name' => '가이드레일',
@@ -660,7 +665,7 @@ private function calculateGuideRails(
'unit' => 'm',
'quantity' => $guideLength * $quantity,
'unit_price' => $priceSide,
'total_price' => round($priceSide * $guideLength * $quantity),
'total_price' => $perUnitSide * $quantity,
];
}
break;