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:
268
CLAUDE.md
268
CLAUDE.md
@@ -221,7 +221,272 @@ ### Testing
|
||||
- PHPUnit configuration: `phpunit.xml` in each Laravel app
|
||||
- Test directories: `tests/` in Laravel applications
|
||||
|
||||
## Development Workflow
|
||||
## SAM 프로젝트 권장 워크플로우
|
||||
|
||||
### 1. 기능 개발 워크플로우 (Feature Development)
|
||||
|
||||
새로운 기능 개발 시 API-First 접근 방식을 따릅니다:
|
||||
|
||||
```
|
||||
1. 기획 및 분석
|
||||
- CURRENT_WORKS.md에 작업 계획 작성
|
||||
- 영향받는 저장소 식별 (api/admin/front)
|
||||
- 데이터베이스 스키마 변경 필요성 검토
|
||||
|
||||
2. 개발 환경 준비
|
||||
- git status로 모든 저장소 상태 확인
|
||||
- Docker 서비스 실행 확인 (composer dev)
|
||||
- 마이그레이션 상태 확인 (php artisan migrate:status)
|
||||
|
||||
3. API 우선 개발 (API-First)
|
||||
- Service 클래스 구현 (비즈니스 로직)
|
||||
- Controller 구현 (FormRequest + ApiResponse)
|
||||
- Swagger 문서 작성
|
||||
- API 테스트 (Postman/Swagger UI)
|
||||
|
||||
4. Frontend 연동
|
||||
- Admin 패널 (Filament) 또는 CodeIgniter 뷰 구현
|
||||
- 필요시 공통 모델을 shared/로 이동
|
||||
|
||||
5. 품질 검증
|
||||
- API 테스트 실행
|
||||
- Pint 코드 포맷팅 (./vendor/bin/pint)
|
||||
- Swagger 문서 검토
|
||||
|
||||
6. 커밋 및 정리
|
||||
- CURRENT_WORKS.md 업데이트
|
||||
- 저장소별 순차 커밋
|
||||
- 임시 파일 정리
|
||||
```
|
||||
|
||||
**확인 포인트**: API 개발 완료 후 → Frontend 연동 진행 여부 확인
|
||||
|
||||
### 2. 버그 수정 워크플로우 (Bugfix)
|
||||
|
||||
```
|
||||
1. 문제 식별
|
||||
- 에러 로그 확인 (Laravel 로그, 브라우저 콘솔)
|
||||
- 영향 범위 파악 (어떤 저장소/컴포넌트)
|
||||
- 재현 방법 문서화
|
||||
|
||||
2. 핫픽스 브랜치 생성 (긴급시)
|
||||
- git checkout -b hotfix/issue-description
|
||||
- 수정 범위를 최소화
|
||||
|
||||
3. 수정 구현
|
||||
- 근본 원인 해결 (임시방편 금지)
|
||||
- 관련 테스트 케이스 추가
|
||||
- 감사 로그 영향 고려
|
||||
|
||||
4. 검증 및 배포
|
||||
- 로컬 테스트 완료
|
||||
- Swagger 문서 업데이트 (API 변경시)
|
||||
- 프로덕션 배포 후 모니터링
|
||||
```
|
||||
|
||||
**확인 포인트**: 긴급도에 따른 핫픽스 브랜치 생성 여부 확인
|
||||
|
||||
### 3. 데이터베이스 변경 워크플로우
|
||||
|
||||
```
|
||||
1. 마이그레이션 설계
|
||||
- 기존 데이터 호환성 검토
|
||||
- 롤백 시나리오 준비
|
||||
- 멀티테넌트 영향도 분석
|
||||
|
||||
2. 개발 환경 테스트
|
||||
- php artisan migrate:status 확인
|
||||
- 테스트 데이터로 마이그레이션 검증
|
||||
- 롤백 테스트 수행
|
||||
|
||||
3. 모델 업데이트
|
||||
- Eloquent 모델 수정
|
||||
- BelongsToTenant 스코프 확인
|
||||
- 감사 로그 설정 점검
|
||||
|
||||
4. API 영향도 점검
|
||||
- 기존 API 호환성 확인
|
||||
- Swagger 스키마 업데이트
|
||||
- 버전 호환성 고려
|
||||
```
|
||||
|
||||
**확인 포인트**: 마이그레이션 실행 전 백업 완료 확인
|
||||
|
||||
### 4. 멀티 저장소 동기화 워크플로우 (상시 적용)
|
||||
|
||||
```
|
||||
작업 순서: api (백엔드) → admin (관리 UI) → front (사용자 UI) → shared (공통)
|
||||
|
||||
의존성 관리:
|
||||
- API 변경사항 완료 후 Frontend 작업
|
||||
- Shared 모델 변경 시 모든 저장소 동기화
|
||||
- Docker 설정 변경 시 전체 재시작
|
||||
|
||||
통합 테스트:
|
||||
- API → Admin 패널 → 사용자 화면 순서로 테스트
|
||||
- 권한 및 멀티테넌트 기능 검증
|
||||
```
|
||||
|
||||
### 5. 코드 리뷰 및 품질 관리 (상시 적용)
|
||||
|
||||
**코드 품질 체크리스트:**
|
||||
```
|
||||
□ SAM API Development Rules 준수
|
||||
□ Service-First 아키텍처 (Controller는 단순)
|
||||
□ FormRequest 검증 사용
|
||||
□ i18n 메시지 키 사용 (__('message.xxx'))
|
||||
□ Swagger 문서 완성도
|
||||
□ 멀티테넌트 스코프 적용 (BelongsToTenant)
|
||||
□ 감사 로그 고려
|
||||
□ Soft Delete 적용
|
||||
□ 테스트 케이스 작성
|
||||
□ Pint 포맷팅 적용
|
||||
```
|
||||
|
||||
### 6. 배포 준비 워크플로우
|
||||
|
||||
```
|
||||
1. 코드 검증
|
||||
- 모든 테스트 통과
|
||||
- Swagger 문서 최신화
|
||||
- 환경변수 설정 확인
|
||||
|
||||
2. 데이터베이스
|
||||
- 마이그레이션 순서 확인
|
||||
- 백업 계획 수립
|
||||
- 롤백 시나리오 준비
|
||||
|
||||
3. 문서화
|
||||
- CURRENT_WORKS.md 최종 업데이트
|
||||
- API 변경사항 문서화
|
||||
- 배포 노트 작성
|
||||
```
|
||||
|
||||
**확인 포인트**: 배포 전 마지막 체크리스트 확인
|
||||
|
||||
### 워크플로우 진행 방식
|
||||
|
||||
- **1~3번**: 시작부터 완료까지 연속 진행 (중간 확인 포인트에서 사용자 확인)
|
||||
- **4~5번**: 상시 적용 (작업할 때마다 자동 적용)
|
||||
- **6번**: 배포 시에만 적용
|
||||
|
||||
### SuperClaude 활용 전략
|
||||
|
||||
각 개발 단계별로 SuperClaude 프레임워크를 전략적으로 활용합니다:
|
||||
|
||||
#### 🎯 기획 단계 (SuperClaude 필수)
|
||||
```
|
||||
/sc:business-panel @requirements.md
|
||||
- 비즈니스 전문가 패널을 통한 요구사항 분석
|
||||
- Porter, Drucker, Collins 등 전문가 관점으로 기능 검증
|
||||
- 시장 적합성 및 비즈니스 가치 평가
|
||||
- 우선순위 및 리소스 배분 결정
|
||||
```
|
||||
|
||||
#### 🔧 백엔드 개발 (SuperClaude 적극 활용)
|
||||
```
|
||||
/sc:architect --system --api-design
|
||||
- 시스템 아키텍처 설계 및 검토
|
||||
- API 설계 패턴 최적화
|
||||
- 데이터베이스 스키마 설계
|
||||
- 성능 및 확장성 고려사항
|
||||
|
||||
/sc:security-engineer --api-security
|
||||
- API 보안 취약점 분석
|
||||
- 멀티테넌트 보안 설계
|
||||
- 인증/인가 시스템 검증
|
||||
- 감사 로그 보안 검토
|
||||
```
|
||||
|
||||
#### 🎨 프론트엔드 개발 (SuperClaude 부분 활용)
|
||||
```
|
||||
/sc:frontend-architect --ui-design --accessibility
|
||||
- UI/UX 아키텍처 설계
|
||||
- 접근성 및 사용성 검증
|
||||
- 컴포넌트 재사용성 최적화
|
||||
- 반응형 디자인 전략
|
||||
|
||||
Magic MCP 활용:
|
||||
- /ui 명령어로 21st.dev 패턴 기반 컴포넌트 생성
|
||||
- 현대적이고 접근성 높은 UI 구현
|
||||
```
|
||||
|
||||
#### ✅ 검증 단계 (SuperClaude 필수)
|
||||
```
|
||||
/sc:quality-engineer --comprehensive-testing
|
||||
- 테스트 전략 수립 및 검증
|
||||
- 코드 품질 메트릭 분석
|
||||
- 성능 테스트 설계
|
||||
- 엣지 케이스 식별
|
||||
|
||||
/sc:security-engineer --penetration-testing
|
||||
- 보안 취약점 스캔
|
||||
- 멀티테넌트 격리 검증
|
||||
- API 보안 테스트
|
||||
- 데이터 보호 검증
|
||||
|
||||
/sc:business-panel --quality-validation
|
||||
- 비즈니스 요구사항 충족도 검증
|
||||
- 사용자 관점 품질 평가
|
||||
- ROI 및 비즈니스 가치 측정
|
||||
```
|
||||
|
||||
#### 🚀 배포 준비 (SuperClaude 선택적 활용)
|
||||
```
|
||||
/sc:devops-architect --deployment-strategy
|
||||
- 배포 전략 최적화
|
||||
- 인프라 자동화 검토
|
||||
- 모니터링 및 로깅 설정
|
||||
- 롤백 시나리오 검증
|
||||
```
|
||||
|
||||
### SuperClaude 모드별 활용 가이드
|
||||
|
||||
#### 🔥 고강도 작업 (--ultrathink)
|
||||
- 복잡한 시스템 설계
|
||||
- 대규모 리팩토링
|
||||
- 보안 아키텍처 설계
|
||||
- 성능 최적화
|
||||
|
||||
#### 🎯 중강도 작업 (--think-hard)
|
||||
- API 설계 및 검토
|
||||
- 데이터베이스 스키마 변경
|
||||
- 비즈니스 로직 구현
|
||||
- 테스트 전략 수립
|
||||
|
||||
#### 💡 일반 작업 (--think)
|
||||
- 기능 구현
|
||||
- 버그 수정
|
||||
- 코드 리뷰
|
||||
- 문서 작성
|
||||
|
||||
### 자동 SuperClaude 활성화 조건
|
||||
|
||||
다음 상황에서 자동으로 SuperClaude를 활성화합니다:
|
||||
|
||||
**기획 단계:**
|
||||
- 새로운 기능 요구사항 분석 시
|
||||
- 비즈니스 프로세스 변경 시
|
||||
- 시장 적합성 검토 시
|
||||
|
||||
**백엔드 개발:**
|
||||
- 새로운 API 엔드포인트 설계 시
|
||||
- 데이터베이스 스키마 변경 시
|
||||
- 보안 관련 기능 구현 시
|
||||
- 성능 최적화 작업 시
|
||||
|
||||
**프론트엔드 개발:**
|
||||
- 새로운 UI 컴포넌트 설계 시
|
||||
- 사용자 경험 개선 작업 시
|
||||
- 접근성 요구사항 구현 시
|
||||
|
||||
**검증 단계:**
|
||||
- 품질 보증 체크리스트 실행 시
|
||||
- 보안 검증 작업 시
|
||||
- 성능 테스트 수행 시
|
||||
- 비즈니스 요구사항 검증 시
|
||||
|
||||
## Development Workflow (기본 환경 설정)
|
||||
|
||||
1. **Environment Setup**: Configure `.env` files for each application
|
||||
2. **Database**: Run migrations in both admin and api applications
|
||||
@@ -450,3 +715,4 @@ ## Important Notes
|
||||
- **Follow SAM API Development Rules strictly when working on API-related tasks**
|
||||
- **Always maintain CURRENT_WORKS.md for project continuity**
|
||||
- **Sync CLAUDE.md files between sessions for consistency**
|
||||
- **🇰🇷 LANGUAGE SETTING: 모든 응답은 한국어로 제공 (별도 요청 시 예외)**
|
||||
288
app/Http/Controllers/Api/V1/Design/BomCalculationController.php
Normal file
288
app/Http/Controllers/Api/V1/Design/BomCalculationController.php
Normal file
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Design;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Design\BomCalculationService;
|
||||
use App\Http\Requests\Design\GetEstimateParametersRequest;
|
||||
use App\Http\Requests\Design\CalculateBomRequest;
|
||||
use App\Http\Requests\Design\SaveCompanyFormulaRequest;
|
||||
use App\Http\Resources\ApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* @group Design/BOM Calculation
|
||||
*
|
||||
* BOM 기반 동적 산출식 계산 API
|
||||
*/
|
||||
class BomCalculationController extends Controller
|
||||
{
|
||||
protected BomCalculationService $bomCalculationService;
|
||||
|
||||
public function __construct(BomCalculationService $bomCalculationService)
|
||||
{
|
||||
$this->bomCalculationService = $bomCalculationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 파라미터 조회
|
||||
*
|
||||
* 특정 모델의 견적 시 필요한 입력 파라미터 스키마를 조회합니다.
|
||||
* BOM에 정의된 조건만 동적으로 추출하여 반환합니다.
|
||||
*
|
||||
* @urlParam model_id int required 모델 ID Example: 1
|
||||
* @queryParam company_name string 업체명 (선택사항) Example: 경동기업
|
||||
*
|
||||
* @response 200 {
|
||||
* "success": true,
|
||||
* "message": "견적 파라미터를 성공적으로 조회했습니다.",
|
||||
* "data": {
|
||||
* "model_info": {
|
||||
* "id": 1,
|
||||
* "name": "스크린셔터 표준형",
|
||||
* "version": "v1.0",
|
||||
* "bom_template_id": 1
|
||||
* },
|
||||
* "company_info": {
|
||||
* "company_type": "경동기업",
|
||||
* "formula_version": "v2.0",
|
||||
* "requested_company": "경동기업"
|
||||
* },
|
||||
* "parameters": {
|
||||
* "required_parameters": [
|
||||
* {
|
||||
* "key": "W0",
|
||||
* "label": "오픈사이즈 가로(mm)",
|
||||
* "type": "integer",
|
||||
* "required": true,
|
||||
* "min": 500,
|
||||
* "max": 15000
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public function getEstimateParameters(GetEstimateParametersRequest $request, int $modelId): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $modelId) {
|
||||
$companyName = $request->query('company_name');
|
||||
|
||||
$result = $this->bomCalculationService->getEstimateParameters($modelId, $companyName);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
'message' => __('message.fetched')
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* BOM 계산 실행
|
||||
*
|
||||
* 입력된 파라미터를 기반으로 BOM 수량을 동적으로 계산합니다.
|
||||
* 업체별 산출식을 적용하여 실시간 견적을 생성합니다.
|
||||
*
|
||||
* @urlParam bom_template_id int required BOM 템플릿 ID Example: 1
|
||||
*
|
||||
* @bodyParam parameters object required 계산 파라미터
|
||||
* @bodyParam parameters.W0 int required 오픈사이즈 가로(mm) Example: 3000
|
||||
* @bodyParam parameters.H0 int required 오픈사이즈 세로(mm) Example: 2500
|
||||
* @bodyParam parameters.product_type string required 제품타입 Example: screen
|
||||
* @bodyParam company_name string 업체명 (선택사항) Example: 경동기업
|
||||
*
|
||||
* @response 200 {
|
||||
* "success": true,
|
||||
* "message": "BOM 계산이 완료되었습니다.",
|
||||
* "data": {
|
||||
* "bom_template": {
|
||||
* "id": 1,
|
||||
* "name": "Main",
|
||||
* "company_type": "경동기업",
|
||||
* "formula_version": "v2.0"
|
||||
* },
|
||||
* "input_parameters": {
|
||||
* "W0": 3000,
|
||||
* "H0": 2500,
|
||||
* "product_type": "screen"
|
||||
* },
|
||||
* "calculated_values": {
|
||||
* "W1": 3160,
|
||||
* "H1": 2850,
|
||||
* "area": 9.006,
|
||||
* "weight": 60.42
|
||||
* },
|
||||
* "bom_items": [
|
||||
* {
|
||||
* "item_id": 1,
|
||||
* "ref_type": "MATERIAL",
|
||||
* "ref_id": 101,
|
||||
* "original_qty": 1,
|
||||
* "calculated_qty": 2,
|
||||
* "is_calculated": true,
|
||||
* "calculation_formula": "bracket_quantity"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public function calculateBom(CalculateBomRequest $request, int $bomTemplateId): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $bomTemplateId) {
|
||||
$data = $request->validated();
|
||||
|
||||
$result = $this->bomCalculationService->calculateBomEstimate(
|
||||
$bomTemplateId,
|
||||
$data['parameters'],
|
||||
$data['company_name'] ?? null
|
||||
);
|
||||
|
||||
if (!$result['success']) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('error.calculation_failed'),
|
||||
'error' => $result['error']
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $result['data'],
|
||||
'message' => __('message.calculation.completed')
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 업체별 산출식 목록 조회
|
||||
*
|
||||
* 특정 업체의 등록된 산출식 목록을 조회합니다.
|
||||
*
|
||||
* @urlParam company_name string required 업체명 Example: 경동기업
|
||||
*
|
||||
* @response 200 {
|
||||
* "success": true,
|
||||
* "message": "업체 산출식 목록을 조회했습니다.",
|
||||
* "data": {
|
||||
* "company_name": "경동기업",
|
||||
* "total_formulas": 5,
|
||||
* "formulas": [
|
||||
* {
|
||||
* "type": "manufacturing_size",
|
||||
* "version": "v2.0",
|
||||
* "description": "제작사이즈 계산식",
|
||||
* "updated_at": "2025-09-22T15:30:00Z"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public function getCompanyFormulas(string $companyName): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($companyName) {
|
||||
$result = $this->bomCalculationService->getCompanyFormulas($companyName);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
'message' => __('message.fetched')
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 업체별 산출식 등록/수정
|
||||
*
|
||||
* 특정 업체의 산출식을 등록하거나 수정합니다.
|
||||
* 기존 산출식이 있으면 새 버전으로 업데이트됩니다.
|
||||
*
|
||||
* @urlParam company_name string required 업체명 Example: 경동기업
|
||||
* @urlParam formula_type string required 산출식 타입 Example: manufacturing_size
|
||||
*
|
||||
* @bodyParam formula_expression string required 계산식 표현식
|
||||
* @bodyParam parameters array required 필요한 파라미터 정의
|
||||
* @bodyParam conditions array 적용 조건 (선택사항)
|
||||
* @bodyParam validation_rules array 검증 규칙 (선택사항)
|
||||
* @bodyParam description string 산출식 설명 (선택사항)
|
||||
*
|
||||
* @response 201 {
|
||||
* "success": true,
|
||||
* "message": "업체 산출식이 등록되었습니다.",
|
||||
* "data": {
|
||||
* "id": 1,
|
||||
* "company_name": "경동기업",
|
||||
* "formula_type": "manufacturing_size",
|
||||
* "version": "v1.0",
|
||||
* "is_active": true,
|
||||
* "created_at": "2025-09-22T15:30:00Z"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public function saveCompanyFormula(SaveCompanyFormulaRequest $request, string $companyName, string $formulaType): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $companyName, $formulaType) {
|
||||
$data = $request->validated();
|
||||
|
||||
$result = $this->bomCalculationService->saveCompanyFormula($companyName, $formulaType, $data);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
'message' => __('message.company_formula.created')
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 계산식 테스트 실행
|
||||
*
|
||||
* 산출식을 실제 적용하기 전에 테스트해볼 수 있습니다.
|
||||
*
|
||||
* @bodyParam formula_expression string required 테스트할 계산식
|
||||
* @bodyParam test_parameters object required 테스트 파라미터
|
||||
*
|
||||
* @response 200 {
|
||||
* "success": true,
|
||||
* "message": "계산식 테스트가 완료되었습니다.",
|
||||
* "data": {
|
||||
* "formula_expression": "bracket_quantity",
|
||||
* "input_parameters": {"W1": 3000},
|
||||
* "result": {"result": 2},
|
||||
* "execution_time_ms": 1.5
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public function testFormula(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$request->validate([
|
||||
'formula_expression' => 'required|string',
|
||||
'test_parameters' => 'required|array'
|
||||
]);
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
// 직접 FormulaParser를 사용하여 테스트
|
||||
$parser = app(\App\Services\Calculation\FormulaParser::class);
|
||||
$result = $parser->execute(
|
||||
$request->input('formula_expression'),
|
||||
$request->input('test_parameters')
|
||||
);
|
||||
|
||||
$executionTime = (microtime(true) - $startTime) * 1000; // ms로 변환
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'formula_expression' => $request->input('formula_expression'),
|
||||
'input_parameters' => $request->input('test_parameters'),
|
||||
'result' => $result,
|
||||
'execution_time_ms' => round($executionTime, 2)
|
||||
],
|
||||
'message' => __('message.formula.test_completed')
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
87
app/Http/Requests/Design/CalculateBomRequest.php
Normal file
87
app/Http/Requests/Design/CalculateBomRequest.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Design;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CalculateBomRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'parameters' => 'required|array',
|
||||
'parameters.W0' => 'nullable|integer|min:500|max:15000',
|
||||
'parameters.H0' => 'nullable|integer|min:500|max:15000',
|
||||
'parameters.product_type' => 'nullable|string|in:screen,steel,blind',
|
||||
'parameters.installation_type' => 'nullable|string|in:window,wall,ceiling',
|
||||
'parameters.power_source' => 'nullable|string|in:manual,electric,automatic',
|
||||
'parameters.motor_type' => 'nullable|string|in:standard,low_noise,high_torque',
|
||||
'parameters.material_grade' => 'nullable|string|in:standard,premium,luxury',
|
||||
'company_name' => 'nullable|string|max:100'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'parameters.required' => __('error.validation.required'),
|
||||
'parameters.array' => __('error.validation.array'),
|
||||
'parameters.W0.integer' => __('error.validation.integer'),
|
||||
'parameters.W0.min' => __('error.validation.min.numeric'),
|
||||
'parameters.W0.max' => __('error.validation.max.numeric'),
|
||||
'parameters.H0.integer' => __('error.validation.integer'),
|
||||
'parameters.H0.min' => __('error.validation.min.numeric'),
|
||||
'parameters.H0.max' => __('error.validation.max.numeric'),
|
||||
'parameters.product_type.string' => __('error.validation.string'),
|
||||
'parameters.product_type.in' => __('error.validation.in'),
|
||||
'parameters.installation_type.string' => __('error.validation.string'),
|
||||
'parameters.installation_type.in' => __('error.validation.in'),
|
||||
'parameters.power_source.string' => __('error.validation.string'),
|
||||
'parameters.power_source.in' => __('error.validation.in'),
|
||||
'parameters.motor_type.string' => __('error.validation.string'),
|
||||
'parameters.motor_type.in' => __('error.validation.in'),
|
||||
'parameters.material_grade.string' => __('error.validation.string'),
|
||||
'parameters.material_grade.in' => __('error.validation.in'),
|
||||
'company_name.string' => __('error.validation.string'),
|
||||
'company_name.max' => __('error.validation.max.string')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'parameters' => '계산 파라미터',
|
||||
'parameters.W0' => '오픈사이즈 가로(mm)',
|
||||
'parameters.H0' => '오픈사이즈 세로(mm)',
|
||||
'parameters.product_type' => '제품타입',
|
||||
'parameters.installation_type' => '설치타입',
|
||||
'parameters.power_source' => '동력원',
|
||||
'parameters.motor_type' => '모터타입',
|
||||
'parameters.material_grade' => '자재등급',
|
||||
'company_name' => '업체명'
|
||||
];
|
||||
}
|
||||
}
|
||||
41
app/Http/Requests/Design/GetEstimateParametersRequest.php
Normal file
41
app/Http/Requests/Design/GetEstimateParametersRequest.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Design;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class GetEstimateParametersRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'company_name' => 'nullable|string|max:100'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'company_name.string' => __('error.validation.string'),
|
||||
'company_name.max' => __('error.validation.max.string')
|
||||
];
|
||||
}
|
||||
}
|
||||
120
app/Http/Requests/Design/SaveCompanyFormulaRequest.php
Normal file
120
app/Http/Requests/Design/SaveCompanyFormulaRequest.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Design;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SaveCompanyFormulaRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'formula_expression' => 'required|string|max:2000',
|
||||
'parameters' => 'required|array',
|
||||
'parameters.*.key' => 'required|string|max:50',
|
||||
'parameters.*.label' => 'required|string|max:100',
|
||||
'parameters.*.type' => 'required|string|in:integer,decimal,string,boolean,array',
|
||||
'parameters.*.required' => 'required|boolean',
|
||||
'parameters.*.min' => 'nullable|numeric',
|
||||
'parameters.*.max' => 'nullable|numeric',
|
||||
'parameters.*.default' => 'nullable',
|
||||
'parameters.*.options' => 'nullable|array',
|
||||
'conditions' => 'nullable|array',
|
||||
'conditions.*.field' => 'nullable|string|max:50',
|
||||
'conditions.*.operator' => 'nullable|string|in:=,!=,>,>=,<,<=,in,not_in',
|
||||
'conditions.*.value' => 'nullable',
|
||||
'validation_rules' => 'nullable|array',
|
||||
'validation_rules.*.field' => 'nullable|string|max:50',
|
||||
'validation_rules.*.rule' => 'nullable|string|max:200',
|
||||
'validation_rules.*.message' => 'nullable|string|max:200',
|
||||
'description' => 'nullable|string|max:500'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'formula_expression.required' => __('error.validation.required'),
|
||||
'formula_expression.string' => __('error.validation.string'),
|
||||
'formula_expression.max' => __('error.validation.max.string'),
|
||||
'parameters.required' => __('error.validation.required'),
|
||||
'parameters.array' => __('error.validation.array'),
|
||||
'parameters.*.key.required' => __('error.validation.required'),
|
||||
'parameters.*.key.string' => __('error.validation.string'),
|
||||
'parameters.*.key.max' => __('error.validation.max.string'),
|
||||
'parameters.*.label.required' => __('error.validation.required'),
|
||||
'parameters.*.label.string' => __('error.validation.string'),
|
||||
'parameters.*.label.max' => __('error.validation.max.string'),
|
||||
'parameters.*.type.required' => __('error.validation.required'),
|
||||
'parameters.*.type.string' => __('error.validation.string'),
|
||||
'parameters.*.type.in' => __('error.validation.in'),
|
||||
'parameters.*.required.required' => __('error.validation.required'),
|
||||
'parameters.*.required.boolean' => __('error.validation.boolean'),
|
||||
'parameters.*.min.numeric' => __('error.validation.numeric'),
|
||||
'parameters.*.max.numeric' => __('error.validation.numeric'),
|
||||
'parameters.*.options.array' => __('error.validation.array'),
|
||||
'conditions.array' => __('error.validation.array'),
|
||||
'conditions.*.field.string' => __('error.validation.string'),
|
||||
'conditions.*.field.max' => __('error.validation.max.string'),
|
||||
'conditions.*.operator.string' => __('error.validation.string'),
|
||||
'conditions.*.operator.in' => __('error.validation.in'),
|
||||
'validation_rules.array' => __('error.validation.array'),
|
||||
'validation_rules.*.field.string' => __('error.validation.string'),
|
||||
'validation_rules.*.field.max' => __('error.validation.max.string'),
|
||||
'validation_rules.*.rule.string' => __('error.validation.string'),
|
||||
'validation_rules.*.rule.max' => __('error.validation.max.string'),
|
||||
'validation_rules.*.message.string' => __('error.validation.string'),
|
||||
'validation_rules.*.message.max' => __('error.validation.max.string'),
|
||||
'description.string' => __('error.validation.string'),
|
||||
'description.max' => __('error.validation.max.string')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'formula_expression' => '계산식 표현식',
|
||||
'parameters' => '파라미터 정의',
|
||||
'parameters.*.key' => '파라미터 키',
|
||||
'parameters.*.label' => '파라미터 레이블',
|
||||
'parameters.*.type' => '파라미터 타입',
|
||||
'parameters.*.required' => '필수 여부',
|
||||
'parameters.*.min' => '최소값',
|
||||
'parameters.*.max' => '최대값',
|
||||
'parameters.*.default' => '기본값',
|
||||
'parameters.*.options' => '옵션 목록',
|
||||
'conditions' => '적용 조건',
|
||||
'conditions.*.field' => '조건 필드',
|
||||
'conditions.*.operator' => '조건 연산자',
|
||||
'conditions.*.value' => '조건 값',
|
||||
'validation_rules' => '검증 규칙',
|
||||
'validation_rules.*.field' => '검증 필드',
|
||||
'validation_rules.*.rule' => '검증 규칙',
|
||||
'validation_rules.*.message' => '검증 메시지',
|
||||
'description' => '설명'
|
||||
];
|
||||
}
|
||||
}
|
||||
102
app/Models/Calculation/CalculationConfig.php
Normal file
102
app/Models/Calculation/CalculationConfig.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Calculation;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class CalculationConfig extends Model
|
||||
{
|
||||
protected $table = 'calculation_configs';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'company_name',
|
||||
'formula_type',
|
||||
'version',
|
||||
'formula_expression',
|
||||
'parameters',
|
||||
'conditions',
|
||||
'validation_rules',
|
||||
'description',
|
||||
'is_active',
|
||||
'created_by',
|
||||
'updated_by'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'parameters' => 'array',
|
||||
'conditions' => 'array',
|
||||
'validation_rules' => 'array',
|
||||
'is_active' => 'boolean'
|
||||
];
|
||||
|
||||
/**
|
||||
* 테넌트별 스코프
|
||||
*/
|
||||
public function scopeForTenant(Builder $query, int $tenantId): Builder
|
||||
{
|
||||
return $query->where('tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성화된 설정만 조회
|
||||
*/
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 업체별 산출식 조회
|
||||
*/
|
||||
public function scopeForCompany(Builder $query, string $companyName): Builder
|
||||
{
|
||||
return $query->where('company_name', $companyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 산출식 타입별 조회
|
||||
*/
|
||||
public function scopeForType(Builder $query, string $formulaType): Builder
|
||||
{
|
||||
return $query->where('formula_type', $formulaType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 최신 버전 조회
|
||||
*/
|
||||
public function scopeLatestVersion(Builder $query): Builder
|
||||
{
|
||||
return $query->orderByDesc('version');
|
||||
}
|
||||
|
||||
/**
|
||||
* 업체별 활성 산출식 목록 조회
|
||||
*/
|
||||
public static function getActiveFormulasForCompany(int $tenantId, string $companyName): array
|
||||
{
|
||||
return static::forTenant($tenantId)
|
||||
->forCompany($companyName)
|
||||
->active()
|
||||
->get()
|
||||
->groupBy('formula_type')
|
||||
->map(function ($formulas) {
|
||||
return $formulas->sortByDesc('version')->first();
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 산출식 조회 (최신 버전)
|
||||
*/
|
||||
public static function getLatestFormula(int $tenantId, string $companyName, string $formulaType): ?static
|
||||
{
|
||||
return static::forTenant($tenantId)
|
||||
->forCompany($companyName)
|
||||
->forType($formulaType)
|
||||
->active()
|
||||
->latestVersion()
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,12 @@ class BomTemplate extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id','model_version_id','name','is_primary','notes',
|
||||
'calculation_schema','company_type','formula_version',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_primary' => 'boolean',
|
||||
'calculation_schema' => 'array',
|
||||
];
|
||||
|
||||
public function modelVersion() {
|
||||
|
||||
@@ -10,11 +10,15 @@ class BomTemplateItem extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id','bom_template_id','ref_type','ref_id','qty','waste_rate','uom_id','notes','sort_order',
|
||||
'is_calculated','calculation_formula','depends_on','calculation_config',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'qty' => 'decimal:6',
|
||||
'waste_rate' => 'decimal:6',
|
||||
'is_calculated' => 'boolean',
|
||||
'depends_on' => 'array',
|
||||
'calculation_config' => 'array',
|
||||
];
|
||||
|
||||
public function template() {
|
||||
|
||||
268
app/Services/Calculation/CalculationEngine.php
Normal file
268
app/Services/Calculation/CalculationEngine.php
Normal file
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Calculation;
|
||||
|
||||
use App\Models\Design\BomTemplate;
|
||||
use App\Models\Design\BomTemplateItem;
|
||||
use App\Models\Calculation\CalculationConfig;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CalculationEngine
|
||||
{
|
||||
protected FormulaParser $parser;
|
||||
protected ParameterValidator $validator;
|
||||
|
||||
public function __construct(FormulaParser $parser, ParameterValidator $validator)
|
||||
{
|
||||
$this->parser = $parser;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* BOM 계산 실행
|
||||
* @param int $bomTemplateId BOM 템플릿 ID
|
||||
* @param array $parameters 입력 파라미터
|
||||
* @param string|null $companyName 업체명 (null시 기본값 사용)
|
||||
* @return array 계산 결과
|
||||
*/
|
||||
public function calculateBOM(int $bomTemplateId, array $parameters, ?string $companyName = null): array
|
||||
{
|
||||
try {
|
||||
// BOM 템플릿 조회
|
||||
$bomTemplate = BomTemplate::with(['items', 'modelVersion.model'])
|
||||
->findOrFail($bomTemplateId);
|
||||
|
||||
// 파라미터 검증
|
||||
$this->validateParameters($bomTemplate, $parameters);
|
||||
|
||||
// 중간 계산값 도출 (W1, H1, 면적, 중량 등)
|
||||
$calculatedValues = $this->calculateIntermediateValues($bomTemplate, $parameters, $companyName);
|
||||
|
||||
// BOM 아이템별 수량 계산
|
||||
$bomItems = $this->calculateBomItems($bomTemplate->items, $calculatedValues, $parameters, $companyName);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'bom_template' => [
|
||||
'id' => $bomTemplate->id,
|
||||
'name' => $bomTemplate->name,
|
||||
'company_type' => $bomTemplate->company_type,
|
||||
'formula_version' => $bomTemplate->formula_version
|
||||
],
|
||||
'input_parameters' => $parameters,
|
||||
'calculated_values' => $calculatedValues,
|
||||
'bom_items' => $bomItems,
|
||||
'calculation_timestamp' => now()
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('BOM 계산 실패', [
|
||||
'bom_template_id' => $bomTemplateId,
|
||||
'parameters' => $parameters,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'bom_template_id' => $bomTemplateId
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적시 필요한 파라미터 스키마 추출
|
||||
* @param int $bomTemplateId BOM 템플릿 ID
|
||||
* @return array 파라미터 스키마
|
||||
*/
|
||||
public function getRequiredParameters(int $bomTemplateId): array
|
||||
{
|
||||
$bomTemplate = BomTemplate::findOrFail($bomTemplateId);
|
||||
|
||||
$schema = $bomTemplate->calculation_schema;
|
||||
if (!$schema) {
|
||||
return $this->getDefaultParameterSchema($bomTemplate);
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 중간 계산값 도출 (W1, H1, 면적, 중량 등)
|
||||
*/
|
||||
protected function calculateIntermediateValues(BomTemplate $bomTemplate, array $parameters, ?string $companyName): array
|
||||
{
|
||||
$company = $companyName ?: $bomTemplate->company_type;
|
||||
$calculated = [];
|
||||
|
||||
// 기본 파라미터에서 추출
|
||||
$W0 = $parameters['W0'] ?? 0;
|
||||
$H0 = $parameters['H0'] ?? 0;
|
||||
$productType = $parameters['product_type'] ?? 'screen';
|
||||
|
||||
// 업체별 제작사이즈 계산
|
||||
$sizeFormula = $this->getCalculationConfig($company, 'manufacturing_size');
|
||||
if ($sizeFormula) {
|
||||
$calculated = array_merge($calculated, $this->parser->execute($sizeFormula->formula_expression, [
|
||||
'W0' => $W0,
|
||||
'H0' => $H0,
|
||||
'product_type' => $productType
|
||||
]));
|
||||
} else {
|
||||
// 기본 공식 (경동기업 기준)
|
||||
if ($productType === 'screen') {
|
||||
$calculated['W1'] = $W0 + 160;
|
||||
$calculated['H1'] = $H0 + 350;
|
||||
} else {
|
||||
$calculated['W1'] = $W0 + 110;
|
||||
$calculated['H1'] = $H0 + 350;
|
||||
}
|
||||
}
|
||||
|
||||
// 면적 계산
|
||||
$calculated['area'] = ($calculated['W1'] * $calculated['H1']) / 1000000;
|
||||
|
||||
// 중량 계산
|
||||
$weightFormula = $this->getCalculationConfig($company, 'weight_calculation');
|
||||
if ($weightFormula) {
|
||||
$weightResult = $this->parser->execute($weightFormula->formula_expression, array_merge($parameters, $calculated));
|
||||
$calculated['weight'] = $weightResult['weight'] ?? 0;
|
||||
} else {
|
||||
// 기본 중량 공식 (스크린)
|
||||
if ($productType === 'screen') {
|
||||
$calculated['weight'] = ($calculated['area'] * 2) + ($W0 / 1000 * 14.17);
|
||||
} else {
|
||||
$calculated['weight'] = $calculated['area'] * 40;
|
||||
}
|
||||
}
|
||||
|
||||
return $calculated;
|
||||
}
|
||||
|
||||
/**
|
||||
* BOM 아이템별 수량 계산
|
||||
*/
|
||||
protected function calculateBomItems(Collection $bomItems, array $calculatedValues, array $parameters, ?string $companyName): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($bomItems as $item) {
|
||||
if (!$item->is_calculated) {
|
||||
// 고정 수량 아이템
|
||||
$results[] = [
|
||||
'item_id' => $item->id,
|
||||
'ref_type' => $item->ref_type,
|
||||
'ref_id' => $item->ref_id,
|
||||
'original_qty' => $item->qty,
|
||||
'calculated_qty' => $item->qty,
|
||||
'is_calculated' => false,
|
||||
'calculation_formula' => null
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 계산식 적용 아이템
|
||||
try {
|
||||
$allParameters = array_merge($parameters, $calculatedValues);
|
||||
$calculatedQty = $this->parser->execute($item->calculation_formula, $allParameters);
|
||||
|
||||
$results[] = [
|
||||
'item_id' => $item->id,
|
||||
'ref_type' => $item->ref_type,
|
||||
'ref_id' => $item->ref_id,
|
||||
'original_qty' => $item->qty,
|
||||
'calculated_qty' => $calculatedQty['result'] ?? $item->qty,
|
||||
'is_calculated' => true,
|
||||
'calculation_formula' => $item->calculation_formula,
|
||||
'depends_on' => $item->depends_on
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('BOM 아이템 계산 실패', [
|
||||
'item_id' => $item->id,
|
||||
'formula' => $item->calculation_formula,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
// 계산 실패시 원래 수량 사용
|
||||
$results[] = [
|
||||
'item_id' => $item->id,
|
||||
'ref_type' => $item->ref_type,
|
||||
'ref_id' => $item->ref_id,
|
||||
'original_qty' => $item->qty,
|
||||
'calculated_qty' => $item->qty,
|
||||
'is_calculated' => false,
|
||||
'calculation_error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 파라미터 검증
|
||||
*/
|
||||
protected function validateParameters(BomTemplate $bomTemplate, array $parameters): void
|
||||
{
|
||||
$schema = $bomTemplate->calculation_schema;
|
||||
if (!$schema) return;
|
||||
|
||||
$this->validator->validate($schema, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 업체별 산출식 설정 조회
|
||||
*/
|
||||
protected function getCalculationConfig(string $companyName, string $formulaType): ?CalculationConfig
|
||||
{
|
||||
return CalculationConfig::where('company_name', $companyName)
|
||||
->where('formula_type', $formulaType)
|
||||
->where('is_active', true)
|
||||
->latest('version')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 파라미터 스키마 생성
|
||||
*/
|
||||
protected function getDefaultParameterSchema(BomTemplate $bomTemplate): array
|
||||
{
|
||||
return [
|
||||
'required_parameters' => [
|
||||
[
|
||||
'key' => 'W0',
|
||||
'label' => '오픈사이즈 가로(mm)',
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'min' => 500,
|
||||
'max' => 15000
|
||||
],
|
||||
[
|
||||
'key' => 'H0',
|
||||
'label' => '오픈사이즈 세로(mm)',
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'min' => 500,
|
||||
'max' => 5000
|
||||
],
|
||||
[
|
||||
'key' => 'product_type',
|
||||
'label' => '제품타입',
|
||||
'type' => 'select',
|
||||
'options' => ['screen' => '스크린', 'steel' => '철재'],
|
||||
'required' => true
|
||||
],
|
||||
[
|
||||
'key' => 'installation_type',
|
||||
'label' => '설치방식',
|
||||
'type' => 'select',
|
||||
'options' => ['wall' => '벽면형', 'side' => '측면형', 'mixed' => '혼합형'],
|
||||
'required' => false
|
||||
]
|
||||
],
|
||||
'company_type' => $bomTemplate->company_type ?: 'default',
|
||||
'formula_version' => $bomTemplate->formula_version ?: 'v1.0'
|
||||
];
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
218
app/Services/Calculation/ParameterValidator.php
Normal file
218
app/Services/Calculation/ParameterValidator.php
Normal file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Calculation;
|
||||
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class ParameterValidator
|
||||
{
|
||||
/**
|
||||
* 파라미터 검증
|
||||
* @param array $schema 파라미터 스키마
|
||||
* @param array $parameters 검증할 파라미터
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function validate(array $schema, array $parameters): void
|
||||
{
|
||||
$rules = $this->buildValidationRules($schema);
|
||||
$messages = $this->buildValidationMessages($schema);
|
||||
|
||||
$validator = Validator::make($parameters, $rules, $messages);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw new ValidationException($validator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 검증 규칙 생성
|
||||
* @param array $schema 파라미터 스키마
|
||||
* @return array 라라벨 검증 규칙
|
||||
*/
|
||||
protected function buildValidationRules(array $schema): array
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
if (!isset($schema['required_parameters'])) {
|
||||
return $rules;
|
||||
}
|
||||
|
||||
foreach ($schema['required_parameters'] as $param) {
|
||||
$key = $param['key'];
|
||||
$paramRules = [];
|
||||
|
||||
// 필수 여부
|
||||
if ($param['required'] ?? false) {
|
||||
$paramRules[] = 'required';
|
||||
} else {
|
||||
$paramRules[] = 'nullable';
|
||||
}
|
||||
|
||||
// 데이터 타입
|
||||
$type = $param['type'] ?? 'string';
|
||||
switch ($type) {
|
||||
case 'integer':
|
||||
$paramRules[] = 'integer';
|
||||
break;
|
||||
case 'numeric':
|
||||
case 'decimal':
|
||||
$paramRules[] = 'numeric';
|
||||
break;
|
||||
case 'boolean':
|
||||
$paramRules[] = 'boolean';
|
||||
break;
|
||||
case 'select':
|
||||
if (isset($param['options'])) {
|
||||
$validOptions = is_array($param['options']) ? array_keys($param['options']) : $param['options'];
|
||||
$paramRules[] = 'in:' . implode(',', $validOptions);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$paramRules[] = 'string';
|
||||
}
|
||||
|
||||
// 최소값/최대값
|
||||
if (isset($param['min'])) {
|
||||
$paramRules[] = 'min:' . $param['min'];
|
||||
}
|
||||
if (isset($param['max'])) {
|
||||
$paramRules[] = 'max:' . $param['max'];
|
||||
}
|
||||
|
||||
// 정규표현식
|
||||
if (isset($param['pattern'])) {
|
||||
$paramRules[] = 'regex:' . $param['pattern'];
|
||||
}
|
||||
|
||||
$rules[$key] = implode('|', $paramRules);
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 검증 메시지 생성
|
||||
* @param array $schema 파라미터 스키마
|
||||
* @return array 검증 메시지
|
||||
*/
|
||||
protected function buildValidationMessages(array $schema): array
|
||||
{
|
||||
$messages = [];
|
||||
|
||||
if (!isset($schema['required_parameters'])) {
|
||||
return $messages;
|
||||
}
|
||||
|
||||
foreach ($schema['required_parameters'] as $param) {
|
||||
$key = $param['key'];
|
||||
$label = $param['label'] ?? $key;
|
||||
|
||||
$messages["{$key}.required"] = "{$label}은(는) 필수 입력 항목입니다.";
|
||||
$messages["{$key}.integer"] = "{$label}은(는) 정수여야 합니다.";
|
||||
$messages["{$key}.numeric"] = "{$label}은(는) 숫자여야 합니다.";
|
||||
$messages["{$key}.boolean"] = "{$label}은(는) 참/거짓 값이어야 합니다.";
|
||||
$messages["{$key}.string"] = "{$label}은(는) 문자열이어야 합니다.";
|
||||
|
||||
if (isset($param['min'])) {
|
||||
$messages["{$key}.min"] = "{$label}은(는) 최소 {$param['min']} 이상이어야 합니다.";
|
||||
}
|
||||
if (isset($param['max'])) {
|
||||
$messages["{$key}.max"] = "{$label}은(는) 최대 {$param['max']} 이하여야 합니다.";
|
||||
}
|
||||
|
||||
if (isset($param['options'])) {
|
||||
$validOptions = is_array($param['options']) ? array_values($param['options']) : $param['options'];
|
||||
$messages["{$key}.in"] = "{$label}은(는) 다음 중 하나여야 합니다: " . implode(', ', $validOptions);
|
||||
}
|
||||
|
||||
if (isset($param['pattern'])) {
|
||||
$messages["{$key}.regex"] = "{$label}의 형식이 올바르지 않습니다.";
|
||||
}
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 범위 검증
|
||||
* @param array $parameters 파라미터
|
||||
* @param array $ranges 범위 정의
|
||||
*/
|
||||
public function validateRanges(array $parameters, array $ranges): void
|
||||
{
|
||||
foreach ($ranges as $key => $range) {
|
||||
if (!isset($parameters[$key])) continue;
|
||||
|
||||
$value = $parameters[$key];
|
||||
|
||||
if (isset($range['min']) && $value < $range['min']) {
|
||||
throw new ValidationException(validator([], []), [
|
||||
$key => ["{$key}는 최소 {$range['min']} 이상이어야 합니다."]
|
||||
]);
|
||||
}
|
||||
|
||||
if (isset($range['max']) && $value > $range['max']) {
|
||||
throw new ValidationException(validator([], []), [
|
||||
$key => ["{$key}는 최대 {$range['max']} 이하여야 합니다."]
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 업체별 특수 검증
|
||||
* @param array $parameters 파라미터
|
||||
* @param string $companyType 업체 타입
|
||||
*/
|
||||
public function validateCompanySpecific(array $parameters, string $companyType): void
|
||||
{
|
||||
switch ($companyType) {
|
||||
case '경동기업':
|
||||
$this->validateKyungdongRules($parameters);
|
||||
break;
|
||||
case '삼성물산':
|
||||
$this->validateSamsungRules($parameters);
|
||||
break;
|
||||
default:
|
||||
// 기본 검증만 수행
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 경동기업 특수 검증 규칙
|
||||
*/
|
||||
protected function validateKyungdongRules(array $parameters): void
|
||||
{
|
||||
// 경동기업 특수 규칙 예시
|
||||
if (isset($parameters['W0']) && isset($parameters['H0'])) {
|
||||
$area = ($parameters['W0'] * $parameters['H0']) / 1000000;
|
||||
|
||||
// 면적이 너무 크면 경고
|
||||
if ($area > 50) {
|
||||
throw new ValidationException(validator([], []), [
|
||||
'area' => ['면적이 50㎡를 초과합니다. 특수 산출식이 필요할 수 있습니다.']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 제품타입별 사이즈 제한
|
||||
if ($parameters['product_type'] === 'screen' && isset($parameters['W0'])) {
|
||||
if ($parameters['W0'] > 12000) {
|
||||
throw new ValidationException(validator([], []), [
|
||||
'W0' => ['스크린 제품은 가로 12,000mm를 초과할 수 없습니다.']
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 삼성물산 특수 검증 규칙
|
||||
*/
|
||||
protected function validateSamsungRules(array $parameters): void
|
||||
{
|
||||
// 삼성물산 특수 규칙 (예시)
|
||||
// 실제로는 해당 업체의 요구사항에 따라 구현
|
||||
}
|
||||
}
|
||||
265
app/Services/Design/BomCalculationService.php
Normal file
265
app/Services/Design/BomCalculationService.php
Normal file
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Design;
|
||||
|
||||
use App\Services\Service;
|
||||
use App\Services\Calculation\CalculationEngine;
|
||||
use App\Models\Design\BomTemplate;
|
||||
use App\Models\Calculation\CalculationConfig;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class BomCalculationService extends Service
|
||||
{
|
||||
protected CalculationEngine $calculationEngine;
|
||||
|
||||
public function __construct(CalculationEngine $calculationEngine)
|
||||
{
|
||||
$this->calculationEngine = $calculationEngine;
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 파라미터 조회
|
||||
* @param int $modelId 모델 ID
|
||||
* @param string|null $companyName 업체명
|
||||
* @return array
|
||||
*/
|
||||
public function getEstimateParameters(int $modelId, ?string $companyName = null): array
|
||||
{
|
||||
try {
|
||||
// 모델의 기본 BOM 템플릿 조회
|
||||
$bomTemplate = BomTemplate::with(['modelVersion.model'])
|
||||
->where('tenant_id', $this->tenantId())
|
||||
->whereHas('modelVersion', function ($query) use ($modelId) {
|
||||
$query->whereHas('model', function ($q) use ($modelId) {
|
||||
$q->where('id', $modelId);
|
||||
});
|
||||
})
|
||||
->where('is_primary', true)
|
||||
->first();
|
||||
|
||||
if (!$bomTemplate) {
|
||||
throw new \Exception("모델 ID {$modelId}에 대한 BOM 템플릿을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
// 파라미터 스키마 추출
|
||||
$parameters = $this->calculationEngine->getRequiredParameters($bomTemplate->id);
|
||||
|
||||
// 업체별 추가 파라미터가 있는지 확인
|
||||
if ($companyName) {
|
||||
$companyParameters = $this->getCompanySpecificParameters($companyName);
|
||||
if ($companyParameters) {
|
||||
$parameters = array_merge_recursive($parameters, $companyParameters);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'model_info' => [
|
||||
'id' => $bomTemplate->modelVersion->model->id,
|
||||
'name' => $bomTemplate->modelVersion->model->name ?? '모델명 없음',
|
||||
'version' => $bomTemplate->modelVersion->version_no ?? 'v1.0',
|
||||
'bom_template_id' => $bomTemplate->id
|
||||
],
|
||||
'company_info' => [
|
||||
'company_type' => $bomTemplate->company_type,
|
||||
'formula_version' => $bomTemplate->formula_version,
|
||||
'requested_company' => $companyName
|
||||
],
|
||||
'parameters' => $parameters,
|
||||
'calculation_preview' => true
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('견적 파라미터 조회 실패', [
|
||||
'model_id' => $modelId,
|
||||
'company_name' => $companyName,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BOM 계산 실행
|
||||
* @param int $bomTemplateId BOM 템플릿 ID
|
||||
* @param array $parameters 입력 파라미터
|
||||
* @param string|null $companyName 업체명
|
||||
* @return array
|
||||
*/
|
||||
public function calculateBomEstimate(int $bomTemplateId, array $parameters, ?string $companyName = null): array
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// BOM 템플릿 조회 및 권한 확인
|
||||
$bomTemplate = BomTemplate::where('tenant_id', $this->tenantId())
|
||||
->findOrFail($bomTemplateId);
|
||||
|
||||
// 업체명이 지정된 경우 BOM 템플릿 업데이트
|
||||
if ($companyName && $bomTemplate->company_type !== $companyName) {
|
||||
$bomTemplate->update(['company_type' => $companyName]);
|
||||
}
|
||||
|
||||
// 계산 엔진 실행
|
||||
$result = $this->calculationEngine->calculateBOM($bomTemplateId, $parameters, $companyName);
|
||||
|
||||
// 계산 이력 로그 저장 (선택사항)
|
||||
$this->logCalculationHistory($bomTemplateId, $parameters, $result, $companyName);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return [
|
||||
'success' => $result['success'],
|
||||
'data' => $result['success'] ? $result : null,
|
||||
'error' => $result['success'] ? null : $result['error']
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::error('BOM 계산 실행 실패', [
|
||||
'bom_template_id' => $bomTemplateId,
|
||||
'parameters' => $parameters,
|
||||
'company_name' => $companyName,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'data' => null,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 업체별 산출식 목록 조회
|
||||
* @param string $companyName 업체명
|
||||
* @return array
|
||||
*/
|
||||
public function getCompanyFormulas(string $companyName): array
|
||||
{
|
||||
$formulas = CalculationConfig::getActiveFormulasForCompany($this->tenantId(), $companyName);
|
||||
|
||||
return [
|
||||
'company_name' => $companyName,
|
||||
'total_formulas' => count($formulas),
|
||||
'formulas' => array_map(function ($formula) {
|
||||
return [
|
||||
'type' => $formula['formula_type'],
|
||||
'version' => $formula['version'],
|
||||
'description' => $formula['description'],
|
||||
'parameters' => $formula['parameters'],
|
||||
'updated_at' => $formula['updated_at']
|
||||
];
|
||||
}, $formulas)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 업체별 산출식 등록/수정
|
||||
* @param string $companyName 업체명
|
||||
* @param string $formulaType 산출식 타입
|
||||
* @param array $formulaData 산출식 데이터
|
||||
* @return array
|
||||
*/
|
||||
public function saveCompanyFormula(string $companyName, string $formulaType, array $formulaData): array
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// 기존 산출식이 있는지 확인
|
||||
$existingFormula = CalculationConfig::getLatestFormula($this->tenantId(), $companyName, $formulaType);
|
||||
|
||||
if ($existingFormula) {
|
||||
// 버전 업그레이드
|
||||
$newVersion = $this->incrementVersion($existingFormula->version);
|
||||
$existingFormula->update(['is_active' => false]); // 기존 비활성화
|
||||
} else {
|
||||
$newVersion = 'v1.0';
|
||||
}
|
||||
|
||||
// 새 산출식 생성
|
||||
$formula = CalculationConfig::create([
|
||||
'tenant_id' => $this->tenantId(),
|
||||
'company_name' => $companyName,
|
||||
'formula_type' => $formulaType,
|
||||
'version' => $newVersion,
|
||||
'formula_expression' => $formulaData['formula_expression'],
|
||||
'parameters' => $formulaData['parameters'],
|
||||
'conditions' => $formulaData['conditions'] ?? null,
|
||||
'validation_rules' => $formulaData['validation_rules'] ?? null,
|
||||
'description' => $formulaData['description'] ?? null,
|
||||
'is_active' => true,
|
||||
'created_by' => $this->apiUserId()
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return [
|
||||
'id' => $formula->id,
|
||||
'company_name' => $formula->company_name,
|
||||
'formula_type' => $formula->formula_type,
|
||||
'version' => $formula->version,
|
||||
'is_active' => $formula->is_active,
|
||||
'created_at' => $formula->created_at
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::error('업체별 산출식 저장 실패', [
|
||||
'company_name' => $companyName,
|
||||
'formula_type' => $formulaType,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 업체별 추가 파라미터 조회
|
||||
*/
|
||||
protected function getCompanySpecificParameters(string $companyName): ?array
|
||||
{
|
||||
// 업체별 특수 파라미터가 있는지 확인
|
||||
$companyConfig = CalculationConfig::forTenant($this->tenantId())
|
||||
->forCompany($companyName)
|
||||
->forType('additional_parameters')
|
||||
->active()
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
return $companyConfig ? $companyConfig->parameters : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 계산 이력 로그 저장
|
||||
*/
|
||||
protected function logCalculationHistory(int $bomTemplateId, array $parameters, array $result, ?string $companyName): void
|
||||
{
|
||||
// 필요에 따라 계산 이력을 별도 테이블에 저장
|
||||
Log::info('BOM 계산 완료', [
|
||||
'bom_template_id' => $bomTemplateId,
|
||||
'company_name' => $companyName,
|
||||
'parameters' => $parameters,
|
||||
'success' => $result['success'],
|
||||
'calculated_items_count' => $result['success'] ? count($result['bom_items']) : 0,
|
||||
'calculated_by' => $this->apiUserId()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 버전 번호 증가
|
||||
*/
|
||||
protected function incrementVersion(string $currentVersion): string
|
||||
{
|
||||
if (preg_match('/^v(\d+)\.(\d+)$/', $currentVersion, $matches)) {
|
||||
$major = (int)$matches[1];
|
||||
$minor = (int)$matches[2];
|
||||
return "v{$major}." . ($minor + 1);
|
||||
}
|
||||
|
||||
return 'v1.0';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
/**
|
||||
* BOM 테이블에 산출식 관련 필드 추가
|
||||
* - bom_templates: 견적시 필요한 파라미터 스키마 정의
|
||||
* - bom_template_items: 개별 아이템의 계산식 정의
|
||||
*/
|
||||
public function up(): void {
|
||||
// BOM 템플릿에 산출식 조건 스키마 추가
|
||||
Schema::table('bom_templates', function (Blueprint $table) {
|
||||
$table->json('calculation_schema')->nullable()->comment('견적 파라미터 스키마 (JSON)');
|
||||
$table->string('company_type', 50)->default('default')->comment('업체 타입 (경동기업, 삼성물산 등)');
|
||||
$table->string('formula_version', 10)->default('v1.0')->comment('산출식 버전');
|
||||
});
|
||||
|
||||
// BOM 아이템에 계산식 필드 추가
|
||||
Schema::table('bom_template_items', function (Blueprint $table) {
|
||||
$table->boolean('is_calculated')->default(false)->comment('계산식 적용 여부');
|
||||
$table->text('calculation_formula')->nullable()->comment('계산식 표현식');
|
||||
$table->json('depends_on')->nullable()->comment('의존하는 파라미터 목록 (JSON)');
|
||||
$table->json('calculation_config')->nullable()->comment('계산 설정 (JSON)');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void {
|
||||
Schema::table('bom_template_items', function (Blueprint $table) {
|
||||
$table->dropColumn(['is_calculated', 'calculation_formula', 'depends_on', 'calculation_config']);
|
||||
});
|
||||
|
||||
Schema::table('bom_templates', function (Blueprint $table) {
|
||||
$table->dropColumn(['calculation_schema', 'company_type', 'formula_version']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
/**
|
||||
* 업체별 산출식 설정 테이블 생성
|
||||
* 다양한 업체(경동기업, 삼성물산 등)의 고유한 산출식을 관리
|
||||
*/
|
||||
public function up(): void {
|
||||
Schema::create('calculation_configs', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트ID');
|
||||
$table->string('company_name', 100)->comment('업체명 (경동기업, 삼성물산 등)');
|
||||
$table->string('formula_type', 100)->comment('산출식 타입 (screen_weight, motor_capacity 등)');
|
||||
$table->string('version', 10)->default('v1.0')->comment('산출식 버전');
|
||||
$table->text('formula_expression')->comment('실제 계산식 표현식');
|
||||
$table->json('parameters')->comment('필요한 입력 파라미터 정의');
|
||||
$table->json('conditions')->nullable()->comment('적용 조건 (제품타입, 사이즈 범위 등)');
|
||||
$table->json('validation_rules')->nullable()->comment('파라미터 검증 규칙');
|
||||
$table->text('description')->nullable()->comment('산출식 설명');
|
||||
$table->boolean('is_active')->default(true)->comment('활성 상태');
|
||||
$table->timestamps();
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
|
||||
$table->unique(['tenant_id', 'company_name', 'formula_type', 'version'], 'uq_calc_config_unique');
|
||||
$table->index(['tenant_id', 'company_name', 'is_active'], 'idx_calc_config_company');
|
||||
$table->index(['tenant_id', 'formula_type', 'is_active'], 'idx_calc_config_type');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void {
|
||||
Schema::dropIfExists('calculation_configs');
|
||||
}
|
||||
};
|
||||
@@ -35,6 +35,7 @@
|
||||
use App\Http\Controllers\Api\V1\Design\ModelVersionController as DesignModelVersionController;
|
||||
use App\Http\Controllers\Api\V1\Design\BomTemplateController as DesignBomTemplateController;
|
||||
use App\Http\Controllers\Api\V1\Design\AuditLogController as DesignAuditLogController;
|
||||
use App\Http\Controllers\Api\V1\Design\BomCalculationController;
|
||||
|
||||
// error test
|
||||
Route::get('/test-error', function () {
|
||||
@@ -360,6 +361,13 @@
|
||||
|
||||
// 감사 로그 조회
|
||||
Route::get ('/audit-logs', [DesignAuditLogController::class, 'index'])->name('v1.design.audit-logs.index');
|
||||
|
||||
// BOM 계산 시스템
|
||||
Route::get ('/models/{modelId}/estimate-parameters', [BomCalculationController::class, 'getEstimateParameters'])->name('v1.design.models.estimate-parameters');
|
||||
Route::post ('/bom-templates/{bomTemplateId}/calculate-bom', [BomCalculationController::class, 'calculateBom'])->name('v1.design.bom-templates.calculate-bom');
|
||||
Route::get ('/companies/{companyName}/formulas', [BomCalculationController::class, 'getCompanyFormulas'])->name('v1.design.companies.formulas');
|
||||
Route::post ('/companies/{companyName}/formulas/{formulaType}', [BomCalculationController::class, 'saveCompanyFormula'])->name('v1.design.companies.formulas.save');
|
||||
Route::post ('/formulas/test', [BomCalculationController::class, 'testFormula'])->name('v1.design.formulas.test');
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user