feat: 업체별 동적 BOM 계산 시스템 구현
- 데이터베이스 스키마 확장: BOM 테이블에 계산 관련 필드 추가 - 계산 엔진 구현: CalculationEngine, FormulaParser, ParameterValidator - API 구현: 견적 파라미터 추출, 실시간 BOM 계산, 업체별 산출식 관리 - FormRequest 검증: 모든 입력 데이터 검증 및 한국어 에러 메시지 - 라우트 등록: 5개 BOM 계산 API 엔드포인트 추가 주요 기능: • BOM에서 필요한 조건만 동적 추출하여 견적 화면에 표시 • 경동기업 하드코딩 산출식을 동적 시스템으로 전환 • 업체별 산출식 버전 관리 및 실시간 테스트 지원 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
284
app/Services/Calculation/FormulaParser.php
Normal file
284
app/Services/Calculation/FormulaParser.php
Normal file
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Calculation;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FormulaParser
|
||||
{
|
||||
/**
|
||||
* 계산식 실행
|
||||
* @param string $formula 계산식 표현식
|
||||
* @param array $variables 변수 값들
|
||||
* @return array|float 계산 결과
|
||||
*/
|
||||
public function execute(string $formula, array $variables): array|float
|
||||
{
|
||||
try {
|
||||
// 안전한 계산식 실행을 위한 파싱
|
||||
$result = $this->parseAndExecute($formula, $variables);
|
||||
|
||||
if (is_array($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return ['result' => $result];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('계산식 실행 실패', [
|
||||
'formula' => $formula,
|
||||
'variables' => $variables,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
throw new \RuntimeException("계산식 실행 실패: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 계산식 파싱 및 실행
|
||||
*/
|
||||
protected function parseAndExecute(string $formula, array $variables): array|float
|
||||
{
|
||||
// 미리 정의된 함수 패턴들 처리
|
||||
if ($this->isPreDefinedFunction($formula)) {
|
||||
return $this->executePreDefinedFunction($formula, $variables);
|
||||
}
|
||||
|
||||
// 단순 수학 표현식 처리
|
||||
if ($this->isSimpleMathExpression($formula)) {
|
||||
return $this->executeSimpleMath($formula, $variables);
|
||||
}
|
||||
|
||||
// 조건식 처리 (IF문 등)
|
||||
if ($this->isConditionalExpression($formula)) {
|
||||
return $this->executeConditionalExpression($formula, $variables);
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException("지원되지 않는 계산식 형태: {$formula}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 미리 정의된 함수 실행
|
||||
*/
|
||||
protected function executePreDefinedFunction(string $formula, array $variables): array
|
||||
{
|
||||
// 경동기업 스크린 제작사이즈 계산
|
||||
if ($formula === 'kyungdong_screen_size') {
|
||||
return [
|
||||
'W1' => ($variables['W0'] ?? 0) + 160,
|
||||
'H1' => ($variables['H0'] ?? 0) + 350
|
||||
];
|
||||
}
|
||||
|
||||
// 경동기업 철재 제작사이즈 계산
|
||||
if ($formula === 'kyungdong_steel_size') {
|
||||
return [
|
||||
'W1' => ($variables['W0'] ?? 0) + 110,
|
||||
'H1' => ($variables['H0'] ?? 0) + 350
|
||||
];
|
||||
}
|
||||
|
||||
// 스크린 중량 계산
|
||||
if ($formula === 'screen_weight_calculation') {
|
||||
$W0 = $variables['W0'] ?? 0;
|
||||
$W1 = $variables['W1'] ?? 0;
|
||||
$H1 = $variables['H1'] ?? 0;
|
||||
$area = ($W1 * $H1) / 1000000;
|
||||
|
||||
return [
|
||||
'area' => $area,
|
||||
'weight' => ($area * 2) + ($W0 / 1000 * 14.17)
|
||||
];
|
||||
}
|
||||
|
||||
// 브라켓 수량 계산
|
||||
if ($formula === 'bracket_quantity') {
|
||||
$W1 = $variables['W1'] ?? 0;
|
||||
if ($W1 <= 3000) return ['result' => 2];
|
||||
if ($W1 <= 6000) return ['result' => 3];
|
||||
if ($W1 <= 9000) return ['result' => 4];
|
||||
if ($W1 <= 12000) return ['result' => 5];
|
||||
return ['result' => 5]; // 최대값
|
||||
}
|
||||
|
||||
// 환봉 수량 계산
|
||||
if ($formula === 'round_bar_quantity') {
|
||||
$W1 = $variables['W1'] ?? 0;
|
||||
$qty = $variables['qty'] ?? 1;
|
||||
|
||||
if ($W1 <= 3000) return ['result' => 1 * $qty];
|
||||
if ($W1 <= 6000) return ['result' => 2 * $qty];
|
||||
if ($W1 <= 9000) return ['result' => 3 * $qty];
|
||||
if ($W1 <= 12000) return ['result' => 4 * $qty];
|
||||
return ['result' => 4 * $qty];
|
||||
}
|
||||
|
||||
// 샤프트 규격 결정
|
||||
if ($formula === 'shaft_size_determination') {
|
||||
$W1 = $variables['W1'] ?? 0;
|
||||
|
||||
if ($W1 <= 6000) return ['result' => 4]; // 4인치
|
||||
if ($W1 <= 8200) return ['result' => 5]; // 5인치
|
||||
return ['result' => 0]; // 미정의
|
||||
}
|
||||
|
||||
// 모터 용량 결정
|
||||
if ($formula === 'motor_capacity_determination') {
|
||||
$shaftSize = $variables['shaft_size'] ?? 4;
|
||||
$weight = $variables['weight'] ?? 0;
|
||||
|
||||
// 샤프트별 중량 매트릭스
|
||||
if ($shaftSize == 4) {
|
||||
if ($weight <= 150) return ['result' => '150K'];
|
||||
if ($weight <= 300) return ['result' => '300K'];
|
||||
if ($weight <= 400) return ['result' => '400K'];
|
||||
} elseif ($shaftSize == 5) {
|
||||
if ($weight <= 123) return ['result' => '150K'];
|
||||
if ($weight <= 246) return ['result' => '300K'];
|
||||
if ($weight <= 327) return ['result' => '400K'];
|
||||
if ($weight <= 500) return ['result' => '500K'];
|
||||
if ($weight <= 600) return ['result' => '600K'];
|
||||
} elseif ($shaftSize == 6) {
|
||||
if ($weight <= 104) return ['result' => '150K'];
|
||||
if ($weight <= 208) return ['result' => '300K'];
|
||||
if ($weight <= 300) return ['result' => '400K'];
|
||||
if ($weight <= 424) return ['result' => '500K'];
|
||||
if ($weight <= 508) return ['result' => '600K'];
|
||||
if ($weight <= 800) return ['result' => '800K'];
|
||||
if ($weight <= 1000) return ['result' => '1000K'];
|
||||
}
|
||||
|
||||
return ['result' => '미정의'];
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException("알 수 없는 미리 정의된 함수: {$formula}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 단순 수학 표현식 실행
|
||||
*/
|
||||
protected function executeSimpleMath(string $formula, array $variables): float
|
||||
{
|
||||
// 변수 치환
|
||||
$expression = $formula;
|
||||
foreach ($variables as $key => $value) {
|
||||
$expression = str_replace($key, (string)$value, $expression);
|
||||
}
|
||||
|
||||
// 안전한 수학 표현식 검증
|
||||
if (!$this->isSafeMathExpression($expression)) {
|
||||
throw new \InvalidArgumentException("안전하지 않은 수학 표현식: {$expression}");
|
||||
}
|
||||
|
||||
// 계산 실행
|
||||
return eval("return {$expression};");
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건식 실행
|
||||
*/
|
||||
protected function executeConditionalExpression(string $formula, array $variables): float
|
||||
{
|
||||
// 간단한 IF 조건식 파싱
|
||||
// 예: "IF(W1 <= 3000, 2, IF(W1 <= 6000, 3, 4))"
|
||||
|
||||
$pattern = '/IF\s*\(\s*([^,]+),\s*([^,]+),\s*(.+)\)/i';
|
||||
|
||||
if (preg_match($pattern, $formula, $matches)) {
|
||||
$condition = trim($matches[1]);
|
||||
$trueValue = trim($matches[2]);
|
||||
$falseValue = trim($matches[3]);
|
||||
|
||||
// 조건 평가
|
||||
if ($this->evaluateCondition($condition, $variables)) {
|
||||
return is_numeric($trueValue) ? (float)$trueValue : $this->execute($trueValue, $variables)['result'];
|
||||
} else {
|
||||
return is_numeric($falseValue) ? (float)$falseValue : $this->execute($falseValue, $variables)['result'];
|
||||
}
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException("지원되지 않는 조건식: {$formula}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건 평가
|
||||
*/
|
||||
protected function evaluateCondition(string $condition, array $variables): bool
|
||||
{
|
||||
// 변수 치환
|
||||
$expression = $condition;
|
||||
foreach ($variables as $key => $value) {
|
||||
$expression = str_replace($key, (string)$value, $expression);
|
||||
}
|
||||
|
||||
// 안전한 조건식 검증
|
||||
if (!$this->isSafeConditionExpression($expression)) {
|
||||
throw new \InvalidArgumentException("안전하지 않은 조건식: {$expression}");
|
||||
}
|
||||
|
||||
return eval("return {$expression};");
|
||||
}
|
||||
|
||||
/**
|
||||
* 미리 정의된 함수인지 확인
|
||||
*/
|
||||
protected function isPreDefinedFunction(string $formula): bool
|
||||
{
|
||||
$predefinedFunctions = [
|
||||
'kyungdong_screen_size',
|
||||
'kyungdong_steel_size',
|
||||
'screen_weight_calculation',
|
||||
'bracket_quantity',
|
||||
'round_bar_quantity',
|
||||
'shaft_size_determination',
|
||||
'motor_capacity_determination'
|
||||
];
|
||||
|
||||
return in_array($formula, $predefinedFunctions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 단순 수학 표현식인지 확인
|
||||
*/
|
||||
protected function isSimpleMathExpression(string $formula): bool
|
||||
{
|
||||
return preg_match('/^[A-Za-z0-9_+\-*\/().\s]+$/', $formula);
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건식인지 확인
|
||||
*/
|
||||
protected function isConditionalExpression(string $formula): bool
|
||||
{
|
||||
return preg_match('/IF\s*\(/i', $formula);
|
||||
}
|
||||
|
||||
/**
|
||||
* 안전한 수학 표현식인지 검증
|
||||
*/
|
||||
protected function isSafeMathExpression(string $expression): bool
|
||||
{
|
||||
// 위험한 함수나 키워드 차단
|
||||
$dangerous = ['exec', 'system', 'shell_exec', 'eval', 'file', 'fopen', 'include', 'require'];
|
||||
|
||||
foreach ($dangerous as $func) {
|
||||
if (stripos($expression, $func) !== false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 허용된 문자만 포함하는지 확인
|
||||
return preg_match('/^[0-9+\-*\/().\s]+$/', $expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* 안전한 조건식인지 검증
|
||||
*/
|
||||
protected function isSafeConditionExpression(string $expression): bool
|
||||
{
|
||||
// 허용된 연산자: ==, !=, <, >, <=, >=, &&, ||
|
||||
$allowedPattern = '/^[0-9+\-*\/().\s<>=!&|]+$/';
|
||||
return preg_match($allowedPattern, $expression);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user