# 수식 엔진 실제 데이터 연동 계획

> **작성일**: 2026-02-19
> **목적**: FormulaEvaluatorService의 테스트 데이터(SF-/SM-)를 실제 품목(BD-)으로 재구성
> **기준 문서**: `docs/features/quotes/README.md`, `docs/rules/item-policy.md`
> **상태**: ✅ 완료 (Phase 1-3,5 완료 / Phase 4 후순위 보류)

---

## 📍 현재 진행 상태

| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | 문서 최종 업데이트 및 검증 결과 반영 |
| **다음 작업** | 없음 (Phase 4 Generic 데이터는 후순위 보류) |
| **진행률** | 4/5 완료 (Phase 1-3,5 ✅ / Phase 4 ⏭️ 후순위) |
| **마지막 업데이트** | 2026-02-20 17:00 |

---

## 1. 개요

### 1.1 배경

수식 엔진(FormulaEvaluatorService)에는 두 가지 실행 경로가 있다:
- **Generic 경로**: `quote_formula_*` 4개 테이블 기반 (데이터 드리븐)
- **Kyungdong 경로**: `KyungdongFormulaHandler` 코드 기반 (tenant_id=287 전용)

**현재 문제:**
1. Generic 경로의 `quote_formula_items` (24건)이 모두 삭제된 SF-/SM- 테스트 품목을 참조
2. `quote_formula_ranges` (12건)도 모두 SF- 코드 반환
3. `quote_formula_mappings`는 비어있음
4. Mapping 수식(id:20,21)이 참조하는 product_id 468, 473도 삭제됨
5. Kyungdong 핸들러는 BD- 품목을 참조하지만, EST- 코드 일부가 items 테이블에 미등록
6. 핸들러가 `KyungdongFormulaHandler`로 하드코딩 → 업체 추가 시 확장 불가 구조

### 1.2 두 경로 비교

| 구분 | Generic 경로 | Kyungdong 경로 |
|------|-------------|---------------|
| **진입 조건** | 전용 핸들러 없는 tenant | 전용 핸들러 있는 tenant |
| **BOM 구성** | quote_formula_items + items.bom 전개 | 코드 기반 동적 조립 |
| **모델 인식** | 없음 (단일 수식 세트) | 모델/마감/타입별 분기 |
| **아이템 참조** | SF-/SM- (삭제됨) | BD- 동적 코드 조합 + EST- 코드 |
| **단가 조회** | prices 테이블 + items.attributes | EstimatePriceService |
| **핸들러 해석** | FormulaHandlerFactory → null → Generic | FormulaHandlerFactory → Tenant{id}/FormulaHandler |
| **상태** | ⏭️ FG.bom 비어있음 (후순위) | ✅ 정비 완료 |

### 1.3 실행 흐름 (MNG → API)

#### 현재 (Before)
```
FormulaEvaluatorService::calculateBomWithDebug()
    │
    ├─ if ($tenantId === 287)       ← 하드코딩!
    │   └─ new KyungdongFormulaHandler()   ← 직접 생성!
    │
    └─ else → Generic 10단계
```

#### 목표 (After) - Strategy + Factory, Zero Config
```
[MNG 품목관리 UI]
    │ 사용자가 FG 선택 + W0/H0/QTY/MP 입력
    ▼
ItemManagementApiController::calculateFormula() (mng, 라인 60-86)
    │ $item->code, {W0, H0, QTY, MP}, session('selected_tenant_id')
    ▼
FormulaApiService::calculateBom() (mng, 라인 24-82)
    │ POST https://nginx/api/v1/quotes/calculate/bom
    │ Headers: X-API-KEY, X-TENANT-ID
    ▼
FormulaEvaluatorService::calculateBomWithDebug() (api, 라인 592-596)
    │
    ├─ FormulaHandlerFactory::make($tenantId)
    │   │ class_exists("Handlers\Tenant{$tenantId}\FormulaHandler") ?
    │   │
    │   ├─ 핸들러 존재 → calculateTenantBom($handler, ...)
    │   │   └─ Tenant287/FormulaHandler::calculateDynamicItems()
    │   │       ├─ calculateSteelItems()  → BD- 절곡품 (10종)
    │   │       ├─ calculatePartItems()   → EST- 부자재 (5종)
    │   │       └─ 모터/제어기/주자재/검사비
    │   │
    │   └─ 핸들러 없음 (null) → 10단계 Generic 계산 (라인 613-791)
    │                           └─ quote_formula_* 테이블 (DB 드리븐)
    │
    ▼
[BOM 결과 JSON 반환]
```

#### 핸들러 자동 발견 원리
```
FormulaHandlerFactory::make(287)
    → class_exists("App\Services\Quote\Handlers\Tenant287\FormulaHandler")
    → YES → new Tenant287\FormulaHandler()
    → 인터페이스 TenantFormulaHandler 구현 보장

FormulaHandlerFactory::make(999)
    → class_exists("App\Services\Quote\Handlers\Tenant999\FormulaHandler")
    → NO → return null → Generic DB 경로
```

**업체 추가 시**: `Handlers/Tenant{id}/FormulaHandler.php` 파일 1개만 생성. 설정/매핑 불필요.

### 1.4 기준 원칙

```
┌─────────────────────────────────────────────────────────────────┐
│  🎯 핵심 원칙                                                    │
├─────────────────────────────────────────────────────────────────┤
│  1. 업체별 핸들러 구조화 (Tenant{id} 기반 자동 발견, Zero Config)   │
│  2. 경동(287) 핸들러가 실제 운영 로직 (우선 정비)                  │
│  3. Generic 경로는 핸들러 없는 테넌트용 (DB 드리븐, 후순위)        │
│  4. 품목 마스터에 실제 품목이 모두 등록되어야 함                    │
│  5. 수식 데이터는 실제 품목 코드만 참조                            │
│  6. 기존 테스트 데이터는 삭제하지 않음 (완전 이관 후 별도 삭제)     │
└─────────────────────────────────────────────────────────────────┘
```

### 1.5 변경 승인 정책

| 분류 | 예시 | 승인 |
|------|------|------|
| ✅ 즉시 가능 | items 테이블에 EST- 품목 등록, 핸들러 디렉토리 구조 변경(이동) | 불필요 |
| ⚠️ 컨펌 필요 | 인터페이스/팩토리 신규 생성, FormulaEvaluatorService 분기 로직 변경, quote_formula_* 데이터 추가 | **필수** |
| 🔴 금지 | 테이블 스키마 변경, 핸들러 핵심 계산 로직 변경 | 별도 협의 |

---

## 2. 현황 분석

### 2.1 items 테이블 현황 (tenant_id=287)

| 코드 접두어 | item_type | 건수 | 설명 | 상태 |
|------------|-----------|------|------|------|
| FG- | FG | 18 | 완제품 (7모델 × 타입/마감 조합) | ✅ 정상 |
| BD- | PT | 58 | 절곡물 (모델별 가이드레일/케이스/마구리 등) | ✅ 정상 |
| PT- (레거시) | PT | ~650 | 레거시 부품 (5자리 숫자 코드) | ✅ 정상 |
| RM- | RM | 28 | 원자재 | ✅ 정상 |
| SM- | SM | 61 | 부자재 (레거시) | ✅ 정상 |
| CS- | CS | 4 | 소모품 | ✅ 정상 |
| SF- | - | 0 | 삭제됨 (테스트 데이터) | ❌ 삭제 완료 |
| EST- | PT | 72 | 부자재 (모터/제어기/샤프트/앵글/파이프/원자재 등) | ✅ 등록 완료 |

### 2.2 KyungdongFormulaHandler가 참조하는 미등록 품목

> **중요**: 핸들러는 `EST-` 접두어를 사용 (이전 문서의 `ST-`는 오류)

