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>
This commit is contained in:
2025-09-24 17:41:26 +09:00
parent eb42d11f5e
commit 2d9217c9b4
14 changed files with 2021 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('estimates', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('model_set_id')->comment('모델셋(카테고리) ID');
$table->string('estimate_no', 50)->comment('견적번호 (자동생성)');
$table->string('estimate_name')->comment('견적명');
$table->string('customer_name')->nullable()->comment('고객명');
$table->string('project_name')->nullable()->comment('프로젝트명');
// 견적 파라미터 (사용자 입력값들)
$table->json('parameters')->comment('견적 파라미터 (W0, H0, 수량 등)');
// 계산 결과 (W1, H1, 중량, 면적 등)
$table->json('calculated_results')->nullable()->comment('계산 결과값들');
// BOM 데이터 (계산된 BOM 정보)
$table->json('bom_data')->nullable()->comment('BOM 계산 결과');
$table->decimal('total_amount', 15, 2)->nullable()->comment('총 견적금액');
$table->enum('status', ['DRAFT', 'SENT', 'APPROVED', 'REJECTED', 'EXPIRED'])
->default('DRAFT')->comment('견적 상태');
$table->text('notes')->nullable()->comment('비고');
$table->date('valid_until')->nullable()->comment('견적 유효기간');
// 공통 감사 필드
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID');
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID');
$table->timestamps();
$table->softDeletes();
// 인덱스
$table->index(['tenant_id', 'status']);
$table->index(['tenant_id', 'created_at']);
$table->index(['tenant_id', 'model_set_id']);
$table->unique(['tenant_id', 'estimate_no']);
// 외래키
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
$table->foreign('model_set_id')->references('id')->on('categories')->onDelete('restrict');
});
Schema::create('estimate_items', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('estimate_id')->comment('견적 ID');
$table->integer('sequence')->default(1)->comment('항목 순번');
$table->string('item_name')->comment('항목명');
$table->text('item_description')->nullable()->comment('항목 설명');
// 항목별 파라미터 (개별 제품 파라미터)
$table->json('parameters')->comment('항목별 파라미터');
// 항목별 계산 결과
$table->json('calculated_values')->nullable()->comment('항목별 계산값');
$table->decimal('unit_price', 12, 2)->default(0)->comment('단가');
$table->decimal('quantity', 8, 2)->default(1)->comment('수량');
$table->decimal('total_price', 15, 2)->default(0)->comment('총 가격 (단가 × 수량)');
// BOM 구성품 정보
$table->json('bom_components')->nullable()->comment('BOM 구성품 목록');
$table->text('notes')->nullable()->comment('항목별 비고');
// 공통 감사 필드
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID');
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID');
$table->timestamps();
$table->softDeletes();
// 인덱스
$table->index(['tenant_id', 'estimate_id']);
$table->index(['estimate_id', 'sequence']);
// 외래키
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
$table->foreign('estimate_id')->references('id')->on('estimates')->onDelete('cascade');
});
}
public function down(): void
{
Schema::dropIfExists('estimate_items');
Schema::dropIfExists('estimates');
}
};