chore(API): 마이그레이션 파일 정리 및 추가

- 기존 마이그레이션 파일 정리
- handover_reports 테이블 마이그레이션 추가
- site_briefings 테이블 마이그레이션 추가
- work_orders process_id 마이그레이션 추가

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-13 19:49:23 +09:00
parent 4764eeb9d4
commit 1044b57e15
26 changed files with 572 additions and 19 deletions

View File

@@ -0,0 +1,193 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* 작업지시 공정유형 변경 마이그레이션
*
* 변경 사항:
* - process_type (varchar: screen/slat/bending) 제거
* - process_id (FK → processes.id) 추가
*
* 데이터 마이그레이션:
* - screen → processes.process_name = '스크린' (P-002)
* - slat → processes.process_name = '슬랫' (P-001)
* - bending → processes.process_name = '절곡' (신규 생성)
*/
return new class extends Migration
{
public function up(): void
{
// 1. 절곡 공정이 없으면 각 테넌트별로 생성
$this->ensureBendingProcessExists();
// 2. process_id 컬럼 추가 (nullable로 먼저 생성)
Schema::table('work_orders', function (Blueprint $table) {
$table->unsignedBigInteger('process_id')
->nullable()
->after('sales_order_id')
->comment('공정 ID (FK → processes.id)');
});
// 3. 기존 process_type 데이터를 process_id로 마이그레이션
$this->migrateProcessTypeToProcessId();
// 4. process_id에 FK 제약 추가 및 NOT NULL로 변경
Schema::table('work_orders', function (Blueprint $table) {
$table->foreign('process_id')
->references('id')
->on('processes')
->onDelete('restrict');
});
// 5. 기존 process_type 컬럼 제거
Schema::table('work_orders', function (Blueprint $table) {
$table->dropColumn('process_type');
});
}
public function down(): void
{
// 1. process_type 컬럼 복원
Schema::table('work_orders', function (Blueprint $table) {
$table->string('process_type', 30)
->default('screen')
->after('sales_order_id')
->comment('공정유형: screen/slat/bending');
});
// 2. process_id에서 process_type으로 데이터 복원
$this->migrateProcessIdToProcessType();
// 3. FK 및 process_id 컬럼 제거
Schema::table('work_orders', function (Blueprint $table) {
$table->dropForeign(['process_id']);
$table->dropColumn('process_id');
});
}
/**
* 절곡 공정이 없는 테넌트에 절곡 공정 생성
*/
private function ensureBendingProcessExists(): void
{
// 작업지시에서 bending을 사용하는 테넌트 조회
$tenantIds = DB::table('work_orders')
->where('process_type', 'bending')
->distinct()
->pluck('tenant_id');
foreach ($tenantIds as $tenantId) {
// 해당 테넌트에 절곡 공정이 있는지 확인
$exists = DB::table('processes')
->where('tenant_id', $tenantId)
->where('process_name', '절곡')
->exists();
if (! $exists) {
// 마지막 공정코드 조회
$lastCode = DB::table('processes')
->where('tenant_id', $tenantId)
->orderByDesc('process_code')
->value('process_code');
// 새 공정코드 생성 (P-003 형식)
$newCodeNum = 1;
if ($lastCode && preg_match('/P-(\d+)/', $lastCode, $matches)) {
$newCodeNum = (int) $matches[1] + 1;
}
$newCode = sprintf('P-%03d', $newCodeNum);
// 절곡 공정 생성
DB::table('processes')->insert([
'tenant_id' => $tenantId,
'process_code' => $newCode,
'process_name' => '절곡',
'process_type' => '생산',
'description' => '절곡 공정 (마이그레이션에 의해 자동 생성)',
'is_active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
}
/**
* process_type → process_id 데이터 마이그레이션
*/
private function migrateProcessTypeToProcessId(): void
{
// 공정명 매핑 (process_type → process_name)
$mappings = [
'screen' => '스크린',
'slat' => '슬랫',
'bending' => '절곡',
];
foreach ($mappings as $processType => $processName) {
// 각 테넌트별로 해당 공정 ID 조회 후 업데이트
$workOrders = DB::table('work_orders')
->where('process_type', $processType)
->whereNull('process_id')
->get(['id', 'tenant_id']);
foreach ($workOrders as $workOrder) {
$processId = DB::table('processes')
->where('tenant_id', $workOrder->tenant_id)
->where('process_name', $processName)
->value('id');
if ($processId) {
DB::table('work_orders')
->where('id', $workOrder->id)
->update(['process_id' => $processId]);
}
}
}
// process_id가 설정되지 않은 레코드 확인 (데이터 무결성)
$orphanCount = DB::table('work_orders')
->whereNull('process_id')
->count();
if ($orphanCount > 0) {
throw new \RuntimeException(
"마이그레이션 실패: {$orphanCount}개의 작업지시에 대응하는 공정을 찾을 수 없습니다. ".
'processes 테이블에 스크린, 슬랫, 절곡 공정이 등록되어 있는지 확인하세요.'
);
}
}
/**
* process_id → process_type 데이터 복원 (롤백용)
*/
private function migrateProcessIdToProcessType(): void
{
// 공정명 → process_type 역매핑
$mappings = [
'스크린' => 'screen',
'슬랫' => 'slat',
'절곡' => 'bending',
];
foreach ($mappings as $processName => $processType) {
DB::table('work_orders')
->whereIn('process_id', function ($query) use ($processName) {
$query->select('id')
->from('processes')
->where('process_name', $processName);
})
->update(['process_type' => $processType]);
}
// 매핑되지 않은 공정은 기본값 screen으로 설정
DB::table('work_orders')
->where('process_type', '')
->orWhereNull('process_type')
->update(['process_type' => 'screen']);
}
};

View File

@@ -27,4 +27,4 @@ public function down(): void
$table->datetime('paid_at')->nullable(false)->change(); $table->datetime('paid_at')->nullable(false)->change();
}); });
} }
}; };

