Files
sam-api/scripts/validation/test_kss01_scenarios.php
kent bf8036a64b 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>
2025-09-30 23:31:14 +09:00

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();