#### EST- 코드 (items 미등록, 핸들러가 동적 생성)

| 코드 패턴 | 라인 | 메서드 | 용도 | 대안 |
|-----------|------|--------|------|------|
| `EST-SMOKE-케이스용` | 519 | calculateSteelItems | 케이스용 연기차단재 | `BD-케이스용 연기차단재` (id:15587) |
| `EST-SMOKE-레일용` | 557 | calculateSteelItems | 가이드레일용 연기차단재 | `BD-가이드레일용 연기차단재` (id:15572) |
| `EST-SHAFT-{size}인치-{length}` | 795 | calculatePartItems | 감기샤프트 | 신규 등록 |
| `EST-PIPE-1.4-{length}` | 854,868 | calculatePartItems | 앵글파이프 | 신규 등록 |
| `EST-ANGLE-BRACKET-{type}` | 891 | calculatePartItems | 모터받침 앵글 | 신규 등록 |
| `EST-ANGLE-MAIN-{type}-{size}` | 912 | calculatePartItems | 부자재 앵글 | 신규 등록 |
| `EST-INSPECTION` | 1010 | calculateDynamicItems | 검사비 | 신규 등록 |
| `EST-RAW-스크린-{type}` | 1019 | calculateDynamicItems | 스크린 원단 | 신규 등록 |
| `EST-RAW-슬랫-{type}` | 1025 | calculateDynamicItems | 슬랫 원단 | 신규 등록 |
| `EST-MOTOR-{voltage}-{capacity}` | 1044 | calculateDynamicItems | 모터 | 신규 등록 |
| `EST-CTRL-{type}` | 1062 | calculateDynamicItems | 제어기 | 신규 등록 |
| `EST-CTRL-뒷박스` | 1087 | calculateDynamicItems | 뒷박스 제어기 | 신규 등록 |

#### 레거시 숫자 코드 (items 등록됨)

| 코드 | 라인 | items.id | items.name | item_type | unit | 용도 |
|------|------|----------|-----------|-----------|------|------|
| `00035` | 564 | 14939 | 철재용하장바(SUS)3000 | PT | EA | 하장바 SUS |
| `00036` | 564 | 14940 | 철재용하장바(SUS1.2T) | SM | M | 하장바 EGI |
| `00021` | 619 | 14928 | 평철12T | PT | M | 무게평철12T |
| `90201` | 631 | 15188 | KD환봉(30파이) | PT | EA | 환봉 30파이 (기본) |
| `90202` | 628 | 15189 | KD환봉 | PT | EA | 환봉 35파이 |
| `90203` | 629 | 15190 | KD환봉 | PT | EA | 환봉 45파이 |
| `90204` | 630 | 15191 | KD환봉 | PT | EA | 환봉 50파이 |
| `00013` | - | 14922 | 점검구3 | PT | EA | 점검구 (핸들러에서 미사용) |

### 2.3 quote_formula_* 현황

#### quote_formulas (21건, tenant_id=1)

| id | type | variable | name | formula | output_type |
|----|------|----------|------|---------|-------------|
| 1 | input | PC | 제품 카테고리 | (없음) | variable |
| 2 | input | W0 | 오픈사이즈 폭 | (없음) | variable |
| 3 | input | H0 | 오픈사이즈 높이 | (없음) | variable |
| 4 | input | GT | 가이드레일 설치유형 | (없음) | variable |
| 5 | input | MP | 모터 전원 | (없음) | variable |
| 6 | input | CT | 연동제어기 | (없음) | variable |
| 7 | input | QTY | 수량 | (없음) | variable |
| 8 | calculation | W1_SCREEN | 제작폭 W1 (스크린) | W0 + 140 | variable |
| 9 | calculation | W1_STEEL | 제작폭 W1 (철재) | W0 + 110 | variable |
| 10 | calculation | H1 | 제작높이 H1 | H0 + 350 | variable |
| 11 | calculation | W | 제작폭 (W) | IF(PC=="스크린", W0+140, W0+110) | variable |
| 12 | calculation | H | 제작높이 (H) | H0 + 350 | variable |
| 13 | calculation | M | 면적 (M) | W * H / 1000000 | variable |
| 14 | calculation | K_SCREEN | 중량 K (스크린) | M * 2 + W0 / 1000 * 14.17 | variable |
| 15 | calculation | K_STEEL | 중량 K (철재) | M * 25 | variable |
| 16 | calculation | K | 중량 (K) | IF(PC=="스크린", M*2+W0/1000*14.17, M*25) | variable |
| 17 | range | MOTOR | 모터 자동선택 | K | item |
| 18 | range | GUIDE | 가이드레일 자동선택 | H | item |
| 19 | range | CASE | 케이스 자동선택 | W | item |
| 20 | mapping | BOM_SCR_001 | FG-SCR-001 BOM 매핑 | (없음) | item |
| 21 | mapping | BOM_STL_001 | FG-STL-001 BOM 매핑 | (없음) | item |

- id 20: product_id=468 (삭제됨)
- id 21: product_id=473 (삭제됨)

#### quote_formula_items (24건) - 전부 삭제된 코드

| id | formula_id | item_code | item_name | sort |
|----|-----------|-----------|-----------|------|
| 1 | 20 | SF-SCR-F01 | 스크린 원단 | 1 |
| 2 | 20 | SF-SCR-F02 | 가이드레일 (좌) | 2 |
| 3 | 20 | SF-SCR-F03 | 가이드레일 (우) | 3 |
| 4 | 20 | SF-SCR-F04 | 케이스 | 4 |
| 5 | 20 | SF-SCR-F05 | 하부프레임 | 5 |
| 6 | 20 | SF-SCR-M01 | 모터 (소형) | 6 |
| 7 | 20 | SF-SCR-C01 | 제어반 | 7 |
| 8 | 20 | SF-SCR-S01 | 셋팅박스 | 8 |
| 9 | 20 | SF-SCR-SW01 | 권선드럼 | 9 |
| 10 | 20 | SF-SCR-B01 | 브라켓 세트 | 10 |
| 11 | 20 | SF-SCR-SW01 | 스위치 | 11 |
| 12 | 20 | SM-B002 | 볼트 M8x25 | 12 |
| 13 | 20 | SM-N002 | 너트 M8 | 13 |
| 14 | 20 | SM-W002 | 와셔 M8 | 14 |
| 15 | 21 | SF-STL-P01 | 도어 패널 | 1 |
| 16 | 21 | SF-STL-F01 | 문틀 프레임 | 2 |
| 17 | 21 | SF-STL-G01 | 유리창 | 3 |
| 18 | 21 | SF-STL-H01 | 힌지 | 4 |
| 19 | 21 | SF-STL-L01 | 잠금장치 | 5 |
| 20 | 21 | SF-STL-C01 | 도어클로저 | 6 |
| 21 | 21 | SF-STL-S01 | 실링재 | 7 |
| 22 | 21 | SF-STL-PT01 | 파우더 도장 | 8 |
| 23 | 21 | SM-B002 | 볼트 M8x25 | 9 |
| 24 | 21 | SM-N002 | 너트 M8 | 10 |

#### quote_formula_ranges (12건) - 전부 삭제된 코드