View File

@@ -28,4 +28,4 @@ public function down(): void
$table->dropColumn(['cancelled_at', 'cancel_reason']); $table->dropColumn(['cancelled_at', 'cancel_reason']);
}); });
} }
}; };

View File

@@ -41,4 +41,4 @@ public function down(): void
$table->string('bad_debt_progress', 20)->nullable()->after('bad_debt_end_date')->comment('악성채권 진행상황'); $table->string('bad_debt_progress', 20)->nullable()->after('bad_debt_end_date')->comment('악성채권 진행상황');
}); });
} }
}; };

View File

@@ -57,4 +57,4 @@ public function down(): void
{ {
Schema::dropIfExists('salaries'); Schema::dropIfExists('salaries');
} }
}; };

View File

@@ -39,4 +39,4 @@ public function down(): void
{ {
Schema::dropIfExists('expected_expenses'); Schema::dropIfExists('expected_expenses');
} }
}; };

View File

@@ -46,4 +46,4 @@ public function down(): void
{ {
Schema::dropIfExists('work_orders'); Schema::dropIfExists('work_orders');
} }
}; };

View File

@@ -43,4 +43,4 @@ public function down(): void
{ {
Schema::dropIfExists('work_order_bending_details'); Schema::dropIfExists('work_order_bending_details');
} }
}; };

View File

@@ -39,4 +39,4 @@ public function down(): void
{ {
Schema::dropIfExists('work_order_issues'); Schema::dropIfExists('work_order_issues');
} }
}; };

View File

@@ -113,4 +113,4 @@ public function down(): void
$table->index('inspection_id'); $table->index('inspection_id');
}); });
} }
}; };

View File

@@ -27,4 +27,4 @@ public function down(): void
{ {
Schema::dropIfExists('positions'); Schema::dropIfExists('positions');
} }
}; };

View File

@@ -35,4 +35,4 @@ public function down(): void
{ {
DB::table('common_codes')->where('code_group', 'position_type')->delete(); DB::table('common_codes')->where('code_group', 'position_type')->delete();
} }
}; };

View File

@@ -27,4 +27,4 @@ public function down(): void
$table->dropColumn('key'); $table->dropColumn('key');
}); });
} }
}; };

View File

@@ -75,4 +75,4 @@ public function down(): void
]); ]);
}); });
} }
}; };

View File

@@ -63,4 +63,4 @@ public function down(): void
]); ]);
}); });
} }
}; };

View File

@@ -30,4 +30,4 @@ public function down(): void
$table->dropColumn('order_id'); $table->dropColumn('order_id');
}); });
} }
}; };

View File

@@ -74,4 +74,4 @@ public function down(): void
{ {
Schema::dropIfExists('contracts'); Schema::dropIfExists('contracts');
} }
}; };

View File

@@ -41,4 +41,4 @@ public function down(): void
{ {
Schema::dropIfExists('process_items'); Schema::dropIfExists('process_items');
} }
}; };

View File

