feat: DB 연결 오버라이딩 및 대시보드 통계 위젯 추가

- DB 연결: 로컬/Docker 환경 오버라이딩 설정 (.env)
- 테넌트 위젯: redirect 버그 수정 (TenantSelectorWidget)
- 통계 위젯: 사용자/제품/자재/주문 카드 추가 (StatsOverviewWidget)
- 리소스 한국어화: Product, Material 모델 레이블 추가
- 대시보드: 위젯 등록 및 캐시 최적화

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-30 23:31:14 +09:00
parent d94ab59fd1
commit bf8036a64b
81 changed files with 22632 additions and 102 deletions

View File

@@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('bom_template_groups', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('model_id')->comment('모델 ID');
$table->string('group_name', 100)->comment('그룹명 (본체, 절곡물, 가이드레일 등)');
$table->unsignedBigInteger('parent_group_id')->nullable()->comment('상위 그룹 ID (계층 구조)');
$table->integer('display_order')->default(0)->comment('표시 순서');
$table->text('description')->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', 'model_id']);
$table->index(['tenant_id', 'model_id', 'parent_group_id']);
$table->unique(['tenant_id', 'model_id', 'group_name'], 'unique_group_per_model');
// 외래키 (설계용, 프로덕션에서는 제거 고려)
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
$table->foreign('model_id')->references('id')->on('models')->onDelete('cascade');
$table->foreign('parent_group_id')->references('id')->on('bom_template_groups')->onDelete('set null');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('bom_template_groups');
}
};

View File

@@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('model_parameters', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('model_id')->comment('모델 ID');
$table->string('parameter_name', 50)->comment('매개변수명 (W0, H0, screen_type 등)');
$table->enum('parameter_type', ['INTEGER', 'DECIMAL', 'STRING', 'BOOLEAN'])->comment('매개변수 타입');
$table->boolean('is_required')->default(true)->comment('필수 여부');
$table->string('default_value')->nullable()->comment('기본값');
$table->decimal('min_value', 15, 4)->nullable()->comment('최소값 (숫자형)');
$table->decimal('max_value', 15, 4)->nullable()->comment('최대값 (숫자형)');
$table->json('allowed_values')->nullable()->comment('허용값 목록 (선택형)');
$table->string('unit', 20)->nullable()->comment('단위 (mm, kg, 개 등)');
$table->text('description')->nullable()->comment('매개변수 설명');
$table->integer('display_order')->default(0)->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', 'model_id']);
$table->index(['tenant_id', 'model_id', 'display_order']);
$table->unique(['tenant_id', 'model_id', 'parameter_name'], 'unique_parameter_per_model');
// 외래키 (설계용)
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
$table->foreign('model_id')->references('id')->on('models')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('model_parameters');
}
};

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('model_formulas', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('model_id')->comment('모델 ID');
$table->string('formula_name', 50)->comment('공식명 (W1, H1, area, weight 등)');
$table->text('formula_expression')->comment('공식 표현식 (W0 + 100, W0 * H0 / 1000000 등)');
$table->enum('result_type', ['INTEGER', 'DECIMAL'])->default('DECIMAL')->comment('결과 타입');
$table->integer('decimal_places')->default(2)->comment('소수점 자릿수');
$table->string('unit', 20)->nullable()->comment('결과 단위 (mm, kg, m² 등)');
$table->text('description')->nullable()->comment('공식 설명');
$table->integer('calculation_order')->default(0)->comment('계산 순서 (의존성 관리)');
$table->json('dependencies')->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', 'model_id']);
$table->index(['tenant_id', 'model_id', 'calculation_order']);
$table->unique(['tenant_id', 'model_id', 'formula_name'], 'unique_formula_per_model');
// 외래키 (설계용)
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
$table->foreign('model_id')->references('id')->on('models')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('model_formulas');
}
};

View File

@@ -0,0 +1,57 @@
<?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('bom_condition_rules', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('model_id')->comment('모델 ID');
$table->string('rule_name', 100)->comment('규칙명');
$table->text('condition_expression')->comment('조건 표현식 (screen_type == "SCREEN", W0 > 1000 등)');
$table->enum('action_type', ['INCLUDE_PRODUCT', 'EXCLUDE_PRODUCT', 'SET_QUANTITY', 'MODIFY_QUANTITY'])->comment('액션 타입');
$table->unsignedBigInteger('target_product_id')->nullable()->comment('대상 제품 ID');
$table->unsignedBigInteger('target_group_id')->nullable()->comment('대상 그룹 ID');
$table->text('quantity_formula')->nullable()->comment('수량 공식 (ceiling(W0/1000), 2 * count 등)');
$table->decimal('fixed_quantity', 15, 4)->nullable()->comment('고정 수량');
$table->text('description')->nullable()->comment('규칙 설명');
$table->integer('priority')->default(100)->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', 'model_id']);
$table->index(['tenant_id', 'model_id', 'priority']);
$table->index(['tenant_id', 'target_product_id']);
$table->index(['tenant_id', 'target_group_id']);
// 외래키 (설계용)
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
$table->foreign('model_id')->references('id')->on('models')->onDelete('cascade');
$table->foreign('target_product_id')->references('id')->on('products')->onDelete('cascade');
$table->foreign('target_group_id')->references('id')->on('bom_template_groups')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('bom_condition_rules');
}
};

