- DB 연결: 로컬/Docker 환경 오버라이딩 설정 (.env) - 테넌트 위젯: redirect 버그 수정 (TenantSelectorWidget) - 통계 위젯: 사용자/제품/자재/주문 카드 추가 (StatsOverviewWidget) - 리소스 한국어화: Product, Material 모델 레이블 추가 - 대시보드: 위젯 등록 및 캐시 최적화 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
582 lines
20 KiB
PHP
Executable File
582 lines
20 KiB
PHP
Executable File
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/**
|
|
* KSS01 Specific Scenario Testing Script
|
|
*
|
|
* This script tests specific KSS01 model scenarios to validate
|
|
* business logic and real-world use cases.
|
|
*
|
|
* Usage: php scripts/validation/test_kss01_scenarios.php
|
|
*/
|
|
|
|
require_once __DIR__ . '/../../bootstrap/app.php';
|
|
|
|
use App\Models\Tenant;
|
|
use App\Models\Design\DesignModel;
|
|
use App\Services\Design\BomResolverService;
|
|
use App\Services\Design\ModelParameterService;
|
|
use App\Services\Design\ModelFormulaService;
|
|
use App\Services\Design\BomConditionRuleService;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class KSS01ScenarioTester
|
|
{
|
|
private Tenant $tenant;
|
|
private DesignModel $model;
|
|
private BomResolverService $bomResolver;
|
|
private array $testResults = [];
|
|
private int $passedScenarios = 0;
|
|
private int $totalScenarios = 0;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->output("\n=== KSS01 Scenario Testing ===");
|
|
$this->output("Testing real-world KSS01 model scenarios...\n");
|
|
|
|
$this->setupServices();
|
|
}
|
|
|
|
private function setupServices(): void
|
|
{
|
|
// Find KSS01 tenant and model
|
|
$this->tenant = Tenant::where('code', 'KSS_DEMO')->first();
|
|
if (!$this->tenant) {
|
|
throw new Exception('KSS_DEMO tenant not found. Please run KSS01ModelSeeder first.');
|
|
}
|
|
|
|
$this->model = DesignModel::where('tenant_id', $this->tenant->id)
|
|
->where('code', 'KSS01')
|
|
->first();
|
|
if (!$this->model) {
|
|
throw new Exception('KSS01 model not found. Please run KSS01ModelSeeder first.');
|
|
}
|
|
|
|
// Setup BOM resolver
|
|
$this->bomResolver = new BomResolverService(
|
|
new ModelParameterService(),
|
|
new ModelFormulaService(),
|
|
new BomConditionRuleService()
|
|
);
|
|
$this->bomResolver->setTenantId($this->tenant->id);
|
|
$this->bomResolver->setApiUserId(1);
|
|
}
|
|
|
|
public function run(): void
|
|
{
|
|
try {
|
|
// Test standard residential scenarios
|
|
$this->testResidentialScenarios();
|
|
|
|
// Test commercial scenarios
|
|
$this->testCommercialScenarios();
|
|
|
|
// Test edge case scenarios
|
|
$this->testEdgeCaseScenarios();
|
|
|
|
// Test material type scenarios
|
|
$this->testMaterialTypeScenarios();
|
|
|
|
// Test installation type scenarios
|
|
$this->testInstallationTypeScenarios();
|
|
|
|
// Test performance under different loads
|
|
$this->testPerformanceScenarios();
|
|
|
|
// Generate scenario report
|
|
$this->generateScenarioReport();
|
|
|
|
} catch (Exception $e) {
|
|
$this->output("\n❌ Critical Error: " . $e->getMessage());
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
private function testResidentialScenarios(): void
|
|
{
|
|
$this->output("\n🏠 Testing Residential Scenarios...");
|
|
|
|
// Small window screen (typical bedroom)
|
|
$this->testScenario('Small Bedroom Window', [
|
|
'W0' => 800,
|
|
'H0' => 600,
|
|
'screen_type' => 'FABRIC',
|
|
'install_type' => 'WALL'
|
|
], [
|
|
'expectMotor' => 'MOTOR_KSS01_STD',
|
|
'expectBrackets' => 2,
|
|
'expectMaterial' => 'FABRIC_KSS01',
|
|
'maxWeight' => 15.0
|
|
]);
|
|
|
|
// Standard patio door
|
|
$this->testScenario('Standard Patio Door', [
|
|
'W0' => 1800,
|
|
'H0' => 2100,
|
|
'screen_type' => 'FABRIC',
|
|
'install_type' => 'WALL'
|
|
], [
|
|
'expectMotor' => 'MOTOR_KSS01_HEAVY', // Large area needs heavy motor
|
|
'expectBrackets' => 3, // Wide opening needs extra brackets
|
|
'expectMaterial' => 'FABRIC_KSS01'
|
|
]);
|
|
|
|
// Large living room window
|
|
$this->testScenario('Large Living Room Window', [
|
|
'W0' => 2400,
|
|
'H0' => 1500,
|
|
'screen_type' => 'FABRIC',
|
|
'install_type' => 'CEILING'
|
|
], [
|
|
'expectMotor' => 'MOTOR_KSS01_HEAVY',
|
|
'expectBrackets' => 3,
|
|
'expectMaterial' => 'FABRIC_KSS01',
|
|
'installationType' => 'CEILING'
|
|
]);
|
|
}
|
|
|
|
private function testCommercialScenarios(): void
|
|
{
|
|
$this->output("\n🏢 Testing Commercial Scenarios...");
|
|
|
|
// Restaurant storefront
|
|
$this->testScenario('Restaurant Storefront', [
|
|
'W0' => 3000,
|
|
'H0' => 2500,
|
|
'screen_type' => 'STEEL',
|
|
'install_type' => 'RECESSED'
|
|
], [
|
|
'expectMotor' => 'MOTOR_KSS01_HEAVY',
|
|
'expectMaterial' => 'STEEL_KSS01',
|
|
'installationType' => 'RECESSED',
|
|
'requiresWeatherSeal' => true
|
|
]);
|
|
|
|
// Office building entrance
|
|
$this->testScenario('Office Building Entrance', [
|
|
'W0' => 2200,
|
|
'H0' => 2400,
|
|
'screen_type' => 'STEEL',
|
|
'install_type' => 'WALL'
|
|
], [
|
|
'expectMotor' => 'MOTOR_KSS01_HEAVY',
|
|
'expectMaterial' => 'STEEL_KSS01',
|
|
'expectBrackets' => 3
|
|
]);
|
|
|
|
// Warehouse opening
|
|
$this->testScenario('Warehouse Opening', [
|
|
'W0' => 4000,
|
|
'H0' => 3000,
|
|
'screen_type' => 'STEEL',
|
|
'install_type' => 'CEILING'
|
|
], [
|
|
'expectMotor' => 'MOTOR_KSS01_HEAVY',
|
|
'expectMaterial' => 'STEEL_KSS01',
|
|
'maxArea' => 15.0 // Large commercial area
|
|
]);
|
|
}
|
|
|
|
private function testEdgeCaseScenarios(): void
|
|
{
|
|
$this->output("\n⚠️ Testing Edge Case Scenarios...");
|
|
|
|
// Minimum size
|
|
$this->testScenario('Minimum Size Opening', [
|
|
'W0' => 600, // Minimum width
|
|
'H0' => 400, // Minimum height
|
|
'screen_type' => 'FABRIC',
|
|
'install_type' => 'WALL'
|
|
], [
|
|
'expectMotor' => 'MOTOR_KSS01_STD',
|
|
'expectBrackets' => 2
|
|
]);
|
|
|
|
// Maximum size
|
|
$this->testScenario('Maximum Size Opening', [
|
|
'W0' => 3000, // Maximum width
|
|
'H0' => 2500, // Maximum height
|
|
'screen_type' => 'STEEL',
|
|
'install_type' => 'CEILING'
|
|
], [
|
|
'expectMotor' => 'MOTOR_KSS01_HEAVY',
|
|
'expectBrackets' => 3,
|
|
'requiresWeatherSeal' => true
|
|
]);
|
|
|
|
// Very wide but short
|
|
$this->testScenario('Wide Short Opening', [
|
|
'W0' => 2800,
|
|
'H0' => 800,
|
|
'screen_type' => 'FABRIC',
|
|
'install_type' => 'WALL'
|
|
], [
|
|
'expectMotor' => 'MOTOR_KSS01_STD', // Area might still be small
|
|
'expectBrackets' => 3 // But width requires extra brackets
|
|
]);
|
|
|
|
// Narrow but tall
|
|
$this->testScenario('Narrow Tall Opening', [
|
|
'W0' => 800,
|
|
'H0' => 2400,
|
|
'screen_type' => 'FABRIC',
|
|
'install_type' => 'WALL'
|
|
], [
|
|
'expectMotor' => 'MOTOR_KSS01_STD',
|
|
'expectBrackets' => 2
|
|
]);
|
|
}
|
|
|
|
private function testMaterialTypeScenarios(): void
|
|
{
|
|
$this->output("\n🧩 Testing Material Type Scenarios...");
|
|
|
|
// Fabric vs Steel comparison
|
|
$baseParams = ['W0' => 1200, 'H0' => 1800, 'install_type' => 'WALL'];
|
|
|
|
$fabricResult = $this->resolveBom(array_merge($baseParams, ['screen_type' => 'FABRIC']));
|
|
$steelResult = $this->resolveBom(array_merge($baseParams, ['screen_type' => 'STEEL']));
|
|
|
|
$this->testComparison('Fabric vs Steel Material Selection', $fabricResult, $steelResult, [
|
|
'fabricHasFabricMaterial' => true,
|
|
'steelHasSteelMaterial' => true,
|
|
'differentMaterials' => true
|
|
]);
|
|
|
|
// Verify material quantities match area
|
|
$this->testScenario('Fabric Material Quantity Calculation', [
|
|
'W0' => 1000,
|
|
'H0' => 800,
|
|
'screen_type' => 'FABRIC',
|
|
'install_type' => 'WALL'
|
|
], [
|
|
'verifyMaterialQuantity' => true
|
|
]);
|
|
}
|
|
|
|
private function testInstallationTypeScenarios(): void
|
|
{
|
|
$this->output("\n🔧 Testing Installation Type Scenarios...");
|
|
|
|
$baseParams = ['W0' => 1500, 'H0' => 2000, 'screen_type' => 'FABRIC'];
|
|
|
|
// Wall installation
|
|
$this->testScenario('Wall Installation', array_merge($baseParams, ['install_type' => 'WALL']), [
|
|
'expectBrackets' => 3,
|
|
'bracketType' => 'BRACKET_KSS01_WALL'
|
|
]);
|
|
|
|
// Ceiling installation
|
|
$this->testScenario('Ceiling Installation', array_merge($baseParams, ['install_type' => 'CEILING']), [
|
|
'expectBrackets' => 3,
|
|
'bracketType' => 'BRACKET_KSS01_CEILING'
|
|
]);
|
|
|
|
// Recessed installation
|
|
$this->testScenario('Recessed Installation', array_merge($baseParams, ['install_type' => 'RECESSED']), [
|
|
'installationType' => 'RECESSED'
|
|
// Recessed might not need brackets or different bracket type
|
|
]);
|
|
}
|
|
|
|
private function testPerformanceScenarios(): void
|
|
{
|
|
$this->output("\n🏁 Testing Performance Scenarios...");
|
|
|
|
// Test batch processing
|
|
$scenarios = [
|
|
['W0' => 800, 'H0' => 600, 'screen_type' => 'FABRIC', 'install_type' => 'WALL'],
|
|
['W0' => 1200, 'H0' => 800, 'screen_type' => 'STEEL', 'install_type' => 'CEILING'],
|
|
['W0' => 1600, 'H0' => 1200, 'screen_type' => 'FABRIC', 'install_type' => 'RECESSED'],
|
|
['W0' => 2000, 'H0' => 1500, 'screen_type' => 'STEEL', 'install_type' => 'WALL'],
|
|
['W0' => 2400, 'H0' => 1800, 'screen_type' => 'FABRIC', 'install_type' => 'CEILING']
|
|
];
|
|
|
|
$this->testBatchPerformance('Batch BOM Resolution', $scenarios, [
|
|
'maxTotalTime' => 2000, // 2 seconds for all scenarios
|
|
'maxAvgTime' => 400 // 400ms average per scenario
|
|
]);
|
|
}
|
|
|
|
private function testScenario(string $name, array $parameters, array $expectations): void
|
|
{
|
|
$this->totalScenarios++;
|
|
|
|
try {
|
|
$result = $this->resolveBom($parameters);
|
|
$passed = $this->validateExpectations($result, $expectations);
|
|
|
|
if ($passed) {
|
|
$this->passedScenarios++;
|
|
$this->output(" ✅ {$name}");
|
|
$this->testResults[] = ['scenario' => $name, 'status' => 'PASS', 'result' => $result];
|
|
} else {
|
|
$this->output(" ❌ {$name}");
|
|
$this->testResults[] = ['scenario' => $name, 'status' => 'FAIL', 'result' => $result, 'expectations' => $expectations];
|
|
}
|
|
|
|
// Output key metrics
|
|
$this->outputScenarioMetrics($name, $result, $parameters);
|
|
|
|
} catch (Exception $e) {
|
|
$this->output(" ❌ {$name} - Exception: {$e->getMessage()}");
|
|
$this->testResults[] = ['scenario' => $name, 'status' => 'ERROR', 'error' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
private function testComparison(string $name, array $result1, array $result2, array $expectations): void
|
|
{
|
|
$this->totalScenarios++;
|
|
|
|
try {
|
|
$passed = $this->validateComparisonExpectations($result1, $result2, $expectations);
|
|
|
|
if ($passed) {
|
|
$this->passedScenarios++;
|
|
$this->output(" ✅ {$name}");
|
|
} else {
|
|
$this->output(" ❌ {$name}");
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$this->output(" ❌ {$name} - Exception: {$e->getMessage()}");
|
|
}
|
|
}
|
|
|
|
private function testBatchPerformance(string $name, array $scenarios, array $expectations): void
|
|
{
|
|
$this->totalScenarios++;
|
|
|
|
$startTime = microtime(true);
|
|
$times = [];
|
|
|
|
try {
|
|
foreach ($scenarios as $params) {
|
|
$scenarioStart = microtime(true);
|
|
$this->resolveBom($params);
|
|
$times[] = (microtime(true) - $scenarioStart) * 1000;
|
|
}
|
|
|
|
$totalTime = (microtime(true) - $startTime) * 1000;
|
|
$avgTime = array_sum($times) / count($times);
|
|
|
|
$passed = $totalTime <= $expectations['maxTotalTime'] &&
|
|
$avgTime <= $expectations['maxAvgTime'];
|
|
|
|
if ($passed) {
|
|
$this->passedScenarios++;
|
|
$this->output(" ✅ {$name}");
|
|
} else {
|
|
$this->output(" ❌ {$name}");
|
|
}
|
|
|
|
$this->output(" Total time: {$totalTime}ms, Avg time: {$avgTime}ms");
|
|
|
|
} catch (Exception $e) {
|
|
$this->output(" ❌ {$name} - Exception: {$e->getMessage()}");
|
|
}
|
|
}
|
|
|
|
private function resolveBom(array $parameters): array
|
|
{
|
|
return $this->bomResolver->resolveBom($this->model->id, $parameters);
|
|
}
|
|
|
|
private function validateExpectations(array $result, array $expectations): bool
|
|
{
|
|
foreach ($expectations as $key => $expected) {
|
|
switch ($key) {
|
|
case 'expectMotor':
|
|
if (!$this->hasProductWithCode($result['resolved_bom'], $expected)) {
|
|
$this->output(" Expected motor {$expected} not found");
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case 'expectMaterial':
|
|
if (!$this->hasMaterialWithCode($result['resolved_bom'], $expected)) {
|
|
$this->output(" Expected material {$expected} not found");
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case 'expectBrackets':
|
|
$bracketCount = $this->countBrackets($result['resolved_bom']);
|
|
if ($bracketCount != $expected) {
|
|
$this->output(" Expected {$expected} brackets, found {$bracketCount}");
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case 'maxWeight':
|
|
if ($result['calculated_values']['weight'] > $expected) {
|
|
$this->output(" Weight {$result['calculated_values']['weight']}kg exceeds max {$expected}kg");
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case 'requiresWeatherSeal':
|
|
if ($expected && !$this->hasMaterialWithCode($result['resolved_bom'], 'SEAL_KSS01_WEATHER')) {
|
|
$this->output(" Expected weather seal not found");
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case 'verifyMaterialQuantity':
|
|
if (!$this->verifyMaterialQuantity($result)) {
|
|
$this->output(" Material quantity doesn't match area");
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function validateComparisonExpectations(array $result1, array $result2, array $expectations): bool
|
|
{
|
|
foreach ($expectations as $key => $expected) {
|
|
switch ($key) {
|
|
case 'differentMaterials':
|
|
$materials1 = $this->extractMaterialCodes($result1['resolved_bom']);
|
|
$materials2 = $this->extractMaterialCodes($result2['resolved_bom']);
|
|
if ($materials1 === $materials2) {
|
|
$this->output(" Expected different materials, but got same");
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function hasProductWithCode(array $bomItems, string $productCode): bool
|
|
{
|
|
foreach ($bomItems as $item) {
|
|
if ($item['target_type'] === 'PRODUCT' &&
|
|
isset($item['target_info']['code']) &&
|
|
$item['target_info']['code'] === $productCode) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private function hasMaterialWithCode(array $bomItems, string $materialCode): bool
|
|
{
|
|
foreach ($bomItems as $item) {
|
|
if ($item['target_type'] === 'MATERIAL' &&
|
|
isset($item['target_info']['code']) &&
|
|
$item['target_info']['code'] === $materialCode) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private function countBrackets(array $bomItems): int
|
|
{
|
|
$count = 0;
|
|
foreach ($bomItems as $item) {
|
|
if ($item['target_type'] === 'PRODUCT' &&
|
|
isset($item['target_info']['code']) &&
|
|
strpos($item['target_info']['code'], 'BRACKET') !== false) {
|
|
$count += $item['quantity'];
|
|
}
|
|
}
|
|
return $count;
|
|
}
|
|
|
|
private function verifyMaterialQuantity(array $result): bool
|
|
{
|
|
$area = $result['calculated_values']['area'];
|
|
|
|
foreach ($result['resolved_bom'] as $item) {
|
|
if ($item['target_type'] === 'MATERIAL' &&
|
|
strpos($item['target_info']['code'], 'FABRIC') !== false ||
|
|
strpos($item['target_info']['code'], 'STEEL') !== false) {
|
|
// Material quantity should roughly match area (allowing for waste)
|
|
return abs($item['quantity'] - $area) < ($area * 0.2); // 20% tolerance
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function extractMaterialCodes(array $bomItems): array
|
|
{
|
|
$codes = [];
|
|
foreach ($bomItems as $item) {
|
|
if ($item['target_type'] === 'MATERIAL') {
|
|
$codes[] = $item['target_info']['code'] ?? 'unknown';
|
|
}
|
|
}
|
|
sort($codes);
|
|
return $codes;
|
|
}
|
|
|
|
private function outputScenarioMetrics(string $name, array $result, array $parameters): void
|
|
{
|
|
$metrics = [
|
|
'Area' => round($result['calculated_values']['area'], 2) . 'm²',
|
|
'Weight' => round($result['calculated_values']['weight'], 1) . 'kg',
|
|
'BOM Items' => count($result['resolved_bom'])
|
|
];
|
|
|
|
$metricsStr = implode(', ', array_map(function($k, $v) {
|
|
return "{$k}: {$v}";
|
|
}, array_keys($metrics), $metrics));
|
|
|
|
$this->output(" {$metricsStr}");
|
|
}
|
|
|
|
private function output(string $message): void
|
|
{
|
|
echo $message . "\n";
|
|
Log::info("KSS01 Scenarios: " . $message);
|
|
}
|
|
|
|
private function generateScenarioReport(): void
|
|
{
|
|
$this->output("\n" . str_repeat("=", 60));
|
|
$this->output("KSS01 SCENARIO TEST REPORT");
|
|
$this->output(str_repeat("=", 60));
|
|
|
|
$successRate = $this->totalScenarios > 0 ? round(($this->passedScenarios / $this->totalScenarios) * 100, 1) : 0;
|
|
|
|
$this->output("Total Scenarios: {$this->totalScenarios}");
|
|
$this->output("Passed: {$this->passedScenarios}");
|
|
$this->output("Failed: " . ($this->totalScenarios - $this->passedScenarios));
|
|
$this->output("Success Rate: {$successRate}%");
|
|
|
|
// Show failed scenarios
|
|
$failedScenarios = array_filter($this->testResults, fn($r) => $r['status'] !== 'PASS');
|
|
if (!empty($failedScenarios)) {
|
|
$this->output("\n❌ FAILED SCENARIOS:");
|
|
foreach ($failedScenarios as $failed) {
|
|
$error = isset($failed['error']) ? " - {$failed['error']}" : '';
|
|
$this->output(" • {$failed['scenario']}{$error}");
|
|
}
|
|
}
|
|
|
|
$this->output("\n" . str_repeat("=", 60));
|
|
|
|
if ($successRate >= 95) {
|
|
$this->output("🎉 KSS01 SCENARIOS PASSED - All business cases validated");
|
|
exit(0);
|
|
} elseif ($successRate >= 85) {
|
|
$this->output("⚠️ KSS01 SCENARIOS WARNING - Some edge cases failed");
|
|
exit(1);
|
|
} else {
|
|
$this->output("❌ KSS01 SCENARIOS FAILED - Critical business logic issues");
|
|
exit(2);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run the scenario tests
|
|
$tester = new KSS01ScenarioTester();
|
|
$tester->run();
|