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:
606
tests/Feature/ParameterBasedBomApiTest.php
Normal file
606
tests/Feature/ParameterBasedBomApiTest.php
Normal file
@@ -0,0 +1,606 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\BomConditionRule;
|
||||
use App\Models\Model;
|
||||
use App\Models\ModelFormula;
|
||||
use App\Models\ModelParameter;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\ParameterBasedBomTestSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ParameterBasedBomApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $user;
|
||||
private Model $kss01Model;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Set up test environment
|
||||
$this->artisan('migrate');
|
||||
$this->seed(ParameterBasedBomTestSeeder::class);
|
||||
|
||||
// Create test user and authenticate
|
||||
$this->user = User::factory()->create(['tenant_id' => 1]);
|
||||
Sanctum::actingAs($this->user);
|
||||
|
||||
// Set required headers
|
||||
$this->withHeaders([
|
||||
'X-API-KEY' => config('app.api_key', 'test-api-key'),
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json',
|
||||
]);
|
||||
|
||||
$this->kss01Model = Model::where('code', 'KSS01')->first();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_get_model_parameters()
|
||||
{
|
||||
// Act
|
||||
$response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/parameters");
|
||||
|
||||
// Assert
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'data' => [
|
||||
'*' => [
|
||||
'id',
|
||||
'name',
|
||||
'label',
|
||||
'type',
|
||||
'default_value',
|
||||
'validation_rules',
|
||||
'options',
|
||||
'sort_order',
|
||||
'is_required',
|
||||
'is_active',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$parameters = $response->json('data');
|
||||
$this->assertCount(5, $parameters); // W0, H0, screen_type, install_type, power_source
|
||||
|
||||
// Check parameter order
|
||||
$this->assertEquals('W0', $parameters[0]['name']);
|
||||
$this->assertEquals('H0', $parameters[1]['name']);
|
||||
$this->assertEquals('screen_type', $parameters[2]['name']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_get_model_formulas()
|
||||
{
|
||||
// Act
|
||||
$response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/formulas");
|
||||
|
||||
// Assert
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'data' => [
|
||||
'*' => [
|
||||
'id',
|
||||
'name',
|
||||
'expression',
|
||||
'description',
|
||||
'return_type',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$formulas = $response->json('data');
|
||||
$this->assertGreaterThanOrEqual(7, count($formulas)); // W1, H1, area, weight, motor, bracket, guide
|
||||
|
||||
// Check formula order
|
||||
$this->assertEquals('W1', $formulas[0]['name']);
|
||||
$this->assertEquals('H1', $formulas[1]['name']);
|
||||
$this->assertEquals('area', $formulas[2]['name']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_get_model_condition_rules()
|
||||
{
|
||||
// Act
|
||||
$response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/rules");
|
||||
|
||||
// Assert
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'data' => [
|
||||
'*' => [
|
||||
'id',
|
||||
'name',
|
||||
'description',
|
||||
'condition_expression',
|
||||
'component_code',
|
||||
'quantity_expression',
|
||||
'priority',
|
||||
'is_active',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$rules = $response->json('data');
|
||||
$this->assertGreaterThanOrEqual(7, count($rules)); // Various case, bottom, shaft, pipe rules
|
||||
|
||||
// Check rules are ordered by priority
|
||||
$priorities = collect($rules)->pluck('priority');
|
||||
$this->assertEquals($priorities->sort()->values(), $priorities->values());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_resolve_bom_for_small_screen()
|
||||
{
|
||||
// Arrange
|
||||
$inputParams = [
|
||||
'W0' => 1000,
|
||||
'H0' => 800,
|
||||
'screen_type' => 'SCREEN',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
// Act
|
||||
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
|
||||
'parameters' => $inputParams
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'data' => [
|
||||
'model_id',
|
||||
'input_parameters',
|
||||
'calculated_formulas' => [
|
||||
'W1',
|
||||
'H1',
|
||||
'area',
|
||||
'weight',
|
||||
'motor',
|
||||
'bracket',
|
||||
'guide',
|
||||
],
|
||||
'bom_items' => [
|
||||
'*' => [
|
||||
'component_code',
|
||||
'quantity',
|
||||
'rule_name',
|
||||
'condition_expression',
|
||||
]
|
||||
],
|
||||
'summary' => [
|
||||
'total_components',
|
||||
'total_weight',
|
||||
'component_categories',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$data = $response->json('data');
|
||||
|
||||
// Check calculated formulas
|
||||
$formulas = $data['calculated_formulas'];
|
||||
$this->assertEquals(1120, $formulas['W1']); // 1000 + 120
|
||||
$this->assertEquals(900, $formulas['H1']); // 800 + 100
|
||||
$this->assertEquals(1.008, $formulas['area']); // 1120 * 900 / 1000000
|
||||
$this->assertEquals('0.5HP', $formulas['motor']); // area <= 3
|
||||
|
||||
// Check BOM items
|
||||
$bomItems = $data['bom_items'];
|
||||
$this->assertGreaterThan(0, count($bomItems));
|
||||
|
||||
// Check specific components
|
||||
$caseItem = collect($bomItems)->first(fn($item) => str_contains($item['component_code'], 'CASE'));
|
||||
$this->assertEquals('CASE-SMALL', $caseItem['component_code']);
|
||||
|
||||
$screenPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SCREEN');
|
||||
$this->assertNotNull($screenPipe);
|
||||
|
||||
$slatPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SLAT');
|
||||
$this->assertNull($slatPipe);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_resolve_bom_for_large_screen()
|
||||
{
|
||||
// Arrange
|
||||
$inputParams = [
|
||||
'W0' => 2500,
|
||||
'H0' => 1500,
|
||||
'screen_type' => 'SCREEN',
|
||||
'install_type' => 'SIDE',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
// Act
|
||||
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
|
||||
'parameters' => $inputParams
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertOk();
|
||||
|
||||
$data = $response->json('data');
|
||||
$formulas = $data['calculated_formulas'];
|
||||
|
||||
// Check large screen calculations
|
||||
$this->assertEquals(2620, $formulas['W1']); // 2500 + 120
|
||||
$this->assertEquals(1600, $formulas['H1']); // 1500 + 100
|
||||
$this->assertEquals(4.192, $formulas['area']); // 2620 * 1600 / 1000000
|
||||
$this->assertEquals('1HP', $formulas['motor']); // 3 < area <= 6
|
||||
|
||||
// Check medium case is selected
|
||||
$bomItems = $data['bom_items'];
|
||||
$caseItem = collect($bomItems)->first(fn($item) => str_contains($item['component_code'], 'CASE'));
|
||||
$this->assertEquals('CASE-MEDIUM', $caseItem['component_code']);
|
||||
|
||||
// Check bracket quantity
|
||||
$bottomItem = collect($bomItems)->first(fn($item) => $item['component_code'] === 'BOTTOM-001');
|
||||
$this->assertEquals(3, $bottomItem['quantity']); // CEIL(2620 / 1000)
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_resolve_bom_for_slat_type()
|
||||
{
|
||||
// Arrange
|
||||
$inputParams = [
|
||||
'W0' => 1500,
|
||||
'H0' => 1000,
|
||||
'screen_type' => 'SLAT',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
// Act
|
||||
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
|
||||
'parameters' => $inputParams
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertOk();
|
||||
|
||||
$bomItems = $response->json('data.bom_items');
|
||||
|
||||
// Check that SLAT pipe is used
|
||||
$screenPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SCREEN');
|
||||
$slatPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SLAT');
|
||||
|
||||
$this->assertNull($screenPipe);
|
||||
$this->assertNotNull($slatPipe);
|
||||
$this->assertEquals('PIPE-SLAT', $slatPipe['component_code']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_validates_missing_required_parameters()
|
||||
{
|
||||
// Arrange - Missing H0 parameter
|
||||
$incompleteParams = [
|
||||
'W0' => 1000,
|
||||
'screen_type' => 'SCREEN',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
// Act
|
||||
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
|
||||
'parameters' => $incompleteParams
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['parameters.H0']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_validates_parameter_ranges()
|
||||
{
|
||||
// Arrange - W0 below minimum
|
||||
$invalidParams = [
|
||||
'W0' => 100, // Below minimum (500)
|
||||
'H0' => 800,
|
||||
'screen_type' => 'SCREEN',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
// Act
|
||||
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
|
||||
'parameters' => $invalidParams
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['parameters.W0']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_validates_select_parameter_options()
|
||||
{
|
||||
// Arrange - Invalid screen_type
|
||||
$invalidParams = [
|
||||
'W0' => 1000,
|
||||
'H0' => 800,
|
||||
'screen_type' => 'INVALID_TYPE',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
// Act
|
||||
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
|
||||
'parameters' => $invalidParams
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['parameters.screen_type']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_create_product_from_model()
|
||||
{
|
||||
// Arrange
|
||||
$inputParams = [
|
||||
'W0' => 1200,
|
||||
'H0' => 900,
|
||||
'screen_type' => 'SCREEN',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
$productData = [
|
||||
'code' => 'KSS01-TEST-001',
|
||||
'name' => '테스트 스크린 블라인드 1200x900',
|
||||
'description' => 'API 테스트로 생성된 제품',
|
||||
];
|
||||
|
||||
// Act
|
||||
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/products", [
|
||||
'parameters' => $inputParams,
|
||||
'product' => $productData,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertCreated()
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'data' => [
|
||||
'product' => [
|
||||
'id',
|
||||
'code',
|
||||
'name',
|
||||
'description',
|
||||
'product_type',
|
||||
'model_id',
|
||||
'parameter_values',
|
||||
],
|
||||
'bom_items' => [
|
||||
'*' => [
|
||||
'id',
|
||||
'product_id',
|
||||
'component_code',
|
||||
'component_type',
|
||||
'quantity',
|
||||
'unit',
|
||||
]
|
||||
],
|
||||
'summary' => [
|
||||
'total_components',
|
||||
'calculated_formulas',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$data = $response->json('data');
|
||||
$product = $data['product'];
|
||||
|
||||
// Check product data
|
||||
$this->assertEquals('KSS01-TEST-001', $product['code']);
|
||||
$this->assertEquals('테스트 스크린 블라인드 1200x900', $product['name']);
|
||||
$this->assertEquals($this->kss01Model->id, $product['model_id']);
|
||||
|
||||
// Check parameter values are stored
|
||||
$parameterValues = json_decode($product['parameter_values'], true);
|
||||
$this->assertEquals(1200, $parameterValues['W0']);
|
||||
$this->assertEquals(900, $parameterValues['H0']);
|
||||
|
||||
// Check BOM items
|
||||
$bomItems = $data['bom_items'];
|
||||
$this->assertGreaterThan(0, count($bomItems));
|
||||
|
||||
foreach ($bomItems as $item) {
|
||||
$this->assertEquals($product['id'], $item['product_id']);
|
||||
$this->assertNotEmpty($item['component_code']);
|
||||
$this->assertGreaterThan(0, $item['quantity']);
|
||||
}
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_preview_product_without_creating()
|
||||
{
|
||||
// Arrange
|
||||
$inputParams = [
|
||||
'W0' => 1800,
|
||||
'H0' => 1200,
|
||||
'screen_type' => 'SCREEN',
|
||||
'install_type' => 'SIDE',
|
||||
'power_source' => 'DC',
|
||||
];
|
||||
|
||||
// Act
|
||||
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/preview", [
|
||||
'parameters' => $inputParams
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'data' => [
|
||||
'suggested_product' => [
|
||||
'code',
|
||||
'name',
|
||||
'description',
|
||||
],
|
||||
'calculated_values' => [
|
||||
'W1',
|
||||
'H1',
|
||||
'area',
|
||||
'weight',
|
||||
'motor',
|
||||
],
|
||||
'bom_preview' => [
|
||||
'total_components',
|
||||
'components' => [
|
||||
'*' => [
|
||||
'component_code',
|
||||
'quantity',
|
||||
'description',
|
||||
]
|
||||
]
|
||||
],
|
||||
'cost_estimate' => [
|
||||
'total_material_cost',
|
||||
'estimated_labor_cost',
|
||||
'total_estimated_cost',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$data = $response->json('data');
|
||||
|
||||
// Check suggested product info
|
||||
$suggestedProduct = $data['suggested_product'];
|
||||
$this->assertStringContains('KSS01', $suggestedProduct['code']);
|
||||
$this->assertStringContains('1800x1200', $suggestedProduct['name']);
|
||||
|
||||
// Check calculated values
|
||||
$calculatedValues = $data['calculated_values'];
|
||||
$this->assertEquals(1920, $calculatedValues['W1']); // 1800 + 120
|
||||
$this->assertEquals(1300, $calculatedValues['H1']); // 1200 + 100
|
||||
|
||||
// Check BOM preview
|
||||
$bomPreview = $data['bom_preview'];
|
||||
$this->assertGreaterThan(0, $bomPreview['total_components']);
|
||||
$this->assertNotEmpty($bomPreview['components']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_authentication_properly()
|
||||
{
|
||||
// Arrange - Remove authentication
|
||||
Sanctum::actingAs(null);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/parameters");
|
||||
|
||||
// Assert
|
||||
$response->assertUnauthorized();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_tenant_isolation()
|
||||
{
|
||||
// Arrange - Create user with different tenant
|
||||
$otherTenantUser = User::factory()->create(['tenant_id' => 2]);
|
||||
Sanctum::actingAs($otherTenantUser);
|
||||
|
||||
// Act
|
||||
$response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/parameters");
|
||||
|
||||
// Assert
|
||||
$response->assertNotFound();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_performance_for_large_datasets()
|
||||
{
|
||||
// Arrange - Use performance test model with many parameters/formulas/rules
|
||||
$performanceModel = Model::where('code', 'PERF-TEST')->first();
|
||||
|
||||
if (!$performanceModel) {
|
||||
$this->markTestSkipped('Performance test model not found. Run in testing environment.');
|
||||
}
|
||||
|
||||
$inputParams = array_fill_keys(
|
||||
ModelParameter::where('model_id', $performanceModel->id)->pluck('name')->toArray(),
|
||||
100
|
||||
);
|
||||
|
||||
// Act
|
||||
$startTime = microtime(true);
|
||||
$response = $this->postJson("/api/v1/design/models/{$performanceModel->id}/bom/resolve", [
|
||||
'parameters' => $inputParams
|
||||
]);
|
||||
$executionTime = microtime(true) - $startTime;
|
||||
|
||||
// Assert
|
||||
$response->assertOk();
|
||||
$this->assertLessThan(2.0, $executionTime, 'BOM resolution should complete within 2 seconds');
|
||||
|
||||
$data = $response->json('data');
|
||||
$this->assertArrayHasKey('calculated_formulas', $data);
|
||||
$this->assertArrayHasKey('bom_items', $data);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_appropriate_error_for_nonexistent_model()
|
||||
{
|
||||
// Act
|
||||
$response = $this->getJson('/api/v1/design/models/999999/parameters');
|
||||
|
||||
// Assert
|
||||
$response->assertNotFound()
|
||||
->assertJson([
|
||||
'success' => false,
|
||||
'message' => 'error.model_not_found',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_concurrent_requests_safely()
|
||||
{
|
||||
// Arrange
|
||||
$inputParams = [
|
||||
'W0' => 1000,
|
||||
'H0' => 800,
|
||||
'screen_type' => 'SCREEN',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
// Act - Simulate concurrent requests
|
||||
$promises = [];
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$promises[] = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
|
||||
'parameters' => $inputParams
|
||||
]);
|
||||
}
|
||||
|
||||
// Assert - All should succeed with same results
|
||||
foreach ($promises as $response) {
|
||||
$response->assertOk();
|
||||
$formulas = $response->json('data.calculated_formulas');
|
||||
$this->assertEquals(1120, $formulas['W1']);
|
||||
$this->assertEquals(900, $formulas['H1']);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user