View File

@@ -0,0 +1,52 @@
<?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('product_parameters', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('model_id')->comment('모델 ID');
$table->unsignedBigInteger('model_version_id')->nullable()->comment('모델 버전 ID');
$table->string('product_code', 100)->nullable()->comment('제품 코드 (참조용)');
$table->json('parameter_values')->comment('매개변수 값들 {W0: 1200, H0: 800, screen_type: "SCREEN"}');
$table->text('notes')->nullable()->comment('비고');
$table->enum('status', ['DRAFT', 'CALCULATED', 'APPROVED'])->default('DRAFT')->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', 'model_id']);
$table->index(['tenant_id', 'model_id', 'model_version_id']);
$table->index(['tenant_id', 'product_code']);
$table->index(['tenant_id', 'status']);
$table->index(['created_at']);
// 외래키 (설계용)
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
$table->foreign('model_id')->references('id')->on('models')->onDelete('cascade');
$table->foreign('model_version_id')->references('id')->on('model_versions')->onDelete('set null');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('product_parameters');
}
};

View File

@@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('product_calculated_values', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('product_parameter_id')->comment('제품 매개변수 ID');
$table->json('calculated_values')->comment('계산된 값들 {W1: 1300, H1: 900, area: 1.17, weight: 45.2}');
$table->json('bom_snapshot')->nullable()->comment('계산된 BOM 결과 스냅샷');
$table->decimal('total_cost', 15, 4)->nullable()->comment('총 비용');
$table->decimal('total_weight', 15, 4)->nullable()->comment('총 중량');
$table->integer('total_items')->nullable()->comment('총 아이템 수');
$table->timestamp('calculation_date')->useCurrent()->comment('계산 일시');
$table->boolean('is_valid')->default(true)->comment('계산 유효성 여부');
$table->text('calculation_errors')->nullable()->comment('계산 오류 메시지');
$table->string('calculation_version', 50)->nullable()->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', 'product_parameter_id']);
$table->index(['tenant_id', 'is_valid']);
$table->index(['calculation_date']);
$table->unique(['tenant_id', 'product_parameter_id'], 'unique_calculation_per_parameter');
// 외래키 (설계용)
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
$table->foreign('product_parameter_id')->references('id')->on('product_parameters')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('product_calculated_values');
}
};

View File

@@ -0,0 +1,52 @@
<?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('bom_template_items', function (Blueprint $table) {
// group_id가 이미 존재하는지 확인 후 추가
if (!Schema::hasColumn('bom_template_items', 'group_id')) {
$table->unsignedBigInteger('group_id')->nullable()->after('bom_template_id')->comment('BOM 그룹 ID');
}
// 나머지 컬럼들 추가
if (!Schema::hasColumn('bom_template_items', 'is_conditional')) {
$table->boolean('is_conditional')->default(false)->after('qty')->comment('조건부 아이템 여부');
}
if (!Schema::hasColumn('bom_template_items', 'condition_expression')) {
$table->text('condition_expression')->nullable()->after('is_conditional')->comment('조건 표현식');
}
if (!Schema::hasColumn('bom_template_items', 'quantity_formula')) {
$table->text('quantity_formula')->nullable()->after('condition_expression')->comment('수량 계산 공식');
}
});
// 인덱스와 외래키는 별도로 처리
Schema::table('bom_template_items', function (Blueprint $table) {
if (!Schema::hasIndex('bom_template_items', ['tenant_id', 'group_id'])) {
$table->index(['tenant_id', 'group_id']);
}
// 외래키 제약조건은 생산 환경에서 제거 (SAM 규칙)
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('bom_template_items', function (Blueprint $table) {
$table->dropForeign(['group_id']);
$table->dropIndex(['tenant_id', 'group_id']);
$table->dropColumn(['group_id', 'is_conditional', 'condition_expression', 'quantity_formula']);
});
}
};

View File

@@ -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('products', function (Blueprint $table) {
$table->boolean('is_parametric')->default(false)->after('product_type')->comment('매개변수 기반 제품 여부');
$table->unsignedBigInteger('base_model_id')->nullable()->after('is_parametric')->comment('기반 모델 ID (매개변수 제품용)');
$table->json('parameter_values')->nullable()->after('base_model_id')->comment('매개변수 값들 (매개변수 제품용)');
$table->json('calculated_values')->nullable()->after('parameter_values')->comment('계산된 값들 (매개변수 제품용)');
// 인덱스 추가
$table->index(['tenant_id', 'is_parametric']);
$table->index(['tenant_id', 'base_model_id']);
// 외래키 제약조건은 생산 환경에서 제거 (SAM 규칙)
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropForeign(['base_model_id']);
$table->dropIndex(['tenant_id', 'is_parametric']);
$table->dropIndex(['tenant_id', 'base_model_id']);
$table->dropColumn(['is_parametric', 'base_model_id', 'parameter_values', 'calculated_values']);
});
}
};