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,490 @@
<?php
namespace Tests\Performance;
use App\Models\BomConditionRule;
use App\Models\Model;
use App\Models\ModelFormula;
use App\Models\ModelParameter;
use App\Models\User;
use App\Services\ProductFromModelService;
use Database\Seeders\ParameterBasedBomTestSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class BomResolutionPerformanceTest extends TestCase
{
use RefreshDatabase;
private User $user;
private Model $performanceModel;
private ProductFromModelService $service;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create(['tenant_id' => 1]);
Sanctum::actingAs($this->user);
$this->service = new ProductFromModelService();
$this->service->setTenantId(1)->setApiUserId(1);
// Set headers
$this->withHeaders([
'X-API-KEY' => config('app.api_key', 'test-api-key'),
'Accept' => 'application/json',
]);
// Seed test data
$this->seed(ParameterBasedBomTestSeeder::class);
// Create performance test model with large dataset
$this->createPerformanceTestModel();
}
private function createPerformanceTestModel(): void
{
$this->performanceModel = Model::factory()->create([
'code' => 'PERF-TEST',
'name' => 'Performance Test Model',
'product_family' => 'SCREEN',
'tenant_id' => 1,
]);
// Create many parameters (50)
for ($i = 1; $i <= 50; $i++) {
ModelParameter::factory()->create([
'model_id' => $this->performanceModel->id,
'name' => "param_{$i}",
'label' => "Parameter {$i}",
'type' => 'NUMBER',
'default_value' => '100',
'sort_order' => $i,
'tenant_id' => 1,
]);
}
// Create many formulas (30)
for ($i = 1; $i <= 30; $i++) {
ModelFormula::factory()->create([
'model_id' => $this->performanceModel->id,
'name' => "formula_{$i}",
'expression' => "param_1 + param_2 + {$i}",
'description' => "Formula {$i}",
'return_type' => 'NUMBER',
'sort_order' => $i,
'tenant_id' => 1,
]);
}
// Create many condition rules (100)
for ($i = 1; $i <= 100; $i++) {
BomConditionRule::factory()->create([
'model_id' => $this->performanceModel->id,
'name' => "rule_{$i}",
'condition_expression' => "formula_1 > {$i}",
'component_code' => "COMPONENT_{$i}",
'quantity_expression' => '1',
'priority' => $i,
'tenant_id' => 1,
]);
}
}
/** @test */
public function it_resolves_simple_bom_within_performance_threshold()
{
// Use KSS01 model for simple test
$kss01 = Model::where('code', 'KSS01')->first();
$inputParams = [
'W0' => 1000,
'H0' => 800,
'screen_type' => 'SCREEN',
'install_type' => 'WALL',
'power_source' => 'AC',
];
$startTime = microtime(true);
$response = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [
'parameters' => $inputParams
]);
$executionTime = microtime(true) - $startTime;
// Should complete within 500ms for simple model
$this->assertLessThan(0.5, $executionTime, 'Simple BOM resolution took too long');
$response->assertOk();
}
/** @test */
public function it_handles_complex_bom_resolution_efficiently()
{
// Create parameters for all 50 parameters
$inputParams = [];
for ($i = 1; $i <= 50; $i++) {
$inputParams["param_{$i}"] = 100 + $i;
}
$startTime = microtime(true);
$response = $this->postJson("/api/v1/design/models/{$this->performanceModel->id}/bom/resolve", [
'parameters' => $inputParams
]);
$executionTime = microtime(true) - $startTime;
// Should complete within 2 seconds even for complex model
$this->assertLessThan(2.0, $executionTime, 'Complex BOM resolution took too long');
$response->assertOk();
}
/** @test */
public function it_handles_concurrent_bom_resolutions()
{
$kss01 = Model::where('code', 'KSS01')->first();
$inputParams = [
'W0' => 1000,
'H0' => 800,
'screen_type' => 'SCREEN',
'install_type' => 'WALL',
'power_source' => 'AC',
];
$startTime = microtime(true);
$responses = [];
// Simulate 10 concurrent requests
for ($i = 0; $i < 10; $i++) {
$responses[] = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [
'parameters' => $inputParams
]);
}
$totalTime = microtime(true) - $startTime;
// All requests should complete successfully
foreach ($responses as $response) {
$response->assertOk();
}
// Total time for 10 concurrent requests should be reasonable
$this->assertLessThan(5.0, $totalTime, 'Concurrent BOM resolutions took too long');
}
/** @test */
public function it_optimizes_formula_evaluation_with_caching()
{
$kss01 = Model::where('code', 'KSS01')->first();
$inputParams = [
'W0' => 1000,
'H0' => 800,
'screen_type' => 'SCREEN',
'install_type' => 'WALL',
'power_source' => 'AC',
];
// First request (cold cache)
$startTime1 = microtime(true);
$response1 = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [
'parameters' => $inputParams
]);
$time1 = microtime(true) - $startTime1;
// Second request with same parameters (warm cache)
$startTime2 = microtime(true);
$response2 = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [
'parameters' => $inputParams
]);
$time2 = microtime(true) - $startTime2;
// Both should succeed
$response1->assertOk();
$response2->assertOk();
// Results should be identical
$this->assertEquals($response1->json('data'), $response2->json('data'));
// Second request should be faster (with caching)
$this->assertLessThan($time1, $time2 * 1.5, 'Caching is not improving performance');
}
/** @test */
public function it_measures_memory_usage_during_bom_resolution()
{
$kss01 = Model::where('code', 'KSS01')->first();
$inputParams = [
'W0' => 1000,
'H0' => 800,
'screen_type' => 'SCREEN',
'install_type' => 'WALL',
'power_source' => 'AC',
];
$memoryBefore = memory_get_usage(true);
$response = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [
'parameters' => $inputParams
]);
$memoryAfter = memory_get_usage(true);
$memoryUsed = $memoryAfter - $memoryBefore;
$response->assertOk();
// Memory usage should be reasonable (less than 50MB for simple BOM)
$this->assertLessThan(50 * 1024 * 1024, $memoryUsed, 'Excessive memory usage detected');
}
/** @test */
public function it_handles_large_datasets_efficiently()
{
// Test with the performance model (50 params, 30 formulas, 100 rules)
$inputParams = [];
for ($i = 1; $i <= 50; $i++) {
$inputParams["param_{$i}"] = rand(50, 200);
}
$memoryBefore = memory_get_usage(true);
$startTime = microtime(true);
$response = $this->postJson("/api/v1/design/models/{$this->performanceModel->id}/bom/resolve", [
'parameters' => $inputParams
]);
$executionTime = microtime(true) - $startTime;
$memoryAfter = memory_get_usage(true);
$memoryUsed = $memoryAfter - $memoryBefore;
$response->assertOk();
// Performance thresholds for large datasets
$this->assertLessThan(5.0, $executionTime, 'Large dataset processing took too long');
$this->assertLessThan(100 * 1024 * 1024, $memoryUsed, 'Excessive memory usage for large dataset');
// Should return reasonable amount of data
$data = $response->json('data');
$this->assertArrayHasKey('calculated_formulas', $data);
$this->assertArrayHasKey('bom_items', $data);
$this->assertGreaterThan(0, count($data['bom_items']));
}
/** @test */
public function it_benchmarks_formula_evaluation_complexity()
{
// Test various formula complexities
$complexityTests = [
'simple' => 'param_1 + param_2',
'medium' => 'param_1 * param_2 + param_3 / param_4',
'complex' => 'CEIL(param_1 / 600) + FLOOR(param_2 * 1.5) + IF(param_3 > 100, param_4, param_5)',
];
$benchmarks = [];
foreach ($complexityTests as $complexity => $expression) {
// Create test formula
$formula = ModelFormula::factory()->create([
'model_id' => $this->performanceModel->id,
'name' => "benchmark_{$complexity}",
'expression' => $expression,
'sort_order' => 999,
'tenant_id' => 1,
]);
$inputParams = [
'param_1' => 1000,
'param_2' => 800,
'param_3' => 150,
'param_4' => 200,
'param_5' => 50,
];
$startTime = microtime(true);
// Evaluate formula multiple times to get average
for ($i = 0; $i < 100; $i++) {
try {
$this->service->evaluateFormula($formula, $inputParams);
} catch (\Exception $e) {
// Some complex formulas might fail, that's okay for benchmarking
}
}
$avgTime = (microtime(true) - $startTime) / 100;
$benchmarks[$complexity] = $avgTime;
// Cleanup
$formula->delete();
}
// Complex formulas should still execute reasonably fast
$this->assertLessThan(0.01, $benchmarks['simple'], 'Simple formula evaluation too slow');
$this->assertLessThan(0.02, $benchmarks['medium'], 'Medium formula evaluation too slow');
$this->assertLessThan(0.05, $benchmarks['complex'], 'Complex formula evaluation too slow');
}
/** @test */
public function it_scales_with_increasing_rule_count()
{
// Test BOM resolution with different rule counts
$scalingTests = [10, 50, 100];
$scalingResults = [];
foreach ($scalingTests as $ruleCount) {
// Create test model with specific rule count
$testModel = Model::factory()->create([
'code' => "SCALE_TEST_{$ruleCount}",
'tenant_id' => 1,
]);
// Create basic parameters
ModelParameter::factory()->create([
'model_id' => $testModel->id,
'name' => 'test_param',
'type' => 'NUMBER',
'tenant_id' => 1,
]);
// Create test formula
ModelFormula::factory()->create([
'model_id' => $testModel->id,
'name' => 'test_formula',
'expression' => 'test_param * 2',
'tenant_id' => 1,
]);
// Create specified number of rules
for ($i = 1; $i <= $ruleCount; $i++) {
BomConditionRule::factory()->create([
'model_id' => $testModel->id,
'condition_expression' => "test_formula > {$i}",
'component_code' => "COMP_{$i}",
'priority' => $i,
'tenant_id' => 1,
]);
}
$startTime = microtime(true);
$response = $this->postJson("/api/v1/design/models/{$testModel->id}/bom/resolve", [
'parameters' => ['test_param' => 100]
]);
$executionTime = microtime(true) - $startTime;
$scalingResults[$ruleCount] = $executionTime;
$response->assertOk();
// Cleanup
$testModel->delete();
}
// Execution time should scale reasonably (not exponentially)
$ratio50to10 = $scalingResults[50] / $scalingResults[10];
$ratio100to50 = $scalingResults[100] / $scalingResults[50];
// Should not scale worse than linearly
$this->assertLessThan(10, $ratio50to10, 'Poor scaling from 10 to 50 rules');
$this->assertLessThan(5, $ratio100to50, 'Poor scaling from 50 to 100 rules');
}
/** @test */
public function it_handles_stress_test_scenarios()
{
$kss01 = Model::where('code', 'KSS01')->first();
// Stress test with many rapid requests
$stressTestCount = 50;
$successCount = 0;
$errors = [];
$totalTime = 0;
for ($i = 0; $i < $stressTestCount; $i++) {
$inputParams = [
'W0' => rand(500, 3000),
'H0' => rand(400, 2000),
'screen_type' => rand(0, 1) ? 'SCREEN' : 'SLAT',
'install_type' => ['WALL', 'SIDE', 'MIXED'][rand(0, 2)],
'power_source' => ['AC', 'DC', 'MANUAL'][rand(0, 2)],
];
$startTime = microtime(true);
try {
$response = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [
'parameters' => $inputParams
]);
$executionTime = microtime(true) - $startTime;
$totalTime += $executionTime;
if ($response->isSuccessful()) {
$successCount++;
} else {
$errors[] = $response->status();
}
} catch (\Exception $e) {
$errors[] = $e->getMessage();
}
}
$avgTime = $totalTime / $stressTestCount;
$successRate = ($successCount / $stressTestCount) * 100;
// Stress test requirements
$this->assertGreaterThanOrEqual(95, $successRate, 'Success rate too low under stress');
$this->assertLessThan(1.0, $avgTime, 'Average response time too high under stress');
if (count($errors) > 0) {
$this->addToAssertionCount(1); // Just to show we're tracking errors
// Log errors for analysis
error_log('Stress test errors: ' . json_encode(array_unique($errors)));
}
}
/** @test */
public function it_monitors_database_query_performance()
{
$kss01 = Model::where('code', 'KSS01')->first();
$inputParams = [
'W0' => 1000,
'H0' => 800,
'screen_type' => 'SCREEN',
'install_type' => 'WALL',
'power_source' => 'AC',
];
// Enable query logging
\DB::enableQueryLog();
$response = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [
'parameters' => $inputParams
]);
$queries = \DB::getQueryLog();
\DB::disableQueryLog();
$response->assertOk();
// Analyze query performance
$queryCount = count($queries);
$totalQueryTime = array_sum(array_column($queries, 'time'));
// Should not have excessive queries (N+1 problem)
$this->assertLessThan(50, $queryCount, 'Too many database queries');
// Total query time should be reasonable
$this->assertLessThan(500, $totalQueryTime, 'Database queries taking too long');
// Check for slow queries
$slowQueries = array_filter($queries, fn($query) => $query['time'] > 100);
$this->assertEmpty($slowQueries, 'Slow queries detected: ' . json_encode($slowQueries));
}
}