# 견적 산출 API 개발 계획 > **작성일**: 2025-12-30 > **목적**: 견적 산출 API 개발 및 React 견적등록 화면 연동 > **참조 로직**: `mng/app/Services/Quote/FormulaEvaluatorService.php` (코드 복사/재구현) > **상태**: 🔄 진행중 (Serena ID: quote-calc-api-state) --- ## 📍 현재 진행 상태 | 항목 | 내용 | |------|------| | **마지막 완료 작업** | 분석 및 계획 수립 | | **다음 작업** | Phase 1.1 API 계산 로직 구현 | | **진행률** | 0/12 (0%) | | **마지막 업데이트** | 2025-12-30 20:00 | --- ## 0. 로컬 개발 환경 ### 도메인 구성 | 서비스 | 도메인 | 설명 | |--------|--------|------| | React (프론트엔드) | `http://dev.sam.kr` | 사용자 화면 | | API (백엔드) | `http://api.sam.kr` | REST API 서버 | | MNG (운영관리자) | `http://mng.sam.kr` | 관리자 패널 | ### 테스트 대상 테넌트 | 항목 | 값 | 비고 | |------|-----|------| | **Tenant ID** | 287 | 프론트_테스트회사 | | **테스트 User ID** | 33 | 홍킬동 (hhhhhh@example.com) | --- ## 1. 작업 규칙 ### 1.0 아키텍처 원칙 (필수) > **React는 오직 `api.sam.kr` (api 프로젝트)만 호출한다** ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ react/ │ ───► │ api/ │ │ mng/ │ │ dev.sam.kr │ │ api.sam.kr │ │ mng.sam.kr │ │ (프론트엔드) │ │ (REST API) │ │ (관리자패널) │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ✅ 호출 허용 │ │ └────────────────────┘ │ │ ❌ 절대 호출 금지 ─────────────────────────┘ ``` **규칙:** - React에서 mng API 직접 호출 **절대 금지** - 필요한 API가 api 프로젝트에 없으면 **api에 새로 개발** - mng의 모델/로직은 **참조만** (코드 복사 또는 재구현) - **MNG와 API는 동일한 DB 사용** (데이터 복제 불필요) ### 1.1 작업 진행 정책 > **단위 작업 → 검수 → 승인 → 문서 업데이트 → 커밋** 순서로 진행 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 📋 작업 흐름 (페이지 단위) │ ├─────────────────────────────────────────────────────────────────┤ │ 1️⃣ 작업 시작: 대상 기능 구현 │ │ 2️⃣ 작업 완료: 코드 수정 완료 후 사용자에게 검수 요청 │ │ 3️⃣ 검수: 사용자가 기능 확인 (브라우저 테스트) │ │ 4️⃣ [승인] 문서 업데이트: 이 문서의 상태 갱신 │ │ 5️⃣ [승인] 커밋: Git 커밋 생성 │ │ 6️⃣ 다음 작업으로 이동 │ └─────────────────────────────────────────────────────────────────┘ ``` **⚠️ 중요 규칙:** - 각 단계에서 `[승인]` 표시된 작업은 **사용자 승인 후** 진행 --- ## 2. 개요 ### 2.1 배경 MNG 시뮬레이터(`mng.sam.kr/quote-formulas/simulator`)의 견적 산출 로직이 정상 작동함을 확인함. 이 로직을 **API 프로젝트에 재구현**하여 React 견적등록 화면(`dev.sam.kr/sales/quote-management/new`)에서 사용. **현재 상태:** - MNG: `FormulaEvaluatorService` - DB 기반 정확한 계산 (**참조용**) - API: `QuoteCalculationService` - 존재하지만 로직 미완성 - React: `handleAutoCalculate()` - 토스트 메시지만 표시 (API 미연동) **목표:** ``` React 입력 → API 계산 → 결과 반환 → React 표시 ``` ### 2.2 기준 원칙 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 🎯 핵심 원칙 │ ├─────────────────────────────────────────────────────────────────┤ │ 1. MNG FormulaEvaluatorService 로직을 API에 재구현 │ │ 2. DB 기반 동적 계산 (하드코딩 금지) │ │ 3. 단가는 DB에서 조회 (localStorage 사용 금지) │ │ 4. 카테고리별 계산 방식도 DB에서 (CategoryGroup 활용) │ │ 5. MNG 직접 호출 절대 금지 │ └─────────────────────────────────────────────────────────────────┘ ``` ### 1.3 변경 승인 정책 | 분류 | 예시 | 승인 | |------|------|------| | ✅ 즉시 가능 | React UI 연동, 파라미터 추가 | 불필요 | | ⚠️ 컨펌 필요 | API 계산 로직 변경, 새 엔드포인트 | **필수** | | 🔴 금지 | DB 스키마 변경, 기존 API 삭제 | 별도 협의 | ### 1.4 준수 규칙 - `docs/quickstart/quick-start.md` - 빠른 시작 가이드 - `docs/standards/quality-checklist.md` - 품질 체크리스트 - `docs/guides/swagger-guide.md` - Swagger 문서 가이드 - `api/CLAUDE.md` - API 개발 규칙 --- ## 2. 대상 범위 ### 2.1 Phase 1: API 계산 로직 구현 | # | 작업 항목 | 상태 | 비고 | |---|----------|:----:|------| | 1.1 | QuoteCalculationService MNG 로직 동기화 | ⏳ | FormulaEvaluatorService 기반 | | 1.2 | 입력 변수 처리 (W0, H0, GT, MP, CT 등) | ⏳ | React QuoteItem 매핑 | | 1.3 | 수식 평가 로직 구현 | ⏳ | evaluate, evaluateRange | | 1.4 | 품목 가격 계산 로직 구현 | ⏳ | area_based, weight_based 구분 | ### 2.2 Phase 2: API 엔드포인트 정비 | # | 작업 항목 | 상태 | 비고 | |---|----------|:----:|------| | 2.1 | POST /api/v1/quotes/calculate 엔드포인트 | ⏳ | 기존 존재, 로직 수정 | | 2.2 | QuoteCalculateRequest 유효성 검증 | ⏳ | FormRequest 수정 | | 2.3 | Swagger 문서 업데이트 | ⏳ | 요청/응답 스키마 | ### 2.3 Phase 3: React 연동 | # | 작업 항목 | 상태 | 비고 | |---|----------|:----:|------| | 3.1 | handleAutoCalculate API 호출 구현 | ⏳ | quoteApi.calculate() 사용 | | 3.2 | 계산 결과 UI 표시 | ⏳ | 품목별 단가/금액 | | 3.3 | 에러 처리 및 로딩 상태 | ⏳ | UX 개선 | | 3.4 | 계산 결과로 QuoteItem 자동 생성 | ⏳ | items 배열 업데이트 | --- ## 3. MNG 핵심 로직 상세 (API 재구현 기준) > **중요**: 이 섹션은 API에서 재구현해야 할 MNG 로직의 완전한 명세입니다. > 새 세션에서 이 문서만 보고 작업할 수 있도록 상세히 기술합니다. ### 3.1 핵심 서비스 구조 ``` ┌──────────────────────────────────────────────────────────────────────────┐ │ FormulaEvaluatorService 핵심 메서드 │ ├──────────────────────────────────────────────────────────────────────────┤ │ 📥 입력 처리 │ │ ├── validateFormula() - 수식 문법 검증 │ │ └── resetVariables() - 변수 초기화 │ │ │ │ 🔢 수식 평가 │ │ ├── evaluate() - 단일 수식 평가 (변수 치환 + 함수 처리) │ │ ├── evaluateRange() - 범위 조건별 수식 평가 │ │ └── evaluateMapping() - 매핑값 기반 수식 평가 │ │ │ │ 📊 BOM 기반 계산 (핵심) │ │ ├── calculateBomWithDebug() - 10단계 디버그 포함 전체 계산 │ │ ├── expandBomWithFormulas() - BOM 트리 전개 │ │ └── calculateCategoryPrice() - 카테고리별 단가 계산 │ │ │ │ 💰 가격 조회 │ │ ├── getItemPrice() - 품목 단가 조회 (Price 모델 → Fallback) │ │ └── getItemCategory() - 품목 카테고리 조회 │ └──────────────────────────────────────────────────────────────────────────┘ ``` ### 3.2 사용 DB 테이블 | 테이블명 | 용도 | 주요 컬럼 | |---------|------|----------| | `items` | 품목 마스터 | code, name, item_type, item_category, process_type, bom(JSON), unit | | `prices` | 단가 정보 | tenant_id, item_code, sales_price | | `category_groups` | 카테고리별 계산 방식 | code, categories(JSON), multiplier_variable | | `quote_formulas` | 수식 정의 | variable, formula, type, output_type | | `quote_formula_ranges` | 범위별 조건 | formula_id, condition_variable, min, max, result_value | | `quote_formula_mappings` | 매핑 정의 | formula_id, source_variable, source_value, result_value | ### 3.3 입력 변수 (React → API) | 변수명 | 의미 | 타입 | 예시 | |--------|------|------|------| | `W0` | 오픈사이즈 가로 (mm) | number | 3000 | | `H0` | 오픈사이즈 세로 (mm) | number | 2500 | | `QTY` | 수량 | number | 1 | | `PC` | 제품 카테고리 | string | "SCREEN", "STEEL" | | `GT` | 가이드레일 설치유형 | string | "wall", "ceiling" | | `MP` | 모터 전원 | string | "single", "three" | | `CT` | 연동제어기 | string | "basic", "advanced" | | `WS` | 마구리 날개치수 | number | 50 | | `INSP` | 검사비 | number | 50000 | ### 3.4 계산 변수 (자동 산출) ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 변수 계산 로직 (제품 카테고리별 마진값) │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ if (PC === 'STEEL') { │ │ marginW = 110; // 철재 마진 │ │ marginH = 350; │ │ K = M × 25; // 철재 중량 │ │ } else { │ │ marginW = 140; // 스크린 기본 마진 │ │ marginH = 350; │ │ K = M × 2 + (W0 / 1000) × 14.17; // 스크린 중량 │ │ } │ │ │ │ W1 = W0 + marginW; // 마진 포함 폭 │ │ H1 = H0 + marginH; // 마진 포함 높이 │ │ M = (W1 × H1) / 1,000,000; // 면적 (㎡) │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 3.5 CategoryGroup 단가 계산 방식 ```php // category_groups 테이블 구조 // code: 'area_based' | 'weight_based' | 'quantity_based' // multiplier_variable: 'M' (면적) | 'K' (중량) | null (수량) // categories: JSON 배열 ['원단', '패널', '도장'] 등 // 단가 계산 로직 if (multiplier_variable === 'M') { // 면적 기반: 기본단가 × M (면적 ㎡) final_price = base_price × M; note = "면적단가 (xxx원/㎡ × x.xx㎡)"; } else if (multiplier_variable === 'K') { // 중량 기반: 기본단가 × K (중량 kg) final_price = base_price × K; note = "중량단가 (xxx원/kg × x.xxkg)"; } else { // 수량 기반: 기본단가 × 수량 final_price = base_price × quantity; note = "수량단가"; } ``` ### 3.6 10단계 BOM 계산 프로세스 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ calculateBomWithDebug() 10단계 프로세스 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Step 1. 입력값 수집 │ │ W0, H0, QTY, PC, GT, MP, CT, WS, INSP + 완제품코드 │ │ │ │ Step 2. 완제품 선택 │ │ items 테이블에서 FG(완제품) 조회 │ │ → code, name, item_category, bom(JSON) 확인 │ │ │ │ Step 3. 변수 계산 │ │ W1, H1, M, K 계산 (3.4 참조) │ │ │ │ Step 4. BOM 전개 │ │ 완제품의 bom JSON → 자식 품목 목록 생성 │ │ 재귀적으로 반제품(SF, PT) 하위 BOM 포함 │ │ │ │ Step 5. 단가 출처 결정 │ │ 각 품목의 item_category → CategoryGroup 매칭 │ │ → multiplier_variable 결정 (M/K/null) │ │ │ │ Step 6. 수량 수식 평가 │ │ BOM의 quantityFormula 평가 (예: "M", "W0/1000", "1") │ │ │ │ Step 7. 금액 계산 │ │ 면적/중량 기반: final_price = base_price × M|K │ │ 수량 기반: total_price = quantity × unit_price │ │ │ │ Step 8. 공정별 그룹화 │ │ items.process_type으로 그룹화 │ │ screen, bending, steel, electric, assembly, other │ │ │ │ Step 9. 소계 계산 │ │ 공정별 subtotal 합산 │ │ │ │ Step 10. 최종 합계 │ │ grand_total = sum(all item total_price) │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 3.7 수식 평가 함수 (evaluate) ```php // 지원 함수 목록 $supportedFunctions = ['SUM', 'ROUND', 'CEIL', 'FLOOR', 'ABS', 'MIN', 'MAX', 'IF', 'AND', 'OR', 'NOT']; // 평가 과정 1. substituteVariables() - 변수명 → 값으로 치환 예: "W0 + 140" → "3000 + 140" 2. processFunctions() - 함수 처리 - ROUND(value, decimals) → round(value, decimals) - SUM(a, b, c) → a + b + c - IF(condition, true_val, false_val) → 조건 평가 후 결과 반환 - MIN/MAX(a, b, ...) → 최소/최대값 - ABS/CEIL/FLOOR(value) → 수학 함수 3. calculateExpression() - 최종 계산 - 안전한 수식 평가 (숫자, 연산자, 괄호만 허용) - eval() 사용 (프로덕션에서는 expression-language 라이브러리 권장) ``` ### 3.8 단가 조회 우선순위 ```php function getItemPrice(string $itemCode): float { // 1차: prices 테이블에서 판매단가 조회 $price = Price::getSalesPriceByItemCode($tenantId, $itemCode); if ($price > 0) return $price; // 2차 Fallback: items.attributes.salesPrice에서 조회 $item = DB::table('items') ->where('tenant_id', $tenantId) ->where('code', $itemCode) ->first(); if ($item && $item->attributes) { $attributes = json_decode($item->attributes, true); return (float) ($attributes['salesPrice'] ?? 0); } return 0; } ``` ### 3.9 BOM JSON 구조 ```json // items.bom 필드 (완제품/반제품) [ { "child_item_id": 123, // 또는 "item_code": "PT-001", // childItemCode "quantity": 1, // 또는 "quantityFormula": "M" // 수식 (면적 기반) }, { "childItemCode": "RM-002", "quantityFormula": "W0/1000", // 폭 기반 수량 "quantity": 1 } ] ``` ### 3.10 API 응답 구조 (목표) ```json { "success": true, "data": { "finished_goods": { "code": "FG-SCR-001", "name": "스크린셔터 3000x2500", "item_category": "SCREEN" }, "variables": { "W0": 3000, "H0": 2500, "W1": 3140, "H1": 2850, "M": 8.949, "K": 60.598 }, "items": [ { "item_code": "RM-FABRIC-01", "item_name": "스크린 원단", "item_category": "원단", "quantity": 8.949, "unit_price": 213465, "total_price": 1910203, "calculation_note": "면적단가 (213,465원/㎡ × 8.949㎡)", "category_group": "area_based" } ], "grouped_items": { "screen": { "name": "스크린 공정", "items": [...], "subtotal": 1910203 } }, "subtotals": { "screen": { "count": 3, "subtotal": 1910203 }, "electric": { "count": 2, "subtotal": 500000 } }, "grand_total": 2410203 } } ``` --- ## 4. 현재 시스템 분석 ### 4.1 MNG FormulaEvaluatorService 핵심 기능 ```php // 1. 수식 검증 validateFormula(string $formula): array // 2. 수식 평가 (변수 치환 + 함수 처리) evaluate(string $formula, array $variables): mixed // 3. 범위 기반 수식 평가 (조건에 따른 결과) evaluateRange(QuoteFormula $formula, array $variables): mixed // 4. 전체 수식 실행 (카테고리별 순서대로) executeAll(Collection $formulasByCategory, array $inputVariables): array // 5. BOM 기반 계산 (디버깅 포함) calculateBomWithDebug(string $fgCode, array $inputVars, int $tenantId): array // 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT ``` ### 3.2 React QuoteItem 인터페이스 ```typescript interface QuoteItem { id: string; floor: string; // 층수 code: string; // 부호 productCategory: string; // PC (제품 카테고리) productName: string; // 제품명 openWidth: string; // W0 (오픈사이즈 가로) openHeight: string; // H0 (오픈사이즈 세로) guideRailType: string; // GT (가이드레일 설치유형) motorPower: string; // MP (모터 전원) controller: string; // CT (연동제어기) quantity: number; // QTY (수량) wingSize: string; // WS (마구리 날개치수) inspectionFee: number; // INSP (검사비) unitPrice?: number; // 단가 (계산 결과) totalAmount?: number; // 합계 (계산 결과) } ``` ### 3.3 API 요청/응답 구조 (목표) **요청:** ```json { "items": [ { "product_code": "SCR-001", "W0": 3000, "H0": 2500, "QTY": 1, "GT": "벽면형", "MP": "220V", "CT": "단독", "WS": 50, "INSP": 50000 } ] } ``` **응답:** ```json { "success": true, "data": { "items": [ { "product_code": "SCR-001", "inputs": { "W0": 3000, "H0": 2500, ... }, "outputs": { "W1": 3140, "H1": 2850, "M": 8.95, "K": 0 }, "bom_items": [ { "item_code": "SF-SCR-F01", "item_name": "스크린 원단", "quantity": 8.95, "unit_price": 213465, "total_price": 1910486 } ], "costs": { "material_cost": 1910486, "labor_cost": 0, "install_cost": 50000, "subtotal": 1960486 } } ], "summary": { "total_material": 1910486, "total_labor": 0, "total_install": 50000, "grand_total": 1960486 } } } ``` --- ## 4. 상세 작업 내용 ### 4.1 Phase 1: API 계산 로직 구현 #### 1.1 QuoteCalculationService MNG 로직 동기화 **현재 상태:** - `api/app/Services/Quote/QuoteCalculationService.php` 존재 - MNG `FormulaEvaluatorService`와 동기화 안됨 **목표 상태:** - MNG 로직을 API로 완전 이전 - DB 기반 수식/단가 조회 - CategoryGroup 활용한 계산 방식 결정 **수정 사항:** - [ ] ✅ FormulaEvaluatorService 메서드 이전 - [ ] ✅ DB 연결 (quote_formulas, items, category_groups 테이블) - [ ] ⚠️ 계산 로직 구현 (컨펌 필요) --- ## 5. 컨펌 대기 목록 | # | 항목 | 변경 내용 | 영향 범위 | 상태 | |---|------|----------|----------|------| | 1 | API 계산 로직 | MNG FormulaEvaluatorService 기반 구현 | api | ⚠️ 컨펌 필요 | | 2 | DB 연결 방식 | MNG DB 직접 조회 vs API DB 복제 | api, database | ⚠️ 컨펌 필요 | --- ## 6. 변경 이력 | 날짜 | 항목 | 변경 내용 | 파일 | 승인 | |------|------|----------|------|------| | 2025-12-30 | 초안 | 계획 문서 작성 | - | - | | 2025-12-30 | MNG 로직 상세 | 섹션 3 추가: API 재구현을 위한 MNG 핵심 로직 상세 명세 | docs/dev_plans/quote-calculation-api-plan.md | - | --- ## 7. 참고 문서 - **MNG 시뮬레이터**: `mng/resources/views/quote-formulas/simulator.blade.php` - **MNG 계산 로직**: `mng/app/Services/Quote/FormulaEvaluatorService.php` - **API 컨트롤러**: `api/app/Http/Controllers/Api/V1/QuoteController.php` - **React 컴포넌트**: `react/src/components/quotes/QuoteRegistration.tsx` - **기존 시뮬레이터 계획**: `docs/dev_plans/simulator-ui-enhancement-plan.md` --- ## 8. 세션 및 메모리 관리 정책 ### 8.1 세션 시작 시 (Load Strategy) ```javascript read_memory("quote-calc-api-state") // 1. 상태 파악 read_memory("quote-calc-api-snapshot") // 2. 사고 흐름 복구 ``` ### 8.2 Serena 메모리 구조 - `quote-calc-api-state`: { phase, progress, next_step, last_decision } - `quote-calc-api-snapshot`: 현재까지의 논의 및 코드 변경점 요약 --- ## 9. 검증 결과 > 작업 완료 후 이 섹션에 검증 결과 추가 ### 9.1 테스트 케이스 | 입력값 | 예상 결과 | 실제 결과 | 상태 | |--------|----------|----------|------| | W0=3000, H0=2500, QTY=1 | MNG와 동일 | - | ⏳ | | W0=1200, H0=2400, QTY=10 | MNG와 동일 | - | ⏳ | ### 9.2 성공 기준 달성 현황 | 기준 | 달성 | 비고 | |------|------|------| | API 계산 로직 MNG와 동일 | ⏳ | FormulaEvaluatorService 기반 | | React에서 API 호출 성공 | ⏳ | handleAutoCalculate 연동 | | 계산 결과 UI 표시 | ⏳ | 품목/단가/금액 표시 | --- *이 문서는 /sc:plan 스킬로 생성되었습니다.*