feat: 조제 원가 미리보기 및 재고 상태 표시 개선

원가 미리보기:
- 조제 실행 전 약재별 예상 원가(용량×단가) 및 합계 표시
- 용량/원산지/로트 변경 시 실시간 갱신
- 추가 약재의 이름 표시 오류 수정 (select 내 전체 옵션 텍스트 → 선택값만)

원산지 자동 선택:
- 처방 로드 시 재고 충분한 최저가 원산지를 자동 선택
- "자동 선택" 상태가 아닌 실제 원산지가 선택되어 원가 즉시 계산

재고 상태 표시:
- checkStockForCompound() TODO 제거, 실제 API 호출로 재고 확인
- 기존 원산지 선택을 덮어쓰지 않고 재고 상태만 갱신
- 선택 가능한 원산지가 2개 이상이면 "N종" 뱃지 표시

조제 폼 초기화:
- 새 조제 시 제수 기본값(1)으로 총 첩수(20)/파우치(30) 자동 설정
- 처방 선택 시 총 첩수가 비어있으면 자동 계산

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
시골약사 2026-02-19 14:50:21 +00:00
parent 974ce5f655
commit 725f14c59a
2 changed files with 149 additions and 6 deletions

View File

@ -1207,10 +1207,16 @@ $(document).ready(function() {
$('#compoundForm').show();
$('#compoundEntryForm')[0].reset();
$('#compoundIngredients').empty();
$('#costPreview').hide();
// 제수 기본값(1)으로 첩수/파우치 초기화
$('#jeCount').val(1);
$('#cheopTotal').val(20);
$('#pouchTotal').val(30);
});
$('#cancelCompoundBtn').on('click', function() {
$('#compoundForm').hide();
$('#costPreview').hide();
});
// 제수 변경 시 첩수 자동 계산
@ -1230,12 +1236,20 @@ $(document).ready(function() {
$('#compoundFormula').on('change', function() {
const formulaId = $(this).val();
// 제수 기반 첩수/파우치 자동 계산 (초기값 반영)
const jeCount = parseFloat($('#jeCount').val()) || 0;
if (jeCount > 0 && !$('#cheopTotal').val()) {
$('#cheopTotal').val(jeCount * 20);
$('#pouchTotal').val(jeCount * 30);
}
// 원래 처방 구성 초기화
originalFormulaIngredients = {};
$('#customPrescriptionBadge').remove(); // 커스텀 뱃지 제거
if (!formulaId) {
$('#compoundIngredients').empty();
$('#costPreview').hide();
return;
}
@ -1364,6 +1378,7 @@ $(document).ready(function() {
});
// 약재별 총 용량 업데이트
let _stockCheckTimer = null;
function updateIngredientTotals() {
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
@ -1373,9 +1388,88 @@ $(document).ready(function() {
$(this).find('.total-grams').text(totalGrams.toFixed(1));
});
checkStockForCompound();
// 커스텀 처방 감지 호출
checkCustomPrescription();
// 원가 미리보기 갱신 (즉시)
updateCostPreview();
// 재고 상태 갱신 (디바운스 300ms)
clearTimeout(_stockCheckTimer);
_stockCheckTimer = setTimeout(() => checkStockForCompound(), 300);
}
// 원가 미리보기 계산
function updateCostPreview() {
const rows = $('#compoundIngredients tr');
if (rows.length === 0) {
$('#costPreview').hide();
return;
}
const items = [];
let totalCost = 0;
let allHavePrice = true;
rows.each(function() {
// 약재명: 처방에서 로드된 행은 텍스트, 추가된 행은 select의 선택값
const firstTd = $(this).find('td:first');
const herbSelect = firstTd.find('.herb-select-compound');
const herbName = herbSelect.length > 0
? (herbSelect.find('option:selected').text().trim() || '미선택')
: firstTd.text().trim().split('(')[0].trim();
const totalGrams = parseFloat($(this).find('.total-grams').text()) || 0;
const originSelect = $(this).find('.origin-select');
const selectedOption = originSelect.find('option:selected');
const unitPrice = parseFloat(selectedOption.attr('data-price')) || 0;
// 수동 배분인 경우 data-lot-assignments에서 계산
const lotAssignmentsStr = $(this).attr('data-lot-assignments');
let itemCost = 0;
if (lotAssignmentsStr) {
try {
const assignments = JSON.parse(lotAssignmentsStr);
assignments.forEach(a => {
itemCost += (a.quantity || 0) * (a.unit_price || 0);
});
} catch(e) {
itemCost = totalGrams * unitPrice;
}
} else if (unitPrice > 0) {
itemCost = totalGrams * unitPrice;
} else {
allHavePrice = false;
}
totalCost += itemCost;
items.push({ name: herbName, grams: totalGrams, unitPrice, cost: itemCost });
});
// UI 렌더링
const tbody = $('#costPreviewItems');
tbody.empty();
items.forEach(item => {
const costText = item.cost > 0
? formatCurrency(Math.round(item.cost))
: '<span class="text-muted">-</span>';
const priceText = item.unitPrice > 0
? `${item.grams.toFixed(1)}g ×${item.unitPrice.toFixed(1)}`
: `${item.grams.toFixed(1)}g`;
tbody.append(`
<tr>
<td>${item.name} <small class="text-muted">${priceText}</small></td>
<td class="text-end">${costText}</td>
</tr>
`);
});
$('#costPreviewTotal').text(formatCurrency(Math.round(totalCost)));
const status = allHavePrice && items.length > 0
? '<span class="badge bg-success">확정</span>'
: '<span class="badge bg-warning text-dark">일부 미확정</span>';
$('#costPreviewStatus').html(status);
$('#costPreview').show();
}
// 커스텀 처방 감지 함수
@ -1452,13 +1546,30 @@ $(document).ready(function() {
// 재고 확인
function checkStockForCompound() {
// 각 약재의 재고 상태를 API로 갱신 (기존 선택 보존)
$('#compoundIngredients tr').each(function() {
const herbId = $(this).data('herb-id');
const herbId = $(this).attr('data-herb-id');
if (!herbId) return;
const totalGrams = parseFloat($(this).find('.total-grams').text()) || 0;
const currentSelection = $(this).find('.origin-select').val();
const $stockStatus = $(this).find('.stock-status');
// TODO: API 호출로 실제 재고 확인
$stockStatus.text('재고 확인 필요');
if (totalGrams > 0) {
$.get(`/api/herbs/${herbId}/available-lots`, function(response) {
if (response.success) {
const totalAvailable = response.data.total_quantity;
const origins = response.data.origins;
const altCount = origins.length;
const altBadge = altCount > 1 ? ` <span class="badge bg-outline-info text-info border border-info" style="font-size:0.65rem">${altCount}종</span>` : '';
if (totalAvailable >= totalGrams) {
$stockStatus.html(`<span class="text-success">충분 (${totalAvailable.toFixed(1)}g)</span>${altBadge}`);
} else {
$stockStatus.html(`<span class="text-danger">부족 (${totalAvailable.toFixed(1)}g)</span>${altBadge}`);
}
}
});
}
});
}
@ -3055,6 +3166,14 @@ $(document).ready(function() {
selectElement.prop('disabled', false);
// 재고 충분한 첫 번째 원산지 자동 선택 (원가 미리보기용)
const firstAvailable = origins.find(o => o.total_quantity >= requiredQty);
if (firstAvailable) {
selectElement.val(firstAvailable.origin_country);
} else if (origins.length > 0) {
selectElement.val(origins[0].origin_country);
}
// 원산지 선택 변경 이벤트 (수동 배분 모달 트리거)
selectElement.off('change').on('change', function() {
const selectedValue = $(this).val();
@ -3072,18 +3191,24 @@ $(document).ready(function() {
// 기존 자동/원산지 선택 - lot_assignments 제거
row.removeAttr('data-lot-assignments');
}
updateCostPreview();
});
// 재고 상태 업데이트
const totalAvailable = response.data.total_quantity;
const statusElement = $(`tr[data-herb-id="${herbId}"] .stock-status`);
const altCount = origins.length;
const altBadge = altCount > 1 ? ` <span class="badge bg-outline-info text-info border border-info" style="font-size:0.65rem; cursor:pointer" title="선택 가능한 원산지 ${altCount}종">${altCount}종</span>` : '';
if (totalAvailable >= requiredQty) {
statusElement.html(`<span class="text-success">충분 (${totalAvailable.toFixed(1)}g)</span>`);
statusElement.html(`<span class="text-success">충분 (${totalAvailable.toFixed(1)}g)</span>${altBadge}`);
} else {
statusElement.html(`<span class="text-danger">부족 (${totalAvailable.toFixed(1)}g)</span>`);
statusElement.html(`<span class="text-danger">부족 (${totalAvailable.toFixed(1)}g)</span>${altBadge}`);
}
}
// 원가 미리보기 갱신
updateCostPreview();
}
});
}

View File

@ -486,6 +486,24 @@
<i class="bi bi-plus"></i> 약재 추가
</button>
</div>
<!-- 원가 미리보기 -->
<div id="costPreview" class="mt-3 p-3 bg-light rounded border" style="display:none;">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="bi bi-calculator"></i> 예상 원가</h6>
<span class="badge bg-secondary" id="costPreviewStatus">계산중...</span>
</div>
<div class="mt-2">
<table class="table table-sm table-borderless mb-0" style="font-size: 0.85rem;">
<tbody id="costPreviewItems"></tbody>
<tfoot>
<tr class="border-top fw-bold">
<td>합계</td>
<td class="text-end" id="costPreviewTotal">₩0</td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-success">
<i class="bi bi-check-circle"></i> 조제 실행