feat(simulator): Phase 4 - BOM 디버깅 UI 및 API 추가
- simulateBom API 엔드포인트 추가 (calculateBomWithDebug 연동) - simulator.blade.php: BOM 디버깅 모드 탭 추가 - 10단계 디버그 스텝 패널 - 공정별 품목 그룹화 표시 - 원가 요약 (공정별 소계, 총합계) - FormulaEvaluatorService: currentTenantId 속성 추가 - 콘솔/API에서 tenant_id 전달 가능하도록 수정 - routes/api.php: simulate-bom 라우트 추가
This commit is contained in:
@@ -317,6 +317,47 @@ public function duplicate(int $id): JsonResponse
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* BOM 기반 시뮬레이션 (디버깅 포함)
|
||||
*
|
||||
* Design 시뮬레이터와 동일한 계산 로직을 사용하여
|
||||
* 10단계 디버깅 정보, 공정별 그룹화, 카테고리 가격 계산을 포함합니다.
|
||||
*/
|
||||
public function simulateBom(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'finished_goods_code' => 'required|string|max:50',
|
||||
'input_variables' => 'required|array',
|
||||
'input_variables.W0' => 'required|numeric|min:1',
|
||||
'input_variables.H0' => 'required|numeric|min:1',
|
||||
]);
|
||||
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
if (! $tenantId) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '테넌트를 선택해주세요.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
// 디버그 모드 활성화
|
||||
$this->evaluatorService->enableDebugMode();
|
||||
|
||||
// BOM 기반 계산 (10단계 디버깅 포함)
|
||||
$result = $this->evaluatorService->calculateBomWithDebug(
|
||||
$validated['finished_goods_code'],
|
||||
$validated['input_variables'],
|
||||
$tenantId
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => $result['success'] ?? false,
|
||||
'data' => $result,
|
||||
'message' => $result['error'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 품목 목록 (시뮬레이터용)
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,8 @@ class FormulaEvaluatorService
|
||||
|
||||
private bool $debugMode = false;
|
||||
|
||||
private ?int $currentTenantId = null;
|
||||
|
||||
/**
|
||||
* 수식 검증
|
||||
*/
|
||||
@@ -377,7 +379,7 @@ private function evaluateCondition(string $condition): bool
|
||||
|
||||
private function getItemPrice(string $itemCode): float
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
$tenantId = $this->currentTenantId ?? session('selected_tenant_id');
|
||||
|
||||
if (! $tenantId) {
|
||||
$this->errors[] = '테넌트 ID가 설정되지 않았습니다.';
|
||||
@@ -438,7 +440,7 @@ public function resetVariables(): void
|
||||
*/
|
||||
public function getItemDetails(string $itemCode): ?array
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
$tenantId = $this->currentTenantId ?? session('selected_tenant_id');
|
||||
|
||||
if (! $tenantId) {
|
||||
return null;
|
||||
@@ -546,7 +548,7 @@ public function getItemTypeLabel(string $itemType): string
|
||||
*/
|
||||
public function enrichItemsWithDetails(array $items): array
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
$tenantId = $this->currentTenantId ?? session('selected_tenant_id');
|
||||
|
||||
if (! $tenantId) {
|
||||
return $items;
|
||||
@@ -789,6 +791,7 @@ public function calculateBomWithDebug(
|
||||
): array {
|
||||
$this->enableDebugMode(true);
|
||||
$tenantId = $tenantId ?? session('selected_tenant_id');
|
||||
$this->currentTenantId = $tenantId;
|
||||
|
||||
// Step 1: 입력값 수집
|
||||
$this->addDebugStep(1, '입력값수집', [
|
||||
|
||||
@@ -66,6 +66,95 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-medium
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 모드 선택 탭 -->
|
||||
<div class="bg-white rounded-lg shadow-sm mb-4">
|
||||
<div class="flex border-b">
|
||||
<button type="button" id="modeFormula" class="flex-1 px-4 py-3 text-sm font-medium text-blue-600 border-b-2 border-blue-600 bg-blue-50 mode-tab" data-mode="formula">
|
||||
📊 수식 시뮬레이션
|
||||
</button>
|
||||
<button type="button" id="modeBom" class="flex-1 px-4 py-3 text-sm font-medium text-gray-500 hover:text-gray-700 mode-tab" data-mode="bom">
|
||||
🔧 BOM 디버깅
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BOM 시뮬레이션 섹션 (숨김 기본) -->
|
||||
<div id="bomSection" class="hidden">
|
||||
<!-- BOM 입력 영역 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 mb-4">
|
||||
<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">
|
||||
<!-- 완제품 코드 선택 -->
|
||||
<div class="w-48">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">완제품 (FG)</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>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 폭 (W0) -->
|
||||
<div class="w-24">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">폭 W0 (mm)</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">수량</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>
|
||||
<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>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- BOM 결과 영역 -->
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-4">
|
||||
<!-- 디버깅 패널 (10단계) -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
||||
🔍 계산 과정 (10단계)
|
||||
</h3>
|
||||
<div id="debugSteps" class="space-y-2 text-xs max-h-[60vh] overflow-y-auto">
|
||||
<p class="text-gray-400 text-center py-4">BOM 계산을 실행하세요</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 공정별 품목 그룹 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
||||
🏭 공정별 품목
|
||||
</h3>
|
||||
<div id="processGroups" class="space-y-3 max-h-[60vh] overflow-y-auto">
|
||||
<p class="text-gray-400 text-center py-4 text-xs">BOM 계산을 실행하세요</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 소계 및 합계 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
||||
💰 원가 요약
|
||||
</h3>
|
||||
<div id="costSummary" class="space-y-2 text-sm">
|
||||
<p class="text-gray-400 text-center py-4 text-xs">BOM 계산을 실행하세요</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 수식 시뮬레이션 섹션 -->
|
||||
<div id="formulaSection">
|
||||
<!-- 결과 영역 (2열 그리드) -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<!-- 좌측: 실행 결과 (계산된 변수) -->
|
||||
@@ -165,6 +254,7 @@ class="flex-1 px-3 py-1.5 text-sm border border-gray-300 rounded focus:outline-n
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- /formulaSection -->
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -861,7 +951,310 @@ function renderBomTree(children, parentQty = 1, depth = 0) {
|
||||
`;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// BOM 시뮬레이션 기능
|
||||
// =========================================================================
|
||||
|
||||
// 모드 전환 설정
|
||||
function setupModeTabs() {
|
||||
document.querySelectorAll('.mode-tab').forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
const mode = this.dataset.mode;
|
||||
switchMode(mode);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 모드 전환
|
||||
function switchMode(mode) {
|
||||
const formulaSection = document.getElementById('formulaSection');
|
||||
const bomSection = document.getElementById('bomSection');
|
||||
const modeFormula = document.getElementById('modeFormula');
|
||||
const modeBom = document.getElementById('modeBom');
|
||||
|
||||
if (mode === 'formula') {
|
||||
formulaSection.classList.remove('hidden');
|
||||
bomSection.classList.add('hidden');
|
||||
modeFormula.classList.add('text-blue-600', 'border-b-2', 'border-blue-600', 'bg-blue-50');
|
||||
modeFormula.classList.remove('text-gray-500');
|
||||
modeBom.classList.remove('text-green-600', 'border-b-2', 'border-green-600', 'bg-green-50');
|
||||
modeBom.classList.add('text-gray-500');
|
||||
} else {
|
||||
formulaSection.classList.add('hidden');
|
||||
bomSection.classList.remove('hidden');
|
||||
modeBom.classList.add('text-green-600', 'border-b-2', 'border-green-600', 'bg-green-50');
|
||||
modeBom.classList.remove('text-gray-500');
|
||||
modeFormula.classList.remove('text-blue-600', 'border-b-2', 'border-blue-600', 'bg-blue-50');
|
||||
modeFormula.classList.add('text-gray-500');
|
||||
|
||||
// FG 목록 로드
|
||||
loadFinishedGoods();
|
||||
}
|
||||
}
|
||||
|
||||
// 완제품 목록 로드
|
||||
async function loadFinishedGoods() {
|
||||
const select = document.getElementById('fgCodeSelect');
|
||||
if (select.options.length > 1) return; // 이미 로드됨
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/quote-formulas/formulas/items?item_type=FG', {
|
||||
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
||||
});
|
||||
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);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('완제품 목록 로드 실패:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// BOM 시뮬레이션 폼 제출
|
||||
document.getElementById('bomSimulatorForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const fgCode = formData.get('finished_goods_code');
|
||||
|
||||
if (!fgCode) {
|
||||
alert('완제품을 선택하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const inputVars = {
|
||||
W0: parseFloat(formData.get('W0')) || 2000,
|
||||
H0: parseFloat(formData.get('H0')) || 3000,
|
||||
QTY: parseInt(formData.get('QTY')) || 1
|
||||
};
|
||||
|
||||
// UI 상태 변경
|
||||
document.getElementById('runBomButton').disabled = true;
|
||||
document.getElementById('bomSpinner').classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/quote-formulas/formulas/simulate-bom', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
finished_goods_code: fgCode,
|
||||
input_variables: inputVars
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
renderBomResults(result.data);
|
||||
} else {
|
||||
alert(result.message || 'BOM 계산에 실패했습니다.');
|
||||
resetBomResults();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('BOM 계산 오류:', err);
|
||||
alert('서버 오류가 발생했습니다.');
|
||||
resetBomResults();
|
||||
} finally {
|
||||
document.getElementById('runBomButton').disabled = false;
|
||||
document.getElementById('bomSpinner').classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// BOM 결과 초기화
|
||||
function resetBomResults() {
|
||||
document.getElementById('debugSteps').innerHTML = '<p class="text-gray-400 text-center py-4">BOM 계산을 실행하세요</p>';
|
||||
document.getElementById('processGroups').innerHTML = '<p class="text-gray-400 text-center py-4 text-xs">BOM 계산을 실행하세요</p>';
|
||||
document.getElementById('costSummary').innerHTML = '<p class="text-gray-400 text-center py-4 text-xs">BOM 계산을 실행하세요</p>';
|
||||
}
|
||||
|
||||
// BOM 결과 렌더링
|
||||
function renderBomResults(data) {
|
||||
renderDebugSteps(data.debug_steps || []);
|
||||
renderProcessGroups(data.grouped_items || {});
|
||||
renderCostSummary(data.subtotals || {}, data.grand_total || 0, data.finished_goods || {});
|
||||
}
|
||||
|
||||
// 디버그 스텝 아이콘
|
||||
const stepIcons = {
|
||||
1: '📥', 2: '🔢', 3: '📦', 4: '🌳', 5: '💵',
|
||||
6: '✖️', 7: '💰', 8: '🏭', 9: '📊', 10: '🎯'
|
||||
};
|
||||
|
||||
// 디버깅 패널 렌더링
|
||||
function renderDebugSteps(steps) {
|
||||
const container = document.getElementById('debugSteps');
|
||||
|
||||
if (!steps || steps.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-400 text-center py-4">디버깅 정보가 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = steps.map(step => {
|
||||
const icon = stepIcons[step.step] || '•';
|
||||
const statusColor = step.status === 'success' ? 'text-green-600' : step.status === 'error' ? 'text-red-600' : 'text-blue-600';
|
||||
const bgColor = step.status === 'success' ? 'bg-green-50 border-green-200' : step.status === 'error' ? 'bg-red-50 border-red-200' : 'bg-blue-50 border-blue-200';
|
||||
|
||||
// 상세 정보 렌더링
|
||||
let detailHtml = '';
|
||||
if (step.details) {
|
||||
if (typeof step.details === 'object') {
|
||||
const entries = Object.entries(step.details);
|
||||
if (entries.length > 0) {
|
||||
detailHtml = `
|
||||
<div class="mt-1 pl-6 text-[10px] text-gray-500">
|
||||
${entries.slice(0, 5).map(([k, v]) => {
|
||||
const val = typeof v === 'object' ? JSON.stringify(v) : v;
|
||||
return `<div><span class="text-gray-400">${k}:</span> ${val}</div>`;
|
||||
}).join('')}
|
||||
${entries.length > 5 ? `<div class="text-gray-400">... +${entries.length - 5}개</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
detailHtml = `<div class="mt-1 pl-6 text-[10px] text-gray-500">${step.details}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="p-2 rounded border ${bgColor}">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="text-sm">${icon}</span>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium ${statusColor}">Step ${step.step}</span>
|
||||
<span class="text-gray-600">${step.name || ''}</span>
|
||||
</div>
|
||||
<div class="text-gray-500 mt-0.5">${step.message || ''}</div>
|
||||
${detailHtml}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 공정명 한글화
|
||||
const processLabels = {
|
||||
'screen': '🎬 스크린 공정',
|
||||
'bending': '🔧 절곡 공정',
|
||||
'steel': '🔩 철재 공정',
|
||||
'electric': '⚡ 전기 공정',
|
||||
'assembly': '🔨 조립 공정',
|
||||
'unknown': '❓ 기타'
|
||||
};
|
||||
|
||||
// 공정별 그룹 렌더링
|
||||
function renderProcessGroups(groupedItems) {
|
||||
const container = document.getElementById('processGroups');
|
||||
|
||||
const processKeys = Object.keys(groupedItems);
|
||||
if (processKeys.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-400 text-center py-4 text-xs">그룹화된 품목이 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = processKeys.map(processType => {
|
||||
const group = groupedItems[processType];
|
||||
const items = group.items || [];
|
||||
const subtotal = group.subtotal || 0;
|
||||
const label = processLabels[processType] || processType;
|
||||
|
||||
return `
|
||||
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<div class="px-3 py-2 bg-gray-100 flex items-center justify-between">
|
||||
<span class="font-medium text-gray-700 text-xs">${label}</span>
|
||||
<span class="text-xs text-gray-500">${items.length}개 품목</span>
|
||||
</div>
|
||||
<div class="p-2 space-y-1 text-xs">
|
||||
${items.slice(0, 8).map(item => renderProcessItem(item)).join('')}
|
||||
${items.length > 8 ? `<div class="text-gray-400 text-center py-1">... +${items.length - 8}개 더</div>` : ''}
|
||||
</div>
|
||||
<div class="px-3 py-1.5 bg-gray-50 text-right border-t">
|
||||
<span class="text-xs text-gray-500">소계:</span>
|
||||
<span class="font-medium text-gray-700 ml-1">${subtotal.toLocaleString()}원</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 공정별 품목 아이템 렌더링
|
||||
function renderProcessItem(item) {
|
||||
const price = item.amount || 0;
|
||||
const qty = item.quantity || 0;
|
||||
const categoryNote = item.calculation_note || '';
|
||||
|
||||
return `
|
||||
<div class="flex items-center gap-2 py-1 px-2 bg-white rounded hover:bg-gray-50">
|
||||
<span class="font-mono text-gray-400 truncate w-20">${item.item_code || ''}</span>
|
||||
<span class="text-gray-700 truncate flex-1">${item.item_name || ''}</span>
|
||||
<span class="text-gray-500 whitespace-nowrap">${qty} ${item.unit || 'EA'}</span>
|
||||
<span class="text-green-600 font-medium whitespace-nowrap">${price.toLocaleString()}원</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 원가 요약 렌더링
|
||||
function renderCostSummary(subtotals, grandTotal, finishedGoods) {
|
||||
const container = document.getElementById('costSummary');
|
||||
|
||||
// 완제품 정보
|
||||
let fgHtml = '';
|
||||
if (finishedGoods && finishedGoods.code) {
|
||||
fgHtml = `
|
||||
<div class="p-3 bg-purple-50 rounded-lg border border-purple-200 mb-3">
|
||||
<div class="text-xs text-purple-600 font-medium">완제품</div>
|
||||
<div class="font-medium text-gray-800">${finishedGoods.code}</div>
|
||||
<div class="text-sm text-gray-600">${finishedGoods.name || ''}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 공정별 소계
|
||||
const subtotalEntries = Object.entries(subtotals);
|
||||
let subtotalsHtml = '';
|
||||
if (subtotalEntries.length > 0) {
|
||||
subtotalsHtml = `
|
||||
<div class="space-y-2 mb-4">
|
||||
<div class="text-xs font-medium text-gray-500 uppercase">공정별 소계</div>
|
||||
${subtotalEntries.map(([process, amount]) => {
|
||||
const label = processLabels[process] || process;
|
||||
return `
|
||||
<div class="flex justify-between items-center py-1.5 px-2 bg-gray-50 rounded">
|
||||
<span class="text-gray-600">${label}</span>
|
||||
<span class="font-medium text-gray-700">${amount.toLocaleString()}원</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 총합계
|
||||
const totalHtml = `
|
||||
<div class="p-3 bg-green-50 rounded-lg border border-green-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-medium text-gray-700">총 원가</span>
|
||||
<span class="text-xl font-bold text-green-600">${grandTotal.toLocaleString()}원</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = fgHtml + subtotalsHtml + totalHtml;
|
||||
}
|
||||
|
||||
// 초기화 실행
|
||||
init();
|
||||
setupModeTabs();
|
||||
</script>
|
||||
@endpush
|
||||
@@ -525,6 +525,7 @@
|
||||
Route::post('/validate', [QuoteFormulaController::class, 'validate'])->name('validate');
|
||||
Route::post('/test', [QuoteFormulaController::class, 'test'])->name('test');
|
||||
Route::post('/simulate', [QuoteFormulaController::class, 'simulate'])->name('simulate');
|
||||
Route::post('/simulate-bom', [QuoteFormulaController::class, 'simulateBom'])->name('simulate-bom');
|
||||
Route::get('/items', [QuoteFormulaController::class, 'items'])->name('items');
|
||||
|
||||
// 기본 CRUD
|
||||
|
||||
Reference in New Issue
Block a user