| id | formula_id | condition_variable | min | max | result_value |
|----|-----------|-------------------|-----|-----|--------------|
| 1 | 17 (MOTOR) | K | 0 | 30 | SF-SCR-M01 |
| 2 | 17 | K | 30 | 50 | SF-SCR-M02 |
| 3 | 17 | K | 50 | 80 | SF-SCR-M03 |
| 4 | 17 | K | 80 | 9999 | SF-SCR-M04 |
| 5 | 18 (GUIDE) | H | 0 | 2500 | SF-SCR-F02 |
| 6 | 18 | H | 2500 | 3500 | SF-SCR-F02 |
| 7 | 18 | H | 3500 | 4500 | SF-SCR-F02 |
| 8 | 18 | H | 4500 | 9999 | SF-SCR-F02 |
| 9 | 19 (CASE) | W | 0 | 2000 | SF-SCR-F04 |
| 10 | 19 | W | 2000 | 3000 | SF-SCR-F04 |
| 11 | 19 | W | 3000 | 4000 | SF-SCR-F04 |
| 12 | 19 | W | 4000 | 9999 | SF-SCR-F04 |

#### quote_formula_mappings (0건) - 비어있음

### 2.4 FG 모델 매트릭스

| 모델 | 카테고리 | 마감 | 가이드레일 타입 | BD 부품 수 |
|------|---------|------|---------------|-----------|
| KSS01 | 스크린 | SUS | 벽면/측면 | 4 (가이드레일×2, 하단마감재, L-BAR) |
| KSS02 | 스크린 | SUS | 벽면/측면 | 4 |
| KSE01 | 스크린 | SUS+EGI | 벽면/측면 | 8 |
| KWE01 | 스크린 | SUS+EGI | 벽면/측면 | 8 |
| KQTS01 | 철재 | SUS | 벽면/측면 | 3 (가이드레일×2, 하단마감재) |
| KTE01 | 철재 | SUS+EGI | 벽면/측면 | 6 |
| KDSS01 | (FG없음) | SUS | 벽면/측면 | 4 |

### 2.5 가이드레일 규격 매핑 (모델별)

```
KSS01/KSS02/KSE01/KWE01 → 벽면: 120*70, 측면: 120*120
KTE01/KQTS01             → 벽면: 130*75, 측면: 130*125
KDSS01                   → 벽면: 150*150, 측면: 150*212
```

---

## 3. 대상 범위

### Phase 1: 누락 품목 등록 (items 테이블) ✅ 완료

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1.1 | EST-SMOKE 코드 → Phase 3.1로 이관 (핸들러 코드 수정) | ⏭️ | Phase 3에서 처리 |
| 1.2 | EST-MOTOR 품목 등록 (150K~2000K, 전압별) | ✅ | 21건 확인 (220V 8종 + 380V 13종) |
| 1.3 | EST-CTRL 품목 등록 (제어기 종류별) | ✅ | 20건 확인 (기본3 + 방범9 + 방화4 + 기타4) |
| 1.4 | EST-SHAFT 품목 등록 (인치×길이별) | ✅ | 16건 확인 (3~12인치) |
| 1.5 | EST-PIPE 품목 등록 | ✅ | 3건 확인 (1.4T×2 + 2T×1) |
| 1.6 | EST-ANGLE 품목 등록 | ✅ | 8건 확인 (BRACKET 4 + MAIN 4) |
| 1.7 | EST-INSPECTION 품목 등록 | ✅ | 1건 확인 |
| 1.8 | EST-RAW 원자재 품목 등록 | ✅ | 6건 확인 (스크린3 + 슬랫3) |

### Phase 2: 핸들러 구조화 (Strategy + Factory, Zero Config) ✅ 완료

> **설계 원칙**: tenant_id 기반 자동 발견. 설정/매핑/options 없이 클래스 존재 여부만으로 라우팅.

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 2.1 | `TenantFormulaHandler` 인터페이스 생성 | ✅ | `Contracts/TenantFormulaHandler.php` |
| 2.2 | `FormulaHandlerFactory` 생성 (class_exists 자동 발견) | ✅ | `FormulaHandlerFactory.php` (35줄) |
| 2.3 | `KyungdongFormulaHandler` → `Tenant287/FormulaHandler`로 이동 | ✅ | namespace + implements 완료, 원본 삭제 |
| 2.4 | `FormulaEvaluatorService` 분기 로직 변경 | ✅ | KYUNGDONG_TENANT_ID 상수 제거, Factory::make() 사용 |
| 2.5 | `calculateKyungdongBom()` → `calculateTenantBom()` 일반화 | ✅ | 메서드명 + 파라미터(handler) + 문자열 일반화 |

### Phase 3: 핸들러 아이템 코드 정비 (Tenant287/FormulaHandler) ✅ 완료

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 3.1 | EST-SMOKE 코드 → BD- 코드로 변경 | ✅ | BD-케이스용 연기차단재(id:15587), BD-가이드레일용 연기차단재(id:15572) |
| 3.2 | 레거시 숫자 코드(00035, 00036 등) 유지 | ✅ | items 테이블에 등록됨, 변경 불필요 |
| 3.3 | lookupItem 실패 시 Log::warning() 추가 | ✅ | tenant_id, code 포함 경고 로그 |
| 3.4 | tinker E2E 테스트 통과 | ✅ | 17건, 1,167,934원 (KQTS01-SUS-벽면형) |

### Phase 4: Generic 수식 데이터 재구성 (quote_formula_* 테이블) ⏭️ 후순위

> **분석 결과**: Generic 경로는 `items.bom` JSON 필드 기반이나, FG 품목의 bom 필드가 비어있음.
> `quote_formula_*` 테이블은 독립 수식 평가 기능용으로, 메인 BOM 계산 경로에서 직접 사용하지 않음.
> Tenant 287은 핸들러 경로를 사용하므로 현재 실질적 영향 없음. 다른 테넌트 추가 시 진행.

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 4.1 | 실제 FG 제품용 mapping 수식 신규 생성 | ⏭️ | 다른 테넌트 추가 시 |
| 4.2 | quote_formula_items에 실제 BD- 코드 BOM 세트 추가 | ⏭️ | FG.bom 필드 구성 선행 필요 |
| 4.3 | quote_formula_ranges에 실제 BD- 코드 범위 추가 | ⏭️ | |
| 4.4 | quote_formula_mappings 구성 (FG → BD 모델별 매핑) | ⏭️ | |
| 4.5 | FormulaEvaluatorService 모델 인식 로직 추가 | ⏭️ | |

### Phase 5: 통합 테스트 및 검증 ✅ 완료

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 5.1 | 7모델 전수 BOM 계산 테스트 (벽면형) | ✅ | 7모델 전부 PASS (18건씩, 1.1M~1.3M원) |
| 5.1b | 측면형 + 대형 규격 테스트 (3000×3000, QTY=2) | ✅ | 3모델 PASS (18건씩, 2.9M~3.2M원) |
| 5.2 | Factory 엣지 케이스 테스트 | ✅ | tenant 0/-1/999999→null, 287→Handler |
| 5.3 | SF-/SM- 잔여 참조 점검 (코드 기준) | ✅ | api/Services/Quote/ 내 참조 0건 |
| 5.4 | React 견적관리 BOM 테스트 | ⏭️ | Phase 4 후순위와 함께 |

---

## 4. 작업 절차

### 4.1 단계별 절차