@@ -0,0 +1,88 @@
<?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('handover_reports', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
// 보고서 기본 정보
$table->string('report_number', 50)->comment('보고서번호');
$table->unsignedBigInteger('contract_id')->nullable()->comment('연결 계약 ID');
$table->string('site_name')->comment('현장명');
// 거래처 정보
$table->unsignedBigInteger('partner_id')->nullable()->comment('거래처 ID');
$table->string('partner_name')->nullable()->comment('거래처명');
// 담당자 정보
$table->unsignedBigInteger('contract_manager_id')->nullable()->comment('계약담당자 ID');
$table->string('contract_manager_name')->nullable()->comment('계약담당자명');
$table->unsignedBigInteger('construction_pm_id')->nullable()->comment('공사PM ID');
$table->string('construction_pm_name')->nullable()->comment('공사PM명');
// 계약 상세
$table->integer('total_sites')->default(0)->comment('총 개소');
$table->decimal('contract_amount', 15, 2)->default(0)->comment('계약금액');
$table->date('contract_date')->nullable()->comment('계약일자');
$table->date('contract_start_date')->nullable()->comment('계약시작일');
$table->date('contract_end_date')->nullable()->comment('계약종료일');
$table->date('completion_date')->nullable()->comment('준공일');
// 상태 정보
$table->string('status', 20)->default('pending')->comment('인수인계상태: pending, completed');
// 2차 배관
$table->boolean('has_secondary_piping')->default(false)->comment('2차 배관 유무');
$table->decimal('secondary_piping_amount', 15, 2)->default(0)->comment('2차 배관 금액');
$table->text('secondary_piping_note')->nullable()->comment('2차 배관 비고');
// 도장 & 코킹
$table->boolean('has_coating')->default(false)->comment('도장 & 코킹 유무');
$table->decimal('coating_amount', 15, 2)->default(0)->comment('도장 & 코킹 금액');
$table->text('coating_note')->nullable()->comment('도장 & 코킹 비고');
// 장비 외 실행금액 (JSON)
$table->json('external_equipment_cost')->nullable()->comment('장비 외 실행금액: shipping_cost, high_altitude_work, public_expense');
// 특이사항
$table->text('special_notes')->nullable()->comment('특이사항');
// 활성화 상태
$table->boolean('is_active')->default(true)->comment('활성화 여부');
// 감사 컬럼
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID');
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID');
// 타임스탬프
$table->timestamps();
$table->softDeletes();
// 인덱스
$table->index('tenant_id');
$table->index('contract_id');
$table->index('partner_id');
$table->index('status');
$table->unique(['tenant_id', 'report_number']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('handover_reports');
}
};

View File

@@ -0,0 +1,53 @@
<?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('handover_report_managers', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('handover_report_id')->comment('인수인계보고서 ID');
// 담당자 정보
$table->string('name')->comment('담당자명');
$table->text('non_performance_reason')->nullable()->comment('미이행 사유');
$table->text('signature')->nullable()->comment('서명 (Base64 또는 URL)');
// 순서
$table->integer('sort_order')->default(0)->comment('정렬 순서');
// 감사 컬럼
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID');
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
// 타임스탬프
$table->timestamps();
// 인덱스
$table->index('tenant_id');
$table->index('handover_report_id');
// 외래키
$table->foreign('handover_report_id')
->references('id')
->on('handover_reports')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('handover_report_managers');
}
};

View File

@@ -0,0 +1,53 @@
<?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('handover_report_items', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('handover_report_id')->comment('인수인계보고서 ID');
// ITEM 정보
$table->integer('item_no')->default(0)->comment('순번');
$table->string('name')->comment('명칭');
$table->string('product')->nullable()->comment('제품');
$table->integer('quantity')->default(0)->comment('수량');
$table->text('remark')->nullable()->comment('비고');
// 감사 컬럼
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID');
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
// 타임스탬프
$table->timestamps();
// 인덱스
$table->index('tenant_id');
$table->index('handover_report_id');
$table->index(['handover_report_id', 'item_no']);
// 외래키
$table->foreign('handover_report_id')
->references('id')
->on('handover_reports')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('handover_report_items');
}
};

View File

@@ -51,4 +51,4 @@ public function down(): void
{ {
Schema::dropIfExists('structure_reviews'); Schema::dropIfExists('structure_reviews');
} }
}; };

View File

@@ -66,4 +66,4 @@ public function down(): void
DB::table('common_codes')->where('code_group', 'order_status')->delete(); DB::table('common_codes')->where('code_group', 'order_status')->delete();
DB::table('common_codes')->where('code_group', 'order_type')->delete(); DB::table('common_codes')->where('code_group', 'order_type')->delete();
} }
}; };

View File

