feat: 한퓨어 엑셀 형식 지원 및 조제 용도 구분(usage_type) 추가

한퓨어 엑셀:
- ExcelProcessor에 hanpure 형식 자동 감지 및 처리 추가
- 옵션항목에서 중량 파싱 (600g*5개 → 3000g 등)
- 주문번호에서 입고일 추출, ingredient_code 직접 활용

조제 용도 구분:
- compounds.usage_type 컬럼 추가 (SALE/SELF_USE/SAMPLE/DISPOSAL)
- 조제 실행 시 용도 선택 드롭다운
- 조제 목록에서 용도 뱃지 클릭으로 사후 변경 가능
- 비판매 용도 시 sell_price_total=0, 매출 통계 제외
- PUT /api/compounds/:id/usage-type API 추가
- 용도 구분 설계 문서 (docs/조제_용도구분_usage_type.md)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 14:34:10 +00:00
parent 69be63d00d
commit 974ce5f655
5 changed files with 368 additions and 22 deletions

View File

@@ -141,10 +141,11 @@ $(document).ready(function() {
const todayCompounds = response.data.filter(c => c.compound_date === today);
$('#todayCompounds').text(todayCompounds.length);
// 이번달 매출/마진 계산
// 이번달 매출/마진 계산 (자가소비/샘플/폐기 제외)
const monthData = response.data.filter(c =>
c.compound_date && c.compound_date.startsWith(currentMonth) &&
['PAID', 'PENDING_DELIVERY', 'DELIVERED', 'COMPLETED'].includes(c.status)
['PAID', 'PENDING_DELIVERY', 'DELIVERED', 'COMPLETED'].includes(c.status) &&
(!c.usage_type || c.usage_type === 'SALE')
);
const monthSales = monthData.reduce((sum, c) => sum + (c.actual_payment_amount || c.sell_price_total || 0), 0);
const monthCost = monthData.reduce((sum, c) => sum + (c.cost_total || 0), 0);
@@ -1627,6 +1628,7 @@ $(document).ready(function() {
je_count: parseFloat($('#jeCount').val()),
cheop_total: parseFloat($('#cheopTotal').val()),
pouch_total: parseFloat($('#pouchTotal').val()),
usage_type: $('#compoundUsageType').val() || 'SALE',
ingredients: ingredients
};
@@ -1637,8 +1639,11 @@ $(document).ready(function() {
data: JSON.stringify(compoundData),
success: function(response) {
if (response.success) {
alert(`조제가 완료되었습니다.\n원가: ${formatCurrency(response.total_cost)}`);
const usageType = $('#compoundUsageType').val();
const usageLabel = {SELF_USE: '자가소비', SAMPLE: '샘플', DISPOSAL: '폐기'}[usageType] || '판매';
alert(`조제가 완료되었습니다. [${usageLabel}]\n원가: ${formatCurrency(response.total_cost)}`);
$('#compoundForm').hide();
$('#compoundUsageType').val('SALE');
loadCompounds();
}
},
@@ -1701,6 +1706,13 @@ $(document).ready(function() {
statusBadge = '<span class="badge bg-secondary">대기</span>';
}
// 용도 뱃지 (클릭으로 변경 가능)
const usageLabels = {SALE: '판매', SELF_USE: '자가소비', SAMPLE: '샘플', DISPOSAL: '폐기'};
const usageColors = {SALE: 'success', SELF_USE: 'warning text-dark', SAMPLE: 'info', DISPOSAL: 'secondary'};
const curUsage = compound.usage_type || 'SALE';
const usageBadge = `<span class="badge bg-${usageColors[curUsage]} change-usage" style="cursor:pointer" data-id="${compound.compound_id}" data-current="${curUsage}" title="클릭하여 용도 변경">${usageLabels[curUsage]}</span>`;
const isSale = curUsage === 'SALE';
const row = $(`
<tr>
<td>${response.data.length - index}</td>
@@ -1712,14 +1724,14 @@ $(document).ready(function() {
<td>${compound.cheop_total || 0}</td>
<td>${compound.pouch_total || 0}</td>
<td>${formatCurrency(compound.cost_total || 0)}</td>
<td>${formatCurrency(compound.sell_price_total || 0)}</td>
<td>${statusBadge}</td>
<td>${isSale ? formatCurrency(compound.sell_price_total || 0) : '-'}</td>
<td>${usageBadge} ${statusBadge}</td>
<td>${compound.prescription_no || '-'}</td>
<td>
<button class="btn btn-sm btn-outline-info view-compound-detail" data-id="${compound.compound_id}">
<i class="bi bi-eye"></i> 상세
</button>
${compound.status === 'PREPARED' ? `
${compound.status === 'PREPARED' && isSale ? `
<button class="btn btn-sm btn-outline-success process-sale" data-id="${compound.compound_id}"
data-formula="${compound.formula_name || '직접조제'}"
data-patient="${compound.patient_name || '직접조제'}"
@@ -1728,6 +1740,8 @@ $(document).ready(function() {
data-price="${compound.sell_price_total || 0}">
<i class="bi bi-cash-coin"></i> 판매
</button>
` : ''}
${compound.status === 'PREPARED' ? `
<button class="btn btn-sm btn-outline-danger cancel-compound" data-id="${compound.compound_id}">
<i class="bi bi-x-circle"></i> 취소
</button>
@@ -1795,6 +1809,40 @@ $(document).ready(function() {
});
}
});
// 용도 변경 뱃지 클릭 이벤트
$('.change-usage').on('click', function() {
const compoundId = $(this).data('id');
const current = $(this).data('current');
const options = {SALE: '판매', SELF_USE: '자가소비', SAMPLE: '샘플', DISPOSAL: '폐기'};
const choices = Object.entries(options)
.map(([k, v]) => `${k === current ? '● ' : ' '}${v}`)
.join('\n');
const input = prompt(`용도를 선택하세요 (현재: ${options[current]})\n\n1: 판매\n2: 자가소비\n3: 샘플\n4: 폐기`, current === 'SALE' ? '1' : current === 'SELF_USE' ? '2' : current === 'SAMPLE' ? '3' : '4');
if (!input) return;
const typeMap = {'1': 'SALE', '2': 'SELF_USE', '3': 'SAMPLE', '4': 'DISPOSAL'};
const newType = typeMap[input.trim()];
if (!newType) { alert('잘못된 입력입니다.'); return; }
if (newType === current) return;
$.ajax({
url: `/api/compounds/${compoundId}/usage-type`,
method: 'PUT',
contentType: 'application/json',
data: JSON.stringify({ usage_type: newType }),
success: function(response) {
if (response.success) {
loadCompounds();
loadDashboard();
} else {
alert(response.error || '변경 실패');
}
},
error: function(xhr) {
alert(xhr.responseJSON?.error || '변경 실패');
}
});
});
} else {
tbody.html('<tr><td colspan="13" class="text-center text-muted">조제 내역이 없습니다.</td></tr>');
$('#todayCompoundCount').text(0);