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'); } };