feat: BOM 기반 견적 계산 API 엔드포인트 추가
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 로직 재구현
|
||||
|
||||
### 작업 목표
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동산출 입력 스키마 조회
|
||||
*/
|
||||
|
||||
90
app/Http/Requests/Quote/QuoteBomCalculateRequest.php
Normal file
90
app/Http/Requests/Quote/QuoteBomCalculateRequest.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Quote;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* BOM 기반 견적 산출 요청
|
||||
*
|
||||
* React 견적등록 화면에서 자동 견적 산출 시 사용됩니다.
|
||||
* 완제품 코드와 입력 변수를 받아 BOM 기반으로 품목/단가/금액을 계산합니다.
|
||||
*/
|
||||
class QuoteBomCalculateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// 필수 입력
|
||||
'finished_goods_code' => '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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 품목 재계산 (기존 견적 기준)
|
||||
*/
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 생성
|
||||
|
||||
Reference in New Issue
Block a user