feat: [quote] 견적관리 API 기반 구축 (Phase 1)

- 마이그레이션 생성: quotes, quote_items, quote_revisions 테이블
- Model 생성: Quote, QuoteItem, QuoteRevision
- BelongsToTenant, SoftDeletes 트레이트 적용
- 상태 관리 메서드 및 스코프 구현
- 개발 계획서 작성 및 진행 상황 문서화
This commit is contained in:
2025-12-04 17:17:05 +09:00
parent ccd8b6f81d
commit 96e9a0ba18
5 changed files with 1053 additions and 0 deletions

View File

@@ -0,0 +1,164 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// 1. 견적 마스터 테이블
Schema::create('quotes', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->comment('테넌트 ID');
// 기본 정보
$table->string('quote_number', 50)->comment('견적번호 (예: KD-SC-251204-01)');
$table->date('registration_date')->comment('등록일');
$table->date('receipt_date')->nullable()->comment('접수일');
$table->string('author', 100)->nullable()->comment('작성자');
// 발주처 정보
$table->foreignId('client_id')->nullable()->comment('거래처 ID (FK)');
$table->string('client_name', 100)->nullable()->comment('거래처명 (직접입력 대응)');
$table->string('manager', 100)->nullable()->comment('담당자');
$table->string('contact', 50)->nullable()->comment('연락처');
// 현장 정보
$table->unsignedBigInteger('site_id')->nullable()->comment('현장 ID');
$table->string('site_name', 200)->nullable()->comment('현장명');
$table->string('site_code', 50)->nullable()->comment('현장코드');
// 제품 정보
$table->enum('product_category', ['SCREEN', 'STEEL'])->comment('제품 카테고리');
$table->unsignedBigInteger('product_id')->nullable()->comment('선택된 제품 ID');
$table->string('product_code', 50)->nullable()->comment('제품코드');
$table->string('product_name', 200)->nullable()->comment('제품명');
// 규격 정보
$table->unsignedInteger('open_size_width')->nullable()->comment('오픈사이즈 폭 (mm)');
$table->unsignedInteger('open_size_height')->nullable()->comment('오픈사이즈 높이 (mm)');
$table->unsignedInteger('quantity')->default(1)->comment('수량');
$table->string('unit_symbol', 10)->nullable()->comment('부호');
$table->string('floors', 20)->nullable()->comment('층수');
// 금액 정보
$table->decimal('material_cost', 15, 2)->default(0)->comment('재료비 합계');
$table->decimal('labor_cost', 15, 2)->default(0)->comment('노무비');
$table->decimal('install_cost', 15, 2)->default(0)->comment('설치비');
$table->decimal('subtotal', 15, 2)->default(0)->comment('소계');
$table->decimal('discount_rate', 5, 2)->default(0)->comment('할인율 (%)');
$table->decimal('discount_amount', 15, 2)->default(0)->comment('할인금액');
$table->decimal('total_amount', 15, 2)->default(0)->comment('최종 금액');
// 상태 관리
$table->enum('status', ['draft', 'sent', 'approved', 'rejected', 'finalized', 'converted'])->default('draft')->comment('상태');
$table->unsignedInteger('current_revision')->default(0)->comment('현재 수정 차수');
$table->boolean('is_final')->default(false)->comment('최종확정 여부');
$table->dateTime('finalized_at')->nullable()->comment('확정일시');
$table->foreignId('finalized_by')->nullable()->comment('확정자 ID');
// 기타 정보
$table->date('completion_date')->nullable()->comment('납기일');
$table->text('remarks')->nullable()->comment('비고');
$table->text('memo')->nullable()->comment('메모');
$table->text('notes')->nullable()->comment('특이사항');
// 자동산출 입력값 저장
$table->json('calculation_inputs')->nullable()->comment('자동산출에 사용된 입력값');
// 감사
$table->foreignId('created_by')->nullable()->comment('생성자');
$table->foreignId('updated_by')->nullable()->comment('수정자');
$table->foreignId('deleted_by')->nullable()->comment('삭제자');
$table->timestamps();
$table->softDeletes();
// 인덱스
$table->index('tenant_id', 'idx_quotes_tenant_id');
$table->index('quote_number', 'idx_quotes_quote_number');
$table->index('status', 'idx_quotes_status');
$table->index('client_id', 'idx_quotes_client_id');
$table->index('product_category', 'idx_quotes_product_category');
$table->index('registration_date', 'idx_quotes_registration_date');
$table->unique(['tenant_id', 'quote_number', 'deleted_at'], 'uq_tenant_quote_number');
});
// 2. 견적 품목 테이블
Schema::create('quote_items', function (Blueprint $table) {
$table->id();
$table->foreignId('quote_id')->constrained()->cascadeOnDelete()->comment('견적 ID');
$table->foreignId('tenant_id')->constrained()->comment('테넌트 ID');
// 품목 정보
$table->unsignedBigInteger('item_id')->nullable()->comment('품목마스터 ID');
$table->string('item_code', 50)->comment('품목코드');
$table->string('item_name', 200)->comment('품명');
$table->string('specification', 100)->nullable()->comment('규격');
$table->string('unit', 20)->comment('단위');
// 수량/금액
$table->decimal('base_quantity', 15, 4)->default(1)->comment('기본수량');
$table->decimal('calculated_quantity', 15, 4)->default(1)->comment('계산된 수량');
$table->decimal('unit_price', 15, 2)->default(0)->comment('단가');
$table->decimal('total_price', 15, 2)->default(0)->comment('금액 (수량 × 단가)');
// 수식 정보
$table->string('formula', 500)->nullable()->comment('수식');
$table->string('formula_result', 200)->nullable()->comment('수식 계산 결과 표시');
$table->string('formula_source', 100)->nullable()->comment('수식 출처');
$table->string('formula_category', 50)->nullable()->comment('수식 카테고리');
$table->string('data_source', 200)->nullable()->comment('데이터 출처');
// 기타
$table->date('delivery_date')->nullable()->comment('품목별 납기일');
$table->text('note')->nullable()->comment('비고');
$table->unsignedInteger('sort_order')->default(0)->comment('정렬순서');
$table->timestamps();
// 인덱스
$table->index('quote_id', 'idx_quote_items_quote_id');
$table->index('tenant_id', 'idx_quote_items_tenant_id');
$table->index('item_code', 'idx_quote_items_item_code');
$table->index('sort_order', 'idx_quote_items_sort_order');
});
// 3. 견적 수정 이력 테이블
Schema::create('quote_revisions', function (Blueprint $table) {
$table->id();
$table->foreignId('quote_id')->constrained()->cascadeOnDelete()->comment('견적 ID');
$table->foreignId('tenant_id')->constrained()->comment('테넌트 ID');
$table->unsignedInteger('revision_number')->comment('수정 차수');
$table->date('revision_date')->comment('수정일');
$table->foreignId('revision_by')->comment('수정자 ID');
$table->string('revision_by_name', 100)->nullable()->comment('수정자 이름');
$table->text('revision_reason')->nullable()->comment('수정 사유');
// 이전 버전 데이터 (JSON 스냅샷)
$table->json('previous_data')->comment('수정 전 견적 전체 데이터');
$table->timestamp('created_at')->nullable();
// 인덱스
$table->index('quote_id', 'idx_quote_revisions_quote_id');
$table->index('tenant_id', 'idx_quote_revisions_tenant_id');
$table->index('revision_number', 'idx_quote_revisions_revision_number');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('quote_revisions');
Schema::dropIfExists('quote_items');
Schema::dropIfExists('quotes');
}
};