Files
sam-docs/plans/kd-quote-logic-plan.md

976 lines
36 KiB
Markdown
Raw Normal View History

# 경동기업 견적 로직 분석 및 구현 계획
> **작성일**: 2026-01-28
> **목적**: 5130 레거시 견적 시스템 분석 → SAM 동적 BOM/견적 로직 구현
> **선행 작업**: [kd-items-migration-plan.md](./kd-items-migration-plan.md) (정적 품목/단가 완료)
> **상태**: 🔄 Phase 0 진행중
---
## 🚀 Quick Start
### 이 문서의 목적
정적 품목 데이터는 이관 완료 (items 651건, prices 651건). 이제 **동적으로 BOM을 계산하고 견적을 산출하는 로직**을 5130에서 분석하여 SAM에 구현.
### 환경 정보
| 항목 | 값 |
|------|-----|
| 레거시 소스 | `5130/` (프로젝트 루트) |
| 대상 테넌트 | 287 (경동기업) |
| 관련 SAM 페이지 | https://dev.sam.kr/sales/quote-management/new |
---
## 📍 현재 진행 상태
| 항목 | 내용 |
|------|------|
| **현재 단계** | Phase 4: SAM 구현 완료 ✅ |
| **다음 작업** | Phase 5: 통합 테스트 및 프론트엔드 연동 |
| **진행률** | 4/5 (100%) - Phase 0~4 완료 |
| **마지막 업데이트** | 2026-01-29 |
### Phase 4.8 테스트 결과 (2026-01-29)
```
📊 테스트 입력값
- W0: 3000mm, H0: 2500mm, QTY: 1
- 철재형, 5인치 브라켓, 매립형 제어기
- KSS01 모델, SUS 마감
📦 계산된 항목 (16개)
1. 주자재(스크린) → 228,750원
2. 모터 400K → 150,000원
3. 제어기 매립형 → 45,000원
4. 케이스 → 45,000원
5. 케이스용 연기차단재 → 10,500원
6. 케이스 마구리 → 10,000원
7. 가이드레일 → 73,200원
8. 레일용 연기차단재 → 15,250원
9. 하장바 → 24,000원
10. L바 → 13,500원
11. 보강평철 → 9,000원
12. 무게평철12T → 24,000원
13. 환봉 → 8,000원
14. 감기샤프트 5인치 → 65,000원
15. 각파이프 → 12,000원
16. 앵글 앵글3T → 18,000원
💰 합계: 751,200원 ✅
```
---
## 1. 배경 및 문제 정의
### 1.1 현재 상황
**SAM 견적 화면에서 FG-KSS01-벽면형-SUS 선택 시:**
- 현재: 3개 항목만 표시 (가이드레일, 하단마감재, L-BAR)
- 기대: 본체, 절곡품, 모터/제어기, 부자재 등 전체 BOM
### 1.2 레거시 DB 구조 (분석 완료)
```
models (모델 마스터)
└─ parts (대분류 부품: 가이드레일, 하단마감재)
└─ parts_sub (세부 절곡품: 1번마감제, 2번본체, 3번-C, 4번-D...)
```
**KSS01 벽면형 예시:**
| 대분류 (parts) | 세부품 (parts_sub) | 재질 | 수량 |
|---------------|-------------------|------|------|
| 가이드레일 | 1번(마감제) | SUS 1.2T | 1 |
| | 2번(본체) | EGI 1.55T | 2 |
| | 3번(벽면형-C) | EGI 1.55T | 1 |
| | 4번(벽면형-D) | EGI 1.55T | 1 |
| 하단마감재 | 1번(하장바) | SUS 1.5T | 1 |
### 1.3 동적 항목 (5130 분석 필요)
| 항목 | 설명 | 레거시 소스 |
|------|------|-------------|
| 모터 | W0, H0 기반 용량 자동 계산 | 5130 로직 분석 필요 |
| 제어기 | 모터 사양에 따라 연동 | 5130 로직 분석 필요 |
| 부자재 | 모델/규격별 자동 추가 | 5130 로직 분석 필요 |
| 절곡품 수량 | 파라미터 기반 동적 계산 | 5130 로직 분석 필요 |
---
## 2. 분석 대상
### 2.1 5130 디렉토리 구조 (분석 완료)
```
5130/
├── estimate/ # 견적 관련 (핵심 분석 대상)
│ ├── README.md # 시스템 문서
│ ├── estimate.php # 스크린 견적 메인 페이지
│ ├── slat.php # 철재 견적 메인 페이지
│ ├── get_screen_amount.php # 스크린 금액 계산 엔진 ⭐
│ ├── get_slat_amount.php # 철재 금액 계산 엔진 ⭐
│ ├── fetch_unitprice.php # 단가 조회 유틸리티 ⭐
│ ├── write_form.php # 견적서 양식 생성
│ └── common/
│ └── calculation.js # 프론트엔드 계산 로직
├── output/ # 출력/리포트
├── dbeditor/ # DB 관리
└── [기타 모듈]/
```
### 2.2 분석 우선순위
| 순위 | 대상 | 목적 |
|------|------|------|
| 1 | 견적 생성 로직 | BOM 자동 구성 방식 파악 |
| 2 | 모터 계산 로직 | W0/H0 → 모터 용량 공식 |
| 3 | 절곡품 계산 로직 | 파라미터 → 수량/단가 공식 |
| 4 | 부자재 추가 로직 | 모델별 자동 추가 규칙 |
| 5 | 가격 산출 로직 | 최종 견적 금액 계산 |
---
## 3. 작업 계획
### Phase 0: 5130 탐색 및 구조 파악 ✅
- [x] 5130/ 디렉토리 구조 분석
- [x] 견적 관련 파일 식별 (estimate/, output/)
- [x] 주요 함수/클래스 목록화 (아래 섹션 4.3 참조)
### Phase 1: 견적 생성 로직 분석 🔄
- [x] 모델 선택 → BOM 구성 흐름 파악
- [x] 동적 항목 추가 조건 분석 (체크박스 기반)
- [x] DB 조회 패턴 파악 (BDmodels, price_* 테이블)
- [ ] 세부 계산 로직 문서화
### Phase 2: 계산 공식 추출 ✅
- [x] 모터 용량 계산 공식 (`calculateMotorSpec` 분석 완료)
- [x] 절곡품 수량/단가 계산 공식 (섹션 4.12 참조)
- [x] 부자재 자동 추가 규칙 (섹션 4.13 참조)
### Phase 3: SAM 설계 ✅
- [x] 기존 견적 시스템 분석 (QuoteCalculationService, FormulaEvaluatorService)
- [x] 5130 로직 통합 설계 → 하이브리드 접근 결정 (섹션 10.1)
- [x] API 엔드포인트 확장 설계 → 기존 엔드포인트 활용
- [x] DB 스키마 변경 필요 여부 → kd_price_tables 신규 테이블 (옵션)
### Phase 4: SAM 구현 🔄
- [x] 4.1 KyungdongFormulaHandler 클래스 생성 (경동 전용) ✅
- [x] 4.2 FormulaEvaluatorService 확장 (tenant 분기) ✅
- [x] 4.3 모터 용량 계산 구현 ✅
- [x] 4.4 kd_price_tables 마이그레이션 + Model 생성 ✅
- [x] 4.5 price_* 테이블 조회 로직 구현 (KdPriceTable 연동) ✅
- [x] 4.6 단가 데이터 마이그레이션 (Seeder) ✅
- [ ] 4.7 절곡품 계산 구현 (10종)
- [ ] 4.8 API 테스트 및 검증
### Phase 5: 검증
- [ ] 레거시 vs SAM 결과 비교
- [ ] 사용자 테스트
- [ ] 배포
---
## 4. 레거시 분석 기록
### 4.1 분석된 테이블
| 테이블 | 용도 | 분석 상태 |
|--------|------|-----------|
| models | 모델 마스터 | ✅ 완료 |
| parts | 대분류 부품 | ✅ 완료 |
| parts_sub | 세부 절곡품 | ✅ 완료 |
| BDmodels | BOM + 단가 JSON | ✅ 완료 |
| price_motor | 모터 단가 | ✅ 완료 |
| price_shaft | 샤프트 계산 참조 | ✅ 완료 |
| price_pipe | 파이프 계산 참조 | ✅ 완료 |
| price_raw_materials | 원자재 단가 | ✅ 완료 |
### 4.2 분석된 5130 코드
| 파일/모듈 | 내용 | 분석 상태 |
|-----------|------|-----------|
| estimate/README.md | 시스템 문서 | ✅ 완료 |
| estimate/estimate.php | 스크린 견적 메인 | ✅ 완료 |
| estimate/get_screen_amount.php | 스크린 금액 계산 엔진 | ✅ 완료 |
| estimate/get_slat_amount.php | 철재 금액 계산 엔진 | ✅ 완료 |
| estimate/fetch_unitprice.php | 단가 조회 유틸리티 | ✅ 완료 |
| estimate/common/calculation.js | 프론트엔드 계산 | ✅ 완료 |
### 4.3 핵심 함수 목록
#### 금액 계산 함수
| 함수명 | 파일 | 역할 |
|--------|------|------|
| `calculateScreenAmount()` | get_screen_amount.php | 스크린 견적 총액 계산 |
| `calculateSlatAmount()` | get_slat_amount.php | 철재 견적 총액 계산 |
| `calculateGuideRailPrice()` | get_screen_amount.php | 가이드레일 단가 계산 |
| `calculateShaftPrice()` | get_screen_amount.php | 감기샤프트 단가 계산 |
#### 단가 조회 함수 (fetch_unitprice.php)
| 함수명 | 역할 | 참조 테이블 |
|--------|------|-------------|
| `searchBracketSize()` | 모터 중량 → 브라켓 크기 | - |
| `calculateMotorSpec()` | 중량/인치 → 모터 용량 (150K~1000K) | - |
| `getPriceForMotor()` | 모터 용량 → 단가 조회 | price_motor |
| `calculateControllerSpec()` | 제어기 타입 → 단가 조회 | price_motor |
| `calculatePipe()` | 파이프 규격 → 단가 조회 | price_pipe |
| `calculateShaft()` | 샤프트 규격 → 단가 조회 | price_shaft |
| `calculateAngle()` | 앵글 규격 → 단가 조회 | price_angle |
| `slatPrice()` | 원자재 → 단가 조회 | price_raw_materials |
### 4.4 모터 용량 계산 공식 (추출 완료)
```
모터 용량 = f(제품타입, 중량, 브라켓인치)
┌──────────┬─────────┬──────────────────────────────────┐
│ 제품타입 │ 인치 │ 중량 범위 → 용량 │
├──────────┼─────────┼──────────────────────────────────┤
│ 스크린 │ 4" │ ≤150kg → 150K │
│ │ │ 150~300kg → 300K │
│ │ │ 300~400kg → 400K │
├──────────┼─────────┼──────────────────────────────────┤
│ 스크린 │ 5" │ ≤123kg → 150K │
│ │ │ 123~246kg → 300K │
│ │ │ 246~327kg → 400K │
│ │ │ 327~500kg → 500K │
│ │ │ 500~600kg → 600K │
├──────────┼─────────┼──────────────────────────────────┤
│ 스크린 │ 6" │ ≤104kg → 150K │
│ │ │ 104~208kg → 300K │
│ │ │ 208~300kg → 400K │
│ │ │ 300~424kg → 500K │
│ │ │ 424~508kg → 600K │
├──────────┼─────────┼──────────────────────────────────┤
│ 철재 │ 4" │ ≤300kg → 300K │
│ │ │ 300~400kg → 400K │
├──────────┼─────────┼──────────────────────────────────┤
│ 철재 │ 5" │ ≤246kg → 300K │
│ │ │ 246~327kg → 400K │
│ │ │ 327~500kg → 500K │
│ │ │ 500~600kg → 600K │
├──────────┼─────────┼──────────────────────────────────┤
│ 철재 │ 6" │ ≤208kg → 300K │
│ │ │ 208~277kg → 400K │
│ │ │ 277~424kg → 500K │
│ │ │ 424~508kg → 600K │
│ │ │ 508~800kg → 800K │
│ │ │ 800~1000kg → 1000K │
├──────────┼─────────┼──────────────────────────────────┤
│ 철재 │ 8" │ ≤324kg → 500K │
│ │ │ 324~388kg → 600K │
│ │ │ 388~611kg → 800K │
│ │ │ 611~1000kg → 1000K │
└──────────┴─────────┴──────────────────────────────────┘
```
### 4.5 견적 항목 구성 (스크린)
체크박스 옵션에 따라 동적으로 항목 포함/제외:
| 체크박스 | 포함 항목 |
|---------|----------|
| `slatcheck` (주자재) | 주자재(스크린), 환봉 |
| `steel` (절곡) | 케이스, 가이드레일, 하장바, L바, 보강평철, 연기차단재(케이스/레일), 케이스 마구리 |
| `motor` (모터) | 모터 (경동견적가포함일 때만) |
| `partscheck` (부자재) | 감기샤프트, 무게평철12T, 각파이프, 앵글 |
| `warranty` (보증) | (금액 조정에 영향) |
### 4.6 가격 산출 흐름
```
1. 검사비 (고정)
2. 주자재 = 원자재단가 × 면적(W×H/1000000)
3. 모터 = 용량별 단가표 조회
4. 제어기 = 매립/노출/뒷박스 × 수량
5. 케이스 = 규격별 단가 × 길이(m)
6. 가이드레일 = 모델|마감재|규격별 단가 × 길이(m) × 2
7. 하장바/L바 = 단가 × 길이(m)
8. 샤프트 = 규격별(3",4",5") × 길이별(3000~8200) 단가표
9. 파이프 = 두께(1.4) × 길이(3000/6000) 단가표
10. 앵글 = 타입(3T/4T) × 두께(2.5) × 수량
```
---
## 5. 기술적 고려사항
### 5.1 SAM 아키텍처 준수
```php
// Service-First 패턴
class QuoteBomService extends Service
{
public function calculateDynamicBom(int $modelId, array $parameters): array
{
// 1. 정적 BOM 조회 (items.bom)
// 2. 파라미터 기반 동적 항목 계산
// 3. 모터/제어기 자동 추가
// 4. 부자재 자동 추가
// 5. 단가 계산
}
}
```
### 5.2 API 설계 (예상)
```
POST /api/v1/quotes/calculate-bom
Request:
{
"model_id": 13147, // FG-KSS01-벽면형-SUS
"parameters": {
"W0": 3000, // 폭
"H0": 2000, // 높이
"installation_type": "벽면형",
"power_source": "220V"
}
}
Response:
{
"static_bom": [...], // 기존 items.bom
"dynamic_items": [...], // 모터, 제어기, 부자재
"calculated_values": {
"motor_capacity": "150K",
"total_area": 6.0,
"estimated_weight": 45.5
},
"pricing": {...}
}
```
---
## 6. 관련 문서
- [kd-items-migration-plan.md](./kd-items-migration-plan.md) - 정적 품목/단가 이관 (완료)
- [kd-orders-migration-plan.md](./kd-orders-migration-plan.md) - 입고/재고/주문 이관
- SAM API Rules - api/CLAUDE.md
---
## 7. 변경 이력
| 날짜 | 항목 | 변경 내용 |
|------|------|----------|
| 2026-01-28 | 문서 생성 | 초기 계획 수립, 레거시 DB 분석 결과 반영 |
| 2026-01-28 | Phase 0 완료 | 5130 estimate 디렉토리 분석 완료, 핵심 함수 목록화 |
| 2026-01-28 | Phase 1-2 진행 | 모터 용량 계산 공식 추출, 가격 산출 흐름 문서화 |
| 2026-01-28 | Phase 2 계속 | 브라켓 크기 공식, BDmodels 구조, SAM 매핑 전략 추가 |
| 2026-01-28 | Phase 3 시작 | 기존 SAM 견적 시스템 분석, 5130 통합 설계 문서화 |
| 2026-01-29 | 설계 결정 | 체크박스 방식 → "전체계산 → 개별제거" 방식으로 변경 |
| 2026-01-29 | Phase 2 완료 | 절곡품/부자재/주자재 계산 공식 추출 완료 (4.12~4.15) |
| 2026-01-29 | Phase 4 설계 | 하이브리드 접근 결정 (범용 + 경동전용 Handler), 구현 계획 수립 |
| 2026-01-29 | Phase 4.1 완료 | KyungdongFormulaHandler 기본 구조 생성 (모터/브라켓 계산) |
| 2026-01-29 | Phase 4.2 완료 | FormulaEvaluatorService 확장 (tenant_id=287 분기 처리) |
| 2026-01-29 | Phase 4.3~4.5 완료 | kd_price_tables 마이그레이션, KdPriceTable 모델, Seeder, 단가 조회 연동 |
| 2026-01-29 | Phase 4.6 완료 | 부자재 계산 (3종: 샤프트, 파이프, 앵글) 구현 |
---
### 4.7 브라켓 크기 결정 공식 (추출 완료)
```
searchBracketSize(중량, 인치) → 브라켓크기
┌──────────┬──────────────────────────────────┐
│ 모터용량 │ 브라켓 사이즈 │
├──────────┼──────────────────────────────────┤
│ 300K │ 530*320 │
│ 400K │ 530*320 │
│ 500K │ 600*350 │
│ 600K │ 600*350 │
│ 800K │ 690*390 │
│ 1000K │ 690*390 │
└──────────┴──────────────────────────────────┘
[중량만으로 판단 (인치 없을 때)]
- ≤300kg → 300K
- ≤400kg → 400K
- ≤500kg → 500K
- ≤600kg → 600K
- ≤800kg → 800K
- ≤1000kg → 1000K
```
### 4.8 견적 입력 컬럼 매핑 (스크린)
| 컬럼 | 필드명 | 설명 |
|------|--------|------|
| col4 | 모델코드 | KSS01, KWS01 등 |
| col5 | 제목 | 현장명 |
| col6 | 가이드레일타입 | 벽면형, 측면형, 혼합형 |
| col7 | 마감재질 | SUS, EGI 등 |
| col10 | 폭(W) | mm 단위 |
| col11 | 높이(H) | mm 단위 |
| col14 | 수량 | 대수 |
| col15~17 | 제어기 | 매립형/노출형/뒷박스 수량 |
| col18_brand | 모터업체 | 경동(견적가포함) 등 |
| col19 | 모터용량 | 150K~1000K |
| col22 | 앵글사이즈 | 모터받침용 |
| col23 | 가이드레일길이 | mm 단위 |
| col31~35 | 연기차단재 | 각 규격별 수량 |
| col36 | 케이스규격 | 또는 col36_custom |
| col37 | 케이스길이 | mm 단위 |
| col45 | 마구리규격 | |
| col48 | 하장바길이 | mm 단위 |
| col49~50 | 하장바 | 수량 관련 |
| col51 | L바길이 | mm 단위 |
| col52~53 | L바 | 수량 관련 |
| col54 | 보강평철길이 | mm 단위 |
| col55~56 | 보강평철 | 수량 관련 |
| col57 | 무게평철 | 수량 |
| col59~65 | 샤프트 | 규격별(3"/4"/5") × 길이별 |
| col68~69 | 각파이프 | 3000/6000 수량 |
| col70 | 환봉 | 수량 |
| col71 | 앵글 | 수량 |
### 4.9 단가 테이블 JSON 구조
**price_shaft (샤프트)**
- col4: 사이즈 (3, 4, 5인치)
- col10: 길이 (m 단위, 예: 3.0 = 3000mm)
- col19: 판매가
**price_pipe (각파이프)**
- col2: 길이 (3000, 6000)
- col4: 두께 (1.4)
- col8: 판매가
**price_angle (앵글)**
- col2: 타입 (스크린용, 철재용)
- col3: 브라켓크기 (530*320, 600*350, 690*390)
- col4: 앵글타입 (앵글3T, 앵글4T)
- col10: 두께 (2.5)
- col19: 판매가
**price_motor (모터/제어기)**
- col2: 용량/타입 (150K, 300K, 매립형, 노출형, 뒷박스)
- col13: 판매가
**price_raw_materials (원자재)**
- col2: 원자재명 (스크린, 슬랫, 조인트바 등)
- col13: 판매가
### 4.10 BDmodels 테이블 구조 (절곡품 단가)
**컬럼 구조:**
| 컬럼 | 설명 | 예시 |
|------|------|------|
| model_name | 모델코드 | KSS01, KWS01, KDSS01 |
| seconditem | 부품분류 | 케이스, 가이드레일, 하단마감재, L-BAR |
| spec | 규격 | 120*70, 650*550 |
| finishing_type | 마감재질 | SUS, EGI |
| unitprice | 단가 | 원/m 또는 원/개 |
**seconditem 종류:**
- 케이스: 케이스박스 (규격별 단가)
- 가이드레일: 레일 (모델+마감+규격별 단가)
- 하단마감재: 하장바 (모델+마감별 단가)
- L-BAR: L바 (모델별 단가)
- 보강평철: 평철 (공통 단가)
- 마구리: 케이스 마감재 (규격별 단가)
- 케이스용 연기차단재: (공통 단가)
- 가이드레일용 연기차단재: (공통 단가)
**단가 조회 키 패턴:**
```php
// 가이드레일: 모델코드|마감재질|규격
$key = "KSS01|SUS|120*70";
$price = $guidrailPrices[$key];
// 케이스: 규격만
$price = $shutterBoxprices["650*550"];
// 하단마감재: 모델코드 + 마감재질 매칭
if ($prodcode == $modelCode && $finishing == $load_finishingType) {
$price = $bottomBarPrices;
}
```
### 4.11 SAM 매핑 전략
**현재 SAM items 테이블의 BOM 구조:**
```json
// items.bom (JSON)
[
{"child_item_id": 123, "quantity": 1}, // 가이드레일
{"child_item_id": 456, "quantity": 1}, // 하단마감재
{"child_item_id": 789, "quantity": 1} // L-BAR
]
```
**문제점:**
- 정적 BOM만 저장 (동적 계산 불가)
- 모터/제어기/부자재 누락
- 파라미터(W, H, 수량) 기반 수량 계산 없음
**해결 방안 (Phase 3 설계 시 반영):**
```php
// 1. 정적 BOM + 동적 계산 분리
class QuoteBomService {
public function calculate(int $modelId, array $params): array
{
// 1. 정적 BOM 조회 (items.bom)
$staticBom = $this->getStaticBom($modelId);
// 2. 동적 항목 계산
$dynamicItems = $this->calculateDynamicItems($params);
// 3. 단가 적용
return $this->applyPricing($staticBom, $dynamicItems, $params);
}
private function calculateDynamicItems(array $params): array
{
$items = [];
// 모터 (체크박스 옵션)
if ($params['motor_check']) {
$motorCapacity = $this->calculateMotorCapacity(
$params['weight'],
$params['bracket_inch']
);
$items['motor'] = $this->getMotorPrice($motorCapacity);
}
// 제어기
$items['controller'] = $this->calculateController($params);
// 샤프트/파이프/앵글 (부자재)
if ($params['parts_check']) {
$items['shaft'] = $this->calculateShaft($params);
$items['pipe'] = $this->calculatePipe($params);
$items['angle'] = $this->calculateAngle($params);
}
return $items;
}
}
```
### 4.12 절곡품 계산 공식 (steel 체크박스)
**절곡품 = BDmodels 테이블 조회 (seconditem별 단가)**
| 품목 | 조회 키 | 계산식 | 비고 |
|------|--------|--------|------|
| 케이스 | `seconditem='케이스', spec=규격` | 단가/1000 × 길이(mm) × 수량 | 기본단가 500*380 기준 면적비 계산 |
| 케이스용 연기차단재 | `seconditem='케이스용 연기차단재'` | 단가 × 길이(m) × 수량 | |
| 케이스 마구리 | `seconditem='마구리', spec=규격` | 단가 × 수량 | col45 규격 |
| 가이드레일 | `model_name\|finishing_type\|spec` | 단가 × 길이(m) × 수량 | 벽면/측면 ×2, 혼합 각1 |
| 레일용 연기차단재 | `seconditem='가이드레일용 연기차단재'` | 단가 × 길이(m) × 2 × 수량 | |
| 하장바 | `model_name, seconditem='하단마감재', finishing_type` | 단가 × 길이(m) × 수량 | col48 길이 |
| L바 | `model_name, seconditem='L-BAR'` | 단가 × 길이(m) × 수량 | col51 길이 |
| 보강평철 | `seconditem='보강평철'` | 단가 × 길이(m) × 수량 | col54 길이 |
| 무게평철12T | 고정 12,000원 | 12,000 × col57(수량) | |
| 환봉 | 고정 2,000원 | 2,000 × col70(수량) | |
**가이드레일 타입별 처리:**
```
벽면형(120*70) → baseKey|120*70 × 2개
측면형(120*100) → baseKey|120*100 × 2개
혼합형(120*70+120*100) → baseKey|120*70 + baseKey|120*100 (각 1개)
```
### 4.13 부자재 계산 공식 (partscheck 체크박스)
**부자재 = price_* 테이블 조회 (JSON itemList)**
| 품목 | 테이블 | 조회 조건 | 계산식 |
|------|--------|----------|--------|
| 감기샤프트 | price_shaft | col4=사이즈, col10=길이(m) | col19(판매가) × 수량 |
| 각파이프 | price_pipe | col4=두께(1.4), col2=길이 | col8(판매가) × 수량 |
| 앵글 | price_angle | col2='앵글3T', col10=두께(2.5) | col19(판매가) × 수량 |
**샤프트 규격별 컬럼 매핑:**
```
col59 → 3" × 300mm (사실상 미사용)
col60 → 4" × 3000mm
col61 → 4" × 4500mm
col62 → 4" × 6000mm
col63 → 5" × 6000mm
col64 → 5" × 7000mm
col65 → 5" × 8200mm
```
**각파이프 컬럼 매핑:**
```
col68 → 1.4T × 3000mm 수량
col69 → 1.4T × 6000mm 수량
```
### 4.14 모터 받침용 앵글 (특수 조건)
```
조건: col22(앵글사이즈) 값이 있고,
(slatcheck만 체크 AND motor/steel/partscheck 모두 미체크) 가 아닐 때
계산: calculateAngle(수량, itemList, '스크린용') × 수량 × 4
```
### 4.15 주자재 계산 공식 (slatcheck 체크박스)
```
스크린 가격 = 원자재단가 × 면적(㎡)
면적 = W × (H + 550) / 1,000,000
↑ 550mm는 스크린 기본 여유분 (350 + 200 추가)
원자재단가: price_raw_materials 테이블에서 col2='실리카' 조회 → col13(판매가)
```
---
## 8. 다음 작업 (TODO)
### 즉시 필요
1. ~~**브라켓 크기 결정 공식**~~ ✅ 완료
2. ~~**BDmodels 조회 패턴**~~ ✅ 완료
3. **절곡품 수량 계산 공식** - parts_sub 기반 동적 수량 결정 로직 (선택적)
### SAM 구현 시 고려사항
1. **전체 계산 → 개별 제거 방식**: 5130의 체크박스 방식 대신, 전체 BOM 계산 후 불필요 항목 제거
2. **단가 테이블 통합**: price_motor, price_shaft, price_pipe 등 → SAM prices 테이블과 연동
3. **BOM 동적 생성 API**: 파라미터(W0, H0) 입력 → 전체 견적 항목 반환 → UI에서 개별 제거
---
## 9. Phase 3: SAM 설계
### 9.1 기존 SAM 견적 시스템 분석
#### 현재 구조
```
api/app/Services/Quote/
├── QuoteCalculationService.php # 견적 계산 메인 서비스
├── FormulaEvaluatorService.php # 수식 평가 엔진
├── QuoteService.php # 견적 CRUD
└── Requests/
└── QuoteBomCalculateRequest.php # BOM 계산 입력값 검증
```
#### QuoteCalculationService 핵심 메서드
| 메서드 | 역할 | 입력 | 출력 |
|--------|------|------|------|
| `calculate()` | 일반 견적 계산 | inputs, productCategory, productId | items, costs, errors |
| `calculateBom()` | BOM 기반 견적 | finishedGoodsCode, inputs, debug | finished_goods, items, grand_total |
| `calculateBomBulk()` | 다건 BOM 계산 | inputItems[], debug | summary, items[] |
| `preview()` | 견적 미리보기 | inputs, productCategory, productId | (calculate와 동일) |
| `recalculate()` | 기존 견적 재계산 | Quote | (calculate와 동일) |
#### FormulaEvaluatorService 10단계 BOM 계산
```
Step 1: 입력값 수집 (W0, H0, QTY, PC, GT, MP, CT, WS, INSP)
Step 2: 완제품 선택 (finishedGoodsCode → Item 조회)
Step 3: 변수 계산 (수식 기반 중간값)
Step 4: BOM 전개 (items.bom JSON → 구성품 목록)
Step 5: 단가 출처 결정 (prices 테이블 or 수식 계산)
Step 6: 수량 수식 평가 (formula → 실제 수량)
Step 7: 단가 계산 (unit_price × quantity)
Step 8: 공정별 그룹화 (category 기준)
Step 9: 소계 계산 (그룹별 합계)
Step 10: 최종 합계 (grand_total)
```
#### 현재 입력 파라미터 (QuoteBomCalculateRequest)
| 파라미터 | 설명 | 필수 |
|---------|------|------|
| W0 | 개구부 폭(mm) | ✅ |
| H0 | 개구부 높이(mm) | ✅ |
| QTY | 수량 | ✅ |
| PC | 제품코드 | ✅ |
| GT | 가이드타입 | ❌ |
| MP | 모터파워 | ❌ |
| CT | 제어타입 | ❌ |
| WS | 와이어사이드 | ❌ |
| INSP | 검사비 | ❌ |
### 9.2 5130 로직 통합 설계
#### 5130 vs SAM 비교
| 항목 | 5130 | SAM (현재) | SAM (목표) |
|------|------|-----------|-----------|
| 모터 계산 | `calculateMotorSpec()` | 없음 (수동 입력) | 자동 계산 |
| 브라켓 크기 | `searchBracketSize()` | 없음 | 자동 계산 |
| 항목 선택 | 체크박스 (사전 선택) | 없음 | **전체계산 → 개별제거** |
| 절곡품 단가 | BDmodels 테이블 | prices 테이블 | prices + 범위 조회 |
| 부자재 | 파라미터 기반 동적 추가 | 정적 BOM | 동적 계산 |
#### 항목 선택 방식 변경 (중요)
**5130 방식 (체크박스):**
```
☑ 주자재 ☑ 절곡 ☐ 모터 ☑ 부자재 → 계산
```
**SAM 방식 (전체계산 → 개별제거):**
```
전체 BOM 계산 → 견적 라인 표시 → 불필요 항목 제거/수량 조정
┌─────────────────────────────────────────────────┐
│ 품목명 │ 수량 │ 단가 │ 금액 │ ⊘ │
├─────────────────────────────────────────────────┤
│ 스크린원단 │ 6㎡ │ 15,000 │ 90,000 │ │
│ 가이드레일 │ 4m │ 12,000 │ 48,000 │ │
│ 모터 300K │ 1 │ 85,000 │ 85,000 │ ✕ │ ← 제거 가능
│ 제어기 매립형 │ 1 │ 25,000 │ 25,000 │ ✕ │ ← 제거 가능
│ 감기샤프트 4" │ 1 │ 35,000 │ 35,000 │ │
└─────────────────────────────────────────────────┘
```
**장점:**
- 더 직관적인 UX (전체를 보고 판단)
- 개별 품목 단위 제어 가능 (카테고리 단위보다 유연)
- SAM 기존 견적 라인 구조와 호환
#### 확장 필요 항목
**1. 입력 파라미터 추가**
```php
// QuoteBomCalculateRequest 확장
'bracket_inch' => 'nullable|string|in:4,5,6,8', // 브라켓 인치
'estimated_weight' => 'nullable|numeric', // 예상 중량
'guide_rail_type' => 'nullable|string', // 가이드레일 타입
'finishing_type' => 'nullable|string', // 마감재질
// 체크박스 옵션은 제거 (전체 계산 후 개별 제거 방식)
```
**2. FormulaEvaluatorService 확장**
```php
// 5130 계산 함수 추가
private function calculateMotorCapacity(string $productType, float $weight, string $bracketInch): string
{
// 4.4 모터 용량 계산 공식 구현
}
private function calculateBracketSize(float $weight, ?string $bracketInch = null): string
{
// 4.7 브라켓 크기 결정 공식 구현
}
private function calculateDynamicItems(array $params): array
{
// 체크박스 옵션에 따른 동적 항목 생성
}
```
**3. 단가 조회 확장**
```php
// 범위 기반 단가 조회 (price_shaft, price_pipe 등)
private function getPriceByRange(string $priceTable, array $conditions): ?float
{
// JSON 데이터에서 범위 조건으로 단가 조회
}
```
### 9.3 API 엔드포인트 설계
#### 기존 엔드포인트 (유지)
```
POST /api/v1/quotes/calculate-bom
POST /api/v1/quotes/calculate-bom-bulk
GET /api/v1/quotes/input-schema
```
#### 신규 엔드포인트 (추가)
```
POST /api/v1/quotes/calculate-motor
- 입력: weight, bracket_inch, product_type
- 출력: motor_capacity, bracket_size
POST /api/v1/quotes/calculate-dynamic-items
- 입력: model_id, W0, H0, options (체크박스)
- 출력: dynamic_items[] (모터, 제어기, 부자재)
GET /api/v1/quotes/price-tables/{table}
- 테이블: motor, shaft, pipe, angle, raw_materials
- 출력: 단가표 데이터 (프론트엔드 참조용)
```
### 9.4 DB 스키마 변경 (최소화)
**변경 불필요:**
- items, prices 테이블: 기존 구조 활용
- quote_formulas: 기존 수식 시스템 활용
**검토 필요:**
- `items.metadata` JSON 필드에 5130 특수 정보 저장 가능
- `quote_formula_ranges`: 범위 기반 단가 조회에 활용
**대안:**
- 5130 price_* 테이블 데이터를 `quote_formula_ranges`로 마이그레이션
- 또는 별도 `kd_price_tables` 테이블 생성 (tenant_id=287 전용)
### 9.5 구현 우선순위
| 순위 | 항목 | 난이도 | 의존성 |
|------|------|--------|--------|
| 1 | 모터 용량 계산 함수 | 낮음 | 없음 |
| 2 | 브라켓 크기 계산 함수 | 낮음 | 없음 |
| 3 | 체크박스 옵션 → 동적 항목 | 중간 | 1, 2 |
| 4 | 범위 기반 단가 조회 | 중간 | 없음 |
| 5 | API 엔드포인트 추가 | 낮음 | 1-4 |
| 6 | 프론트엔드 연동 | 중간 | 5 |
---
## 10. Phase 4: 구현 상세 계획
### 10.1 아키텍처 결정: 하이브리드 접근
**배경:**
- 5130 경동 로직은 3차원 조건, 외부 테이블 조회 등 복잡
- 현재 SAM quote_formulas 시스템으로 표현 불가
- 범용으로 만들면 다른 테넌트에 불필요한 복잡성
**결정:**
```
[범용 레이어] - quote_formulas 테이블
├── 단순 계산, 1차원 범위, 단순 매핑
└── 기본 테넌트들이 사용
[테넌트 전용 레이어] - 전용 Handler 클래스
├── tenant_id = 287 (경동기업)
│ └── KyungdongFormulaHandler.php
└── tenant_id = 기타 → 기본 수식 시스템
```
### 10.2 파일 구조
```
api/app/Services/Quote/
├── QuoteCalculationService.php # 기존 (수정)
├── FormulaEvaluatorService.php # 기존 (확장)
└── Handlers/
└── KyungdongFormulaHandler.php # 신규 (경동 전용)
api/database/seeders/Kyungdong/
├── KyungdongItemSeeder.php # 기존 (품목/단가)
└── KyungdongPriceTableSeeder.php # 신규 (price_* 데이터)
```
### 10.3 KyungdongFormulaHandler 설계
```php
namespace App\Services\Quote\Handlers;
class KyungdongFormulaHandler
{
// 모터 용량 계산 (3차원 조건)
public function calculateMotorCapacity(
string $productType, // screen, steel
float $weight,
string $bracketInch // 4, 5, 6, 8
): string; // 150K, 300K, ...
// 브라켓 크기 결정
public function calculateBracketSize(
float $weight,
?string $bracketInch = null
): string; // 530*320, 600*350, 690*390
// 절곡품 계산 (10종)
public function calculateSteelItems(array $params): array;
// 부자재 계산 (3종)
public function calculatePartItems(array $params): array;
// 주자재 계산 (스크린)
public function calculateScreenPrice(
float $width,
float $height
): float;
// BDmodels 단가 조회
private function getBDModelPrice(
string $modelName,
string $secondItem,
?string $finishingType = null,
?string $spec = null
): float;
// price_* 테이블 조회
private function getPriceFromTable(
string $tableName,
array $conditions
): float;
}
```
### 10.4 FormulaEvaluatorService 확장
```php
// FormulaEvaluatorService.php
public function calculateBomWithDebug(
string $finishedGoodsCode,
array $inputs,
int $tenantId
): array {
// 테넌트별 분기
if ($tenantId === 287) {
return $this->calculateKyungdongBom($finishedGoodsCode, $inputs);
}
// 기본 로직 (기존 코드)
return $this->calculateDefaultBom($finishedGoodsCode, $inputs);
}
private function calculateKyungdongBom(
string $finishedGoodsCode,
array $inputs
): array {
$handler = new KyungdongFormulaHandler();
// 1. 기본 BOM 전개 (items.bom)
$staticBom = $this->getStaticBom($finishedGoodsCode);
// 2. 동적 항목 계산
$dynamicItems = $handler->calculateDynamicItems($inputs);
// 3. 전체 항목 병합
return $this->mergeAndCalculatePrices($staticBom, $dynamicItems, $inputs);
}
```
### 10.5 단가 데이터 마이그레이션
**옵션 A: 기존 prices 테이블 활용**
- items에 품목 추가, prices에 단가 추가
- 장점: 기존 구조 활용
- 단점: 복잡한 조회 조건 표현 어려움
**옵션 B: 전용 테이블 생성 (권장)**
```sql
-- 경동 전용 단가 테이블
CREATE TABLE kd_price_tables (
id BIGINT PRIMARY KEY,
tenant_id BIGINT DEFAULT 287,
table_name VARCHAR(50), -- motor, shaft, pipe, angle, bdmodels
item_data JSON, -- 원본 JSON 데이터
created_at TIMESTAMP,
updated_at TIMESTAMP
);
```
### 10.6 구현 순서
| 순서 | 작업 | 상태 |
|------|------|------|
| 1 | KyungdongFormulaHandler 기본 구조 | ✅ 완료 |
| 2 | 모터/브라켓 계산 메서드 | ✅ 완료 |
| 3 | kd_price_tables 마이그레이션 | ✅ 완료 |
| 4 | KdPriceTable 모델 생성 | ✅ 완료 |
| 5 | KdPriceTableSeeder 생성 | ✅ 완료 |
| 6 | 단가 조회 메서드 (KdPriceTable 연동) | ✅ 완료 |
| 7 | 부자재 계산 (3종) | ✅ 완료 |
| 8 | 절곡품 계산 (10종) | ⏳ 대기 |
| 9 | FormulaEvaluatorService 연동 | ✅ 완료 |
| 10 | API 테스트 및 검증 | ⏳ 대기 |
| 8 | API 테스트 | |
---
*이 문서는 5130 분석 진행에 따라 지속 업데이트됩니다.*