```
Phase 1: 누락 품목 등록
├── 1.1 EST-SMOKE → BD- 매핑 (코드만 변경, 품목 신규 등록 불필요)
├── 1.2~1.8 EST- 품목 등록 (items 테이블 INSERT)
│   ├── 코드: EST- 접두어 유지 (핸들러 코드와 일치)
│   ├── item_type: PT, tenant_id: 287
│   └── options: { lot_managed: false, consumption_method: "none" }
└── 등록 후 lookupItem() 호출로 매핑 확인

Phase 2: 핸들러 구조화 (Strategy + Factory, Zero Config)
├── 2.1 TenantFormulaHandler 인터페이스 생성
│   └── Contracts/TenantFormulaHandler.php (신규)
├── 2.2 FormulaHandlerFactory 생성
│   └── class_exists("Handlers\Tenant{$tenantId}\FormulaHandler") 자동 발견
├── 2.3 KyungdongFormulaHandler → Tenant287/FormulaHandler 이동
│   ├── namespace 변경: Handlers → Handlers\Tenant287
│   ├── implements TenantFormulaHandler 추가
│   └── 클래스 docblock에 "경동기업 (tenant_id: 287)" 명시
├── 2.4 FormulaEvaluatorService 분기 로직 변경
│   ├── 제거: private const KYUNGDONG_TENANT_ID = 287
│   ├── 제거: if ($tenantId === self::KYUNGDONG_TENANT_ID)
│   └── 추가: $handler = FormulaHandlerFactory::make($tenantId)
└── 2.5 calculateKyungdongBom() → calculateTenantBom($handler, ...) 일반화

Phase 3: 핸들러(Tenant287) 아이템 코드 정비
├── 3.1 EST-SMOKE 코드 변경 (2곳)
│   ├── 라인 519: 'EST-SMOKE-케이스용' → 'BD-케이스용 연기차단재'
│   └── 라인 557: 'EST-SMOKE-레일용' → 'BD-가이드레일용 연기차단재'
├── 3.2 레거시 코드 검토 (00035, 00036, 00021, 90201~90204)
│   └── 현재 items 테이블에 등록되어 있으므로 동작함. 변경 여부 검토만.
├── 3.3 lookupItem()에 미등록 품목 경고 로깅 추가
│   └── 라인 42-48: null 반환 시 Log::warning()
└── 3.4 MNG 연동 테스트 (https://mng.sam.kr/item-management)

Phase 4: Generic 수식 데이터 재구성 (기존 데이터 유지, 실제 데이터 추가)
├── 4.1 실제 FG 제품용 mapping 수식 신규 생성 (quote_formulas INSERT)
├── 4.2~4.4 실제 데이터 INSERT (기존 테스트 데이터와 병행)
│   ├── quote_formula_items: BD-/EST- 코드 기반 BOM 구성
│   ├── quote_formula_ranges: 실제 규격별 BD- 코드 반환
│   └── quote_formula_mappings: FG 모델 → BD 부품 매핑
└── 4.5 FormulaEvaluatorService에 모델 인식 로직 추가

Phase 5: 통합 테스트
├── 5.1 MNG 품목관리 - 7모델 전수 테스트
├── 5.2 React 견적관리 - BOM 계산 테스트
├── 5.3 단가 정합성 검증
└── 5.4 잔여 테스트 데이터 참조 점검
```

### 4.2 EST- 품목 등록 상세

#### items INSERT 템플릿

```sql
INSERT INTO items (tenant_id, item_type, code, name, unit, is_active, created_at, updated_at)
VALUES (287, 'PT', '{code}', '{name}', '{unit}', 1, NOW(), NOW());
```

#### 등록 대상 품목 목록

```
EST-MOTOR-{voltage}-{capacity}:  모터 (전압-용량)
├── EST-MOTOR-220V-150K     150K 모터 220V
├── EST-MOTOR-220V-300K     300K 모터 220V
├── EST-MOTOR-220V-400K     400K 모터 220V
├── EST-MOTOR-220V-500K     500K 모터 220V
├── EST-MOTOR-220V-600K     600K 모터 220V
├── EST-MOTOR-380V-500K     500K 모터 380V
├── EST-MOTOR-380V-600K     600K 모터 380V
├── EST-MOTOR-380V-800K     800K 모터 380V
├── EST-MOTOR-380V-1000K    1000K 모터 380V
└── item_type: PT, unit: EA

EST-CTRL-{type}:  제어기
├── EST-CTRL-뒷박스         뒷박스 제어기
├── EST-CTRL-일반           일반 제어기
├── EST-CTRL-동보           동보 제어기
├── EST-CTRL-자탈           자탈 제어기
├── EST-CTRL-셋팅           셋팅 박스
└── item_type: PT, unit: EA

EST-SHAFT-{inch}인치-{length}:  감기샤프트
├── EST-SHAFT-3인치-300     3인치 300mm
├── EST-SHAFT-4인치-3000    4인치 3000mm
├── EST-SHAFT-4인치-4500    4인치 4500mm
├── EST-SHAFT-4인치-6000    4인치 6000mm
├── EST-SHAFT-5인치-6000    5인치 6000mm
├── EST-SHAFT-5인치-7000    5인치 7000mm
├── EST-SHAFT-5인치-8200    5인치 8200mm
└── item_type: PT, unit: EA

EST-PIPE-1.4-{length}:  앵글파이프
├── EST-PIPE-1.4-3000     1.4T 3000mm
├── EST-PIPE-1.4-4500     1.4T 4500mm (핸들러에 없지만 패턴상 추가)
├── EST-PIPE-1.4-6000     1.4T 6000mm
└── item_type: PT, unit: EA

EST-ANGLE-BRACKET-{type}:  모터받침 앵글
├── EST-ANGLE-BRACKET-스크린용
├── EST-ANGLE-BRACKET-철제300K
├── EST-ANGLE-BRACKET-철제400K
├── EST-ANGLE-BRACKET-철제500K이상
└── item_type: PT, unit: EA

EST-ANGLE-MAIN-{type}-{size}:  부자재 앵글
├── EST-ANGLE-MAIN-앵글3T-2.5
├── EST-ANGLE-MAIN-앵글3T-10
├── EST-ANGLE-MAIN-앵글4T-2.5
└── item_type: PT, unit: EA

EST-INSPECTION:  검사비
└── item_type: PT, unit: EA

EST-RAW-스크린-{type}:  스크린 원단
├── EST-RAW-스크린-실리카
└── item_type: PT, unit: ㎡

EST-RAW-슬랫-{type}:  슬랫 원단
├── EST-RAW-슬랫-방화
└── item_type: PT, unit: ㎡
```

> **참고**: 핸들러가 동적으로 코드를 조합하므로, 실제 사용되는 코드 조합만 등록.
> 등록 후 `lookupItem()` 호출 시 item_id/name이 정상 반환되는지 확인.

---

## 5. 컨펌 대기 목록

| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|---|------|----------|----------|------|
| 1 | 핸들러 구조화 | 인터페이스 + 팩토리 신규, 핸들러 이동 | Services/Quote/ 전체 | ✅ 완료 |
| 2 | FormulaEvaluatorService 분기 변경 | if(287) → Factory::make() | 전체 테넌트 | ✅ 완료 |
| 3 | EST- 품목 코드 체계 | 72건 이미 등록 확인 | items 테이블 | ✅ 완료 (사전 등록됨) |
| 4 | EST-SMOKE → BD- 코드 변경 | 핸들러 라인 519, 557 변경 | Tenant287/FormulaHandler | ✅ 완료 |
| 5 | 레거시 숫자코드 유지 | 00035, 00036 등 유지 결정 | Tenant287/FormulaHandler | ✅ 유지 (items에 등록됨) |
| 6 | Generic 경로에 모델 인식 추가 | 후순위 보류 (Phase 4) | 핸들러 없는 테넌트 | ⏭️ 후순위 |

---

## 6. 변경 이력

| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|------|------|----------|------|------|
| 2026-02-19 | - | 문서 초안 작성 | - | - |
| 2026-02-19 | - | 자기완결성 보완 (부록 추가) | - | - |
| 2026-02-20 | Phase 1 | EST- 품목 72건 이미 등록 확인 → Phase 1 완료 | items 테이블 | ✅ |
| 2026-02-20 | Phase 2 | TenantFormulaHandler 인터페이스 + FormulaHandlerFactory 생성 | Contracts/TenantFormulaHandler.php, FormulaHandlerFactory.php | ✅ |
| 2026-02-20 | Phase 2 | KyungdongFormulaHandler → Tenant287/FormulaHandler 이동 | Handlers/Tenant287/FormulaHandler.php (신규), Handlers/KyungdongFormulaHandler.php (삭제) | ✅ |
| 2026-02-20 | Phase 2 | FormulaEvaluatorService 분기 로직 변경 (if(287) → Factory::make()) | FormulaEvaluatorService.php | ✅ |
| 2026-02-20 | Phase 2 | calculateKyungdongBom() → calculateTenantBom() 일반화 | FormulaEvaluatorService.php | ✅ |
| 2026-02-20 | Phase 3 | EST-SMOKE-케이스용 → BD-케이스용 연기차단재 (id:15587) | Tenant287/FormulaHandler.php | ✅ |
| 2026-02-20 | Phase 3 | EST-SMOKE-레일용 → BD-가이드레일용 연기차단재 (id:15572) | Tenant287/FormulaHandler.php | ✅ |
| 2026-02-20 | Phase 3 | lookupItem() 미등록 품목 Log::warning() 추가 | Tenant287/FormulaHandler.php | ✅ |
| 2026-02-20 | Phase 4 | Generic 경로 분석 → items.bom 기반, FG.bom 비어있음 → 후순위 결정 | - | ⏭️ |
| 2026-02-20 | Phase 5 | 벽부형 7모델 + 측면형 3모델 tinker 통합 테스트 PASS | - | ✅ |
| 2026-02-20 | Phase 5 | Factory 엣지케이스 + SF-/SM- 잔존 참조 점검 완료 | - | ✅ |
| 2026-02-20 | - | 문서 최종 업데이트 (검증결과, 변경이력, 상태 반영) | formula-engine-real-data-plan.md | ✅ |

---

## 7. 참고 문서

- **견적 시스템**: `docs/features/quotes/README.md`
- **품목 정책**: `docs/rules/item-policy.md`
- **DB 스키마**: `docs/specs/database-schema.md`
- **빠른 시작**: `docs/quickstart/quick-start.md`

---

## 8. 관련 파일 및 코드 위치

### 8.1 API (api/) - 핵심 코드 위치

| 파일 | 메서드 | 라인 | 역할 |
|------|--------|------|------|
| `Services/Quote/FormulaEvaluatorService.php` | `calculateBomWithDebug()` | 592-596 | 메인 엔트리 |
| 같은 파일 | (경동 분기 if문) | 609-611 | **Phase 2에서 Factory로 교체** |
| 같은 파일 | `calculateKyungdongBom()` | 1574-1881 | **Phase 2에서 calculateTenantBom()으로 일반화** |
| 같은 파일 | `KYUNGDONG_TENANT_ID` | 35 | **Phase 2에서 제거** |
| 같은 파일 | `expandBomWithFormulas()` | 1261-1333 | items.bom 재귀 전개 (Generic, 유지) |
| 같은 파일 | `calculateCategoryPrice()` | 812-862 | 카테고리 그룹 기반 단가 (유지) |
| 같은 파일 | `getItemPrice()` | 1066-1097 | 단가 조회 (유지) |
| **신규** `Contracts/TenantFormulaHandler.php` | - | - | **Phase 2에서 생성** |
| **신규** `FormulaHandlerFactory.php` | `make()` | - | **Phase 2에서 생성** |
| `Handlers/KyungdongFormulaHandler.php` | - | - | **→ `Handlers/Tenant287/FormulaHandler.php`로 이동** |
| `Handlers/Tenant287/FormulaHandler.php` | `calculateDynamicItems()` | 963 | **메인 엔트리** (이동 후) |
| 같은 파일 | `calculateSteelItems()` | 448 | 절곡품 10종 계산 |
| 같은 파일 | `calculatePartItems()` | 778 | 부자재 5종 계산 |
| 같은 파일 | `lookupItem()` | 35-49 | 품목 코드 → id/name 조회 (캐싱) |
| 같은 파일 | `withItemMapping()` | 72-87 | 아이템에 item_code/item_id 매핑 |
| 같은 파일 | `getGuideRailSpecs()` | 666-672 | 모델별 가이드레일 규격 매핑 |
| 같은 파일 | `calculateGuideRails()` | 675-730 | 가이드레일 타입별 계산 |
| `Services/Quote/EstimatePriceService.php` | (전체) | - | 단가 조회 서비스 (유지) |
| `Services/FormulaApiService.php` | `calculateBom()` | - | API 서버 호출 래퍼 (유지) |

### 8.2 MNG (mng/)

| 파일 | 메서드 | 라인 | 역할 |
|------|--------|------|------|
| `Controllers/Api/Admin/ItemManagementApiController.php` | `calculateFormula()` | 60-86 | 수식 BOM 계산 API |
| `Services/FormulaApiService.php` | `calculateBom()` | 24-82 | POST /api/v1/quotes/calculate/bom |
| `Services/ItemManagementService.php` | `getBomTree()` | - | BOM 트리 조회 (items.bom) |
| `views/item-management/index.blade.php` | JS `calculateFormula()` | - | 프론트 수식 계산 호출 |

### 8.3 DB 테이블 스키마

#### items 테이블

| 컬럼 | 타입 | NULL | 설명 |
|------|------|------|------|
| id | bigint unsigned | NO | PK |
| tenant_id | bigint unsigned | NO | 테넌트 |
| item_type | varchar(15) | NO | FG/PT/SM/RM/CS |
| code | varchar(100) | NO | 품목 코드 |
| name | varchar(255) | NO | 품목명 |
| unit | varchar(20) | YES | 단위 (EA/M/㎡) |
| category_id | bigint unsigned | YES | 카테고리 FK |
| process_type | varchar(20) | YES | 공정 유형 |
| item_category | varchar(50) | YES | 품목 카테고리 |
| bom | json | YES | BOM JSON (FG는 현재 NULL) |
| attributes | json | YES | 동적 속성 |
| options | json | YES | 관리 옵션 |
| is_active | tinyint(1) | NO | 활성 (기본 1) |

#### quote_formula_items 테이블

| 컬럼 | 타입 | NULL | 설명 |
|------|------|------|------|
| id | bigint unsigned | NO | PK |
| formula_id | bigint unsigned | NO | quote_formulas FK |
| item_code | varchar(50) | NO | 품목 코드 |
| item_name | varchar(200) | NO | 품목명 |
| specification | varchar(100) | YES | 규격 |
| unit | varchar(20) | NO | 단위 |
| quantity_formula | varchar(500) | NO | 수량 수식 |
| unit_price_formula | varchar(500) | YES | 단가 수식 |
| sort_order | int unsigned | NO | 정렬 |

#### quote_formula_ranges 테이블

| 컬럼 | 타입 | NULL | 설명 |
|------|------|------|------|
| id | bigint unsigned | NO | PK |
| formula_id | bigint unsigned | NO | quote_formulas FK |
| min_value | decimal(15,4) | NO | 최소값 |
| max_value | decimal(15,4) | NO | 최대값 |
| condition_variable | varchar(50) | NO | 조건 변수 (K/H/W) |
| result_value | varchar(500) | NO | 결과값 (품목 코드) |
| result_type | enum('fixed','formula') | NO | 결과 유형 |
| sort_order | int unsigned | NO | 정렬 |

#### quote_formula_mappings 테이블

| 컬럼 | 타입 | NULL | 설명 |
|------|------|------|------|
| id | bigint unsigned | NO | PK |
| formula_id | bigint unsigned | NO | quote_formulas FK |
| source_variable | varchar(50) | NO | 원본 변수 |
| source_value | varchar(200) | NO | 원본 값 |
| result_value | varchar(500) | NO | 결과값 |
| result_type | enum('fixed','formula') | NO | 결과 유형 |
| sort_order | int unsigned | NO | 정렬 |

