Files
sam-api/app/Services/Estimate/EstimateService.php
hskwon 2d9217c9b4 feat: 견적 시스템 API
- 5130의 71개 하드코딩 컬럼을 동적 카테고리 필드 시스템으로 전환
- 모터 브라켓 계산 등 핵심 비즈니스 로직 FormulaParser에 통합
- 파라미터 기반 동적 견적 폼 시스템 구축
- 견적 상태 워크플로 (DRAFT → SENT → APPROVED/REJECTED/EXPIRED)
- 모델셋 관리 API: 카테고리+제품+BOM 통합 관리
- 견적 관리 API: 생성/수정/복제/상태변경/미리보기 기능

주요 구현 사항:
- EstimateController/EstimateService: 견적 비즈니스 로직
- ModelSetController/ModelSetService: 모델셋 관리 로직
- Estimate/EstimateItem 모델: 견적 데이터 구조
- 동적 견적 필드 마이그레이션: 스크린/철재 제품 구조
- API 라우트 17개 엔드포인트 추가
- 다국어 메시지 지원 (성공/에러 메시지)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 17:43:29 +09:00

346 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services\Estimate;
use App\Models\Commons\Category;
use App\Models\Estimate\Estimate;
use App\Models\Estimate\EstimateItem;
use App\Services\ModelSet\ModelSetService;
use App\Services\Service;
use App\Services\Calculation\CalculationEngine;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Pagination\LengthAwarePaginator;
class EstimateService extends Service
{
protected ModelSetService $modelSetService;
protected CalculationEngine $calculationEngine;
public function __construct(
ModelSetService $modelSetService,
CalculationEngine $calculationEngine
) {
parent::__construct();
$this->modelSetService = $modelSetService;
$this->calculationEngine = $calculationEngine;
}
/**
* 견적 목록 조회
*/
public function getEstimates(array $filters = []): LengthAwarePaginator
{
$query = Estimate::with(['modelSet', 'items'])
->where('tenant_id', $this->tenantId());
// 필터링
if (!empty($filters['status'])) {
$query->where('status', $filters['status']);
}
if (!empty($filters['customer_name'])) {
$query->where('customer_name', 'like', '%' . $filters['customer_name'] . '%');
}
if (!empty($filters['model_set_id'])) {
$query->where('model_set_id', $filters['model_set_id']);
}
if (!empty($filters['date_from'])) {
$query->whereDate('created_at', '>=', $filters['date_from']);
}
if (!empty($filters['date_to'])) {
$query->whereDate('created_at', '<=', $filters['date_to']);
}
if (!empty($filters['search'])) {
$searchTerm = $filters['search'];
$query->where(function ($q) use ($searchTerm) {
$q->where('estimate_name', 'like', '%' . $searchTerm . '%')
->orWhere('estimate_no', 'like', '%' . $searchTerm . '%')
->orWhere('project_name', 'like', '%' . $searchTerm . '%');
});
}
return $query->orderBy('created_at', 'desc')
->paginate($filters['per_page'] ?? 20);
}
/**
* 견적 상세 조회
*/
public function getEstimateDetail($estimateId): array
{
$estimate = Estimate::with(['modelSet.fields', 'items'])
->where('tenant_id', $this->tenantId())
->findOrFail($estimateId);
return [
'estimate' => $estimate,
'model_set_schema' => $this->modelSetService->getModelSetCategoryFields($estimate->model_set_id),
'calculation_summary' => $this->summarizeCalculations($estimate),
];
}
/**
* 견적 생성
*/
public function createEstimate(array $data): array
{
return DB::transaction(function () use ($data) {
// 견적번호 생성
$estimateNo = Estimate::generateEstimateNo($this->tenantId());
// 모델셋 기반 BOM 계산
$bomCalculation = $this->modelSetService->calculateModelSetBom(
$data['model_set_id'],
$data['parameters']
);
// 견적 생성
$estimate = Estimate::create([
'tenant_id' => $this->tenantId(),
'model_set_id' => $data['model_set_id'],
'estimate_no' => $estimateNo,
'estimate_name' => $data['estimate_name'],
'customer_name' => $data['customer_name'] ?? null,
'project_name' => $data['project_name'] ?? null,
'parameters' => $data['parameters'],
'calculated_results' => $bomCalculation['calculated_values'] ?? [],
'bom_data' => $bomCalculation,
'total_amount' => $bomCalculation['total_amount'] ?? 0,
'notes' => $data['notes'] ?? null,
'valid_until' => now()->addDays(30), // 기본 30일 유효
'created_by' => $this->apiUserId(),
]);
// 견적 항목 생성 (BOM 기반)
if (!empty($bomCalculation['bom_items'])) {
$this->createEstimateItems($estimate, $bomCalculation['bom_items']);
}
return $this->getEstimateDetail($estimate->id);
});
}
/**
* 견적 수정
*/
public function updateEstimate($estimateId, array $data): array
{
return DB::transaction(function () use ($estimateId, $data) {
$estimate = Estimate::where('tenant_id', $this->tenantId())
->findOrFail($estimateId);
// 파라미터가 변경되면 재계산
if (isset($data['parameters'])) {
$bomCalculation = $this->modelSetService->calculateModelSetBom(
$estimate->model_set_id,
$data['parameters']
);
$data['calculated_results'] = $bomCalculation['calculated_values'] ?? [];
$data['bom_data'] = $bomCalculation;
$data['total_amount'] = $bomCalculation['total_amount'] ?? 0;
// 기존 견적 항목 삭제 후 재생성
$estimate->items()->delete();
if (!empty($bomCalculation['bom_items'])) {
$this->createEstimateItems($estimate, $bomCalculation['bom_items']);
}
}
$estimate->update([
...$data,
'updated_by' => $this->apiUserId(),
]);
return $this->getEstimateDetail($estimate->id);
});
}
/**
* 견적 삭제
*/
public function deleteEstimate($estimateId): void
{
DB::transaction(function () use ($estimateId) {
$estimate = Estimate::where('tenant_id', $this->tenantId())
->findOrFail($estimateId);
// 진행 중인 견적은 삭제 불가
if (in_array($estimate->status, ['SENT', 'APPROVED'])) {
throw new \Exception(__('error.estimate.cannot_delete_sent_or_approved'));
}
$estimate->update(['deleted_by' => $this->apiUserId()]);
$estimate->delete();
});
}
/**
* 견적 복제
*/
public function cloneEstimate($estimateId, array $data): array
{
return DB::transaction(function () use ($estimateId, $data) {
$originalEstimate = Estimate::with('items')
->where('tenant_id', $this->tenantId())
->findOrFail($estimateId);
// 새 견적번호 생성
$newEstimateNo = Estimate::generateEstimateNo($this->tenantId());
// 견적 복제
$newEstimate = Estimate::create([
'tenant_id' => $this->tenantId(),
'model_set_id' => $originalEstimate->model_set_id,
'estimate_no' => $newEstimateNo,
'estimate_name' => $data['estimate_name'],
'customer_name' => $data['customer_name'] ?? $originalEstimate->customer_name,
'project_name' => $data['project_name'] ?? $originalEstimate->project_name,
'parameters' => $originalEstimate->parameters,
'calculated_results' => $originalEstimate->calculated_results,
'bom_data' => $originalEstimate->bom_data,
'total_amount' => $originalEstimate->total_amount,
'notes' => $data['notes'] ?? $originalEstimate->notes,
'valid_until' => now()->addDays(30),
'created_by' => $this->apiUserId(),
]);
// 견적 항목 복제
foreach ($originalEstimate->items as $item) {
EstimateItem::create([
'tenant_id' => $this->tenantId(),
'estimate_id' => $newEstimate->id,
'sequence' => $item->sequence,
'item_name' => $item->item_name,
'item_description' => $item->item_description,
'parameters' => $item->parameters,
'calculated_values' => $item->calculated_values,
'unit_price' => $item->unit_price,
'quantity' => $item->quantity,
'total_price' => $item->total_price,
'bom_components' => $item->bom_components,
'notes' => $item->notes,
'created_by' => $this->apiUserId(),
]);
}
return $this->getEstimateDetail($newEstimate->id);
});
}
/**
* 견적 상태 변경
*/
public function changeEstimateStatus($estimateId, string $status, ?string $notes = null): array
{
$estimate = Estimate::where('tenant_id', $this->tenantId())
->findOrFail($estimateId);
$validTransitions = [
'DRAFT' => ['SENT', 'REJECTED'],
'SENT' => ['APPROVED', 'REJECTED', 'EXPIRED'],
'APPROVED' => ['EXPIRED'],
'REJECTED' => ['DRAFT'],
'EXPIRED' => ['DRAFT'],
];
if (!in_array($status, $validTransitions[$estimate->status] ?? [])) {
throw new \Exception(__('error.estimate.invalid_status_transition'));
}
$estimate->update([
'status' => $status,
'notes' => $notes ? ($estimate->notes . "\n\n" . $notes) : $estimate->notes,
'updated_by' => $this->apiUserId(),
]);
return $this->getEstimateDetail($estimate->id);
}
/**
* 동적 견적 폼 스키마 조회
*/
public function getEstimateFormSchema($modelSetId): array
{
$parameters = $this->modelSetService->getEstimateParameters($modelSetId);
return [
'model_set' => $parameters['category'],
'form_schema' => [
'input_fields' => $parameters['input_fields'],
'calculated_fields' => $parameters['calculated_fields'],
],
'calculation_schema' => $parameters['calculation_schema'],
];
}
/**
* 견적 파라미터 미리보기 계산
*/
public function previewCalculation($modelSetId, array $parameters): array
{
return $this->modelSetService->calculateModelSetBom($modelSetId, $parameters);
}
/**
* 견적 항목 생성
*/
protected function createEstimateItems(Estimate $estimate, array $bomItems): void
{
foreach ($bomItems as $index => $bomItem) {
EstimateItem::create([
'tenant_id' => $this->tenantId(),
'estimate_id' => $estimate->id,
'sequence' => $index + 1,
'item_name' => $bomItem['name'] ?? '견적 항목 ' . ($index + 1),
'item_description' => $bomItem['description'] ?? '',
'parameters' => $bomItem['parameters'] ?? [],
'calculated_values' => $bomItem['calculated_values'] ?? [],
'unit_price' => $bomItem['unit_price'] ?? 0,
'quantity' => $bomItem['quantity'] ?? 1,
'bom_components' => $bomItem['components'] ?? [],
'created_by' => $this->apiUserId(),
]);
}
}
/**
* 계산 결과 요약
*/
protected function summarizeCalculations(Estimate $estimate): array
{
$summary = [
'total_items' => $estimate->items->count(),
'total_amount' => $estimate->total_amount,
'key_calculations' => [],
];
// 주요 계산 결과 추출
if (!empty($estimate->calculated_results)) {
$results = $estimate->calculated_results;
if (isset($results['W1'], $results['H1'])) {
$summary['key_calculations']['제작사이즈'] = $results['W1'] . ' × ' . $results['H1'] . ' mm';
}
if (isset($results['weight'])) {
$summary['key_calculations']['중량'] = $results['weight'] . ' kg';
}
if (isset($results['area'])) {
$summary['key_calculations']['면적'] = $results['area'] . ' ㎡';
}
if (isset($results['bracket_size'])) {
$summary['key_calculations']['모터브라켓'] = $results['bracket_size'];
}
}
return $summary;
}
}