feat(MNG): 견적 공식 시뮬레이터 UI 개선
- FormulaEvaluatorService: 공식 평가 로직 개선 - simulator.blade.php: 시뮬레이터 UI/UX 개선 - 입력 필드 레이아웃 최적화 - 계산 결과 표시 개선 - 에러 처리 강화 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -795,10 +795,17 @@ public function calculateBomWithDebug(
|
||||
$tenantId = $tenantId ?? session('selected_tenant_id');
|
||||
$this->currentTenantId = $tenantId;
|
||||
|
||||
// Step 1: 입력값 수집
|
||||
// Step 1: 입력값 수집 (React 동기화 변수 포함)
|
||||
$this->addDebugStep(1, '입력값수집', [
|
||||
'W0' => $inputVariables['W0'] ?? null,
|
||||
'H0' => $inputVariables['H0'] ?? null,
|
||||
'QTY' => $inputVariables['QTY'] ?? 1,
|
||||
'PC' => $inputVariables['PC'] ?? '',
|
||||
'GT' => $inputVariables['GT'] ?? 'wall',
|
||||
'MP' => $inputVariables['MP'] ?? 'single',
|
||||
'CT' => $inputVariables['CT'] ?? 'basic',
|
||||
'WS' => $inputVariables['WS'] ?? 50,
|
||||
'INSP' => $inputVariables['INSP'] ?? 50000,
|
||||
'finished_goods' => $finishedGoodsCode,
|
||||
]);
|
||||
|
||||
|
||||
@@ -85,31 +85,78 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-medium
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-3">완제품 BOM 시뮬레이션</h3>
|
||||
<form id="bomSimulatorForm">
|
||||
<div class="flex flex-wrap gap-3 items-end">
|
||||
<!-- 완제품 코드 선택 -->
|
||||
<!-- 제품 카테고리 (PC) -->
|
||||
<div class="w-28">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">카테고리</label>
|
||||
<select id="productCategory" name="PC" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">전체</option>
|
||||
<option value="SCREEN">스크린</option>
|
||||
<option value="STEEL">철재</option>
|
||||
<option value="BENDING">절곡</option>
|
||||
<option value="ALUMINUM">알루미늄</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 제품명 (완제품 FG) -->
|
||||
<div class="w-48">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">완제품 (FG)</label>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">제품명</label>
|
||||
<select id="fgCodeSelect" name="finished_goods_code" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">완제품 선택...</option>
|
||||
<option value="">선택...</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 폭 (W0) -->
|
||||
<div class="w-24">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">폭 W0 (mm)</label>
|
||||
<div class="w-20">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">W0</label>
|
||||
<input type="number" name="W0" value="2000" min="100" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<!-- 높이 (H0) -->
|
||||
<div class="w-24">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">높이 H0 (mm)</label>
|
||||
<input type="number" name="H0" value="3000" min="100" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<!-- 수량 -->
|
||||
<div class="w-20">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">H0</label>
|
||||
<input type="number" name="H0" value="2500" min="100" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<!-- 가이드레일 설치유형 (GT) -->
|
||||
<div class="w-28">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">가이드레일</label>
|
||||
<select id="guideRailType" name="GT" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="wall">벽부착</option>
|
||||
<option value="ceiling">천장매립</option>
|
||||
<option value="floor">바닥매립</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 모터 전원 (MP) -->
|
||||
<div class="w-24">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">모터전원</label>
|
||||
<select id="motorPower" name="MP" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="single">단상</option>
|
||||
<option value="three">삼상</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 연동제어기 (CT) -->
|
||||
<div class="w-24">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">제어기</label>
|
||||
<select id="controller" name="CT" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="basic">기본</option>
|
||||
<option value="smart">스마트</option>
|
||||
<option value="premium">프리미엄</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 마구리 날개치수 (WS) -->
|
||||
<div class="w-20">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">마구리</label>
|
||||
<input type="number" id="wingSize" name="WS" value="50" min="0" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<!-- 검사비 (INSP) -->
|
||||
<div class="w-24">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">검사비</label>
|
||||
<input type="number" id="inspectionFee" name="INSP" value="50000" min="0" step="1000" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<!-- 수량 (QTY) -->
|
||||
<div class="w-16">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">수량</label>
|
||||
<input type="number" name="QTY" value="1" min="1" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<!-- 실행 버튼 -->
|
||||
<button type="submit" id="runBomButton" class="bg-green-600 hover:bg-green-700 text-white px-4 py-1.5 rounded font-medium text-sm transition-colors flex items-center gap-2">
|
||||
<span>BOM 계산</span>
|
||||
<span>실행</span>
|
||||
<svg id="bomSpinner" class="hidden animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
@@ -987,15 +1034,23 @@ function switchMode(mode) {
|
||||
modeFormula.classList.remove('text-blue-600', 'border-b-2', 'border-blue-600', 'bg-blue-50');
|
||||
modeFormula.classList.add('text-gray-500');
|
||||
|
||||
// FG 목록 로드
|
||||
// FG 목록 로드 및 카테고리 필터 설정
|
||||
loadFinishedGoods();
|
||||
setupCategoryFilter();
|
||||
}
|
||||
}
|
||||
|
||||
// 완제품 목록 저장 (카테고리 필터링용)
|
||||
let allFinishedGoods = [];
|
||||
|
||||
// 완제품 목록 로드
|
||||
async function loadFinishedGoods() {
|
||||
const select = document.getElementById('fgCodeSelect');
|
||||
if (select.options.length > 1) return; // 이미 로드됨
|
||||
if (allFinishedGoods.length > 0) {
|
||||
// 이미 로드됨 - 필터링만 적용
|
||||
filterFinishedGoods();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/quote-formulas/formulas/items?item_type=FG', {
|
||||
@@ -1004,18 +1059,49 @@ function switchMode(mode) {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.data && result.data.items) {
|
||||
result.data.items.forEach(item => {
|
||||
const option = document.createElement('option');
|
||||
option.value = item.code;
|
||||
option.textContent = `${item.code} - ${item.name}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
allFinishedGoods = result.data.items;
|
||||
filterFinishedGoods();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('완제품 목록 로드 실패:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 카테고리별 완제품 필터링
|
||||
function filterFinishedGoods() {
|
||||
const select = document.getElementById('fgCodeSelect');
|
||||
const categorySelect = document.getElementById('productCategory');
|
||||
const selectedCategory = categorySelect ? categorySelect.value : '';
|
||||
|
||||
// 기존 옵션 초기화
|
||||
select.innerHTML = '<option value="">제품을 선택하세요...</option>';
|
||||
|
||||
// 필터링
|
||||
const filtered = selectedCategory
|
||||
? allFinishedGoods.filter(item => {
|
||||
const itemCategory = (item.item_category || '').toUpperCase();
|
||||
return itemCategory === selectedCategory.toUpperCase();
|
||||
})
|
||||
: allFinishedGoods;
|
||||
|
||||
// 옵션 추가
|
||||
filtered.forEach(item => {
|
||||
const option = document.createElement('option');
|
||||
option.value = item.code;
|
||||
option.dataset.category = item.item_category || '';
|
||||
option.textContent = item.name; // 제품명만 표시
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// 카테고리 변경 이벤트
|
||||
function setupCategoryFilter() {
|
||||
const categorySelect = document.getElementById('productCategory');
|
||||
if (categorySelect) {
|
||||
categorySelect.addEventListener('change', filterFinishedGoods);
|
||||
}
|
||||
}
|
||||
|
||||
// BOM 시뮬레이션 폼 제출
|
||||
document.getElementById('bomSimulatorForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
@@ -1030,8 +1116,15 @@ function switchMode(mode) {
|
||||
|
||||
const inputVars = {
|
||||
W0: parseFloat(formData.get('W0')) || 2000,
|
||||
H0: parseFloat(formData.get('H0')) || 3000,
|
||||
QTY: parseInt(formData.get('QTY')) || 1
|
||||
H0: parseFloat(formData.get('H0')) || 2500,
|
||||
QTY: parseInt(formData.get('QTY')) || 1,
|
||||
// 새 변수들 (React 동기화)
|
||||
PC: formData.get('PC') || '',
|
||||
GT: formData.get('GT') || 'wall',
|
||||
MP: formData.get('MP') || 'single',
|
||||
CT: formData.get('CT') || 'basic',
|
||||
WS: parseInt(formData.get('WS')) || 50,
|
||||
INSP: parseInt(formData.get('INSP')) || 50000
|
||||
};
|
||||
|
||||
// UI 상태 변경
|
||||
|
||||
Reference in New Issue
Block a user