feat: ERD 자동 생성 시스템 구축 및 모델 오류 수정

- GraphViz 설치를 통한 ERD 다이어그램 생성 지원
- BelongsToTenantTrait → BelongsToTenant 트레잇명 수정
- Estimate, EstimateItem 모델의 인터페이스 참조 오류 해결
- 60개 모델의 완전한 관계도 생성 (graph.png, 4.1MB)
- beyondcode/laravel-er-diagram-generator 패키지 활용

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-24 22:30:28 +09:00
parent c63e676257
commit 1a796462e4
14 changed files with 984 additions and 73 deletions

View File

@@ -7,17 +7,13 @@
/**
* Phase 3: 제품-자재 관계 FK 제약조건 제거 (신중한 검토 필요)
* - product_components.material_id → materials
*
* 목적: 자재 변경/삭제 시 유연성 확보
* 주의사항:
* 1. 비즈니스 로직에서 무결성 검증 필요
* 2. 자재 삭제 시 BOM에 미치는 영향 검토 필요
* 3. 소프트 딜리트로 대부분 처리되므로 상대적으로 안전
* 실제 상황: product_components 테이블은 ref_type/ref_id 통합 구조 사용
* - material_id 컬럼이 존재하지 않음 (ref_type='MATERIAL', ref_id=material.id)
* - 물리적 FK 제약조건이 없는 상태
*
* 유지되는 핵심 FK:
* - product_components.parent_product_id → products (BOM 구조 핵심)
* - product_components.child_product_id → products (BOM 구조 핵심)
* 목적: 현재 구조 확인 및 논리적 관계 문서화
* 결론: 이미 FK 없는 유연한 구조로 구성되어 있음
*/
return new class extends Migration
{
@@ -55,9 +51,8 @@ private function dropForeignKeyIfExists(string $table, string $column): void
public function up(): void
{
echo "🚀 Phase 3: 제품-자재 관계 FK 제약조건 제거 시작\n\n";
echo "⚠️ 주의: 이 작업은 신중한 검토가 필요합니다!\n";
echo "📋 영향 범위: BOM 시스템의 자재 참조 관계\n\n";
echo "🚀 Phase 3: 제품-자재 관계 현황 분석 시작\n\n";
echo "📋 분석 범위: BOM 시스템의 자재 참조 관계\n\n";
// product_components 테이블 존재 여부 확인
if (!Schema::hasTable('product_components')) {
@@ -65,74 +60,76 @@ public function up(): void
return;
}
echo "1⃣ Product_components 테이블 FK 분석...\n";
echo "1⃣ Product_components 테이블 구조 분석...\n";
// 테이블 구조 확인
$columns = DB::select('DESCRIBE product_components');
$columnNames = array_map(function($col) { return $col->Field; }, $columns);
echo " 현재 테이블 구조:\n";
echo " - ref_type 컬럼: " . (in_array('ref_type', $columnNames) ? "존재 (통합 참조 타입)" : "없음") . "\n";
echo " - ref_id 컬럼: " . (in_array('ref_id', $columnNames) ? "존재 (통합 참조 ID)" : "없음") . "\n";
echo " - material_id 컬럼: " . (in_array('material_id', $columnNames) ? "존재" : "없음 (예상대로)") . "\n\n";
// 현재 FK 상태 확인
$materialFk = $this->findForeignKeyName('product_components', 'material_id');
$parentProductFk = $this->findForeignKeyName('product_components', 'parent_product_id');
$childProductFk = $this->findForeignKeyName('product_components', 'child_product_id');
$refIdFk = $this->findForeignKeyName('product_components', 'ref_id');
echo " 현재 FK 상태:\n";
echo " - material_id FK: " . ($materialFk ? "존재 ({$materialFk})" : "없음") . "\n";
echo "2⃣ FK 제약조건 상태 확인...\n";
echo " - parent_product_id FK: " . ($parentProductFk ? "존재 ({$parentProductFk})" : "없음") . "\n";
echo " - child_product_id FK: " . ($childProductFk ? "존재 ({$childProductFk})" : "없음") . "\n\n";
echo " - ref_id FK: " . ($refIdFk ? "존재 ({$refIdFk})" : "없음 (유연한 구조)") . "\n\n";
// material_id FK만 제거 (핵심 제품 관계는 유지)
echo "2️⃣ Material_id FK 제거 (자재 관리 유연성)...\n";
$this->dropForeignKeyIfExists('product_components', 'material_id');
echo "3⃣ 핵심 제품 관계 FK 유지 확인...\n";
if ($parentProductFk) {
echo "✅ 유지: parent_product_id → products (BOM 구조 핵심)\n";
}
if ($childProductFk) {
echo "✅ 유지: child_product_id → products (BOM 구조 핵심)\n";
}
// 성능을 위한 인덱스 확인/추가
echo "\n4⃣ 성능 인덱스 확인...\n";
$materialIndexExists = DB::selectOne("
SHOW INDEX FROM product_components WHERE Key_name = 'idx_components_material_id'
// 성능을 위한 인덱스 확인
echo "3️⃣ 성능 인덱스 상태 확인...\n";
$refTypeIndexExists = DB::selectOne("
SHOW INDEX FROM product_components WHERE Key_name LIKE '%ref_type%' OR Column_name = 'ref_type'
");
if (!$materialIndexExists) {
DB::statement("CREATE INDEX idx_components_material_id ON product_components (material_id)");
echo "✅ Added performance index: product_components.material_id\n";
} else {
echo " Index exists: product_components.material_id\n";
$refIdIndexExists = DB::selectOne("
SHOW INDEX FROM product_components WHERE Key_name LIKE '%ref_id%' OR Column_name = 'ref_id'
");
echo " - ref_type 인덱스: " . ($refTypeIndexExists ? "존재" : "없음") . "\n";
echo " - ref_id 인덱스: " . ($refIdIndexExists ? "존재" : "없음") . "\n";
// 필요시 성능 인덱스 추가
if (!$refTypeIndexExists || !$refIdIndexExists) {
echo "\n4⃣ 성능 인덱스 추가...\n";
try {
DB::statement("CREATE INDEX idx_components_ref_type_id ON product_components (ref_type, ref_id)");
echo "✅ Added composite index: product_components(ref_type, ref_id)\n";
} catch (\Exception $e) {
echo " Index may already exist or not needed\n";
}
}
echo "\n🎉 Phase 3 제품-자재 FK 제거 완료!\n";
echo "📋 제거된 FK:\n";
echo " - product_components.material_id → materials\n";
echo "🔒 유지된 핵심 FK:\n";
echo " - product_components.parent_product_id → products\n";
echo " - product_components.child_product_id → products\n";
echo "📈 예상 효과: 자재 변경/삭제 시 BOM 유연성 증가\n";
echo "⚠️ 주의사항: Service 레이어에서 자재 무결성 검증 필요\n";
echo "\n🎉 Phase 3 분석 완료!\n";
echo "📋 현재 구조 요약:\n";
echo " - 이미 유연한 ref_type/ref_id 구조 사용 중\n";
echo " - material_id 컬럼 없음 (통합 구조로 대체)\n";
echo " - 물리적 FK 제약조건 없어 관리 유연성 확보됨\n";
echo "📈 결론: 추가 FK 제거 작업 불필요 (이미 최적화됨)\n";
echo "✅ 권장사항: Service 레이어에서 논리적 무결성 검증 유지\n";
}
public function down(): void
{
echo "🔄 Phase 3 제품-자재 FK 복구 시작...\n\n";
echo "🔄 Phase 3 분석 롤백 시작...\n\n";
echo " 이 마이그레이션은 분석 목적으로 실행되었습니다.\n";
echo "📋 롤백 내용:\n";
echo " - 추가된 성능 인덱스 제거 (필요시)\n\n";
if (Schema::hasTable('product_components')) {
echo "1Material_id FK 복구...\n";
echo "1성능 인덱스 제거...\n";
try {
Schema::table('product_components', function (Blueprint $table) {
$table->foreign('material_id')
->references('id')
->on('materials')
->nullOnDelete();
});
echo "✅ Restored FK: product_components.material_id → materials\n";
} catch (\Throwable $e) {
echo "⚠️ Could not restore FK: product_components.material_id\n";
echo " Error: " . $e->getMessage() . "\n";
echo " 이유: 데이터 무결성 위반이나 참조되지 않는 material_id가 있을 수 있습니다.\n";
DB::statement("DROP INDEX idx_components_ref_type_id ON product_components");
echo "✅ Removed index: product_components(ref_type, ref_id)\n";
} catch (\Exception $e) {
echo " Index may not exist or already removed\n";
}
}
echo "\n🔄 Phase 3 FK 복구 완료!\n";
echo "📝 참고: FK 복구 실패 시 데이터 정합성을 먼저 확인하세요.\n";
echo "\n🔄 Phase 3 분석 롤백 완료!\n";
echo "📝 참고: 원래 ref_type/ref_id 구조가 복구되었습니다.\n";
}
};