merge: origin/develop 병합
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
<?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
|
||||
{
|
||||
// 문서양식 기본 테이블
|
||||
Schema::create('document_templates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('name', 100)->comment('양식명');
|
||||
$table->string('category', 50)->nullable()->comment('분류 (품질, 생산 등)');
|
||||
$table->string('title', 200)->nullable()->comment('문서 제목');
|
||||
$table->string('company_name', 100)->nullable()->comment('회사명');
|
||||
$table->string('company_address', 255)->nullable()->comment('회사 주소');
|
||||
$table->string('company_contact', 100)->nullable()->comment('회사 연락처');
|
||||
$table->string('footer_remark_label', 50)->default('부적합 내용')->comment('비고 라벨');
|
||||
$table->string('footer_judgement_label', 50)->default('종합판정')->comment('판정 라벨');
|
||||
$table->json('footer_judgement_options')->nullable()->comment('판정 옵션 (적합/부적합 등)');
|
||||
$table->boolean('is_active')->default(true)->comment('활성 여부');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['tenant_id', 'category']);
|
||||
$table->index(['tenant_id', 'is_active']);
|
||||
});
|
||||
|
||||
// 결재라인
|
||||
Schema::create('document_template_approval_lines', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('template_id')->constrained('document_templates')->cascadeOnDelete();
|
||||
$table->string('name', 50)->comment('결재 단계명 (작성, 검토, 승인)');
|
||||
$table->string('dept', 50)->nullable()->comment('부서');
|
||||
$table->string('role', 50)->nullable()->comment('직책/담당자');
|
||||
$table->unsignedSmallInteger('sort_order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['template_id', 'sort_order']);
|
||||
});
|
||||
|
||||
// 기본 필드 (품명, LOT NO 등)
|
||||
Schema::create('document_template_basic_fields', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('template_id')->constrained('document_templates')->cascadeOnDelete();
|
||||
$table->string('label', 50)->comment('필드 라벨');
|
||||
$table->string('field_type', 20)->default('text')->comment('필드 타입 (text, date 등)');
|
||||
$table->string('default_value', 255)->nullable()->comment('기본값');
|
||||
$table->unsignedSmallInteger('sort_order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['template_id', 'sort_order']);
|
||||
});
|
||||
|
||||
// 검사 기준서 섹션
|
||||
Schema::create('document_template_sections', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('template_id')->constrained('document_templates')->cascadeOnDelete();
|
||||
$table->string('title', 100)->comment('섹션 제목 (가이드레일, 연기차단재 등)');
|
||||
$table->string('image_path', 255)->nullable()->comment('도해 이미지 경로');
|
||||
$table->unsignedSmallInteger('sort_order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['template_id', 'sort_order']);
|
||||
});
|
||||
|
||||
// 검사 기준서 섹션 항목
|
||||
Schema::create('document_template_section_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('section_id')->constrained('document_template_sections')->cascadeOnDelete();
|
||||
$table->string('category', 50)->nullable()->comment('구분 (겉모양, 치수 등)');
|
||||
$table->string('item', 100)->comment('검사항목');
|
||||
$table->string('standard', 255)->nullable()->comment('검사기준');
|
||||
$table->string('method', 50)->nullable()->comment('검사방법');
|
||||
$table->string('frequency', 50)->nullable()->comment('검사주기');
|
||||
$table->string('regulation', 100)->nullable()->comment('관련규정');
|
||||
$table->unsignedSmallInteger('sort_order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['section_id', 'sort_order']);
|
||||
});
|
||||
|
||||
// 검사 데이터 테이블 컬럼 설정
|
||||
Schema::create('document_template_columns', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('template_id')->constrained('document_templates')->cascadeOnDelete();
|
||||
$table->string('label', 50)->comment('컬럼명');
|
||||
$table->string('width', 20)->default('100px')->comment('컬럼 너비');
|
||||
$table->string('column_type', 20)->default('text')->comment('타입 (text, check, measurement, select, complex)');
|
||||
$table->string('group_name', 50)->nullable()->comment('그룹명 (상단 헤더 병합용)');
|
||||
$table->json('sub_labels')->nullable()->comment('서브라벨 배열');
|
||||
$table->unsignedSmallInteger('sort_order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['template_id', 'sort_order']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('document_template_columns');
|
||||
Schema::dropIfExists('document_template_section_items');
|
||||
Schema::dropIfExists('document_template_sections');
|
||||
Schema::dropIfExists('document_template_basic_fields');
|
||||
Schema::dropIfExists('document_template_approval_lines');
|
||||
Schema::dropIfExists('document_templates');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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
|
||||
{
|
||||
Schema::table('attendance_settings', function (Blueprint $table) {
|
||||
$table->boolean('use_auto')->default(false)->after('use_gps')->comment('자동 출퇴근 사용 여부');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('attendance_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('use_auto');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
<?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
|
||||
{
|
||||
Schema::create('global_categories', function (Blueprint $table) {
|
||||
$table->id()->comment('글로벌 카테고리 PK');
|
||||
$table->unsignedBigInteger('parent_id')->nullable()->comment('상위 카테고리ID(NULL=최상위)');
|
||||
$table->string('code_group', 30)->comment('코드 그룹');
|
||||
$table->string('profile_code', 30)->nullable()->comment('역할 프로필 코드');
|
||||
$table->string('code', 30)->comment('카테고리 코드(영문, 고유)');
|
||||
$table->string('name', 100)->comment('카테고리명');
|
||||
$table->boolean('is_active')->default(true)->comment('활성여부');
|
||||
$table->integer('sort_order')->default(1)->comment('정렬순서');
|
||||
$table->string('description', 255)->nullable()->comment('비고');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('등록자');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
|
||||
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 인덱스
|
||||
$table->unique(['code_group', 'code'], 'uq_global_codegroup_code');
|
||||
$table->index('parent_id', 'idx_global_parent_id');
|
||||
$table->index('code_group', 'idx_global_code_group');
|
||||
|
||||
// 자기 참조 FK
|
||||
$table->foreign('parent_id', 'fk_global_category_parent')
|
||||
->references('id')
|
||||
->on('global_categories')
|
||||
->onDelete('set null');
|
||||
});
|
||||
|
||||
// tenant 1의 카테고리를 글로벌 카테고리로 복사
|
||||
$this->seedFromTenant1();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('global_categories');
|
||||
}
|
||||
|
||||
/**
|
||||
* tenant 1의 카테고리를 글로벌로 복사
|
||||
*/
|
||||
private function seedFromTenant1(): void
|
||||
{
|
||||
$categories = \DB::table('categories')
|
||||
->where('tenant_id', 1)
|
||||
->whereNull('deleted_at')
|
||||
->orderBy('parent_id')
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
if ($categories->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$idMap = []; // old_id => new_id
|
||||
|
||||
// 1단계: parent_id가 NULL인 루트 카테고리
|
||||
foreach ($categories->where('parent_id', null) as $cat) {
|
||||
$newId = \DB::table('global_categories')->insertGetId([
|
||||
'parent_id' => null,
|
||||
'code_group' => $cat->code_group,
|
||||
'profile_code' => $cat->profile_code,
|
||||
'code' => $cat->code,
|
||||
'name' => $cat->name,
|
||||
'is_active' => $cat->is_active,
|
||||
'sort_order' => $cat->sort_order,
|
||||
'description' => $cat->description,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$idMap[$cat->id] = $newId;
|
||||
}
|
||||
|
||||
// 2단계: 자식 카테고리 (재귀적으로 처리)
|
||||
$remaining = $categories->whereNotNull('parent_id');
|
||||
$maxIterations = 10;
|
||||
$iteration = 0;
|
||||
|
||||
while ($remaining->isNotEmpty() && $iteration < $maxIterations) {
|
||||
$processed = [];
|
||||
foreach ($remaining as $cat) {
|
||||
if (isset($idMap[$cat->parent_id])) {
|
||||
$newId = \DB::table('global_categories')->insertGetId([
|
||||
'parent_id' => $idMap[$cat->parent_id],
|
||||
'code_group' => $cat->code_group,
|
||||
'profile_code' => $cat->profile_code,
|
||||
'code' => $cat->code,
|
||||
'name' => $cat->name,
|
||||
'is_active' => $cat->is_active,
|
||||
'sort_order' => $cat->sort_order,
|
||||
'description' => $cat->description,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$idMap[$cat->id] = $newId;
|
||||
$processed[] = $cat->id;
|
||||
}
|
||||
}
|
||||
$remaining = $remaining->whereNotIn('id', $processed);
|
||||
$iteration++;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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
|
||||
{
|
||||
Schema::table('today_issues', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('target_user_id')
|
||||
->nullable()
|
||||
->after('source_id')
|
||||
->comment('특정 대상 사용자 ID (null이면 테넌트 전체)');
|
||||
|
||||
$table->foreign('target_user_id')
|
||||
->references('id')
|
||||
->on('users')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->index(['tenant_id', 'target_user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('today_issues', function (Blueprint $table) {
|
||||
$table->dropForeign(['target_user_id']);
|
||||
$table->dropIndex(['tenant_id', 'target_user_id']);
|
||||
$table->dropColumn('target_user_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
115
database/migrations/2026_01_28_200000_create_documents_table.php
Normal file
115
database/migrations/2026_01_28_200000_create_documents_table.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?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
|
||||
{
|
||||
// 실제 문서
|
||||
Schema::create('documents', function (Blueprint $table) {
|
||||
$table->id()->comment('ID');
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete()->comment('테넌트 ID');
|
||||
$table->foreignId('template_id')->constrained('document_templates')->comment('템플릿 ID');
|
||||
|
||||
// 문서 정보
|
||||
$table->string('document_no', 50)->comment('문서번호');
|
||||
$table->string('title', 255)->comment('문서 제목');
|
||||
$table->enum('status', ['DRAFT', 'PENDING', 'APPROVED', 'REJECTED', 'CANCELLED'])
|
||||
->default('DRAFT')->comment('상태 (DRAFT:임시저장, PENDING:결재중, APPROVED:승인, REJECTED:반려, CANCELLED:취소)');
|
||||
|
||||
// 연결 정보 (다형성)
|
||||
$table->string('linkable_type', 100)->nullable()->comment('연결 모델 타입');
|
||||
$table->unsignedBigInteger('linkable_id')->nullable()->comment('연결 모델 ID');
|
||||
|
||||
// 결재 정보
|
||||
$table->timestamp('submitted_at')->nullable()->comment('결재 요청일');
|
||||
$table->timestamp('completed_at')->nullable()->comment('결재 완료일');
|
||||
|
||||
// 감사 컬럼
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->comment('생성자 ID');
|
||||
$table->foreignId('updated_by')->nullable()->constrained('users')->comment('수정자 ID');
|
||||
$table->foreignId('deleted_by')->nullable()->constrained('users')->comment('삭제자 ID');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 인덱스
|
||||
$table->index(['tenant_id', 'status']);
|
||||
$table->index('document_no');
|
||||
$table->index(['linkable_type', 'linkable_id']);
|
||||
});
|
||||
|
||||
// 문서 결재
|
||||
Schema::create('document_approvals', function (Blueprint $table) {
|
||||
$table->id()->comment('ID');
|
||||
$table->foreignId('document_id')->constrained()->cascadeOnDelete()->comment('문서 ID');
|
||||
$table->foreignId('user_id')->constrained()->comment('결재자 ID');
|
||||
|
||||
$table->unsignedTinyInteger('step')->default(1)->comment('결재 순서');
|
||||
$table->string('role', 50)->comment('역할 (작성/검토/승인)');
|
||||
$table->enum('status', ['PENDING', 'APPROVED', 'REJECTED'])
|
||||
->default('PENDING')->comment('상태 (PENDING:대기, APPROVED:승인, REJECTED:반려)');
|
||||
|
||||
$table->text('comment')->nullable()->comment('결재 의견');
|
||||
$table->timestamp('acted_at')->nullable()->comment('결재 처리일');
|
||||
|
||||
// 감사 컬럼
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->comment('생성자 ID');
|
||||
$table->foreignId('updated_by')->nullable()->constrained('users')->comment('수정자 ID');
|
||||
$table->timestamps();
|
||||
|
||||
// 인덱스
|
||||
$table->index(['document_id', 'step']);
|
||||
$table->index(['user_id', 'status']);
|
||||
});
|
||||
|
||||
// 문서 데이터 (EAV 패턴)
|
||||
Schema::create('document_data', function (Blueprint $table) {
|
||||
$table->id()->comment('ID');
|
||||
$table->foreignId('document_id')->constrained()->cascadeOnDelete()->comment('문서 ID');
|
||||
|
||||
$table->unsignedBigInteger('section_id')->nullable()->comment('섹션 ID (document_template_sections 참조)');
|
||||
$table->unsignedBigInteger('column_id')->nullable()->comment('컬럼 ID (document_template_columns 참조)');
|
||||
$table->unsignedSmallInteger('row_index')->default(0)->comment('행 인덱스 (테이블 데이터용)');
|
||||
|
||||
$table->string('field_key', 100)->comment('필드 키');
|
||||
$table->text('field_value')->nullable()->comment('필드 값');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// 인덱스
|
||||
$table->index(['document_id', 'section_id']);
|
||||
$table->index(['document_id', 'field_key']);
|
||||
});
|
||||
|
||||
// 문서 첨부파일
|
||||
Schema::create('document_attachments', function (Blueprint $table) {
|
||||
$table->id()->comment('ID');
|
||||
$table->foreignId('document_id')->constrained()->cascadeOnDelete()->comment('문서 ID');
|
||||
$table->foreignId('file_id')->constrained('files')->comment('파일 ID');
|
||||
|
||||
$table->string('attachment_type', 50)->default('general')->comment('첨부 유형 (general, signature, image 등)');
|
||||
$table->string('description', 255)->nullable()->comment('설명');
|
||||
|
||||
// 감사 컬럼
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->comment('생성자 ID');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('document_attachments');
|
||||
Schema::dropIfExists('document_data');
|
||||
Schema::dropIfExists('document_approvals');
|
||||
Schema::dropIfExists('documents');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* 재고 입출고 거래 이력 테이블
|
||||
*
|
||||
* 모든 재고 변동을 스택으로 쌓아 이력 관리합니다.
|
||||
* audit_logs와 별개로 재고 전용 거래 이력을 제공합니다.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('stock_transactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
|
||||
// 재고 참조
|
||||
$table->unsignedBigInteger('stock_id')->comment('재고 ID');
|
||||
$table->unsignedBigInteger('stock_lot_id')->nullable()->comment('LOT ID (입고/출고 시)');
|
||||
|
||||
// 거래 유형
|
||||
$table->string('type', 20)->comment('거래유형: IN(입고), OUT(출고), RESERVE(예약), RELEASE(예약해제)');
|
||||
|
||||
// 수량
|
||||
$table->decimal('qty', 15, 3)->comment('변동 수량 (양수: 증가, 음수: 감소)');
|
||||
$table->decimal('balance_qty', 15, 3)->comment('거래 후 재고 잔량');
|
||||
|
||||
// 참조 정보 (다형성)
|
||||
$table->string('reference_type', 50)->nullable()->comment('참조 유형: receiving, work_order, shipment, order');
|
||||
$table->unsignedBigInteger('reference_id')->nullable()->comment('참조 ID');
|
||||
|
||||
// 상세 정보
|
||||
$table->string('lot_no', 50)->nullable()->comment('LOT번호');
|
||||
$table->string('reason', 50)->nullable()->comment('사유: receiving, work_order_input, shipment, order_confirm, order_cancel');
|
||||
$table->string('remark', 500)->nullable()->comment('비고');
|
||||
|
||||
// 품목 스냅샷 (조회 성능용)
|
||||
$table->string('item_code', 50)->comment('품목코드');
|
||||
$table->string('item_name', 200)->comment('품목명');
|
||||
|
||||
// 감사 정보
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자');
|
||||
$table->timestamp('created_at')->useCurrent()->comment('생성일시');
|
||||
|
||||
// 인덱스
|
||||
$table->index('tenant_id');
|
||||
$table->index('stock_id');
|
||||
$table->index('stock_lot_id');
|
||||
$table->index('type');
|
||||
$table->index('reference_type');
|
||||
$table->index(['stock_id', 'created_at']);
|
||||
$table->index(['tenant_id', 'item_code']);
|
||||
$table->index(['tenant_id', 'type', 'created_at']);
|
||||
$table->index(['reference_type', 'reference_id']);
|
||||
|
||||
// 외래키
|
||||
$table->foreign('stock_id')->references('id')->on('stocks')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('stock_transactions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* 경동기업 전용 단가 테이블
|
||||
*
|
||||
* 5130 레거시 시스템의 price_* 테이블 데이터를 저장
|
||||
* - price_motor: 모터/제어기 단가
|
||||
* - price_shaft: 샤프트 계산 참조
|
||||
* - price_pipe: 파이프 계산 참조
|
||||
* - price_angle: 앵글 계산 참조
|
||||
* - price_raw_materials: 원자재 단가
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('kd_price_tables', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->default(287)->comment('경동기업 테넌트 ID');
|
||||
$table->string('table_type', 50)->comment('테이블 유형: motor, shaft, pipe, angle, raw_material, bdmodels');
|
||||
$table->string('item_code', 100)->nullable()->comment('품목 코드 (연동용)');
|
||||
$table->string('item_name', 200)->nullable()->comment('품목명');
|
||||
|
||||
// 조회 조건 필드들
|
||||
$table->string('category', 100)->nullable()->comment('분류 (모터용량, 재질, 타입 등)');
|
||||
$table->string('spec1', 100)->nullable()->comment('규격1 (사이즈, 두께 등)');
|
||||
$table->string('spec2', 100)->nullable()->comment('규격2 (길이, 브라켓크기 등)');
|
||||
$table->string('spec3', 100)->nullable()->comment('규격3 (추가 조건)');
|
||||
|
||||
// 단가 정보
|
||||
$table->decimal('unit_price', 15, 2)->default(0)->comment('단가');
|
||||
$table->string('unit', 20)->default('EA')->comment('단위');
|
||||
|
||||
// 원본 JSON 데이터 (레거시 호환용)
|
||||
$table->json('raw_data')->nullable()->comment('원본 JSON 데이터');
|
||||
|
||||
// 메타 정보
|
||||
$table->boolean('is_active')->default(true)->comment('활성 여부');
|
||||
$table->timestamps();
|
||||
|
||||
// 인덱스
|
||||
$table->index(['tenant_id', 'table_type']);
|
||||
$table->index(['table_type', 'category']);
|
||||
$table->index(['table_type', 'spec1', 'spec2']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('kd_price_tables');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_definitions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code', 100)->unique()->comment('통계 코드 (sales_daily_revenue)');
|
||||
$table->string('domain', 50)->index()->comment('도메인 (sales, finance, production)');
|
||||
$table->string('name', 200)->comment('통계명 (일일 매출액)');
|
||||
$table->text('description')->nullable();
|
||||
$table->json('source_tables')->comment('원본 테이블 목록');
|
||||
$table->string('aggregation', 20)->default('daily')->index()->comment('집계 주기');
|
||||
$table->text('query_template')->nullable()->comment('집계 SQL 템플릿');
|
||||
$table->boolean('is_active')->default(true)->index();
|
||||
$table->json('config')->nullable()->comment('추가 설정 (임계값, 단위 등)');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_definitions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_job_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->string('job_type', 100)->comment('작업 유형 (sales_daily, finance_monthly)');
|
||||
$table->date('target_date')->comment('집계 대상 날짜');
|
||||
$table->enum('status', ['pending', 'running', 'completed', 'failed'])->default('pending');
|
||||
$table->unsignedInteger('records_processed')->default(0);
|
||||
$table->text('error_message')->nullable();
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->unsignedInteger('duration_ms')->nullable();
|
||||
$table->timestamp('created_at')->nullable();
|
||||
|
||||
$table->index(['tenant_id', 'job_type']);
|
||||
$table->index('status');
|
||||
$table->index('target_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_job_logs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('dim_date', function (Blueprint $table) {
|
||||
$table->date('date_key')->primary()->comment('날짜 키');
|
||||
$table->smallInteger('year')->comment('연도');
|
||||
$table->tinyInteger('quarter')->comment('분기 (1~4)');
|
||||
$table->tinyInteger('month')->comment('월');
|
||||
$table->tinyInteger('week')->comment('ISO 주차');
|
||||
$table->tinyInteger('day_of_week')->comment('요일 (1:월~7:일)');
|
||||
$table->tinyInteger('day_of_month')->comment('일');
|
||||
$table->boolean('is_weekend')->comment('주말 여부');
|
||||
$table->boolean('is_holiday')->default(false)->comment('공휴일 여부');
|
||||
$table->string('holiday_name', 100)->nullable()->comment('공휴일명');
|
||||
$table->smallInteger('fiscal_year')->nullable()->comment('회계연도');
|
||||
$table->tinyInteger('fiscal_quarter')->nullable()->comment('회계분기');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('dim_date');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_sales_daily', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->date('stat_date');
|
||||
|
||||
// 수주
|
||||
$table->unsignedInteger('order_count')->default(0)->comment('신규 수주 건수');
|
||||
$table->decimal('order_amount', 18, 2)->default(0)->comment('수주 금액');
|
||||
$table->unsignedInteger('order_item_count')->default(0)->comment('수주 품목 수');
|
||||
|
||||
// 매출
|
||||
$table->unsignedInteger('sales_count')->default(0)->comment('매출 건수');
|
||||
$table->decimal('sales_amount', 18, 2)->default(0)->comment('매출 금액');
|
||||
$table->decimal('sales_tax_amount', 18, 2)->default(0)->comment('세액');
|
||||
|
||||
// 고객
|
||||
$table->unsignedInteger('new_client_count')->default(0)->comment('신규 고객 수');
|
||||
$table->unsignedInteger('active_client_count')->default(0)->comment('활성 고객 수');
|
||||
|
||||
// 수주 상태별 건수
|
||||
$table->unsignedInteger('order_draft_count')->default(0);
|
||||
$table->unsignedInteger('order_confirmed_count')->default(0);
|
||||
$table->unsignedInteger('order_in_progress_count')->default(0);
|
||||
$table->unsignedInteger('order_completed_count')->default(0);
|
||||
$table->unsignedInteger('order_cancelled_count')->default(0);
|
||||
|
||||
// 출하
|
||||
$table->unsignedInteger('shipment_count')->default(0);
|
||||
$table->decimal('shipment_amount', 18, 2)->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_date'], 'uk_tenant_date');
|
||||
$table->index('stat_date');
|
||||
$table->index('tenant_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_sales_daily');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_finance_daily', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->date('stat_date');
|
||||
|
||||
// 입출금
|
||||
$table->unsignedInteger('deposit_count')->default(0);
|
||||
$table->decimal('deposit_amount', 18, 2)->default(0);
|
||||
$table->unsignedInteger('withdrawal_count')->default(0);
|
||||
$table->decimal('withdrawal_amount', 18, 2)->default(0);
|
||||
$table->decimal('net_cashflow', 18, 2)->default(0)->comment('입금 - 출금');
|
||||
|
||||
// 매입
|
||||
$table->unsignedInteger('purchase_count')->default(0);
|
||||
$table->decimal('purchase_amount', 18, 2)->default(0);
|
||||
$table->decimal('purchase_tax_amount', 18, 2)->default(0);
|
||||
|
||||
// 미수/미지급
|
||||
$table->decimal('receivable_balance', 18, 2)->default(0)->comment('미수금 잔액');
|
||||
$table->decimal('payable_balance', 18, 2)->default(0)->comment('미지급 잔액');
|
||||
$table->decimal('overdue_receivable', 18, 2)->default(0)->comment('연체 미수금');
|
||||
|
||||
// 어음
|
||||
$table->unsignedInteger('bill_issued_count')->default(0);
|
||||
$table->decimal('bill_issued_amount', 18, 2)->default(0);
|
||||
$table->unsignedInteger('bill_matured_count')->default(0);
|
||||
$table->decimal('bill_matured_amount', 18, 2)->default(0);
|
||||
|
||||
// 카드
|
||||
$table->unsignedInteger('card_transaction_count')->default(0);
|
||||
$table->decimal('card_transaction_amount', 18, 2)->default(0);
|
||||
|
||||
// 은행
|
||||
$table->decimal('bank_balance_total', 18, 2)->default(0)->comment('전 계좌 잔액 합계');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_date'], 'uk_tenant_date');
|
||||
$table->index('stat_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_finance_daily');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_finance_monthly', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->smallInteger('stat_year');
|
||||
$table->tinyInteger('stat_month');
|
||||
|
||||
$table->decimal('deposit_total', 18, 2)->default(0);
|
||||
$table->decimal('withdrawal_total', 18, 2)->default(0);
|
||||
$table->decimal('net_cashflow', 18, 2)->default(0);
|
||||
$table->decimal('purchase_total', 18, 2)->default(0);
|
||||
$table->decimal('card_total', 18, 2)->default(0);
|
||||
|
||||
$table->decimal('receivable_end', 18, 2)->default(0)->comment('월말 미수금');
|
||||
$table->decimal('payable_end', 18, 2)->default(0)->comment('월말 미지급');
|
||||
$table->decimal('bank_balance_end', 18, 2)->default(0)->comment('월말 잔액');
|
||||
|
||||
$table->decimal('mom_cashflow_change', 8, 2)->nullable()->comment('전월 대비 현금흐름 변화 (%)');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_year', 'stat_month'], 'uk_tenant_year_month');
|
||||
$table->index(['stat_year', 'stat_month']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_finance_monthly');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_sales_monthly', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->smallInteger('stat_year');
|
||||
$table->tinyInteger('stat_month');
|
||||
|
||||
// 일일 합산
|
||||
$table->unsignedInteger('order_count')->default(0);
|
||||
$table->decimal('order_amount', 18, 2)->default(0);
|
||||
$table->unsignedInteger('sales_count')->default(0);
|
||||
$table->decimal('sales_amount', 18, 2)->default(0);
|
||||
$table->unsignedInteger('shipment_count')->default(0);
|
||||
$table->decimal('shipment_amount', 18, 2)->default(0);
|
||||
|
||||
// 월간 고유 지표
|
||||
$table->unsignedInteger('unique_client_count')->default(0)->comment('거래 고객 수');
|
||||
$table->decimal('avg_order_amount', 18, 2)->default(0)->comment('평균 수주 금액');
|
||||
$table->unsignedBigInteger('top_client_id')->nullable()->comment('최다 거래 고객');
|
||||
$table->decimal('top_client_amount', 18, 2)->default(0);
|
||||
$table->decimal('mom_growth_rate', 8, 2)->nullable()->comment('전월 대비 성장률 (%)');
|
||||
$table->decimal('yoy_growth_rate', 8, 2)->nullable()->comment('전년동월 대비 (%)');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_year', 'stat_month'], 'uk_tenant_year_month');
|
||||
$table->index(['stat_year', 'stat_month']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_sales_monthly');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_production_daily', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->date('stat_date');
|
||||
|
||||
// 작업지시
|
||||
$table->unsignedInteger('wo_created_count')->default(0)->comment('신규 작업지시');
|
||||
$table->unsignedInteger('wo_completed_count')->default(0)->comment('완료 작업지시');
|
||||
$table->unsignedInteger('wo_in_progress_count')->default(0)->comment('진행중');
|
||||
$table->unsignedInteger('wo_overdue_count')->default(0)->comment('납기 초과');
|
||||
|
||||
// 생산량
|
||||
$table->decimal('production_qty', 18, 2)->default(0)->comment('생산 수량');
|
||||
$table->decimal('defect_qty', 18, 2)->default(0)->comment('불량 수량');
|
||||
$table->decimal('defect_rate', 5, 2)->default(0)->comment('불량률 (%)');
|
||||
|
||||
// 작업 효율
|
||||
$table->decimal('planned_hours', 10, 2)->default(0)->comment('계획 공수');
|
||||
$table->decimal('actual_hours', 10, 2)->default(0)->comment('실적 공수');
|
||||
$table->decimal('efficiency_rate', 5, 2)->default(0)->comment('효율 (%)');
|
||||
|
||||
// 작업자
|
||||
$table->unsignedInteger('active_worker_count')->default(0);
|
||||
$table->unsignedInteger('issue_count')->default(0)->comment('발생 이슈 수');
|
||||
|
||||
// 납기
|
||||
$table->unsignedInteger('on_time_delivery_count')->default(0);
|
||||
$table->unsignedInteger('late_delivery_count')->default(0);
|
||||
$table->decimal('delivery_rate', 5, 2)->default(0)->comment('납기준수율 (%)');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_date'], 'uk_tenant_date');
|
||||
$table->index('stat_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_production_daily');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_production_monthly', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->smallInteger('stat_year');
|
||||
$table->tinyInteger('stat_month');
|
||||
|
||||
$table->unsignedInteger('wo_total_count')->default(0);
|
||||
$table->unsignedInteger('wo_completed_count')->default(0);
|
||||
$table->decimal('production_qty', 18, 2)->default(0);
|
||||
$table->decimal('defect_qty', 18, 2)->default(0);
|
||||
$table->decimal('avg_defect_rate', 5, 2)->default(0);
|
||||
$table->decimal('avg_efficiency_rate', 5, 2)->default(0);
|
||||
$table->decimal('avg_delivery_rate', 5, 2)->default(0);
|
||||
$table->decimal('total_planned_hours', 10, 2)->default(0);
|
||||
$table->decimal('total_actual_hours', 10, 2)->default(0);
|
||||
$table->unsignedInteger('issue_total_count')->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_year', 'stat_month'], 'uk_tenant_year_month');
|
||||
$table->index(['stat_year', 'stat_month']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_production_monthly');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('dim_client', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->unsignedBigInteger('client_id')->comment('원본 clients.id');
|
||||
$table->string('client_name', 200);
|
||||
$table->unsignedBigInteger('client_group_id')->nullable();
|
||||
$table->string('client_group_name', 200)->nullable();
|
||||
$table->string('client_type', 50)->nullable()->comment('고객/공급업체/양쪽');
|
||||
$table->string('region', 100)->nullable();
|
||||
$table->date('valid_from');
|
||||
$table->date('valid_to')->nullable()->comment('NULL = 현재 유효');
|
||||
$table->boolean('is_current')->default(true);
|
||||
|
||||
$table->index(['tenant_id', 'client_id'], 'idx_tenant_client');
|
||||
$table->index('is_current', 'idx_current');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('dim_client');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('dim_product', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->unsignedBigInteger('item_id')->comment('원본 items.id');
|
||||
$table->string('item_code', 100);
|
||||
$table->string('item_name', 300);
|
||||
$table->string('item_type', 50)->nullable()->comment('item_type from items');
|
||||
$table->unsignedBigInteger('category_id')->nullable();
|
||||
$table->string('category_name', 200)->nullable();
|
||||
$table->date('valid_from');
|
||||
$table->date('valid_to')->nullable()->comment('NULL = 현재 유효');
|
||||
$table->boolean('is_current')->default(true);
|
||||
|
||||
$table->index(['tenant_id', 'item_id'], 'idx_tenant_item');
|
||||
$table->index('is_current', 'idx_current');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('dim_product');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_inventory_daily', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->date('stat_date');
|
||||
|
||||
// 재고 현황
|
||||
$table->unsignedInteger('total_sku_count')->default(0)->comment('총 SKU 수');
|
||||
$table->decimal('total_stock_qty', 18, 2)->default(0)->comment('총 재고 수량');
|
||||
$table->decimal('total_stock_value', 18, 2)->default(0)->comment('총 재고 금액');
|
||||
|
||||
// 입출고
|
||||
$table->unsignedInteger('receipt_count')->default(0)->comment('입고 건수');
|
||||
$table->decimal('receipt_qty', 18, 2)->default(0);
|
||||
$table->decimal('receipt_amount', 18, 2)->default(0);
|
||||
$table->unsignedInteger('issue_count')->default(0)->comment('출고 건수');
|
||||
$table->decimal('issue_qty', 18, 2)->default(0);
|
||||
$table->decimal('issue_amount', 18, 2)->default(0);
|
||||
|
||||
// 안전재고
|
||||
$table->unsignedInteger('below_safety_count')->default(0)->comment('안전재고 미달 품목 수');
|
||||
$table->unsignedInteger('zero_stock_count')->default(0)->comment('재고 0 품목 수');
|
||||
$table->unsignedInteger('excess_stock_count')->default(0)->comment('과잉 재고 품목 수');
|
||||
|
||||
// 품질검사
|
||||
$table->unsignedInteger('inspection_count')->default(0);
|
||||
$table->unsignedInteger('inspection_pass_count')->default(0);
|
||||
$table->unsignedInteger('inspection_fail_count')->default(0);
|
||||
$table->decimal('inspection_pass_rate', 5, 2)->default(0)->comment('합격률 (%)');
|
||||
|
||||
// 재고회전
|
||||
$table->decimal('turnover_rate', 8, 2)->default(0)->comment('재고회전율');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_date'], 'uk_tenant_date');
|
||||
$table->index('stat_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_inventory_daily');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_quote_pipeline_daily', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->date('stat_date');
|
||||
|
||||
// 견적
|
||||
$table->unsignedInteger('quote_created_count')->default(0);
|
||||
$table->decimal('quote_amount', 18, 2)->default(0);
|
||||
$table->unsignedInteger('quote_approved_count')->default(0);
|
||||
$table->unsignedInteger('quote_rejected_count')->default(0);
|
||||
$table->unsignedInteger('quote_conversion_count')->default(0)->comment('수주 전환 건수');
|
||||
$table->decimal('quote_conversion_rate', 5, 2)->default(0)->comment('전환율 (%)');
|
||||
|
||||
// 영업 기회 (sales_prospects - tenant_id 없어 manager_id로 연결)
|
||||
$table->unsignedInteger('prospect_created_count')->default(0);
|
||||
$table->unsignedInteger('prospect_won_count')->default(0);
|
||||
$table->unsignedInteger('prospect_lost_count')->default(0);
|
||||
$table->decimal('prospect_amount', 18, 2)->default(0)->comment('파이프라인 금액');
|
||||
|
||||
// 입찰
|
||||
$table->unsignedInteger('bidding_count')->default(0);
|
||||
$table->unsignedInteger('bidding_won_count')->default(0);
|
||||
$table->decimal('bidding_amount', 18, 2)->default(0);
|
||||
|
||||
// 상담
|
||||
$table->unsignedInteger('consultation_count')->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_date'], 'uk_tenant_date');
|
||||
$table->index('stat_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_quote_pipeline_daily');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_hr_attendance_daily', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->date('stat_date');
|
||||
|
||||
// 근태
|
||||
$table->unsignedInteger('total_employees')->default(0)->comment('전체 직원 수');
|
||||
$table->unsignedInteger('attendance_count')->default(0)->comment('출근 인원');
|
||||
$table->unsignedInteger('late_count')->default(0)->comment('지각');
|
||||
$table->unsignedInteger('absent_count')->default(0)->comment('결근');
|
||||
$table->decimal('attendance_rate', 5, 2)->default(0)->comment('출근율 (%)');
|
||||
|
||||
// 휴가
|
||||
$table->unsignedInteger('leave_count')->default(0)->comment('휴가 사용');
|
||||
$table->unsignedInteger('leave_annual_count')->default(0)->comment('연차');
|
||||
$table->unsignedInteger('leave_sick_count')->default(0)->comment('병가');
|
||||
$table->unsignedInteger('leave_other_count')->default(0)->comment('기타');
|
||||
|
||||
// 초과근무
|
||||
$table->decimal('overtime_hours', 10, 2)->default(0);
|
||||
$table->unsignedInteger('overtime_employee_count')->default(0);
|
||||
|
||||
// 인건비
|
||||
$table->decimal('total_labor_cost', 18, 2)->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_date'], 'uk_tenant_date');
|
||||
$table->index('stat_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_hr_attendance_daily');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_kpi_targets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->smallInteger('stat_year');
|
||||
$table->tinyInteger('stat_month')->nullable()->comment('NULL = 연간 목표');
|
||||
|
||||
$table->string('domain', 50)->comment('sales, production, finance 등');
|
||||
$table->string('metric_code', 100)->comment('monthly_sales_amount 등');
|
||||
$table->decimal('target_value', 18, 2);
|
||||
$table->string('unit', 20)->default('KRW')->comment('KRW, %, count, hours');
|
||||
$table->string('description', 300)->nullable();
|
||||
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_year', 'stat_month', 'metric_code'], 'uk_tenant_metric');
|
||||
$table->index('domain', 'idx_domain');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_kpi_targets');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_alerts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->string('domain', 50);
|
||||
$table->string('alert_type', 100)->comment('below_target, anomaly, threshold');
|
||||
$table->enum('severity', ['info', 'warning', 'critical'])->default('info');
|
||||
$table->string('title', 300);
|
||||
$table->text('message');
|
||||
$table->string('metric_code', 100)->nullable();
|
||||
$table->decimal('current_value', 18, 2)->nullable();
|
||||
$table->decimal('threshold_value', 18, 2)->nullable();
|
||||
$table->boolean('is_read')->default(false);
|
||||
$table->boolean('is_resolved')->default(false);
|
||||
$table->timestamp('resolved_at')->nullable();
|
||||
$table->unsignedBigInteger('resolved_by')->nullable();
|
||||
$table->timestamp('created_at')->nullable();
|
||||
|
||||
$table->index(['tenant_id', 'is_read'], 'idx_tenant_unread');
|
||||
$table->index('severity', 'idx_severity');
|
||||
$table->index('domain', 'idx_domain');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_alerts');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection('sam_stat')->create('stat_project_monthly', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->smallInteger('stat_year');
|
||||
$table->tinyInteger('stat_month');
|
||||
|
||||
// 프로젝트 현황
|
||||
$table->unsignedInteger('active_site_count')->default(0);
|
||||
$table->unsignedInteger('completed_site_count')->default(0);
|
||||
$table->unsignedInteger('new_contract_count')->default(0);
|
||||
$table->decimal('contract_total_amount', 18, 2)->default(0);
|
||||
|
||||
// 원가
|
||||
$table->decimal('expected_expense_total', 18, 2)->default(0);
|
||||
$table->decimal('actual_expense_total', 18, 2)->default(0);
|
||||
$table->decimal('labor_cost_total', 18, 2)->default(0);
|
||||
$table->decimal('material_cost_total', 18, 2)->default(0);
|
||||
|
||||
// 수익률
|
||||
$table->decimal('gross_profit', 18, 2)->default(0);
|
||||
$table->decimal('gross_profit_rate', 5, 2)->default(0);
|
||||
|
||||
// 이슈
|
||||
$table->unsignedInteger('handover_report_count')->default(0);
|
||||
$table->unsignedInteger('structure_review_count')->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_year', 'stat_month'], 'uk_tenant_year_month');
|
||||
$table->index(['stat_year', 'stat_month'], 'idx_year_month');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection('sam_stat')->dropIfExists('stat_project_monthly');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection('sam_stat')->create('stat_system_daily', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->date('stat_date');
|
||||
|
||||
// API 사용량
|
||||
$table->unsignedInteger('api_request_count')->default(0);
|
||||
$table->unsignedInteger('api_error_count')->default(0);
|
||||
$table->unsignedInteger('api_avg_response_ms')->default(0);
|
||||
|
||||
// 사용자 활동
|
||||
$table->unsignedInteger('active_user_count')->default(0);
|
||||
$table->unsignedInteger('login_count')->default(0);
|
||||
|
||||
// 감사
|
||||
$table->unsignedInteger('audit_create_count')->default(0);
|
||||
$table->unsignedInteger('audit_update_count')->default(0);
|
||||
$table->unsignedInteger('audit_delete_count')->default(0);
|
||||
|
||||
// 알림
|
||||
$table->unsignedInteger('fcm_sent_count')->default(0);
|
||||
$table->unsignedInteger('fcm_failed_count')->default(0);
|
||||
|
||||
// 파일
|
||||
$table->unsignedInteger('file_upload_count')->default(0);
|
||||
$table->decimal('file_upload_size_mb', 10, 2)->default(0);
|
||||
|
||||
// 결재
|
||||
$table->unsignedInteger('approval_submitted_count')->default(0);
|
||||
$table->unsignedInteger('approval_completed_count')->default(0);
|
||||
$table->decimal('approval_avg_hours', 8, 2)->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_date'], 'uk_tenant_date');
|
||||
$table->index('stat_date', 'idx_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection('sam_stat')->dropIfExists('stat_system_daily');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection('sam_stat')->create('stat_events', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->string('domain', 50);
|
||||
$table->string('event_type', 100);
|
||||
$table->string('entity_type', 100);
|
||||
$table->unsignedBigInteger('entity_id');
|
||||
$table->json('payload')->nullable();
|
||||
$table->timestamp('occurred_at');
|
||||
|
||||
$table->index(['tenant_id', 'domain'], 'idx_tenant_domain');
|
||||
$table->index('occurred_at', 'idx_occurred');
|
||||
$table->index(['entity_type', 'entity_id'], 'idx_entity');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection('sam_stat')->dropIfExists('stat_events');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection('sam_stat')->create('stat_snapshots', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->date('snapshot_date');
|
||||
$table->string('domain', 50);
|
||||
$table->string('snapshot_type', 50)->default('daily');
|
||||
$table->json('data');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
|
||||
$table->unique(['tenant_id', 'snapshot_date', 'domain', 'snapshot_type'], 'uk_tenant_date_domain');
|
||||
$table->index('snapshot_date', 'idx_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection('sam_stat')->dropIfExists('stat_snapshots');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 일간 통계 테이블 RANGE 파티셔닝 준비
|
||||
*
|
||||
* 실행 방법: php artisan migrate --path=database/migrations/stats/2026_01_29_300001_prepare_partitioning_daily_tables.php
|
||||
*
|
||||
* 주의: MySQL에서 파티셔닝 적용 시 UNIQUE KEY에 파티션 컬럼(stat_date)이 포함되어야 합니다.
|
||||
* 이 마이그레이션은 기존 데이터를 보존하며 파티셔닝을 적용합니다.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
/**
|
||||
* 파티셔닝 대상 테이블과 고유 키 정의
|
||||
*/
|
||||
private function getTargetTables(): array
|
||||
{
|
||||
return [
|
||||
'stat_sales_daily' => [
|
||||
'unique_columns' => 'tenant_id, stat_date',
|
||||
'unique_name' => 'uk_tenant_date',
|
||||
],
|
||||
'stat_finance_daily' => [
|
||||
'unique_columns' => 'tenant_id, stat_date',
|
||||
'unique_name' => 'uk_tenant_date',
|
||||
],
|
||||
'stat_production_daily' => [
|
||||
'unique_columns' => 'tenant_id, stat_date',
|
||||
'unique_name' => 'uk_tenant_date',
|
||||
],
|
||||
'stat_inventory_daily' => [
|
||||
'unique_columns' => 'tenant_id, stat_date',
|
||||
'unique_name' => 'uk_tenant_date',
|
||||
],
|
||||
'stat_quote_pipeline_daily' => [
|
||||
'unique_columns' => 'tenant_id, stat_date',
|
||||
'unique_name' => 'uk_tenant_date',
|
||||
],
|
||||
'stat_hr_attendance_daily' => [
|
||||
'unique_columns' => 'tenant_id, stat_date',
|
||||
'unique_name' => 'uk_tenant_date',
|
||||
],
|
||||
'stat_system_daily' => [
|
||||
'unique_columns' => 'tenant_id, stat_date',
|
||||
'unique_name' => 'uk_tenant_date',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 연도별 파티션 정의 생성 (2024~2028 + MAXVALUE)
|
||||
*/
|
||||
private function getPartitionDefinitions(): string
|
||||
{
|
||||
$partitions = [];
|
||||
for ($year = 2024; $year <= 2028; $year++) {
|
||||
$partitions[] = "PARTITION p{$year} VALUES LESS THAN ('{$year}-01-01')";
|
||||
}
|
||||
$partitions[] = 'PARTITION p_future VALUES LESS THAN MAXVALUE';
|
||||
|
||||
return implode(",\n ", $partitions);
|
||||
}
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
$tables = $this->getTargetTables();
|
||||
$partitionDefs = $this->getPartitionDefinitions();
|
||||
|
||||
foreach ($tables as $table => $config) {
|
||||
// 테이블 존재 여부 확인
|
||||
$exists = DB::connection('sam_stat')
|
||||
->select("SHOW TABLES LIKE '{$table}'");
|
||||
|
||||
if (empty($exists)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 이미 파티셔닝되어 있는지 확인
|
||||
$partitionInfo = DB::connection('sam_stat')
|
||||
->select('SELECT PARTITION_NAME FROM INFORMATION_SCHEMA.PARTITIONS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND PARTITION_NAME IS NOT NULL',
|
||||
[$table]);
|
||||
|
||||
if (! empty($partitionInfo)) {
|
||||
continue; // 이미 파티셔닝됨
|
||||
}
|
||||
|
||||
// AUTO_INCREMENT PK를 일반 PK로 변경 (파티션 키 포함)
|
||||
// MySQL 파티셔닝 제약: UNIQUE KEY에 파티션 컬럼 포함 필수
|
||||
DB::connection('sam_stat')->statement("
|
||||
ALTER TABLE `{$table}`
|
||||
DROP PRIMARY KEY,
|
||||
ADD PRIMARY KEY (`id`, `stat_date`),
|
||||
DROP INDEX `{$config['unique_name']}`,
|
||||
ADD UNIQUE KEY `{$config['unique_name']}` ({$config['unique_columns']})
|
||||
");
|
||||
|
||||
// RANGE 파티셔닝 적용
|
||||
DB::connection('sam_stat')->statement("
|
||||
ALTER TABLE `{$table}`
|
||||
PARTITION BY RANGE COLUMNS(`stat_date`) (
|
||||
{$partitionDefs}
|
||||
)
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$tables = $this->getTargetTables();
|
||||
|
||||
foreach ($tables as $table => $config) {
|
||||
$exists = DB::connection('sam_stat')
|
||||
->select("SHOW TABLES LIKE '{$table}'");
|
||||
|
||||
if (empty($exists)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 파티션 제거
|
||||
DB::connection('sam_stat')->statement("
|
||||
ALTER TABLE `{$table}` REMOVE PARTITIONING
|
||||
");
|
||||
|
||||
// PK 원복
|
||||
DB::connection('sam_stat')->statement("
|
||||
ALTER TABLE `{$table}`
|
||||
DROP PRIMARY KEY,
|
||||
ADD PRIMARY KEY (`id`)
|
||||
");
|
||||
}
|
||||
}
|
||||
};
|
||||
81
database/seeders/DimDateSeeder.php
Normal file
81
database/seeders/DimDateSeeder.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonPeriod;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DimDateSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$start = Carbon::create(2020, 1, 1);
|
||||
$end = Carbon::create(2030, 12, 31);
|
||||
$period = CarbonPeriod::create($start, $end);
|
||||
|
||||
$holidays = $this->getKoreanHolidays();
|
||||
|
||||
$batch = [];
|
||||
$batchSize = 500;
|
||||
|
||||
foreach ($period as $date) {
|
||||
$dateStr = $date->format('Y-m-d');
|
||||
$holiday = $holidays[$dateStr] ?? null;
|
||||
|
||||
$batch[] = [
|
||||
'date_key' => $dateStr,
|
||||
'year' => $date->year,
|
||||
'quarter' => $date->quarter,
|
||||
'month' => $date->month,
|
||||
'week' => (int) $date->isoWeek(),
|
||||
'day_of_week' => $date->dayOfWeekIso,
|
||||
'day_of_month' => $date->day,
|
||||
'is_weekend' => $date->isWeekend(),
|
||||
'is_holiday' => $holiday !== null,
|
||||
'holiday_name' => $holiday,
|
||||
'fiscal_year' => $date->year,
|
||||
'fiscal_quarter' => $date->quarter,
|
||||
];
|
||||
|
||||
if (count($batch) >= $batchSize) {
|
||||
DB::connection('sam_stat')->table('dim_date')->insert($batch);
|
||||
$batch = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($batch)) {
|
||||
DB::connection('sam_stat')->table('dim_date')->insert($batch);
|
||||
}
|
||||
|
||||
$totalCount = DB::connection('sam_stat')->table('dim_date')->count();
|
||||
$this->command->info("dim_date 시딩 완료: {$totalCount}건 (2020-01-01 ~ 2030-12-31)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 한국 공휴일 목록 (고정 공휴일만, 음력 공휴일은 수동 추가 필요)
|
||||
*/
|
||||
private function getKoreanHolidays(): array
|
||||
{
|
||||
$holidays = [];
|
||||
|
||||
for ($year = 2020; $year <= 2030; $year++) {
|
||||
// 고정 공휴일
|
||||
$fixed = [
|
||||
"{$year}-01-01" => '신정',
|
||||
"{$year}-03-01" => '삼일절',
|
||||
"{$year}-05-05" => '어린이날',
|
||||
"{$year}-06-06" => '현충일',
|
||||
"{$year}-08-15" => '광복절',
|
||||
"{$year}-10-03" => '개천절',
|
||||
"{$year}-10-09" => '한글날',
|
||||
"{$year}-12-25" => '크리스마스',
|
||||
];
|
||||
|
||||
$holidays = array_merge($holidays, $fixed);
|
||||
}
|
||||
|
||||
return $holidays;
|
||||
}
|
||||
}
|
||||
565
database/seeders/Kyungdong/KdPriceTableSeeder.php
Normal file
565
database/seeders/Kyungdong/KdPriceTableSeeder.php
Normal file
@@ -0,0 +1,565 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\Kyungdong;
|
||||
|
||||
use App\Models\Kyungdong\KdPriceTable;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 경동기업 전용 단가 테이블 Seeder
|
||||
*
|
||||
* 5130 레거시 시스템의 price_* 테이블 데이터를 kd_price_tables로 마이그레이션
|
||||
*
|
||||
* 실행: php artisan db:seed --class="Database\Seeders\Kyungdong\KdPriceTableSeeder"
|
||||
*/
|
||||
class KdPriceTableSeeder extends Seeder
|
||||
{
|
||||
private const TENANT_ID = 287;
|
||||
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
$this->command->info('🔧 경동기업 단가 테이블 마이그레이션 (kd_price_tables)');
|
||||
$this->command->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
|
||||
// 기존 데이터 삭제
|
||||
DB::table('kd_price_tables')->where('tenant_id', self::TENANT_ID)->delete();
|
||||
$this->command->info(' → 기존 데이터 삭제 완료');
|
||||
|
||||
// chandj 연결 가능 여부 확인
|
||||
$chandjAvailable = $this->checkChandjConnection();
|
||||
|
||||
if ($chandjAvailable) {
|
||||
$this->command->info(' → chandj 데이터베이스 연결됨');
|
||||
$this->migrateFromChandj();
|
||||
} else {
|
||||
$this->command->warn(' → chandj 데이터베이스 연결 불가 - 샘플 데이터 사용');
|
||||
$this->insertSampleData();
|
||||
}
|
||||
|
||||
$count = DB::table('kd_price_tables')->where('tenant_id', self::TENANT_ID)->count();
|
||||
$this->command->info('');
|
||||
$this->command->info("✅ 완료: kd_price_tables {$count}건");
|
||||
}
|
||||
|
||||
/**
|
||||
* chandj 데이터베이스 연결 확인
|
||||
*/
|
||||
private function checkChandjConnection(): bool
|
||||
{
|
||||
try {
|
||||
DB::connection('chandj')->getPdo();
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* chandj 데이터베이스에서 마이그레이션
|
||||
*/
|
||||
private function migrateFromChandj(): void
|
||||
{
|
||||
$this->migrateMotorPrices();
|
||||
$this->migrateShaftPrices();
|
||||
$this->migratePipePrices();
|
||||
$this->migrateAnglePrices();
|
||||
$this->migrateRawMaterialPrices();
|
||||
}
|
||||
|
||||
/**
|
||||
* price_motor → kd_price_tables
|
||||
*/
|
||||
private function migrateMotorPrices(): void
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('📦 [1/5] price_motor 마이그레이션...');
|
||||
|
||||
$priceMotor = DB::connection('chandj')
|
||||
->table('price_motor')
|
||||
->where(function ($q) {
|
||||
$q->where('is_deleted', 0)->orWhereNull('is_deleted');
|
||||
})
|
||||
->orderByDesc('registedate')
|
||||
->first();
|
||||
|
||||
if (! $priceMotor || empty($priceMotor->itemList)) {
|
||||
$this->command->info(' → 데이터 없음');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$itemList = json_decode($priceMotor->itemList, true);
|
||||
if (! is_array($itemList)) {
|
||||
$this->command->info(' → JSON 파싱 실패');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$now = now();
|
||||
|
||||
foreach ($itemList as $item) {
|
||||
$col1 = $item['col1'] ?? ''; // 전압/카테고리 (220, 380, 제어기 등)
|
||||
$col2 = $item['col2'] ?? ''; // 용량/품목명
|
||||
$salesPrice = (float) str_replace(',', '', $item['col13'] ?? '0');
|
||||
|
||||
if (empty($col2) || $salesPrice <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 카테고리 결정
|
||||
$category = match ($col1) {
|
||||
'220', '380' => $col2, // 모터 용량 (150K, 300K 등)
|
||||
default => $col1, // 제어기, 방화, 방범 등
|
||||
};
|
||||
|
||||
KdPriceTable::create([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'table_type' => KdPriceTable::TYPE_MOTOR,
|
||||
'item_name' => trim("{$col1} {$col2}"),
|
||||
'category' => $category,
|
||||
'spec1' => $col1,
|
||||
'spec2' => $col2,
|
||||
'unit_price' => $salesPrice,
|
||||
'unit' => 'EA',
|
||||
'raw_data' => $item,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->command->info(" → {$count}건 완료");
|
||||
}
|
||||
|
||||
/**
|
||||
* price_shaft → kd_price_tables
|
||||
*/
|
||||
private function migrateShaftPrices(): void
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('📦 [2/5] price_shaft 마이그레이션...');
|
||||
|
||||
$priceShaft = DB::connection('chandj')
|
||||
->table('price_shaft')
|
||||
->where(function ($q) {
|
||||
$q->where('is_deleted', 0)->orWhereNull('is_deleted');
|
||||
})
|
||||
->orderByDesc('registedate')
|
||||
->first();
|
||||
|
||||
if (! $priceShaft || empty($priceShaft->itemList)) {
|
||||
$this->command->info(' → 데이터 없음');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$itemList = json_decode($priceShaft->itemList, true);
|
||||
if (! is_array($itemList)) {
|
||||
$this->command->info(' → JSON 파싱 실패');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($itemList as $item) {
|
||||
$size = $item['col4'] ?? ''; // 사이즈 (3, 4, 5인치)
|
||||
$length = $item['col10'] ?? ''; // 길이 (m 단위)
|
||||
$salesPrice = (float) str_replace(',', '', $item['col19'] ?? '0');
|
||||
|
||||
if (empty($size) || $salesPrice <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KdPriceTable::create([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'table_type' => KdPriceTable::TYPE_SHAFT,
|
||||
'item_name' => "감기샤프트 {$size}인치 {$length}m",
|
||||
'category' => '감기샤프트',
|
||||
'spec1' => $size,
|
||||
'spec2' => $length,
|
||||
'unit_price' => $salesPrice,
|
||||
'unit' => 'EA',
|
||||
'raw_data' => $item,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->command->info(" → {$count}건 완료");
|
||||
}
|
||||
|
||||
/**
|
||||
* price_pipe → kd_price_tables
|
||||
*/
|
||||
private function migratePipePrices(): void
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('📦 [3/5] price_pipe 마이그레이션...');
|
||||
|
||||
$pricePipe = DB::connection('chandj')
|
||||
->table('price_pipe')
|
||||
->where(function ($q) {
|
||||
$q->where('is_deleted', 0)->orWhereNull('is_deleted');
|
||||
})
|
||||
->orderByDesc('registedate')
|
||||
->first();
|
||||
|
||||
if (! $pricePipe || empty($pricePipe->itemList)) {
|
||||
$this->command->info(' → 데이터 없음');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$itemList = json_decode($pricePipe->itemList, true);
|
||||
if (! is_array($itemList)) {
|
||||
$this->command->info(' → JSON 파싱 실패');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($itemList as $item) {
|
||||
$length = $item['col2'] ?? ''; // 길이 (3000, 6000)
|
||||
$thickness = $item['col4'] ?? ''; // 두께 (1.4)
|
||||
$salesPrice = (float) str_replace(',', '', $item['col8'] ?? '0');
|
||||
|
||||
if (empty($thickness) || $salesPrice <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KdPriceTable::create([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'table_type' => KdPriceTable::TYPE_PIPE,
|
||||
'item_name' => "각파이프 {$thickness}T {$length}mm",
|
||||
'category' => '각파이프',
|
||||
'spec1' => $thickness,
|
||||
'spec2' => $length,
|
||||
'unit_price' => $salesPrice,
|
||||
'unit' => 'EA',
|
||||
'raw_data' => $item,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->command->info(" → {$count}건 완료");
|
||||
}
|
||||
|
||||
/**
|
||||
* price_angle → kd_price_tables
|
||||
*/
|
||||
private function migrateAnglePrices(): void
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('📦 [4/5] price_angle 마이그레이션...');
|
||||
|
||||
$priceAngle = DB::connection('chandj')
|
||||
->table('price_angle')
|
||||
->where(function ($q) {
|
||||
$q->where('is_deleted', 0)->orWhereNull('is_deleted');
|
||||
})
|
||||
->orderByDesc('registedate')
|
||||
->first();
|
||||
|
||||
if (! $priceAngle || empty($priceAngle->itemList)) {
|
||||
$this->command->info(' → 데이터 없음');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$itemList = json_decode($priceAngle->itemList, true);
|
||||
if (! is_array($itemList)) {
|
||||
$this->command->info(' → JSON 파싱 실패');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($itemList as $item) {
|
||||
$type = $item['col2'] ?? ''; // 타입 (스크린용, 철재용)
|
||||
$bracketSize = $item['col3'] ?? ''; // 브라켓크기
|
||||
$angleType = $item['col4'] ?? ''; // 앵글타입 (앵글3T, 앵글4T)
|
||||
$thickness = $item['col10'] ?? ''; // 두께
|
||||
$salesPrice = (float) str_replace(',', '', $item['col19'] ?? '0');
|
||||
|
||||
if (empty($type) || $salesPrice <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KdPriceTable::create([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'table_type' => KdPriceTable::TYPE_ANGLE,
|
||||
'item_name' => "앵글 {$type} {$bracketSize} {$angleType}",
|
||||
'category' => $type,
|
||||
'spec1' => $bracketSize,
|
||||
'spec2' => $angleType,
|
||||
'spec3' => $thickness,
|
||||
'unit_price' => $salesPrice,
|
||||
'unit' => 'EA',
|
||||
'raw_data' => $item,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->command->info(" → {$count}건 완료");
|
||||
}
|
||||
|
||||
/**
|
||||
* price_raw_materials → kd_price_tables
|
||||
*/
|
||||
private function migrateRawMaterialPrices(): void
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('📦 [5/5] price_raw_materials 마이그레이션...');
|
||||
|
||||
$priceRaw = DB::connection('chandj')
|
||||
->table('price_raw_materials')
|
||||
->where(function ($q) {
|
||||
$q->where('is_deleted', 0)->orWhereNull('is_deleted');
|
||||
})
|
||||
->orderByDesc('registedate')
|
||||
->first();
|
||||
|
||||
if (! $priceRaw || empty($priceRaw->itemList)) {
|
||||
$this->command->info(' → 데이터 없음');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$itemList = json_decode($priceRaw->itemList, true);
|
||||
if (! is_array($itemList)) {
|
||||
$this->command->info(' → JSON 파싱 실패');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($itemList as $item) {
|
||||
$name = $item['col2'] ?? '';
|
||||
$spec = $item['col3'] ?? '';
|
||||
$salesPrice = (float) str_replace(',', '', $item['col19'] ?? $item['col13'] ?? '0');
|
||||
|
||||
if (empty($name) || $salesPrice <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KdPriceTable::create([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'table_type' => KdPriceTable::TYPE_RAW_MATERIAL,
|
||||
'item_name' => $name,
|
||||
'category' => '원자재',
|
||||
'spec1' => $spec,
|
||||
'unit_price' => $salesPrice,
|
||||
'unit' => '㎡',
|
||||
'raw_data' => $item,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->command->info(" → {$count}건 완료");
|
||||
}
|
||||
|
||||
/**
|
||||
* chandj 연결 불가 시 샘플 데이터 삽입
|
||||
*/
|
||||
private function insertSampleData(): void
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('📦 샘플 데이터 삽입 중...');
|
||||
|
||||
// 모터 샘플 데이터 (5130 분석 결과 기반)
|
||||
$motorData = [
|
||||
['category' => '150K', 'spec1' => '220', 'spec2' => '150K', 'unit_price' => 85000],
|
||||
['category' => '300K', 'spec1' => '220', 'spec2' => '300K', 'unit_price' => 120000],
|
||||
['category' => '400K', 'spec1' => '220', 'spec2' => '400K', 'unit_price' => 150000],
|
||||
['category' => '500K', 'spec1' => '220', 'spec2' => '500K', 'unit_price' => 180000],
|
||||
['category' => '600K', 'spec1' => '220', 'spec2' => '600K', 'unit_price' => 220000],
|
||||
['category' => '800K', 'spec1' => '220', 'spec2' => '800K', 'unit_price' => 280000],
|
||||
['category' => '1000K', 'spec1' => '220', 'spec2' => '1000K', 'unit_price' => 350000],
|
||||
['category' => '매립형', 'spec1' => '제어기', 'spec2' => '매립형', 'unit_price' => 45000],
|
||||
['category' => '노출형', 'spec1' => '제어기', 'spec2' => '노출형', 'unit_price' => 55000],
|
||||
['category' => '뒷박스', 'spec1' => '제어기', 'spec2' => '뒷박스', 'unit_price' => 35000],
|
||||
];
|
||||
|
||||
foreach ($motorData as $data) {
|
||||
KdPriceTable::create(array_merge($data, [
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'table_type' => KdPriceTable::TYPE_MOTOR,
|
||||
'item_name' => "모터/제어기 {$data['category']}",
|
||||
'unit' => 'EA',
|
||||
'is_active' => true,
|
||||
]));
|
||||
}
|
||||
$this->command->info(' → 모터/제어기 '.count($motorData).'건');
|
||||
|
||||
// 샤프트 샘플 데이터
|
||||
$shaftData = [
|
||||
['spec1' => '3', 'spec2' => '3.0', 'unit_price' => 45000],
|
||||
['spec1' => '4', 'spec2' => '3.0', 'unit_price' => 55000],
|
||||
['spec1' => '5', 'spec2' => '3.0', 'unit_price' => 65000],
|
||||
['spec1' => '3', 'spec2' => '4.0', 'unit_price' => 60000],
|
||||
['spec1' => '4', 'spec2' => '4.0', 'unit_price' => 75000],
|
||||
['spec1' => '5', 'spec2' => '4.0', 'unit_price' => 90000],
|
||||
];
|
||||
|
||||
foreach ($shaftData as $data) {
|
||||
KdPriceTable::create(array_merge($data, [
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'table_type' => KdPriceTable::TYPE_SHAFT,
|
||||
'item_name' => "감기샤프트 {$data['spec1']}인치 {$data['spec2']}m",
|
||||
'category' => '감기샤프트',
|
||||
'unit' => 'EA',
|
||||
'is_active' => true,
|
||||
]));
|
||||
}
|
||||
$this->command->info(' → 샤프트 '.count($shaftData).'건');
|
||||
|
||||
// 파이프 샘플 데이터
|
||||
$pipeData = [
|
||||
['spec1' => '1.4', 'spec2' => '3000', 'unit_price' => 12000],
|
||||
['spec1' => '1.4', 'spec2' => '6000', 'unit_price' => 24000],
|
||||
];
|
||||
|
||||
foreach ($pipeData as $data) {
|
||||
KdPriceTable::create(array_merge($data, [
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'table_type' => KdPriceTable::TYPE_PIPE,
|
||||
'item_name' => "각파이프 {$data['spec1']}T {$data['spec2']}mm",
|
||||
'category' => '각파이프',
|
||||
'unit' => 'EA',
|
||||
'is_active' => true,
|
||||
]));
|
||||
}
|
||||
$this->command->info(' → 파이프 '.count($pipeData).'건');
|
||||
|
||||
// 앵글 샘플 데이터
|
||||
$angleData = [
|
||||
['category' => '스크린용', 'spec1' => '530*320', 'spec2' => '앵글3T', 'unit_price' => 8000],
|
||||
['category' => '스크린용', 'spec1' => '600*350', 'spec2' => '앵글3T', 'unit_price' => 10000],
|
||||
['category' => '스크린용', 'spec1' => '690*390', 'spec2' => '앵글4T', 'unit_price' => 12000],
|
||||
['category' => '철재용', 'spec1' => '530*320', 'spec2' => '앵글3T', 'unit_price' => 9000],
|
||||
['category' => '철재용', 'spec1' => '600*350', 'spec2' => '앵글3T', 'unit_price' => 11000],
|
||||
['category' => '철재용', 'spec1' => '690*390', 'spec2' => '앵글4T', 'unit_price' => 14000],
|
||||
];
|
||||
|
||||
foreach ($angleData as $data) {
|
||||
KdPriceTable::create(array_merge($data, [
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'table_type' => KdPriceTable::TYPE_ANGLE,
|
||||
'item_name' => "앵글 {$data['category']} {$data['spec1']} {$data['spec2']}",
|
||||
'unit' => 'EA',
|
||||
'is_active' => true,
|
||||
]));
|
||||
}
|
||||
$this->command->info(' → 앵글 '.count($angleData).'건');
|
||||
|
||||
// 원자재 샘플 데이터
|
||||
$rawData = [
|
||||
['item_name' => '실리카', 'spec1' => '스크린용', 'unit_price' => 25000],
|
||||
['item_name' => '불투명', 'spec1' => '스크린용', 'unit_price' => 22000],
|
||||
['item_name' => '화이바원단', 'spec1' => '스크린용', 'unit_price' => 28000],
|
||||
];
|
||||
|
||||
foreach ($rawData as $data) {
|
||||
KdPriceTable::create(array_merge($data, [
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'table_type' => KdPriceTable::TYPE_RAW_MATERIAL,
|
||||
'category' => '원자재',
|
||||
'unit' => '㎡',
|
||||
'is_active' => true,
|
||||
]));
|
||||
}
|
||||
$this->command->info(' → 원자재 '.count($rawData).'건');
|
||||
|
||||
// BDmodels 샘플 데이터 (절곡품)
|
||||
$this->insertBDModelsSampleData();
|
||||
}
|
||||
|
||||
/**
|
||||
* BDmodels 샘플 데이터 삽입 (절곡품)
|
||||
*/
|
||||
private function insertBDModelsSampleData(): void
|
||||
{
|
||||
$bdmodelsData = [
|
||||
// 케이스 (규격별 단가 - 원/m)
|
||||
['category' => '케이스', 'spec2' => '500*380', 'unit_price' => 15000, 'unit' => 'm'],
|
||||
['category' => '케이스', 'spec2' => '550*430', 'unit_price' => 18000, 'unit' => 'm'],
|
||||
['category' => '케이스', 'spec2' => '650*550', 'unit_price' => 22000, 'unit' => 'm'],
|
||||
|
||||
// 케이스 마구리 (규격별 단가 - 원/개)
|
||||
['category' => '마구리', 'spec2' => '500*380', 'unit_price' => 5000, 'unit' => 'EA'],
|
||||
['category' => '마구리', 'spec2' => '550*430', 'unit_price' => 6000, 'unit' => 'EA'],
|
||||
['category' => '마구리', 'spec2' => '650*550', 'unit_price' => 7500, 'unit' => 'EA'],
|
||||
|
||||
// 케이스용 연기차단재 (공통 단가 - 원/m)
|
||||
['category' => '케이스용 연기차단재', 'unit_price' => 3500, 'unit' => 'm'],
|
||||
|
||||
// 가이드레일 (모델+마감+규격별 단가 - 원/m)
|
||||
['category' => '가이드레일', 'item_code' => 'KSS01', 'spec1' => 'SUS', 'spec2' => '120*70', 'unit_price' => 12000, 'unit' => 'm'],
|
||||
['category' => '가이드레일', 'item_code' => 'KSS01', 'spec1' => 'SUS', 'spec2' => '120*100', 'unit_price' => 15000, 'unit' => 'm'],
|
||||
['category' => '가이드레일', 'item_code' => 'KSS01', 'spec1' => 'EGI', 'spec2' => '120*70', 'unit_price' => 10000, 'unit' => 'm'],
|
||||
['category' => '가이드레일', 'item_code' => 'KSS01', 'spec1' => 'EGI', 'spec2' => '120*100', 'unit_price' => 13000, 'unit' => 'm'],
|
||||
['category' => '가이드레일', 'item_code' => 'KWS01', 'spec1' => 'SUS', 'spec2' => '120*70', 'unit_price' => 13000, 'unit' => 'm'],
|
||||
['category' => '가이드레일', 'item_code' => 'KWS01', 'spec1' => 'SUS', 'spec2' => '120*100', 'unit_price' => 16000, 'unit' => 'm'],
|
||||
|
||||
// 가이드레일용 연기차단재 (공통 단가 - 원/m)
|
||||
['category' => '가이드레일용 연기차단재', 'unit_price' => 2500, 'unit' => 'm'],
|
||||
|
||||
// 하단마감재/하장바 (모델+마감별 단가 - 원/m)
|
||||
['category' => '하단마감재', 'item_code' => 'KSS01', 'spec1' => 'SUS', 'unit_price' => 8000, 'unit' => 'm'],
|
||||
['category' => '하단마감재', 'item_code' => 'KSS01', 'spec1' => 'EGI', 'unit_price' => 6500, 'unit' => 'm'],
|
||||
['category' => '하단마감재', 'item_code' => 'KWS01', 'spec1' => 'SUS', 'unit_price' => 8500, 'unit' => 'm'],
|
||||
|
||||
// L-BAR (모델별 단가 - 원/m)
|
||||
['category' => 'L-BAR', 'item_code' => 'KSS01', 'unit_price' => 4500, 'unit' => 'm'],
|
||||
['category' => 'L-BAR', 'item_code' => 'KWS01', 'unit_price' => 5000, 'unit' => 'm'],
|
||||
|
||||
// 보강평철 (공통 단가 - 원/m)
|
||||
['category' => '보강평철', 'unit_price' => 3000, 'unit' => 'm'],
|
||||
];
|
||||
|
||||
foreach ($bdmodelsData as $data) {
|
||||
$itemCode = $data['item_code'] ?? null;
|
||||
$spec1 = $data['spec1'] ?? null;
|
||||
$spec2 = $data['spec2'] ?? null;
|
||||
|
||||
$itemName = $data['category'];
|
||||
if ($itemCode) {
|
||||
$itemName .= " {$itemCode}";
|
||||
}
|
||||
if ($spec1) {
|
||||
$itemName .= " {$spec1}";
|
||||
}
|
||||
if ($spec2) {
|
||||
$itemName .= " {$spec2}";
|
||||
}
|
||||
|
||||
KdPriceTable::create([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'table_type' => KdPriceTable::TYPE_BDMODELS,
|
||||
'item_code' => $itemCode,
|
||||
'item_name' => $itemName,
|
||||
'category' => $data['category'],
|
||||
'spec1' => $spec1,
|
||||
'spec2' => $spec2,
|
||||
'unit_price' => $data['unit_price'],
|
||||
'unit' => $data['unit'],
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
$this->command->info(' → BDmodels(절곡품) '.count($bdmodelsData).'건');
|
||||
}
|
||||
}
|
||||
911
database/seeders/Kyungdong/KyungdongItemSeeder.php
Normal file
911
database/seeders/Kyungdong/KyungdongItemSeeder.php
Normal file
@@ -0,0 +1,911 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\Kyungdong;
|
||||
|
||||
use Database\Seeders\DummyDataSeeder;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 경동기업 품목/단가 마이그레이션 Seeder
|
||||
*
|
||||
* Phase 1.0: chandj.KDunitprice (601건) → items, prices
|
||||
* Phase 1.1: chandj.models (18건) → items (FG), prices
|
||||
* Phase 1.2: chandj.item_list (9건) → items (PT), prices
|
||||
* Phase 2.1: chandj.BDmodels.seconditem → items (PT) 누락 부품 추가
|
||||
* Phase 2.2: chandj.BDmodels → items.bom JSON (FG ↔ PT 연결)
|
||||
* Phase 3.1: chandj.price_motor → items (SM) + prices 누락 품목
|
||||
* Phase 3.2: chandj.price_raw_materials → items (RM) + prices 누락 품목
|
||||
*
|
||||
* @see docs/plans/kd-items-migration-plan.md
|
||||
*/
|
||||
class KyungdongItemSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* item_div → item_type 매핑
|
||||
*/
|
||||
private const ITEM_TYPE_MAP = [
|
||||
'[제품]' => 'FG',
|
||||
'[상품]' => 'FG',
|
||||
'[반제품]' => 'PT',
|
||||
'[부재료]' => 'SM',
|
||||
'[원재료]' => 'RM',
|
||||
'[무형상품]' => 'CS',
|
||||
];
|
||||
|
||||
/**
|
||||
* finishing_type 약어 매핑
|
||||
*/
|
||||
private const FINISHING_MAP = [
|
||||
'SUS마감' => 'SUS',
|
||||
'EGI마감' => 'EGI',
|
||||
];
|
||||
|
||||
/**
|
||||
* 경동기업 품목/단가 마이그레이션 실행
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$userId = DummyDataSeeder::USER_ID;
|
||||
|
||||
$this->command->info('🚀 경동기업 품목/단가 마이그레이션 시작...');
|
||||
$this->command->info(" 대상 테넌트: ID {$tenantId}");
|
||||
|
||||
// 1. 기존 데이터 삭제
|
||||
$this->cleanupExistingData($tenantId);
|
||||
|
||||
// Phase 1.0: KDunitprice → items
|
||||
$itemCount = $this->migrateItems($tenantId, $userId);
|
||||
|
||||
// Phase 1.1: models → items (FG)
|
||||
$modelCount = $this->migrateModels($tenantId, $userId);
|
||||
|
||||
// Phase 1.2: item_list → items (PT)
|
||||
$itemListCount = $this->migrateItemList($tenantId, $userId);
|
||||
|
||||
// Phase 2.1: BDmodels.seconditem → items (PT) 누락 부품
|
||||
$bdPartsCount = $this->migrateBDmodelsParts($tenantId, $userId);
|
||||
|
||||
// prices 생성 (모든 items 기반)
|
||||
$priceCount = $this->migratePrices($tenantId, $userId);
|
||||
|
||||
// Phase 2.2: BDmodels → items.bom JSON
|
||||
$bomCount = $this->migrateBom($tenantId);
|
||||
|
||||
// Phase 3.1: price_motor → items (SM) + prices
|
||||
$motorResult = $this->migratePriceMotor($tenantId, $userId);
|
||||
|
||||
// Phase 3.2: price_raw_materials → items (RM) + prices
|
||||
$rawMatResult = $this->migratePriceRawMaterials($tenantId, $userId);
|
||||
|
||||
$totalItems = $itemCount + $modelCount + $itemListCount + $bdPartsCount + $motorResult['items'] + $rawMatResult['items'];
|
||||
$totalPrices = $priceCount + $motorResult['prices'] + $rawMatResult['prices'];
|
||||
|
||||
$this->command->info('');
|
||||
$this->command->info('✅ 마이그레이션 완료:');
|
||||
$this->command->info(" → items: {$totalItems}건");
|
||||
$this->command->info(" - KDunitprice: {$itemCount}건");
|
||||
$this->command->info(" - models: {$modelCount}건");
|
||||
$this->command->info(" - item_list: {$itemListCount}건");
|
||||
$this->command->info(" - BDmodels부품: {$bdPartsCount}건");
|
||||
$this->command->info(" - price_motor: {$motorResult['items']}건");
|
||||
$this->command->info(" - price_raw_materials: {$rawMatResult['items']}건");
|
||||
$this->command->info(" → prices: {$totalPrices}건");
|
||||
$this->command->info(" → BOM 연결: {$bomCount}건");
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 데이터 삭제 (tenant_id 기준)
|
||||
*/
|
||||
private function cleanupExistingData(int $tenantId): void
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('🧹 기존 데이터 삭제 중...');
|
||||
|
||||
// prices 먼저 삭제 (FK 관계)
|
||||
$priceCount = DB::table('prices')->where('tenant_id', $tenantId)->count();
|
||||
DB::table('prices')->where('tenant_id', $tenantId)->delete();
|
||||
$this->command->info(" → prices: {$priceCount}건 삭제");
|
||||
|
||||
// items 삭제
|
||||
$itemCount = DB::table('items')->where('tenant_id', $tenantId)->count();
|
||||
DB::table('items')->where('tenant_id', $tenantId)->delete();
|
||||
$this->command->info(" → items: {$itemCount}건 삭제");
|
||||
}
|
||||
|
||||
/**
|
||||
* KDunitprice → items 마이그레이션
|
||||
*/
|
||||
private function migrateItems(int $tenantId, int $userId): int
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('📦 KDunitprice → items 마이그레이션...');
|
||||
|
||||
// chandj.KDunitprice에서 데이터 조회 (is_deleted=NULL이 활성 상태)
|
||||
$kdItems = DB::connection('chandj')
|
||||
->table('KDunitprice')
|
||||
->whereNull('is_deleted')
|
||||
->whereNotNull('prodcode')
|
||||
->where('prodcode', '!=', '')
|
||||
->get();
|
||||
|
||||
$this->command->info(" → 소스 데이터: {$kdItems->count()}건");
|
||||
|
||||
$items = [];
|
||||
$now = now();
|
||||
$batchCount = 0;
|
||||
|
||||
foreach ($kdItems as $kd) {
|
||||
$items[] = [
|
||||
'tenant_id' => $tenantId,
|
||||
'item_type' => $this->mapItemType($kd->item_div),
|
||||
'code' => $kd->prodcode,
|
||||
'name' => $kd->item_name,
|
||||
'unit' => $kd->unit,
|
||||
'category_id' => null,
|
||||
'process_type' => null,
|
||||
'item_category' => null,
|
||||
'bom' => null,
|
||||
'attributes' => json_encode([
|
||||
'spec' => $kd->spec,
|
||||
'item_div' => $kd->item_div,
|
||||
'legacy_source' => 'KDunitprice',
|
||||
'legacy_num' => $kd->num,
|
||||
]),
|
||||
'attributes_archive' => null,
|
||||
'options' => null,
|
||||
'description' => null,
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
// 500건씩 배치 INSERT
|
||||
if (count($items) >= 500) {
|
||||
DB::table('items')->insert($items);
|
||||
$batchCount += count($items);
|
||||
$this->command->info(" → {$batchCount}건 완료...");
|
||||
$items = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 남은 데이터 INSERT
|
||||
if (! empty($items)) {
|
||||
DB::table('items')->insert($items);
|
||||
$batchCount += count($items);
|
||||
}
|
||||
|
||||
$this->command->info(" ✓ items: {$batchCount}건 생성 완료");
|
||||
|
||||
return $batchCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* items 기반 → prices 마이그레이션
|
||||
*/
|
||||
private function migratePrices(int $tenantId, int $userId): int
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('💰 items → prices 마이그레이션...');
|
||||
|
||||
// 생성된 items 조회
|
||||
$items = DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->get(['id', 'code', 'item_type', 'attributes']);
|
||||
|
||||
// KDunitprice 단가 (code → unitprice)
|
||||
$kdPrices = DB::connection('chandj')
|
||||
->table('KDunitprice')
|
||||
->whereNull('is_deleted')
|
||||
->whereNotNull('prodcode')
|
||||
->where('prodcode', '!=', '')
|
||||
->pluck('unitprice', 'prodcode');
|
||||
|
||||
// item_list 단가 (item_name → col13)
|
||||
$itemListPrices = DB::connection('chandj')
|
||||
->table('item_list')
|
||||
->pluck('col13', 'item_name');
|
||||
|
||||
$prices = [];
|
||||
$now = now();
|
||||
$batchCount = 0;
|
||||
|
||||
foreach ($items as $item) {
|
||||
$attributes = json_decode($item->attributes, true) ?? [];
|
||||
$legacySource = $attributes['legacy_source'] ?? '';
|
||||
|
||||
// 소스별 단가 결정
|
||||
$unitPrice = match ($legacySource) {
|
||||
'KDunitprice' => $kdPrices[$item->code] ?? 0,
|
||||
'item_list' => $itemListPrices[$attributes['legacy_num'] ? $this->getItemListName($item->code) : ''] ?? $attributes['base_price'] ?? 0,
|
||||
'models' => 0, // models는 단가 없음
|
||||
default => 0,
|
||||
};
|
||||
|
||||
// item_list의 경우 attributes에 저장된 base_price 사용
|
||||
if ($legacySource === 'item_list' && isset($attributes['base_price'])) {
|
||||
$unitPrice = $attributes['base_price'];
|
||||
}
|
||||
|
||||
$prices[] = [
|
||||
'tenant_id' => $tenantId,
|
||||
'item_type_code' => $item->item_type,
|
||||
'item_id' => $item->id,
|
||||
'client_group_id' => null,
|
||||
'purchase_price' => 0,
|
||||
'processing_cost' => null,
|
||||
'loss_rate' => null,
|
||||
'margin_rate' => null,
|
||||
'sales_price' => $unitPrice,
|
||||
'rounding_rule' => 'round',
|
||||
'rounding_unit' => 1,
|
||||
'supplier' => null,
|
||||
'effective_from' => now()->toDateString(),
|
||||
'effective_to' => null,
|
||||
'note' => "{$legacySource} 마이그레이션",
|
||||
'status' => 'active',
|
||||
'is_final' => false,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
// 500건씩 배치 INSERT
|
||||
if (count($prices) >= 500) {
|
||||
DB::table('prices')->insert($prices);
|
||||
$batchCount += count($prices);
|
||||
$this->command->info(" → {$batchCount}건 완료...");
|
||||
$prices = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 남은 데이터 INSERT
|
||||
if (! empty($prices)) {
|
||||
DB::table('prices')->insert($prices);
|
||||
$batchCount += count($prices);
|
||||
}
|
||||
|
||||
$this->command->info(" ✓ prices: {$batchCount}건 생성 완료");
|
||||
|
||||
return $batchCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* PT-{name} 코드에서 name 추출
|
||||
*/
|
||||
private function getItemListName(string $code): string
|
||||
{
|
||||
return str_starts_with($code, 'PT-') ? substr($code, 3) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 1.1: models → items (FG) 마이그레이션
|
||||
*/
|
||||
private function migrateModels(int $tenantId, int $userId): int
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('📦 [Phase 1.1] models → items (FG) 마이그레이션...');
|
||||
|
||||
$models = DB::connection('chandj')
|
||||
->table('models')
|
||||
->where(function ($q) {
|
||||
$q->where('is_deleted', 0)->orWhereNull('is_deleted');
|
||||
})
|
||||
->get();
|
||||
|
||||
$this->command->info(" → 소스 데이터: {$models->count()}건");
|
||||
|
||||
$items = [];
|
||||
$now = now();
|
||||
|
||||
foreach ($models as $model) {
|
||||
$finishingShort = self::FINISHING_MAP[$model->finishing_type] ?? 'STD';
|
||||
$code = "FG-{$model->model_name}-{$model->guiderail_type}-{$finishingShort}";
|
||||
$name = "{$model->model_name} {$model->major_category} {$model->finishing_type} {$model->guiderail_type}";
|
||||
|
||||
$items[] = [
|
||||
'tenant_id' => $tenantId,
|
||||
'item_type' => 'FG',
|
||||
'code' => $code,
|
||||
'name' => trim($name),
|
||||
'unit' => 'EA',
|
||||
'category_id' => null,
|
||||
'process_type' => null,
|
||||
'item_category' => $model->major_category,
|
||||
'bom' => null,
|
||||
'attributes' => json_encode([
|
||||
'model_name' => $model->model_name,
|
||||
'major_category' => $model->major_category,
|
||||
'finishing_type' => $model->finishing_type,
|
||||
'guiderail_type' => $model->guiderail_type,
|
||||
'legacy_source' => 'models',
|
||||
'legacy_model_id' => $model->model_id,
|
||||
]),
|
||||
'attributes_archive' => null,
|
||||
'options' => null,
|
||||
'description' => null,
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($items)) {
|
||||
DB::table('items')->insert($items);
|
||||
}
|
||||
|
||||
$this->command->info(" ✓ items (FG): {$models->count()}건 생성 완료");
|
||||
|
||||
return $models->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 1.2: item_list → items (PT) 마이그레이션
|
||||
*/
|
||||
private function migrateItemList(int $tenantId, int $userId): int
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('📦 [Phase 1.2] item_list → items (PT) 마이그레이션...');
|
||||
|
||||
$itemList = DB::connection('chandj')
|
||||
->table('item_list')
|
||||
->get();
|
||||
|
||||
$this->command->info(" → 소스 데이터: {$itemList->count()}건");
|
||||
|
||||
$items = [];
|
||||
$now = now();
|
||||
|
||||
foreach ($itemList as $item) {
|
||||
$code = "PT-{$item->item_name}";
|
||||
|
||||
$items[] = [
|
||||
'tenant_id' => $tenantId,
|
||||
'item_type' => 'PT',
|
||||
'code' => $code,
|
||||
'name' => $item->item_name,
|
||||
'unit' => 'EA',
|
||||
'category_id' => null,
|
||||
'process_type' => null,
|
||||
'item_category' => null,
|
||||
'bom' => null,
|
||||
'attributes' => json_encode([
|
||||
'base_price' => $item->col13,
|
||||
'legacy_source' => 'item_list',
|
||||
'legacy_num' => $item->num,
|
||||
]),
|
||||
'attributes_archive' => null,
|
||||
'options' => null,
|
||||
'description' => null,
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($items)) {
|
||||
DB::table('items')->insert($items);
|
||||
}
|
||||
|
||||
$this->command->info(" ✓ items (PT): {$itemList->count()}건 생성 완료");
|
||||
|
||||
return $itemList->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* item_div → item_type 매핑
|
||||
*/
|
||||
private function mapItemType(?string $itemDiv): string
|
||||
{
|
||||
return self::ITEM_TYPE_MAP[$itemDiv] ?? 'SM';
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2.1: BDmodels.seconditem → items (PT) 누락 부품 추가
|
||||
*
|
||||
* item_list에 없는 BDmodels.seconditem을 PT items로 생성
|
||||
*/
|
||||
private function migrateBDmodelsParts(int $tenantId, int $userId): int
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('📦 [Phase 2.1] BDmodels.seconditem → items (PT) 누락 부품...');
|
||||
|
||||
// BDmodels에서 고유한 seconditem 목록 조회
|
||||
$bdSecondItems = DB::connection('chandj')
|
||||
->table('BDmodels')
|
||||
->where(function ($q) {
|
||||
$q->where('is_deleted', 0)->orWhereNull('is_deleted');
|
||||
})
|
||||
->whereNotNull('seconditem')
|
||||
->where('seconditem', '!=', '')
|
||||
->distinct()
|
||||
->pluck('seconditem');
|
||||
|
||||
// 이미 존재하는 PT items 코드 조회
|
||||
$existingPtCodes = DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('item_type', 'PT')
|
||||
->pluck('code')
|
||||
->map(fn ($code) => str_starts_with($code, 'PT-') ? substr($code, 3) : $code)
|
||||
->toArray();
|
||||
|
||||
$items = [];
|
||||
$now = now();
|
||||
|
||||
foreach ($bdSecondItems as $secondItem) {
|
||||
// 이미 PT items에 있으면 스킵
|
||||
if (in_array($secondItem, $existingPtCodes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$code = "PT-{$secondItem}";
|
||||
|
||||
$items[] = [
|
||||
'tenant_id' => $tenantId,
|
||||
'item_type' => 'PT',
|
||||
'code' => $code,
|
||||
'name' => $secondItem,
|
||||
'unit' => 'EA',
|
||||
'category_id' => null,
|
||||
'process_type' => null,
|
||||
'item_category' => null,
|
||||
'bom' => null,
|
||||
'attributes' => json_encode([
|
||||
'legacy_source' => 'BDmodels_seconditem',
|
||||
]),
|
||||
'attributes_archive' => null,
|
||||
'options' => null,
|
||||
'description' => null,
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($items)) {
|
||||
DB::table('items')->insert($items);
|
||||
}
|
||||
|
||||
$this->command->info(" → 소스 데이터: {$bdSecondItems->count()}건 (중복 제외 ".count($items).'건 신규)');
|
||||
$this->command->info(' ✓ items (PT): '.count($items).'건 생성 완료');
|
||||
|
||||
return count($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2.2: BDmodels → items.bom JSON (FG ↔ PT 연결)
|
||||
*
|
||||
* models 기반 FG items에 BOM 연결
|
||||
* bom: [{child_item_id: X, quantity: Y}, ...]
|
||||
*/
|
||||
private function migrateBom(int $tenantId): int
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('🔗 [Phase 2.2] BDmodels → items.bom JSON 연결...');
|
||||
|
||||
// PT items 조회 (code → id 매핑)
|
||||
$ptItems = DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('item_type', 'PT')
|
||||
->pluck('id', 'code')
|
||||
->toArray();
|
||||
|
||||
// PT- prefix 없는 버전도 매핑 추가
|
||||
$ptItemsByName = [];
|
||||
foreach ($ptItems as $code => $id) {
|
||||
$name = str_starts_with($code, 'PT-') ? substr($code, 3) : $code;
|
||||
$ptItemsByName[$name] = $id;
|
||||
}
|
||||
|
||||
// FG items 조회 (models 기반)
|
||||
$fgItems = DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('item_type', 'FG')
|
||||
->whereNotNull('attributes')
|
||||
->get(['id', 'code', 'attributes']);
|
||||
|
||||
// BDmodels 데이터 조회
|
||||
$bdModels = DB::connection('chandj')
|
||||
->table('BDmodels')
|
||||
->where(function ($q) {
|
||||
$q->where('is_deleted', 0)->orWhereNull('is_deleted');
|
||||
})
|
||||
->whereNotNull('model_name')
|
||||
->where('model_name', '!=', '')
|
||||
->get(['model_name', 'seconditem', 'savejson']);
|
||||
|
||||
// model_name → seconditems 그룹핑
|
||||
$modelBomMap = [];
|
||||
foreach ($bdModels as $bd) {
|
||||
if (empty($bd->seconditem)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$modelName = $bd->model_name;
|
||||
if (! isset($modelBomMap[$modelName])) {
|
||||
$modelBomMap[$modelName] = [];
|
||||
}
|
||||
|
||||
// savejson에서 수량 파싱 (col8이 수량)
|
||||
$quantity = 1;
|
||||
if (! empty($bd->savejson)) {
|
||||
$json = json_decode($bd->savejson, true);
|
||||
if (is_array($json) && ! empty($json)) {
|
||||
// 첫 번째 항목의 col8(수량) 사용
|
||||
$quantity = (int) ($json[0]['col8'] ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 중복 체크 후 추가
|
||||
$found = false;
|
||||
foreach ($modelBomMap[$modelName] as &$existing) {
|
||||
if ($existing['seconditem'] === $bd->seconditem) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (! $found) {
|
||||
$modelBomMap[$modelName][] = [
|
||||
'seconditem' => $bd->seconditem,
|
||||
'quantity' => $quantity,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$updatedCount = 0;
|
||||
|
||||
foreach ($fgItems as $fgItem) {
|
||||
$attributes = json_decode($fgItem->attributes, true) ?? [];
|
||||
$modelName = $attributes['model_name'] ?? null;
|
||||
|
||||
if (empty($modelName) || ! isset($modelBomMap[$modelName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$bomArray = [];
|
||||
foreach ($modelBomMap[$modelName] as $bomItem) {
|
||||
$childItemId = $ptItemsByName[$bomItem['seconditem']] ?? null;
|
||||
if ($childItemId) {
|
||||
$bomArray[] = [
|
||||
'child_item_id' => $childItemId,
|
||||
'quantity' => $bomItem['quantity'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($bomArray)) {
|
||||
DB::table('items')
|
||||
->where('id', $fgItem->id)
|
||||
->update(['bom' => json_encode($bomArray)]);
|
||||
$updatedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info(' → BDmodels 모델: '.count($modelBomMap).'개');
|
||||
$this->command->info(" ✓ items.bom 연결: {$updatedCount}건 완료");
|
||||
|
||||
return $updatedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 3.1: price_motor → items (SM) + prices
|
||||
*
|
||||
* price_motor JSON에서 누락된 품목만 추가
|
||||
* - 제어기, 방화/방범 콘트롤박스, 스위치, 리모콘 등
|
||||
*
|
||||
* @return array{items: int, prices: int}
|
||||
*/
|
||||
private function migratePriceMotor(int $tenantId, int $userId): array
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('📦 [Phase 3.1] price_motor → items (SM) 누락 품목...');
|
||||
|
||||
// 최신 price_motor 데이터 조회
|
||||
$priceMotor = DB::connection('chandj')
|
||||
->table('price_motor')
|
||||
->where(function ($q) {
|
||||
$q->where('is_deleted', 0)->orWhereNull('is_deleted');
|
||||
})
|
||||
->orderByDesc('registedate')
|
||||
->first();
|
||||
|
||||
if (! $priceMotor || empty($priceMotor->itemList)) {
|
||||
$this->command->info(' → 소스 데이터 없음');
|
||||
|
||||
return ['items' => 0, 'prices' => 0];
|
||||
}
|
||||
|
||||
$itemList = json_decode($priceMotor->itemList, true);
|
||||
if (! is_array($itemList)) {
|
||||
$this->command->info(' → JSON 파싱 실패');
|
||||
|
||||
return ['items' => 0, 'prices' => 0];
|
||||
}
|
||||
|
||||
// 기존 items 이름 조회 (중복 체크용)
|
||||
$existingNames = DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->pluck('name')
|
||||
->map(fn ($n) => mb_strtolower($n))
|
||||
->toArray();
|
||||
|
||||
$items = [];
|
||||
$now = now();
|
||||
$newItemCodes = [];
|
||||
|
||||
foreach ($itemList as $idx => $item) {
|
||||
$col1 = $item['col1'] ?? ''; // 전압/카테고리 (220, 380, 제어기, 방화, 방범)
|
||||
$col2 = $item['col2'] ?? ''; // 용량/품목명
|
||||
$salesPrice = (float) str_replace(',', '', $item['col13'] ?? '0');
|
||||
|
||||
// 모터 품목은 KDunitprice에 이미 있으므로 스킵
|
||||
if (in_array($col1, ['220', '380'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 품목명 생성
|
||||
$name = trim("{$col1} {$col2}");
|
||||
if (empty($name) || $name === ' ') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 이미 존재하는 품목 스킵 (유사 이름 체크)
|
||||
$nameLower = mb_strtolower($name);
|
||||
$exists = false;
|
||||
foreach ($existingNames as $existingName) {
|
||||
if (str_contains($existingName, $nameLower) || str_contains($nameLower, $existingName)) {
|
||||
$exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 코드 생성
|
||||
$code = 'PM-'.str_pad($idx + 1, 3, '0', STR_PAD_LEFT);
|
||||
|
||||
$items[] = [
|
||||
'tenant_id' => $tenantId,
|
||||
'item_type' => 'SM',
|
||||
'code' => $code,
|
||||
'name' => $name,
|
||||
'unit' => 'EA',
|
||||
'category_id' => null,
|
||||
'process_type' => null,
|
||||
'item_category' => null,
|
||||
'bom' => null,
|
||||
'attributes' => json_encode([
|
||||
'price_category' => $col1,
|
||||
'price_spec' => $col2,
|
||||
'legacy_source' => 'price_motor',
|
||||
]),
|
||||
'attributes_archive' => null,
|
||||
'options' => null,
|
||||
'description' => null,
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
$newItemCodes[$code] = $salesPrice;
|
||||
$existingNames[] = $nameLower; // 중복 방지
|
||||
}
|
||||
|
||||
if (! empty($items)) {
|
||||
DB::table('items')->insert($items);
|
||||
}
|
||||
|
||||
// prices 생성
|
||||
$priceCount = 0;
|
||||
if (! empty($newItemCodes)) {
|
||||
$newItems = DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('code', array_keys($newItemCodes))
|
||||
->get(['id', 'code', 'item_type']);
|
||||
|
||||
$prices = [];
|
||||
foreach ($newItems as $item) {
|
||||
$prices[] = [
|
||||
'tenant_id' => $tenantId,
|
||||
'item_type_code' => $item->item_type,
|
||||
'item_id' => $item->id,
|
||||
'client_group_id' => null,
|
||||
'purchase_price' => 0,
|
||||
'processing_cost' => null,
|
||||
'loss_rate' => null,
|
||||
'margin_rate' => null,
|
||||
'sales_price' => $newItemCodes[$item->code],
|
||||
'rounding_rule' => 'round',
|
||||
'rounding_unit' => 1,
|
||||
'supplier' => null,
|
||||
'effective_from' => $priceMotor->registedate ?? now()->toDateString(),
|
||||
'effective_to' => null,
|
||||
'note' => 'price_motor 마이그레이션',
|
||||
'status' => 'active',
|
||||
'is_final' => false,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($prices)) {
|
||||
DB::table('prices')->insert($prices);
|
||||
$priceCount = count($prices);
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info(' → 소스 데이터: '.count($itemList).'건 (누락 '.count($items).'건 추가)');
|
||||
$this->command->info(' ✓ items: '.count($items).'건, prices: '.$priceCount.'건 생성 완료');
|
||||
|
||||
return ['items' => count($items), 'prices' => $priceCount];
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 3.2: price_raw_materials → items (RM) + prices
|
||||
*
|
||||
* price_raw_materials JSON에서 누락된 원자재 품목 추가
|
||||
*
|
||||
* @return array{items: int, prices: int}
|
||||
*/
|
||||
private function migratePriceRawMaterials(int $tenantId, int $userId): array
|
||||
{
|
||||
$this->command->info('');
|
||||
$this->command->info('📦 [Phase 3.2] price_raw_materials → items (RM) 누락 품목...');
|
||||
|
||||
// 최신 price_raw_materials 데이터 조회
|
||||
$priceRaw = DB::connection('chandj')
|
||||
->table('price_raw_materials')
|
||||
->where(function ($q) {
|
||||
$q->where('is_deleted', 0)->orWhereNull('is_deleted');
|
||||
})
|
||||
->orderByDesc('registedate')
|
||||
->first();
|
||||
|
||||
if (! $priceRaw || empty($priceRaw->itemList)) {
|
||||
$this->command->info(' → 소스 데이터 없음');
|
||||
|
||||
return ['items' => 0, 'prices' => 0];
|
||||
}
|
||||
|
||||
$itemList = json_decode($priceRaw->itemList, true);
|
||||
if (! is_array($itemList)) {
|
||||
$this->command->info(' → JSON 파싱 실패');
|
||||
|
||||
return ['items' => 0, 'prices' => 0];
|
||||
}
|
||||
|
||||
// 기존 items 이름 조회 (중복 체크용)
|
||||
$existingNames = DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->pluck('name')
|
||||
->map(fn ($n) => mb_strtolower($n))
|
||||
->toArray();
|
||||
|
||||
$items = [];
|
||||
$now = now();
|
||||
$newItemCodes = [];
|
||||
|
||||
foreach ($itemList as $idx => $item) {
|
||||
$col1 = $item['col1'] ?? ''; // 카테고리 (슬랫, 스크린)
|
||||
$col2 = $item['col2'] ?? ''; // 품목명 (방화, 실리카, 화이바)
|
||||
$salesPrice = (float) str_replace(',', '', $item['col13'] ?? '0');
|
||||
|
||||
// 품목명 생성
|
||||
$name = trim("{$col1} {$col2}");
|
||||
if (empty($name) || $name === ' ') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 이미 존재하는 품목 스킵
|
||||
$nameLower = mb_strtolower($name);
|
||||
$exists = false;
|
||||
foreach ($existingNames as $existingName) {
|
||||
// 정확히 일치하거나 유사한 이름 체크
|
||||
$col2Lower = mb_strtolower($col2);
|
||||
if (str_contains($existingName, $col2Lower) || $existingName === $nameLower) {
|
||||
$exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 코드 생성
|
||||
$code = 'RM-'.str_pad($idx + 1, 3, '0', STR_PAD_LEFT);
|
||||
|
||||
$items[] = [
|
||||
'tenant_id' => $tenantId,
|
||||
'item_type' => 'RM',
|
||||
'code' => $code,
|
||||
'name' => $name,
|
||||
'unit' => 'EA',
|
||||
'category_id' => null,
|
||||
'process_type' => null,
|
||||
'item_category' => $col1,
|
||||
'bom' => null,
|
||||
'attributes' => json_encode([
|
||||
'raw_category' => $col1,
|
||||
'raw_name' => $col2,
|
||||
'legacy_source' => 'price_raw_materials',
|
||||
]),
|
||||
'attributes_archive' => null,
|
||||
'options' => null,
|
||||
'description' => null,
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
$newItemCodes[$code] = $salesPrice;
|
||||
$existingNames[] = $nameLower;
|
||||
}
|
||||
|
||||
if (! empty($items)) {
|
||||
DB::table('items')->insert($items);
|
||||
}
|
||||
|
||||
// prices 생성
|
||||
$priceCount = 0;
|
||||
if (! empty($newItemCodes)) {
|
||||
$newItems = DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('code', array_keys($newItemCodes))
|
||||
->get(['id', 'code', 'item_type']);
|
||||
|
||||
$prices = [];
|
||||
foreach ($newItems as $item) {
|
||||
$prices[] = [
|
||||
'tenant_id' => $tenantId,
|
||||
'item_type_code' => $item->item_type,
|
||||
'item_id' => $item->id,
|
||||
'client_group_id' => null,
|
||||
'purchase_price' => 0,
|
||||
'processing_cost' => null,
|
||||
'loss_rate' => null,
|
||||
'margin_rate' => null,
|
||||
'sales_price' => $newItemCodes[$item->code],
|
||||
'rounding_rule' => 'round',
|
||||
'rounding_unit' => 1,
|
||||
'supplier' => null,
|
||||
'effective_from' => $priceRaw->registedate ?? now()->toDateString(),
|
||||
'effective_to' => null,
|
||||
'note' => 'price_raw_materials 마이그레이션',
|
||||
'status' => 'active',
|
||||
'is_final' => false,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($prices)) {
|
||||
DB::table('prices')->insert($prices);
|
||||
$priceCount = count($prices);
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info(' → 소스 데이터: '.count($itemList).'건 (누락 '.count($items).'건 추가)');
|
||||
$this->command->info(' ✓ items: '.count($items).'건, prices: '.$priceCount.'건 생성 완료');
|
||||
|
||||
return ['items' => count($items), 'prices' => $priceCount];
|
||||
}
|
||||
}
|
||||
131
database/seeders/QuoteItemCategorySeeder.php
Normal file
131
database/seeders/QuoteItemCategorySeeder.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 견적 품목 카테고리 시더
|
||||
*
|
||||
* 상위 카테고리 추가 및 기존 세부 항목 연결
|
||||
* - 본체, 절곡품(>가이드레일,케이스,하단마감재), 모터&제어기, 부자재
|
||||
*/
|
||||
class QuoteItemCategorySeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = 1;
|
||||
$codeGroup = 'item_category';
|
||||
$now = now();
|
||||
|
||||
// 1. 상위 카테고리 추가
|
||||
$parentCategories = [
|
||||
['code' => 'BODY', 'name' => '본체', 'sort_order' => 1],
|
||||
['code' => 'BENDING', 'name' => '절곡품', 'sort_order' => 2],
|
||||
['code' => 'MOTOR_CTRL', 'name' => '모터 & 제어기', 'sort_order' => 3],
|
||||
['code' => 'ACCESSORY', 'name' => '부자재', 'sort_order' => 4],
|
||||
];
|
||||
|
||||
$parentIds = [];
|
||||
foreach ($parentCategories as $cat) {
|
||||
// 이미 존재하면 업데이트, 없으면 생성
|
||||
DB::table('categories')->updateOrInsert(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'code_group' => $codeGroup,
|
||||
'code' => $cat['code'],
|
||||
],
|
||||
[
|
||||
'parent_id' => null,
|
||||
'name' => $cat['name'],
|
||||
'sort_order' => $cat['sort_order'],
|
||||
'is_active' => true,
|
||||
'updated_at' => $now,
|
||||
'created_at' => $now,
|
||||
]
|
||||
);
|
||||
|
||||
// ID 조회
|
||||
$parentIds[$cat['code']] = DB::table('categories')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code_group', $codeGroup)
|
||||
->where('code', $cat['code'])
|
||||
->value('id');
|
||||
}
|
||||
|
||||
// 2. 절곡품 하위 3개 중간 카테고리 추가
|
||||
$bendingSubCategories = [
|
||||
['code' => 'BENDING_GUIDE', 'name' => '가이드레일', 'sort_order' => 1],
|
||||
['code' => 'BENDING_CASE', 'name' => '케이스', 'sort_order' => 2],
|
||||
['code' => 'BENDING_BOTTOM', 'name' => '하단마감재', 'sort_order' => 3],
|
||||
];
|
||||
|
||||
$bendingSubIds = [];
|
||||
foreach ($bendingSubCategories as $cat) {
|
||||
// 이미 존재하면 업데이트, 없으면 생성
|
||||
DB::table('categories')->updateOrInsert(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'code_group' => $codeGroup,
|
||||
'code' => $cat['code'],
|
||||
],
|
||||
[
|
||||
'parent_id' => $parentIds['BENDING'],
|
||||
'name' => $cat['name'],
|
||||
'sort_order' => $cat['sort_order'],
|
||||
'is_active' => true,
|
||||
'updated_at' => $now,
|
||||
'created_at' => $now,
|
||||
]
|
||||
);
|
||||
|
||||
// ID 조회
|
||||
$bendingSubIds[$cat['code']] = DB::table('categories')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code_group', $codeGroup)
|
||||
->where('code', $cat['code'])
|
||||
->value('id');
|
||||
}
|
||||
|
||||
// 3. 기존 세부 항목들의 parent_id 업데이트
|
||||
$mappings = [
|
||||
// 본체
|
||||
'BODY' => ['SILICA_BODY', 'WIRE_BODY', 'FIBER_BODY', 'COLUMNLESS_BODY', 'SLAT_BODY'],
|
||||
|
||||
// 절곡품 > 가이드레일
|
||||
'BENDING_GUIDE' => ['GUIDE_RAIL', 'SMOKE_SEAL'],
|
||||
|
||||
// 절곡품 > 케이스
|
||||
'BENDING_CASE' => ['SHUTTER_BOX', 'TOP_COVER', 'END_PLATE'],
|
||||
|
||||
// 절곡품 > 하단마감재
|
||||
'BENDING_BOTTOM' => ['BOTTOM_TRIM', 'HAJANG_BAR', 'SPECIAL_TRIM', 'FLOOR_CUT_PLATE'],
|
||||
|
||||
// 모터 & 제어기
|
||||
'MOTOR_CTRL' => ['MOTOR_SET', 'INTERLOCK_CTRL', 'EMBED_BACK_BOX'],
|
||||
|
||||
// 부자재
|
||||
'ACCESSORY' => ['JOINT_BAR', 'SQUARE_PIPE', 'WINDING_SHAFT', 'ANGLE', 'ROUND_BAR', 'L_BAR', 'REINF_FLAT_BAR', 'WEIGHT_FLAT_BAR'],
|
||||
];
|
||||
|
||||
foreach ($mappings as $parentCode => $childCodes) {
|
||||
// 상위 카테고리 ID 찾기
|
||||
$parentId = $parentIds[$parentCode] ?? $bendingSubIds[$parentCode] ?? null;
|
||||
|
||||
if ($parentId) {
|
||||
DB::table('categories')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code_group', $codeGroup)
|
||||
->whereIn('code', $childCodes)
|
||||
->whereNull('deleted_at')
|
||||
->update(['parent_id' => $parentId, 'updated_at' => $now]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info('견적 품목 카테고리 시딩 완료!');
|
||||
$this->command->info('- 상위 카테고리 4개 추가');
|
||||
$this->command->info('- 절곡품 하위 카테고리 3개 추가');
|
||||
$this->command->info('- 기존 세부 항목 parent_id 연결 완료');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user