#### quote_formulas 테이블

| 컬럼 | 타입 | NULL | 설명 |
|------|------|------|------|
| id | bigint unsigned | NO | PK |
| tenant_id | bigint unsigned | NO | 테넌트 |
| category_id | bigint unsigned | NO | 카테고리 FK |
| product_id | bigint unsigned | YES | 매핑 대상 제품 FK |
| name | varchar(200) | NO | 수식명 |
| variable | varchar(50) | NO | 변수명 |
| type | enum('input','calculation','range','mapping') | NO | 유형 |
| formula | text | YES | 수식 표현식 |
| output_type | enum('variable','item') | NO | 출력 유형 |
| sort_order | int unsigned | NO | 정렬 |
| is_active | tinyint(1) | NO | 활성 |

---

## 9. 검증 결과

### 9.1 테스트 케이스 (tinker 수동 실행)

#### 벽부형 7모델 (W0=2000, H0=2500, QTY=1)

| 모델 | FG 코드 | BOM 항목수 | 총 금액 | 상태 |
|------|---------|----------|--------|------|
| KQTS01 | FG-KQTS01-벽면형-SUS | 18건 | 1,167,934원 | ✅ |
| KSS01 | FG-KSS01-벽면형-SUS | 18건 | ~1.1M원 | ✅ |
| KSS02 | FG-KSS02-벽면형-SUS | 18건 | ~1.1M원 | ✅ |
| KSE01 | FG-KSE01-벽면형-SUS | 18건 | ~1.2M원 | ✅ |
| KSE01-EGI | FG-KSE01-벽면형-EGI | 18건 | ~1.2M원 | ✅ |
| KWE01 | FG-KWE01-벽면형-SUS | 18건 | ~1.2M원 | ✅ |
| KTE01 | FG-KTE01-벽면형-SUS | 18건 | ~1.3M원 | ✅ |

#### 측면형 + 대형 규격 (W0=4000, H0=5000, QTY=2)

| 모델 | FG 코드 | BOM 항목수 | 총 금액 | 상태 |
|------|---------|----------|--------|------|
| KQTS01 | FG-KQTS01-측면형-SUS | 18건 | ~2.9M원 | ✅ |
| KSE01 | FG-KSE01-측면형-SUS | 18건 | ~3.1M원 | ✅ |
| KTE01-EGI | FG-KTE01-측면형-EGI | 18건 | ~3.2M원 | ✅ |

#### Factory 엣지 케이스

| tenant_id | 예상 | 실제 | 상태 |
|-----------|------|------|------|
| 287 | Tenant287\FormulaHandler 인스턴스 | ✅ 정상 반환 | ✅ |
| 0 | null | null | ✅ |
| -1 | null | null | ✅ |
| 999999 | null | null | ✅ |

#### SF-/SM- 잔존 참조 점검

| 검색 범위 | 패턴 | 결과 | 상태 |
|-----------|------|------|------|
| api/app/Services/Quote/ | SF- / SM- 코드 참조 | 0건 | ✅ |

### 9.2 성공 기준

| 기준 | 달성 | 비고 |
|------|------|------|
| FormulaHandlerFactory::make(287)이 Tenant287 핸들러 반환 | ✅ | 자동 발견 정상 동작 |
| FormulaHandlerFactory::make(999)이 null 반환 → Generic 경로 | ✅ | 미등록 테넌트 정상 |
| tinker에서 FG 선택 시 BOM 계산 성공 | ✅ | 벽부 7모델 + 측면 3모델 전수 PASS |
| BOM 결과의 모든 item_code가 items에 존재 | ✅ | BD- 코드 정상 매핑 (lookupItem null 없음) |
| React 견적관리 BOM 벌크 계산 정상 | ⏭️ | Phase 4 후순위와 함께 |
| SF-/SM- 코드 참조 잔존 없음 | ✅ | api/Services/Quote/ 내 0건 확인 |

---

## 부록 A. FG 품목 전체 목록 (18건)

| id | code | model | guiderail | finishing | major_category | legacy_model_id |
|----|------|-------|-----------|-----------|---------------|-----------------|
| 15515 | FG-KSS01-벽면형-SUS | KSS01 | 벽면형 | SUS마감 | 스크린 | 12 |
| 15516 | FG-KSS01-측면형-SUS | KSS01 | 측면형 | SUS마감 | 스크린 | 13 |
| 15517 | FG-KSE01-벽면형-SUS | KSE01 | 벽면형 | SUS마감 | 스크린 | 14 |
| 15518 | FG-KSE01-벽면형-EGI | KSE01 | 벽면형 | EGI마감 | 스크린 | 15 |
| 15519 | FG-KSE01-측면형-SUS | KSE01 | 측면형 | SUS마감 | 스크린 | 16 |
| 15520 | FG-KSE01-측면형-EGI | KSE01 | 측면형 | EGI마감 | 스크린 | 17 |
| 15521 | FG-KWE01-벽면형-SUS | KWE01 | 벽면형 | SUS마감 | 스크린 | 18 |
| 15522 | FG-KWE01-벽면형-EGI | KWE01 | 벽면형 | EGI마감 | 스크린 | 19 |
| 15523 | FG-KWE01-측면형-SUS | KWE01 | 측면형 | SUS마감 | 스크린 | 20 |
| 15524 | FG-KWE01-측면형-EGI | KWE01 | 측면형 | EGI마감 | 스크린 | 21 |
| 15525 | FG-KQTS01-벽면형-SUS | KQTS01 | 벽면형 | SUS마감 | 철재 | 22 |
| 15526 | FG-KQTS01-측면형-SUS | KQTS01 | 측면형 | SUS마감 | 철재 | 23 |
| 15527 | FG-KTE01-측면형-SUS | KTE01 | 측면형 | SUS마감 | 철재 | 24 |
| 15528 | FG-KTE01-벽면형-SUS | KTE01 | 벽면형 | SUS마감 | 철재 | 25 |
| 15529 | FG-KTE01-측면형-EGI | KTE01 | 측면형 | EGI마감 | 철재 | 26 |
| 15530 | FG-KTE01-벽면형-EGI | KTE01 | 벽면형 | EGI마감 | 철재 | 27 |
| 15531 | FG-KSS02-측면형-SUS | KSS02 | 측면형 | SUS마감 | 스크린 | 28 |
| 15532 | FG-KSS02-벽면형-SUS | KSS02 | 벽면형 | SUS마감 | 스크린 | 29 |

---

## 부록 B. BD- 품목 전체 목록 (58건, 모두 item_type=PT)

### 가이드레일 (17건)