@@ -0,0 +1,78 @@
<?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('site_briefings', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->comment('테넌트 ID');
// 기본 정보
$table->string('briefing_code', 50)->nullable()->comment('현설번호 (자동생성)');
$table->string('title', 200)->comment('현장설명회명/현장명');
$table->text('description')->nullable()->comment('설명/업무보고');
// 거래처/현장 연결
$table->foreignId('partner_id')->nullable()->comment('거래처 ID');
$table->foreignId('site_id')->nullable()->comment('현장 ID');
// 일정 정보
$table->date('briefing_date')->comment('현장설명회 일자');
$table->string('briefing_time', 10)->nullable()->comment('현장설명회 시간 (HH:MM)');
$table->enum('briefing_type', ['online', 'offline'])->default('offline')->comment('구분: online|offline');
$table->string('location', 200)->nullable()->comment('현장설명회 장소');
$table->string('address', 255)->nullable()->comment('주소');
// 상태 정보
$table->enum('status', ['scheduled', 'ongoing', 'completed', 'cancelled', 'postponed'])
->default('scheduled')->comment('상태: scheduled|ongoing|completed|cancelled|postponed');
$table->enum('bid_status', ['pending', 'bidding', 'closed', 'failed', 'awarded'])
->default('pending')->comment('입찰상태: pending|bidding|closed|failed|awarded');
$table->date('bid_date')->nullable()->comment('입찰일자');
// 참석자 정보
$table->string('attendee', 100)->nullable()->comment('담당 참석자');
$table->enum('attendance_status', ['scheduled', 'attended', 'absent'])
->default('scheduled')->comment('참석상태: scheduled|attended|absent');
$table->integer('attendee_count')->default(0)->comment('총 참석자 수');
// 공사 정보
$table->integer('site_count')->default(0)->comment('개소 수');
$table->date('construction_start_date')->nullable()->comment('공사기간 시작');
$table->date('construction_end_date')->nullable()->comment('공사기간 종료');
$table->enum('vat_type', ['excluded', 'included'])->default('excluded')->comment('부가세: excluded|included');
// 감사 필드
$table->foreignId('created_by')->nullable()->comment('생성자');
$table->foreignId('updated_by')->nullable()->comment('수정자');
$table->foreignId('deleted_by')->nullable()->comment('삭제자');
$table->softDeletes();
$table->timestamps();
// 인덱스
$table->index('tenant_id');
$table->index('partner_id');
$table->index('site_id');
$table->index('briefing_date');
$table->index('status');
$table->index('bid_status');
$table->unique(['tenant_id', 'briefing_code']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('site_briefings');
}
};

View File

@@ -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
{
public function up(): void
{
Schema::table('site_briefings', function (Blueprint $table) {
// attendee 컬럼 삭제 후 attendees JSON 컬럼 추가
$table->dropColumn('attendee');
});
Schema::table('site_briefings', function (Blueprint $table) {
// JSON 컬럼으로 참석자 목록 저장
// 구조: [{"user_id": 1, "name": "홍길동", "type": "internal"}, {"name": "외부인", "company": "업체명", "phone": "010-1234-5678", "type": "external"}]
$table->json('attendees')->nullable()->after('bid_date')->comment('참석자 목록 (JSON)');
});
}
public function down(): void
{
Schema::table('site_briefings', function (Blueprint $table) {
$table->dropColumn('attendees');
});
Schema::table('site_briefings', function (Blueprint $table) {
$table->string('attendee', 100)->nullable()->after('bid_date')->comment('참석자 (단일)');
});
}
};

View File

@@ -0,0 +1,55 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* 현장설명회 참석완료 시 견적 자동등록 기능을 위한 스키마 변경
* - quotes에 site_briefing_id FK 추가
* - product_category nullable로 변경
* - status에 'pending' (견적대기) 추가
*/
public function up(): void
{
Schema::table('quotes', function (Blueprint $table) {
// 현장설명회 연결
$table->foreignId('site_briefing_id')
->nullable()
->after('order_id')
->comment('현장설명회 ID (자동생성 시)');
// 인덱스 추가
$table->index('site_briefing_id', 'idx_quotes_site_briefing_id');
});
// product_category를 nullable enum으로 변경
// MySQL에서 enum 변경은 raw SQL 필요
DB::statement("ALTER TABLE quotes MODIFY product_category ENUM('SCREEN', 'STEEL') NULL COMMENT '제품 카테고리'");
// status에 'pending' 추가
DB::statement("ALTER TABLE quotes MODIFY status ENUM('pending', 'draft', 'sent', 'approved', 'rejected', 'finalized', 'converted') DEFAULT 'draft' COMMENT '상태'");
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('quotes', function (Blueprint $table) {
$table->dropIndex('idx_quotes_site_briefing_id');
$table->dropColumn('site_briefing_id');
});
// product_category를 NOT NULL로 복원
DB::statement("ALTER TABLE quotes MODIFY product_category ENUM('SCREEN', 'STEEL') NOT NULL COMMENT '제품 카테고리'");
// status에서 'pending' 제거
DB::statement("ALTER TABLE quotes MODIFY status ENUM('draft', 'sent', 'approved', 'rejected', 'finalized', 'converted') DEFAULT 'draft' COMMENT '상태'");
}
};