Files
sam-docs/plans/archive/formula-engine-real-data-plan.md
권혁성 28b69e5449 docs: archive 37개 + COMPLETED 3개 복원 - 향후 docs/ 정식 문서화 시 참조용
- 완료 문서의 상세 내용은 추후 docs/ 구조화 시 정식 문서에 반영 예정
- HISTORY.md는 요약 인덱스로 유지, 개별 파일은 상세 참조용 보관

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:32:20 +09:00

1078 lines
48 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.

# 수식 엔진 실제 데이터 연동 계획
> **작성일**: 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 스킬로 생성되었습니다.*