| id | code | name |
|----|------|------|
| 15589 | BD-가이드레일-KDSS01-SUS-150*150 | 가이드레일 KDSS01 SUS 150*150 |
| 15590 | BD-가이드레일-KDSS01-SUS-150*212 | 가이드레일 KDSS01 SUS 150*212 |
| 15592 | BD-가이드레일-KQTS01-SUS-130*125 | 가이드레일 KQTS01 SUS 130*125 |
| 15593 | BD-가이드레일-KQTS01-SUS-130*75 | 가이드레일 KQTS01 SUS 130*75 |
| 15596 | BD-가이드레일-KSE01-SUS-120*120 | 가이드레일 KSE01 SUS 120*120 |
| 15597 | BD-가이드레일-KSE01-SUS-120*70 | 가이드레일 KSE01 SUS 120*70 |
| 15598 | BD-가이드레일-KSE01-EGI-120*120 | 가이드레일 KSE01 EGI 120*120 |
| 15599 | BD-가이드레일-KSE01-EGI-120*70 | 가이드레일 KSE01 EGI 120*70 |
| 15603 | BD-가이드레일-KSS01-SUS-120*120 | 가이드레일 KSS01 SUS 120*120 |
| 15604 | BD-가이드레일-KSS01-SUS-120*70 | 가이드레일 KSS01 SUS 120*70 |
| 15607 | BD-가이드레일-KSS02-SUS-120*120 | 가이드레일 KSS02 SUS 120*120 |
| 15608 | BD-가이드레일-KSS02-SUS-120*70 | 가이드레일 KSS02 SUS 120*70 |
| 15610 | BD-가이드레일-KTE01-SUS-130*125 | 가이드레일 KTE01 SUS 130*125 |
| 15611 | BD-가이드레일-KTE01-SUS-130*75 | 가이드레일 KTE01 SUS 130*75 |
| 15612 | BD-가이드레일-KTE01-EGI-130*125 | 가이드레일 KTE01 EGI 130*125 |
| 15613 | BD-가이드레일-KTE01-EGI-130*75 | 가이드레일 KTE01 EGI 130*75 |
| 15617 | BD-가이드레일-KWE01-SUS-120*120 | 가이드레일 KWE01 SUS 120*120 |
| 15618 | BD-가이드레일-KWE01-SUS-120*70 | 가이드레일 KWE01 SUS 120*70 |
| 15619 | BD-가이드레일-KWE01-EGI-120*120 | 가이드레일 KWE01 EGI 120*120 |
| 15620 | BD-가이드레일-KWE01-EGI-120*70 | 가이드레일 KWE01 EGI 120*70 |

### 하단마감재 (10건)

| id | code | name |
|----|------|------|
| 15591 | BD-하단마감재-KDSS01-SUS-140*78 | 하단마감재 KDSS01 SUS 140*78 |
| 15594 | BD-하단마감재-KQTS01-SUS-60*30 | 하단마감재 KQTS01 SUS 60*30 |
| 15600 | BD-하단마감재-KSE01-SUS-64*43 | 하단마감재 KSE01 SUS 64*43 |
| 15601 | BD-하단마감재-KSE01-EGI-60*40 | 하단마감재 KSE01 EGI 60*40 |
| 15605 | BD-하단마감재-KSS01-SUS-60*40 | 하단마감재 KSS01 SUS 60*40 |
| 15609 | BD-하단마감재-KSS02-SUS-60*40 | 하단마감재 KSS02 SUS 60*40 |
| 15614 | BD-하단마감재-KTE01-SUS-64*34 | 하단마감재 KTE01 SUS 64*34 |
| 15615 | BD-하단마감재-KTE01-EGI-60*30 | 하단마감재 KTE01 EGI 60*30 |
| 15621 | BD-하단마감재-KWE01-SUS-64*43 | 하단마감재 KWE01 SUS 64*43 |
| 15622 | BD-하단마감재-KWE01-EGI-60*40 | 하단마감재 KWE01 EGI 60*40 |

### L-BAR (5건)

| id | code | name |
|----|------|------|
| 15588 | BD-L-BAR-KDSS01-17*100 | L-BAR KDSS01 17*100 |
| 15595 | BD-L-BAR-KSE01-17*60 | L-BAR KSE01 17*60 |
| 15602 | BD-L-BAR-KSS01-17*60 | L-BAR KSS01 17*60 |
| 15606 | BD-L-BAR-KSS02-17*60 | L-BAR KSS02 17*60 |
| 15616 | BD-L-BAR-KWE01-17*60 | L-BAR KWE01 17*60 |

### 케이스 (11건)

| id | code | name |
|----|------|------|
| 15577 | BD-케이스-500*350 | 케이스 500*350 |
| 15578 | BD-케이스-500*380 | 케이스 500*380 |
| 15579 | BD-케이스-600*500 | 케이스 600*500 |
| 15580 | BD-케이스-600*550 | 케이스 600*550 |
| 15581 | BD-케이스-650*500 | 케이스 650*500 |
| 15582 | BD-케이스-650*550 | 케이스 650*550 |
| 15583 | BD-케이스-700*550 | 케이스 700*550 |
| 15584 | BD-케이스-700*600 | 케이스 700*600 |
| 15585 | BD-케이스-780*600 | 케이스 780*600 |
| 15586 | BD-케이스-780*650 | 케이스 780*650 |
| 15587 | BD-케이스용 연기차단재 | 케이스용 연기차단재 |

### 마구리 (10건)

| id | code | name |
|----|------|------|
| 15565 | BD-마구리-505*355 | 마구리 505*355 |
| 15566 | BD-마구리-505*385 | 마구리 505*385 |
| 15567 | BD-마구리-605*555 | 마구리 605*555 |
| 15568 | BD-마구리-655*555 | 마구리 655*555 |
| 15569 | BD-마구리-705*605 | 마구리 705*605 |
| 15570 | BD-마구리-785*685 | 마구리 785*685 |
| 15573 | BD-마구리-655*505 | 마구리 655*505 |
| 15574 | BD-마구리-705*555 | 마구리 705*555 |
| 15575 | BD-마구리-785*605 | 마구리 785*605 |
| 15576 | BD-마구리-785*655 | 마구리 785*655 |

### 기타 (5건)

| id | code | name |
|----|------|------|
| 15571 | BD-보강평철-50 | 보강평철 50 |
| 15572 | BD-가이드레일용 연기차단재 | 가이드레일용 연기차단재 |

---

## 부록 C. 코드 변경 포인트

### C.1 EST-SMOKE → BD- 변경 (Phase 3.1)

**파일**: `api/app/Services/Quote/Handlers/Tenant287/FormulaHandler.php` (이동 후)

```
라인 519: 'EST-SMOKE-케이스용'  →  'BD-케이스용 연기차단재'   (id: 15587)
라인 557: 'EST-SMOKE-레일용'    →  'BD-가이드레일용 연기차단재' (id: 15572)
```

### C.2 레거시 숫자 코드 매핑 (Phase 3.2 검토 대상)

| 라인 | 현재 코드 | items.id | items.name | 비고 |
|------|----------|----------|-----------|------|
| 564 | 00035 | 14939 | 철재용하장바(SUS)3000 | 하장바 SUS |
| 564 | 00036 | 14940 | 철재용하장바(SUS1.2T) | 하장바 EGI (SM타입) |
| 619 | 00021 | 14928 | 평철12T | 무게평철12T |
| 631 | 90201 | 15188 | KD환봉(30파이) | 환봉 기본 |
| 628 | 90202 | 15189 | KD환봉 | 환봉 35파이 |
| 629 | 90203 | 15190 | KD환봉 | 환봉 45파이 |
| 630 | 90204 | 15191 | KD환봉 | 환봉 50파이 |

> 모두 items 테이블에 존재하므로 lookupItem() 정상 동작.
> 변경 여부는 코드 가독성 차원에서 검토 (기능적 문제 없음).

### C.3 lookupItem 로깅 추가 (Phase 3.3)

**파일**: `api/app/Services/Quote/Handlers/Tenant287/FormulaHandler.php`
**위치**: 라인 42-48 `lookupItem()` 메서드

```php
// 변경 전 (라인 46)
$cache[$code] = ['id' => $item?->id, 'name' => $item?->name];

// 변경 후
$cache[$code] = ['id' => $item?->id, 'name' => $item?->name];
if (!$item) {
    \Log::warning("[Tenant287\FormulaHandler] 미등록 품목: {$code}");
}
```

---

## 부록 D. calculateDynamicItems 입력 파라미터

KyungdongFormulaHandler의 메인 엔트리 `calculateDynamicItems()` (라인 963)가 수신하는 파라미터:

```php
$inputs = [
    // 기본 치수
    'W0' => float,           // 폭 (mm)
    'H0' => float,           // 높이 (mm)
    'QTY' => int,            // 수량

    // 제품 정보
    'product_type' => string,    // 'screen' | 'slat' | 'steel'
    'model_name' => string,      // 'KSS01' | 'KSE01' | ...
    'finishing_type' => string,  // 'SUS마감' | 'EGI마감' (→ 내부에서 '마감' 제거)

    // 가이드레일
    'guide_type' => string,      // '벽면형' | '측면형' | '혼합형'

    // 케이스
    'case_spec' => string,       // '500*380' 등

    // 모터/제어기
    'bracket_inch' => string,    // '4' | '5' | '6' | '8'
    'motor_power' => string,     // 'single' | 'three'
    'controller_type' => string, // '일반' | '동보' | '자탈' 등

    // 기타 (선택)
    'weight_plate_qty' => int,
    'round_bar_qty' => int,
    'round_bar_phi' => int,      // 30 | 35 | 45 | 50
];
```

**반환값** (아이템 배열):

```php
[
    [
        'category' => string,       // 'steel' | 'parts' | 'inspection' | 'material' | 'motor' | 'controller'
        'item_name' => string,
        'item_code' => string,      // EST-*, BD-*, 또는 레거시 숫자코드
        'item_id' => int|null,      // items.id (lookupItem 결과)
        'specification' => string,
        'unit' => string,           // 'EA' | 'm' | '㎡'
        'quantity' => float,
        'unit_price' => float,
        'total_price' => float,
    ],
    // ...
]
```

---

## 부록 E. 핸들러 구조화 설계 (Phase 2 상세)

### E.1 디렉토리 구조 (Before → After)

```
Before:
api/app/Services/Quote/
├── FormulaEvaluatorService.php     ← if (287) 하드코딩
├── EstimatePriceService.php
└── Handlers/
    └── KyungdongFormulaHandler.php  ← 독립 클래스, 인터페이스 없음

After:
api/app/Services/Quote/
├── FormulaEvaluatorService.php     ← Factory::make($tenantId) 사용
├── FormulaHandlerFactory.php       ← 신규: 자동 발견 팩토리
├── EstimatePriceService.php
├── Contracts/
│   └── TenantFormulaHandler.php    ← 신규: 인터페이스
└── Handlers/
    └── Tenant287/                  ← 경동기업 (tenant_id: 287)
        └── FormulaHandler.php      ← KyungdongFormulaHandler 이동
    └── Tenant{N}/                  ← 향후 업체 추가 시
        └── FormulaHandler.php
```

### E.2 인터페이스 설계

```php
// api/app/Services/Quote/Contracts/TenantFormulaHandler.php
namespace App\Services\Quote\Contracts;

interface TenantFormulaHandler
{
    /**
     * 동적 BOM 항목 계산 (메인 엔트리)
     */
    public function calculateDynamicItems(array $inputs): array;

    /**
     * 모터 용량 계산
     */
    public function calculateMotorCapacity(string $productType, float $weight, string $bracketInch): string;

    /**
     * 브라켓 사이즈 계산
     */
    public function calculateBracketSize(float $weight, ?string $bracketInch = null): string;
}
```

### E.3 팩토리 설계

```php
// api/app/Services/Quote/FormulaHandlerFactory.php
namespace App\Services\Quote;

use App\Services\Quote\Contracts\TenantFormulaHandler;

class FormulaHandlerFactory
{
    /**
     * tenant_id로 핸들러 자동 발견.
     * Handlers/Tenant{id}/FormulaHandler.php가 존재하면 인스턴스 반환.
     * 없으면 null → Generic DB 경로.
     */
    public static function make(int $tenantId): ?TenantFormulaHandler
    {
        $class = "App\\Services\\Quote\\Handlers\\Tenant{$tenantId}\\FormulaHandler";

        if (!class_exists($class)) {
            return null;
        }

        $handler = new $class();

        if (!$handler instanceof TenantFormulaHandler) {
            throw new \RuntimeException(
                "Tenant{$tenantId} FormulaHandler must implement TenantFormulaHandler"
            );
        }

        return $handler;
    }
}
```

### E.4 핸들러 이동 (Tenant287)

```php
// api/app/Services/Quote/Handlers/Tenant287/FormulaHandler.php
namespace App\Services\Quote\Handlers\Tenant287;

use App\Services\Quote\Contracts\TenantFormulaHandler;
use App\Services\Quote\EstimatePriceService;

/**
 * 경동기업 수식 핸들러 (tenant_id: 287)
 *
 * 방화셔터/스크린/철재 제품의 BOM 동적 계산.
 * KyungdongFormulaHandler에서 이동됨.
 */
class FormulaHandler implements TenantFormulaHandler
{
    private const TENANT_ID = 287;

    // ... 기존 KyungdongFormulaHandler 코드 그대로 유지
}
```

### E.5 FormulaEvaluatorService 변경 포인트

```php
// 변경 전 (라인 35)
private const KYUNGDONG_TENANT_ID = 287;

// 변경 전 (라인 609-611)
if ($tenantId === self::KYUNGDONG_TENANT_ID) {
    return $this->calculateKyungdongBom($finishedGoodsCode, $inputVariables, $tenantId);
}

// ─────────────────────────────────────────

// 변경 후 (라인 35 제거)
// KYUNGDONG_TENANT_ID 상수 제거

// 변경 후 (라인 609-611)
$handler = FormulaHandlerFactory::make($tenantId);
if ($handler) {
    return $this->calculateTenantBom($handler, $finishedGoodsCode, $inputVariables, $tenantId);
}
// else → 기존 Generic 10단계 그대로 실행

// calculateKyungdongBom() → calculateTenantBom() 리네이밍
// $handler 파라미터 추가, 내부의 new KyungdongFormulaHandler() 제거
```

### E.6 향후 업체 추가 절차

```
1. Handlers/Tenant{id}/FormulaHandler.php 파일 1개 생성
2. implements TenantFormulaHandler
3. 끝. (설정 파일, DB 옵션, 매핑 테이블 변경 없음)
```

---

## 10. 자기완결성 점검 결과

### 10.1 체크리스트 검증

| # | 검증 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경 |
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2 |
| 3 | 작업 범위가 구체적인가? | ✅ | 4 Phase + 부록 |
| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 순서 = 의존성 |
| 5 | 참고 파일 경로 + 라인번호가 정확한가? | ✅ | 섹션 8 + 부록 C/E |
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 4.1 + 4.2 (SQL), 부록 E (코드 설계) |
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9 |
| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드/건수/라인번호 |

### 10.2 새 세션 시뮬레이션 테스트

| 질문 | 답변 가능 | 참조 섹션 |
|------|:--------:|----------|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
| Q2. 어디서부터 시작해야 하는가? | ✅ | 3 Phase 1, 4.1 단계별 절차 |
| Q3. 어떤 파일의 몇 번째 줄을 수정해야 하는가? | ✅ | 8.1 코드 위치, 부록 C/E |
| Q4. 어떤 품목을 등록해야 하는가? | ✅ | 4.2 등록 상세, 부록 A/B |
| Q5. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 |
| Q6. 핸들러가 어떤 파라미터를 받는가? | ✅ | 부록 D |
| Q7. DB INSERT 어떻게 하는가? | ✅ | 4.2 SQL 템플릿 |
| Q8. 기존 데이터 건드려도 되는가? | ✅ | 1.4 원칙 6번 (삭제 금지) |
| Q9. 핸들러 구조는 어떻게 만드는가? | ✅ | 부록 E (인터페이스/팩토리/이동 상세) |
| Q10. 향후 업체 추가 시 절차는? | ✅ | 부록 E.6 (파일 1개 생성, 끝) |

**결과**: 10/10 통과 → ✅ 자기완결성 확보

---

*이 문서는 /sc:plan 스킬로 생성되었습니다.*
