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