diff --git a/plans/kd-quote-logic-plan.md b/plans/kd-quote-logic-plan.md index dfcbcf1..d8d1591 100644 --- a/plans/kd-quote-logic-plan.md +++ b/plans/kd-quote-logic-plan.md @@ -3,7 +3,7 @@ > **작성일**: 2026-01-28 > **목적**: 5130 레거시 견적 시스템 분석 → SAM 동적 BOM/견적 로직 구현 > **선행 작업**: [kd-items-migration-plan.md](./kd-items-migration-plan.md) (정적 품목/단가 완료) -> **상태**: 🔵 분석 대기 +> **상태**: 🔄 Phase 0 진행중 --- @@ -25,10 +25,38 @@ | 항목 | 내용 | |------|------| -| **현재 단계** | Phase 0: 분석 대기 | -| **다음 작업** | 5130 견적 로직 파일 탐색 | -| **진행률** | 0/5 (0%) | -| **마지막 업데이트** | 2026-01-28 | +| **현재 단계** | 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원 ✅ +``` --- @@ -70,13 +98,22 @@ models (모델 마스터) ## 2. 분석 대상 -### 2.1 5130 디렉토리 구조 (예상) +### 2.1 5130 디렉토리 구조 (분석 완료) ``` 5130/ -├── estimate/ # 견적 관련 (우선 분석) -├── output/ # 출력/리포트 -├── dbeditor/ # DB 관리 +├── 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 관리 └── [기타 모듈]/ ``` @@ -94,30 +131,37 @@ models (모델 마스터) ## 3. 작업 계획 -### Phase 0: 5130 탐색 및 구조 파악 -- [ ] 5130/ 디렉토리 구조 분석 -- [ ] 견적 관련 파일 식별 -- [ ] 주요 함수/클래스 목록화 +### Phase 0: 5130 탐색 및 구조 파악 ✅ +- [x] 5130/ 디렉토리 구조 분석 +- [x] 견적 관련 파일 식별 (estimate/, output/) +- [x] 주요 함수/클래스 목록화 (아래 섹션 4.3 참조) -### Phase 1: 견적 생성 로직 분석 -- [ ] 모델 선택 → BOM 구성 흐름 파악 -- [ ] 동적 항목 추가 조건 분석 -- [ ] DB 조회 패턴 파악 +### Phase 1: 견적 생성 로직 분석 🔄 +- [x] 모델 선택 → BOM 구성 흐름 파악 +- [x] 동적 항목 추가 조건 분석 (체크박스 기반) +- [x] DB 조회 패턴 파악 (BDmodels, price_* 테이블) +- [ ] 세부 계산 로직 문서화 -### Phase 2: 계산 공식 추출 -- [ ] 모터 용량 계산 공식 -- [ ] 절곡품 수량/단가 계산 공식 -- [ ] 부자재 자동 추가 규칙 +### Phase 2: 계산 공식 추출 ✅ +- [x] 모터 용량 계산 공식 (`calculateMotorSpec` 분석 완료) +- [x] 절곡품 수량/단가 계산 공식 (섹션 4.12 참조) +- [x] 부자재 자동 추가 규칙 (섹션 4.13 참조) -### Phase 3: SAM 설계 -- [ ] API 엔드포인트 설계 -- [ ] Service 클래스 설계 -- [ ] DB 스키마 변경 필요 여부 +### Phase 3: SAM 설계 ✅ +- [x] 기존 견적 시스템 분석 (QuoteCalculationService, FormulaEvaluatorService) +- [x] 5130 로직 통합 설계 → 하이브리드 접근 결정 (섹션 10.1) +- [x] API 엔드포인트 확장 설계 → 기존 엔드포인트 활용 +- [x] DB 스키마 변경 필요 여부 → kd_price_tables 신규 테이블 (옵션) -### Phase 4: SAM 구현 -- [ ] BOM 동적 계산 Service -- [ ] 견적 API 수정 -- [ ] 프론트엔드 연동 +### 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 결과 비교 @@ -141,13 +185,111 @@ models (모델 마스터) | price_pipe | 파이프 계산 참조 | ✅ 완료 | | price_raw_materials | 원자재 단가 | ✅ 완료 | -### 4.2 분석 예정 (5130 코드) +### 4.2 분석된 5130 코드 -| 파일/모듈 | 예상 내용 | 분석 상태 | -|-----------|----------|-----------| -| estimate/* | 견적 생성 로직 | ⏳ 대기 | -| (TBD) | 모터 계산 | ⏳ 대기 | -| (TBD) | 절곡품 계산 | ⏳ 대기 | +| 파일/모듈 | 내용 | 분석 상태 | +|-----------|------|-----------| +| 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) × 수량 +``` --- @@ -213,6 +355,621 @@ Response: | 날짜 | 항목 | 변경 내용 | |------|------|----------| | 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 테스트 | | ---