From 660300cebfdbe284768e00b5b7c50166ac88371f Mon Sep 17 00:00:00 2001 From: kent Date: Fri, 2 Jan 2026 11:24:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20BOM=20=EA=B8=B0=EB=B0=98=20=EA=B2=AC?= =?UTF-8?q?=EC=A0=81=20=EA=B3=84=EC=82=B0=20API=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - QuoteBomCalculateRequest.php 생성 (BOM 계산용 FormRequest) - QuoteCalculationService.calculateBom() 메서드 추가 - QuoteController.calculateBom() 액션 추가 - POST /api/v1/quotes/calculate/bom 라우트 등록 - Swagger 문서 업데이트 (스키마 + 엔드포인트) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CURRENT_WORKS.md | 32 +++++++ .../Controllers/Api/V1/QuoteController.php | 18 ++++ .../Quote/QuoteBomCalculateRequest.php | 90 +++++++++++++++++++ .../Quote/QuoteCalculationService.php | 37 ++++++++ app/Swagger/v1/QuoteApi.php | 72 +++++++++++++++ routes/api.php | 1 + 6 files changed, 250 insertions(+) create mode 100644 app/Http/Requests/Quote/QuoteBomCalculateRequest.php diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index 988be38..52a1713 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1,5 +1,37 @@ # SAM API 작업 현황 +## 2026-01-02 (목) - Phase 1.1 견적 산출 API 엔드포인트 구현 + +### 작업 목표 +- React 프론트엔드에서 BOM 기반 견적 계산 API 호출 가능하도록 구현 +- MNG FormulaEvaluatorService.calculateBomWithDebug() 연결 + +### 생성된 파일 +| 파일명 | 설명 | +|--------|------| +| `app/Http/Requests/Quote/QuoteBomCalculateRequest.php` | BOM 계산용 FormRequest | +| `docs/changes/20260102_quote_bom_calculation_api.md` | 변경 내용 문서 | + +### 수정된 파일 +| 파일명 | 설명 | +|--------|------| +| `app/Services/Quote/QuoteCalculationService.php` | calculateBom 메서드 추가 | +| `app/Http/Controllers/Api/V1/QuoteController.php` | calculateBom 액션 추가 | +| `routes/api.php` | /calculate/bom 라우트 추가 | +| `app/Swagger/v1/QuoteApi.php` | 스키마 및 엔드포인트 문서 추가 | + +### 주요 변경 내용 +1. **BOM 기반 견적 계산 API**: `POST /api/v1/quotes/calculate/bom` +2. **입력 변수**: finished_goods_code, W0, H0, QTY, PC, GT, MP, CT, WS, INSP +3. **10단계 디버깅**: debug=true 옵션으로 계산 과정 확인 가능 +4. **Swagger 문서화**: QuoteBomCalculateRequest, QuoteBomCalculationResult 스키마 + +### 관련 문서 +- 계획 문서: `docs/plans/quote-calculation-api-plan.md` +- FormulaEvaluatorService: Phase 1.1에서 구현 완료 + +--- + ## 2025-12-30 (월) - Phase 1.1 견적 계산 MNG 로직 재구현 ### 작업 목표 diff --git a/app/Http/Controllers/Api/V1/QuoteController.php b/app/Http/Controllers/Api/V1/QuoteController.php index 462df18..57e223b 100644 --- a/app/Http/Controllers/Api/V1/QuoteController.php +++ b/app/Http/Controllers/Api/V1/QuoteController.php @@ -4,6 +4,7 @@ use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; +use App\Http\Requests\Quote\QuoteBomCalculateRequest; use App\Http\Requests\Quote\QuoteBulkDeleteRequest; use App\Http\Requests\Quote\QuoteCalculateRequest; use App\Http\Requests\Quote\QuoteIndexRequest; @@ -144,6 +145,23 @@ public function calculate(QuoteCalculateRequest $request) }, __('message.quote.calculated')); } + /** + * BOM 기반 자동산출 (10단계 디버깅 포함) + * + * React 견적등록 화면에서 완제품 코드와 입력 변수를 받아 + * BOM 기반으로 품목/단가/금액을 계산합니다. + */ + public function calculateBom(QuoteBomCalculateRequest $request) + { + return ApiResponse::handle(function () use ($request) { + return $this->calculationService->calculateBom( + $request->finished_goods_code, + $request->getInputVariables(), + $request->boolean('debug', false) + ); + }, __('message.quote.calculated')); + } + /** * 자동산출 입력 스키마 조회 */ diff --git a/app/Http/Requests/Quote/QuoteBomCalculateRequest.php b/app/Http/Requests/Quote/QuoteBomCalculateRequest.php new file mode 100644 index 0000000..494277f --- /dev/null +++ b/app/Http/Requests/Quote/QuoteBomCalculateRequest.php @@ -0,0 +1,90 @@ + 'required|string|max:50', + 'W0' => 'required|numeric|min:100|max:20000', + 'H0' => 'required|numeric|min:100|max:20000', + + // 선택 입력 (기본값 있음) + 'QTY' => 'nullable|integer|min:1', + 'PC' => 'nullable|string|in:SCREEN,STEEL', + 'GT' => 'nullable|string|in:wall,ceiling,floor', + 'MP' => 'nullable|string|in:single,three', + 'CT' => 'nullable|string|in:basic,smart,premium', + 'WS' => 'nullable|numeric|min:0|max:500', + 'INSP' => 'nullable|numeric|min:0', + + // 디버그 모드 (개발용) + 'debug' => 'nullable|boolean', + ]; + } + + public function attributes(): array + { + return [ + 'finished_goods_code' => __('validation.attributes.finished_goods_code'), + 'W0' => __('validation.attributes.open_width'), + 'H0' => __('validation.attributes.open_height'), + 'QTY' => __('validation.attributes.quantity'), + 'PC' => __('validation.attributes.product_category'), + 'GT' => __('validation.attributes.guide_rail_type'), + 'MP' => __('validation.attributes.motor_power'), + 'CT' => __('validation.attributes.controller'), + 'WS' => __('validation.attributes.wing_size'), + 'INSP' => __('validation.attributes.inspection_fee'), + ]; + } + + public function messages(): array + { + return [ + 'finished_goods_code.required' => __('error.finished_goods_code_required'), + 'W0.required' => __('error.open_width_required'), + 'W0.min' => __('error.open_width_min'), + 'W0.max' => __('error.open_width_max'), + 'H0.required' => __('error.open_height_required'), + 'H0.min' => __('error.open_height_min'), + 'H0.max' => __('error.open_height_max'), + ]; + } + + /** + * 입력 변수 배열 반환 (FormulaEvaluatorService용) + */ + public function getInputVariables(): array + { + $validated = $this->validated(); + + return [ + 'W0' => (float) $validated['W0'], + 'H0' => (float) $validated['H0'], + 'QTY' => (int) ($validated['QTY'] ?? 1), + 'PC' => $validated['PC'] ?? 'SCREEN', + 'GT' => $validated['GT'] ?? 'wall', + 'MP' => $validated['MP'] ?? 'single', + 'CT' => $validated['CT'] ?? 'basic', + 'WS' => (float) ($validated['WS'] ?? 50), + 'INSP' => (float) ($validated['INSP'] ?? 50000), + ]; + } +} diff --git a/app/Services/Quote/QuoteCalculationService.php b/app/Services/Quote/QuoteCalculationService.php index 0981c5d..7df1b05 100644 --- a/app/Services/Quote/QuoteCalculationService.php +++ b/app/Services/Quote/QuoteCalculationService.php @@ -76,6 +76,43 @@ public function preview(array $inputs, ?string $productCategory = null, ?int $pr return $this->calculate($inputs, $productCategory, $productId); } + /** + * BOM 기반 견적 산출 (10단계 디버깅 포함) + * + * MNG FormulaEvaluatorService의 calculateBomWithDebug와 동일한 로직을 사용합니다. + * React 견적등록 화면에서 자동 견적 산출 시 호출됩니다. + * + * @param string $finishedGoodsCode 완제품 코드 + * @param array $inputs 입력 변수 (W0, H0, QTY, PC, GT, MP, CT, WS, INSP) + * @param bool $debug 디버그 모드 (기본 false) + * @return array 산출 결과 (finished_goods, variables, items, grouped_items, subtotals, grand_total) + */ + public function calculateBom(string $finishedGoodsCode, array $inputs, bool $debug = false): array + { + $tenantId = $this->tenantId(); + + if (! $tenantId) { + return [ + 'success' => false, + 'error' => __('error.tenant_not_set'), + ]; + } + + // FormulaEvaluatorService의 calculateBomWithDebug 호출 + $result = $this->formulaEvaluator->calculateBomWithDebug( + $finishedGoodsCode, + $inputs, + $tenantId + ); + + // 디버그 모드가 아니면 debug_steps 제거 + if (! $debug && isset($result['debug_steps'])) { + unset($result['debug_steps']); + } + + return $result; + } + /** * 견적 품목 재계산 (기존 견적 기준) */ diff --git a/app/Swagger/v1/QuoteApi.php b/app/Swagger/v1/QuoteApi.php index e2115b0..098af41 100644 --- a/app/Swagger/v1/QuoteApi.php +++ b/app/Swagger/v1/QuoteApi.php @@ -251,6 +251,45 @@ * @OA\Property(property="product_category", type="string", example="SCREEN"), * @OA\Property(property="generated_at", type="string", format="date-time") * ) + * + * @OA\Schema( + * schema="QuoteBomCalculateRequest", + * type="object", + * required={"finished_goods_code","W0","H0"}, + * + * @OA\Property(property="finished_goods_code", type="string", example="SC-1000", description="완제품 코드"), + * @OA\Property(property="W0", type="number", format="float", example=3000, minimum=100, maximum=20000, description="개구부 폭(mm)"), + * @OA\Property(property="H0", type="number", format="float", example=2500, minimum=100, maximum=20000, description="개구부 높이(mm)"), + * @OA\Property(property="QTY", type="integer", example=1, minimum=1, description="수량"), + * @OA\Property(property="PC", type="string", enum={"SCREEN","STEEL"}, example="SCREEN", description="제품 카테고리"), + * @OA\Property(property="GT", type="string", enum={"wall","ceiling","floor"}, example="wall", description="가이드레일 타입"), + * @OA\Property(property="MP", type="string", enum={"single","three"}, example="single", description="모터 전원"), + * @OA\Property(property="CT", type="string", enum={"basic","smart","premium"}, example="basic", description="컨트롤러"), + * @OA\Property(property="WS", type="number", format="float", example=50, description="날개 크기"), + * @OA\Property(property="INSP", type="number", format="float", example=50000, description="검사비"), + * @OA\Property(property="debug", type="boolean", example=false, description="디버그 모드 (10단계 디버깅 정보 포함)") + * ) + * + * @OA\Schema( + * schema="QuoteBomCalculationResult", + * type="object", + * + * @OA\Property(property="success", type="boolean", example=true), + * @OA\Property(property="finished_goods", type="object", description="완제품 정보", + * @OA\Property(property="code", type="string", example="SC-1000"), + * @OA\Property(property="name", type="string", example="전동스크린 1000형") + * ), + * @OA\Property(property="variables", type="object", description="계산 변수 (W0, H0, W1, H1, M, K 등)"), + * @OA\Property(property="items", type="array", @OA\Items(ref="#/components/schemas/QuoteItem"), description="산출된 품목"), + * @OA\Property(property="grouped_items", type="object", description="카테고리별 품목 그룹"), + * @OA\Property(property="subtotals", type="object", description="카테고리별 소계", + * @OA\Property(property="material", type="number", format="float"), + * @OA\Property(property="labor", type="number", format="float"), + * @OA\Property(property="install", type="number", format="float") + * ), + * @OA\Property(property="grand_total", type="number", format="float", description="총계"), + * @OA\Property(property="debug_steps", type="array", nullable=true, @OA\Items(type="object"), description="디버그 모드시 10단계 디버깅 정보") + * ) */ class QuoteApi { @@ -504,6 +543,39 @@ public function calculationSchema() {} */ public function calculate() {} + /** + * @OA\Post( + * path="/api/v1/quotes/calculate/bom", + * tags={"Quote"}, + * summary="BOM 기반 자동산출 (10단계 디버깅)", + * description="완제품 코드와 입력 변수를 받아 BOM 기반으로 품목/단가/금액을 자동 계산합니다. MNG FormulaEvaluatorService와 동일한 10단계 디버깅을 지원합니다.", + * security={{"BearerAuth":{}}}, + * + * @OA\RequestBody( + * required=true, + * + * @OA\JsonContent(ref="#/components/schemas/QuoteBomCalculateRequest") + * ), + * + * @OA\Response( + * response=200, + * description="산출 성공", + * + * @OA\JsonContent( + * + * @OA\Property(property="success", type="boolean", example=true), + * @OA\Property(property="message", type="string", example="견적이 산출되었습니다."), + * @OA\Property(property="data", ref="#/components/schemas/QuoteBomCalculationResult") + * ) + * ), + * + * @OA\Response(response=400, description="유효성 검증 실패"), + * @OA\Response(response=401, description="인증 필요"), + * @OA\Response(response=404, description="완제품 코드 없음") + * ) + */ + public function calculateBom() {} + /** * @OA\Post( * path="/api/v1/quotes/{id}/pdf", diff --git a/routes/api.php b/routes/api.php index 2951f2c..7c59abf 100644 --- a/routes/api.php +++ b/routes/api.php @@ -929,6 +929,7 @@ // 자동산출 Route::get('/calculation/schema', [QuoteController::class, 'calculationSchema'])->name('v1.quotes.calculation-schema'); // 입력 스키마 Route::post('/calculate', [QuoteController::class, 'calculate'])->name('v1.quotes.calculate'); // 자동산출 실행 + Route::post('/calculate/bom', [QuoteController::class, 'calculateBom'])->name('v1.quotes.calculate-bom'); // BOM 기반 자동산출 // 문서 관리 Route::post('/{id}/pdf', [QuoteController::class, 'generatePdf'])->whereNumber('id')->name('v1.quotes.pdf'); // PDF 생성