feat: 수동입고 기능 구현 및 입고일 날짜 포맷 버그 수정

- 수동입고 API (POST /api/purchase-receipts/manual) 추가
- 수동입고 모달 UI 구현 (도매상 선택, 품목 동적 추가, 금액 자동계산)
- 도매상 등록 모달 z-index 처리 (수동입고 모달 위에 표시)
- Excel 입고 시 receipt_date 튜플/대시 없는 날짜 포맷 정규화
- inventory_lots에 lot_number, expiry_date 저장 누락 수정
- CLAUDE.md 추가 (lot_id vs lot_number 구분 가이드)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 07:34:56 +00:00
parent 3d13c0b1f3
commit 3a39951fdc
4 changed files with 471 additions and 1 deletions

View File

@@ -2028,6 +2028,13 @@ $(document).ready(function() {
response.data.forEach(supplier => {
filterSelect.append(`<option value="${supplier.supplier_id}">${supplier.name}</option>`);
});
// 수동 입고용 셀렉트 박스도 업데이트
const manualSelect = $('#manualReceiptSupplier');
manualSelect.empty().append('<option value="">도매상을 선택하세요</option>');
response.data.forEach(supplier => {
manualSelect.append(`<option value="${supplier.supplier_id}">${supplier.name}</option>`);
});
}
});
}
@@ -2066,6 +2073,211 @@ $(document).ready(function() {
});
});
// ==================== 수동 입고 ====================
// 전체 약재 목록 로드 (입고용 - 재고 필터 없음)
function loadAllHerbsForSelect(selectElement) {
$.get('/api/herbs/masters', function(response) {
if (response.success) {
selectElement.empty().append('<option value="">약재 선택</option>');
response.data.forEach(herb => {
let displayName = herb.herb_name;
if (herb.herb_name_hanja) {
displayName += ` (${herb.herb_name_hanja})`;
}
selectElement.append(`<option value="${herb.ingredient_code}" data-herb-name="${herb.herb_name}">${displayName}</option>`);
});
}
});
}
let manualReceiptLineCount = 0;
function addManualReceiptLine() {
manualReceiptLineCount++;
const row = `
<tr data-row="${manualReceiptLineCount}">
<td>
<select class="form-control form-control-sm manual-herb-select">
<option value="">약재 선택</option>
</select>
</td>
<td>
<input type="number" class="form-control form-control-sm manual-qty-input text-end"
min="0.1" step="0.1" placeholder="0.0">
</td>
<td>
<input type="number" class="form-control form-control-sm manual-price-input text-end"
min="0" step="0.1" placeholder="0.0">
</td>
<td class="text-end manual-line-total">0</td>
<td>
<input type="text" class="form-control form-control-sm manual-origin-input" placeholder="예: 중국">
</td>
<td>
<input type="text" class="form-control form-control-sm manual-lot-input" placeholder="로트번호">
</td>
<td>
<input type="date" class="form-control form-control-sm manual-expiry-input">
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger remove-manual-line">
<i class="bi bi-x"></i>
</button>
</td>
</tr>`;
$('#manualReceiptLines').append(row);
const newRow = $(`#manualReceiptLines tr[data-row="${manualReceiptLineCount}"]`);
loadAllHerbsForSelect(newRow.find('.manual-herb-select'));
// 금액 자동 계산
newRow.find('.manual-qty-input, .manual-price-input').on('input', function() {
updateManualReceiptLineTotals();
});
// 삭제 버튼
newRow.find('.remove-manual-line').on('click', function() {
$(this).closest('tr').remove();
updateManualReceiptLineTotals();
});
}
function updateManualReceiptLineTotals() {
let totalQty = 0;
let totalAmount = 0;
$('#manualReceiptLines tr').each(function() {
const qty = parseFloat($(this).find('.manual-qty-input').val()) || 0;
const price = parseFloat($(this).find('.manual-price-input').val()) || 0;
const lineTotal = qty * price;
$(this).find('.manual-line-total').text(lineTotal.toLocaleString('ko-KR'));
totalQty += qty;
totalAmount += lineTotal;
});
$('#manualReceiptTotalQty').text(totalQty.toLocaleString('ko-KR'));
$('#manualReceiptTotalAmount').text(totalAmount.toLocaleString('ko-KR'));
}
// 모달 열릴 때 초기화
$('#manualReceiptModal').on('show.bs.modal', function() {
const today = new Date().toISOString().split('T')[0];
$('#manualReceiptDate').val(today);
$('#manualReceiptSupplier').val('');
$('#manualReceiptNotes').val('');
$('#manualReceiptLines').empty();
manualReceiptLineCount = 0;
updateManualReceiptLineTotals();
addManualReceiptLine();
});
// 품목 추가 버튼
$('#addManualReceiptLineBtn').on('click', function() {
addManualReceiptLine();
});
// 새 도매상 등록 버튼 (수동 입고 모달에서)
$('#manualReceiptAddSupplierBtn').on('click', function() {
$('#supplierModal').modal('show');
});
// 도매상 모달이 수동입고 모달 위에 뜨도록 z-index 조정
$('#supplierModal').on('shown.bs.modal', function() {
if ($('#manualReceiptModal').hasClass('show')) {
$(this).css('z-index', 1060);
$('.modal-backdrop').last().css('z-index', 1055);
}
});
$('#supplierModal').on('hidden.bs.modal', function() {
$(this).css('z-index', '');
});
// 입고 저장
$('#saveManualReceiptBtn').on('click', function() {
const supplierId = $('#manualReceiptSupplier').val();
const receiptDate = $('#manualReceiptDate').val();
const notes = $('#manualReceiptNotes').val();
if (!supplierId) {
alert('도매상을 선택해주세요.');
return;
}
if (!receiptDate) {
alert('입고일을 입력해주세요.');
return;
}
const lines = [];
let valid = true;
$('#manualReceiptLines tr').each(function() {
const ingredientCode = $(this).find('.manual-herb-select').val();
const qty = parseFloat($(this).find('.manual-qty-input').val()) || 0;
const price = parseFloat($(this).find('.manual-price-input').val()) || 0;
if (!ingredientCode) {
valid = false;
alert('약재를 선택해주세요.');
return false;
}
if (qty <= 0) {
valid = false;
alert('수량을 입력해주세요.');
return false;
}
if (price <= 0) {
valid = false;
alert('단가를 입력해주세요.');
return false;
}
lines.push({
ingredient_code: ingredientCode,
quantity_g: qty,
unit_price_per_g: price,
origin_country: $(this).find('.manual-origin-input').val(),
lot_number: $(this).find('.manual-lot-input').val(),
expiry_date: $(this).find('.manual-expiry-input').val()
});
});
if (!valid) return;
if (lines.length === 0) {
alert('입고 품목을 1개 이상 추가해주세요.');
return;
}
const btn = $(this);
btn.prop('disabled', true).text('저장 중...');
$.ajax({
url: '/api/purchase-receipts/manual',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
supplier_id: supplierId,
receipt_date: receiptDate,
notes: notes,
lines: lines
}),
success: function(response) {
if (response.success) {
alert(`수동 입고 완료!\n입고번호: ${response.receipt_no}\n품목 수: ${response.summary.item_count}\n총 금액: ${response.summary.total_amount}`);
$('#manualReceiptModal').modal('hide');
loadPurchaseReceipts();
} else {
alert('오류: ' + response.error);
}
},
error: function(xhr) {
const msg = xhr.responseJSON ? xhr.responseJSON.error : '서버 오류가 발생했습니다.';
alert('오류: ' + msg);
},
complete: function() {
btn.prop('disabled', false).html('<i class="bi bi-check-circle"></i> 입고 저장');
}
});
});
// 입고장 업로드
$('#purchaseUploadForm').on('submit', function(e) {
e.preventDefault();