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,436 @@
<?php
namespace Tests\Feature\Design;
use Tests\TestCase;
use App\Models\Design\DesignModel;
use App\Models\Design\ModelParameter;
use App\Models\Design\ModelFormula;
use App\Models\User;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Auth;
use Laravel\Sanctum\Sanctum;
class ModelFormulaTest extends TestCase
{
use RefreshDatabase;
private User $user;
private Tenant $tenant;
private DesignModel $model;
protected function setUp(): void
{
parent::setUp();
// Create test tenant and user
$this->tenant = Tenant::factory()->create();
$this->user = User::factory()->create();
// Associate user with tenant
$this->user->tenants()->attach($this->tenant->id, ['is_active' => true, 'is_default' => true]);
// Create test design model
$this->model = DesignModel::factory()->create([
'tenant_id' => $this->tenant->id,
'code' => 'TEST01',
'name' => 'Test Model',
'is_active' => true
]);
// Create test parameters
ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_name' => 'W0',
'parameter_type' => 'NUMBER'
]);
ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_name' => 'H0',
'parameter_type' => 'NUMBER'
]);
// Authenticate user
Sanctum::actingAs($this->user, ['*']);
Auth::login($this->user);
}
/** @test */
public function can_create_model_formula()
{
$formulaData = [
'model_id' => $this->model->id,
'formula_name' => 'W1',
'expression' => 'W0 + 100',
'description' => 'Outer width calculation',
'sort_order' => 1
];
$response = $this->postJson('/api/v1/design/model-formulas', $formulaData);
$response->assertStatus(201)
->assertJson([
'success' => true,
'message' => 'message.created'
]);
$this->assertDatabaseHas('model_formulas', [
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'W1',
'expression' => 'W0 + 100'
]);
}
/** @test */
public function can_list_model_formulas()
{
// Create test formulas
ModelFormula::factory()->count(3)->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id
]);
$response = $this->getJson('/api/v1/design/model-formulas?model_id=' . $this->model->id);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'message.fetched'
])
->assertJsonStructure([
'data' => [
'data' => [
'*' => [
'id',
'formula_name',
'expression',
'description',
'dependencies',
'sort_order'
]
]
]
]);
}
/** @test */
public function can_update_model_formula()
{
$formula = ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'area',
'expression' => 'W0 * H0'
]);
$updateData = [
'formula_name' => 'area_updated',
'expression' => '(W0 * H0) / 1000000',
'description' => 'Area in square meters'
];
$response = $this->putJson('/api/v1/design/model-formulas/' . $formula->id, $updateData);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'message.updated'
]);
$this->assertDatabaseHas('model_formulas', [
'id' => $formula->id,
'formula_name' => 'area_updated',
'expression' => '(W0 * H0) / 1000000'
]);
}
/** @test */
public function can_delete_model_formula()
{
$formula = ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id
]);
$response = $this->deleteJson('/api/v1/design/model-formulas/' . $formula->id);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'message.deleted'
]);
$this->assertSoftDeleted('model_formulas', [
'id' => $formula->id
]);
}
/** @test */
public function validates_formula_expression_syntax()
{
// Test invalid expression
$invalidData = [
'model_id' => $this->model->id,
'formula_name' => 'invalid_formula',
'expression' => 'W0 +++ H0' // Invalid syntax
];
$response = $this->postJson('/api/v1/design/model-formulas', $invalidData);
$response->assertStatus(422);
// Test valid expression
$validData = [
'model_id' => $this->model->id,
'formula_name' => 'valid_formula',
'expression' => 'sqrt(W0^2 + H0^2)'
];
$response = $this->postJson('/api/v1/design/model-formulas', $validData);
$response->assertStatus(201);
}
/** @test */
public function can_calculate_formulas_with_dependencies()
{
// Create formulas with dependencies
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'W1',
'expression' => 'W0 + 100',
'sort_order' => 1
]);
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'H1',
'expression' => 'H0 + 100',
'sort_order' => 2
]);
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'area',
'expression' => 'W1 * H1',
'sort_order' => 3
]);
$inputParameters = [
'W0' => 800,
'H0' => 600
];
$response = $this->postJson('/api/v1/design/model-formulas/calculate', [
'model_id' => $this->model->id,
'parameters' => $inputParameters
]);
$response->assertStatus(200)
->assertJson([
'success' => true,
'data' => [
'W1' => 900, // 800 + 100
'H1' => 700, // 600 + 100
'area' => 630000 // 900 * 700
]
]);
}
/** @test */
public function can_detect_circular_dependencies()
{
// Create circular dependency: A depends on B, B depends on A
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'A',
'expression' => 'B + 10'
]);
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'B',
'expression' => 'A + 20'
]);
$response = $this->postJson('/api/v1/design/model-formulas/calculate', [
'model_id' => $this->model->id,
'parameters' => ['W0' => 100]
]);
$response->assertStatus(422) // Validation error for circular dependency
->assertJsonFragment([
'error' => 'Circular dependency detected'
]);
}
/** @test */
public function can_handle_complex_mathematical_expressions()
{
// Test various mathematical functions
$complexFormulas = [
['name' => 'sqrt_test', 'expression' => 'sqrt(W0^2 + H0^2)'],
['name' => 'trig_test', 'expression' => 'sin(W0 * pi() / 180)'],
['name' => 'conditional_test', 'expression' => 'if(W0 > 1000, W0 * 1.2, W0 * 1.1)'],
['name' => 'round_test', 'expression' => 'round(W0 / 100) * 100'],
['name' => 'max_test', 'expression' => 'max(W0, H0)']
];
foreach ($complexFormulas as $formulaData) {
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => $formulaData['name'],
'expression' => $formulaData['expression']
]);
}
$response = $this->postJson('/api/v1/design/model-formulas/calculate', [
'model_id' => $this->model->id,
'parameters' => ['W0' => 1200, 'H0' => 800]
]);
$response->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'sqrt_test',
'trig_test',
'conditional_test',
'round_test',
'max_test'
]
]);
}
/** @test */
public function can_validate_formula_dependencies()
{
// Create formula that references non-existent parameter
$invalidFormulaData = [
'model_id' => $this->model->id,
'formula_name' => 'invalid_ref',
'expression' => 'NONEXISTENT_PARAM + 100'
];
$response = $this->postJson('/api/v1/design/model-formulas', $invalidFormulaData);
$response->assertStatus(422);
// Create valid formula that references existing parameter
$validFormulaData = [
'model_id' => $this->model->id,
'formula_name' => 'valid_ref',
'expression' => 'W0 + 100'
];
$response = $this->postJson('/api/v1/design/model-formulas', $validFormulaData);
$response->assertStatus(201);
}
/** @test */
public function enforces_tenant_isolation()
{
// Create formula for different tenant
$otherTenant = Tenant::factory()->create();
$otherFormula = ModelFormula::factory()->create([
'tenant_id' => $otherTenant->id
]);
// Should not be able to access other tenant's formula
$response = $this->getJson('/api/v1/design/model-formulas/' . $otherFormula->id);
$response->assertStatus(404);
}
/** @test */
public function can_export_and_import_formulas()
{
// Create test formulas
ModelFormula::factory()->count(3)->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id
]);
// Export formulas
$response = $this->getJson('/api/v1/design/model-formulas/export?model_id=' . $this->model->id);
$response->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'formulas' => [
'*' => [
'formula_name',
'expression',
'description',
'sort_order'
]
]
]
]);
$exportData = $response->json('data.formulas');
// Import formulas to new model
$newModel = DesignModel::factory()->create([
'tenant_id' => $this->tenant->id,
'code' => 'TEST02'
]);
$response = $this->postJson('/api/v1/design/model-formulas/import', [
'model_id' => $newModel->id,
'formulas' => $exportData
]);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'message.bulk_import'
]);
$this->assertDatabaseCount('model_formulas', 6); // 3 original + 3 imported
}
/** @test */
public function can_reorder_formulas_for_calculation_sequence()
{
$formula1 = ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'area',
'expression' => 'W1 * H1',
'sort_order' => 1
]);
$formula2 = ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'W1',
'expression' => 'W0 + 100',
'sort_order' => 2
]);
// Reorder so W1 is calculated before area
$reorderData = [
['id' => $formula2->id, 'sort_order' => 1],
['id' => $formula1->id, 'sort_order' => 2]
];
$response = $this->postJson('/api/v1/design/model-formulas/reorder', [
'items' => $reorderData
]);
$response->assertStatus(200);
$this->assertDatabaseHas('model_formulas', [
'id' => $formula2->id,
'sort_order' => 1
]);
}
}