feat: 100처방 UI, 처방 가감 표시, 어울림 스타일링

- 처방 관리 페이지에 100처방 원방 마스터 섹션 추가 (검색 포함)
- 100처방 상세 모달 (구성약재, 참고자료 편집, 내 처방으로 등록)
- 내 처방 목록에 100처방 뱃지 및 가감 정보 표시
  - 변경: 파란 뱃지, 추가: 초록 뱃지, 제거: 빨간 뱃지
  - 원방 그대로: 회색 뱃지
- "어울림" 접두어 초록색 볼드 스타일링
- stock_ledger에 RETURN(반환)/DISCARD(폐기) 한글 라벨 추가
- 수동입고 원산지 드롭다운 변경, 재고 상세 유통기한 표시

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
시골약사 2026-02-18 14:15:44 +00:00
parent 51e0c99c77
commit 1679f75d33
2 changed files with 612 additions and 35 deletions

View File

@ -577,16 +577,69 @@ $(document).ready(function() {
// 처방 목록 로드
function loadFormulas() {
// 100처방 이름 목록을 먼저 가져온 후 내 처방 렌더링
$.get('/api/official-formulas', function(offRes) {
const officialNames = new Map();
if (offRes.success) {
offRes.data.forEach(f => officialNames.set(f.formula_name, f.formula_number));
}
$.get('/api/formulas', function(response) {
if (response.success) {
const tbody = $('#formulasList');
tbody.empty();
response.data.forEach(formula => {
// 100처방 매칭: 정확 매칭 우선, 없으면 내 처방명이 100처방명으로 시작하는지 확인
let officialNum = officialNames.get(formula.formula_name);
if (officialNum == null) {
for (const [name, num] of officialNames) {
if (formula.formula_name.startsWith(name)) {
officialNum = num;
break;
}
}
}
const officialBadge = officialNum != null
? ` <span class="badge bg-info">100처방 #${officialNum}</span>`
: '';
// 처방명 스타일링: "어울림" 접두어 색상 처리
let displayName = formula.formula_name;
if (displayName.startsWith('어울림 ')) {
displayName = `<span class="text-success fw-bold">어울림</span> ${displayName.substring(4)}`;
}
// 가감 정보 표시 (100처방 기반 처방)
let customInfo = '';
if (formula.official_formula_id && formula.is_custom) {
let details = [];
if (formula.custom_modified && formula.custom_modified.length > 0) {
details.push(...formula.custom_modified.map(m =>
`<span class="badge bg-primary bg-opacity-75 me-1">${m}</span>`
));
}
if (formula.custom_added && formula.custom_added.length > 0) {
details.push(...formula.custom_added.map(a =>
`<span class="badge bg-success bg-opacity-75 me-1">+${a}</span>`
));
}
if (formula.custom_removed && formula.custom_removed.length > 0) {
details.push(...formula.custom_removed.map(r =>
`<span class="badge bg-danger bg-opacity-75 me-1">-${r}</span>`
));
}
if (details.length > 0) {
customInfo = `<br><span class="d-inline-flex flex-wrap gap-1 mt-1">${details.join('')}</span>`;
}
} else if (formula.official_formula_id && !formula.is_custom) {
customInfo = ` <span class="badge bg-secondary">원방 그대로</span>`;
}
tbody.append(`
<tr>
<td>${formula.formula_code || '-'}</td>
<td>${formula.formula_name}</td>
<td>${displayName}${officialBadge}${customInfo}</td>
<td>${formula.base_cheop}</td>
<td>${formula.base_pouches}파우치</td>
<td>
@ -633,9 +686,214 @@ $(document).ready(function() {
}
});
}
// 내 처방 렌더링 완료 후 100처방 로드
loadOfficialFormulas();
});
}); // /api/official-formulas 콜백 닫기
}
// 100처방 원방 마스터 로드
function loadOfficialFormulas(search) {
const params = search ? `?search=${encodeURIComponent(search)}` : '';
// 내 처방 이름 목록을 API에서 가져와서 비교
$.get('/api/formulas', function(formulasRes) {
const myFormulaNames = new Set();
if (formulasRes.success) {
formulasRes.data.forEach(f => myFormulaNames.add(f.formula_name));
}
$.get(`/api/official-formulas${params}`, function(response) {
if (response.success) {
const tbody = $('#officialFormulasList');
tbody.empty();
$('#officialFormulaCount').text(response.data.length);
response.data.forEach(formula => {
// 등록 여부: 정확 매칭 또는 내 처방명이 100처방명으로 시작
const isRegistered = myFormulaNames.has(formula.formula_name)
|| [...myFormulaNames].some(name => name.startsWith(formula.formula_name));
const statusBadge = isRegistered
? '<span class="badge bg-success">등록됨</span>'
: '<span class="badge bg-outline-secondary text-muted">미등록</span>';
const hasNotes = formula.reference_notes ? '<i class="bi bi-journal-text text-info ms-1" title="참고자료 있음"></i>' : '';
tbody.append(`
<tr class="official-formula-row" style="cursor:pointer"
data-id="${formula.official_formula_id}"
data-number="${formula.formula_number}"
data-name="${formula.formula_name}"
data-hanja="${formula.formula_name_hanja || ''}"
data-source="${formula.source_text || ''}"
data-description="${(formula.description || '').replace(/"/g, '&quot;')}"
data-notes="${(formula.reference_notes || '').replace(/"/g, '&quot;')}">
<td class="text-center">${formula.formula_number}</td>
<td><strong>${formula.formula_name}</strong>${hasNotes}</td>
<td class="text-muted">${formula.formula_name_hanja || '-'}</td>
<td>${formula.source_text || '-'}</td>
<td class="text-center">${statusBadge}</td>
</tr>
`);
});
if (response.data.length === 0) {
tbody.html('<tr><td colspan="5" class="text-center text-muted">검색 결과가 없습니다.</td></tr>');
}
}
});
}); // /api/formulas 콜백 닫기
}
// 100처방 검색 이벤트
let officialSearchTimer = null;
$(document).on('input', '#officialFormulaSearch', function() {
clearTimeout(officialSearchTimer);
const search = $(this).val().trim();
officialSearchTimer = setTimeout(() => {
loadOfficialFormulas(search);
}, 300);
});
// 100처방 행 클릭 → 상세/참고자료 모달
$(document).on('click', '.official-formula-row', function() {
const row = $(this);
const id = row.data('id');
$('#officialFormulaModal').data('formula-id', id);
$('#ofModalNumber').text(row.data('number'));
$('#ofModalName').text(row.data('name'));
$('#ofModalHanja').text(row.data('hanja') || '');
$('#ofModalSource').text(row.data('source') || '-');
$('#ofEditHanja').val(row.data('hanja') || '');
$('#ofEditDescription').val(row.data('description') || '');
$('#ofEditReferenceNotes').val(row.data('notes') || '');
// 원방 구성 약재 로드
$.get(`/api/official-formulas/${id}/ingredients`, function(res) {
const section = $('#ofIngredientsSection');
const tbody = $('#ofIngredientsList');
tbody.empty();
if (res.success && res.data.length > 0) {
let totalGrams = 0;
res.data.forEach((ing, idx) => {
totalGrams += ing.grams_per_cheop;
tbody.append(`
<tr>
<td class="text-muted">${idx + 1}</td>
<td><strong>${ing.herb_name}</strong> <small class="text-muted">${ing.herb_name_hanja || ''}</small></td>
<td class="text-end">${ing.grams_per_cheop}g</td>
<td><small class="text-muted">${ing.notes || '-'}</small></td>
</tr>
`);
});
$('#ofIngredientCount').text(res.data.length);
$('#ofTotalGrams').text(totalGrams.toFixed(1));
section.show();
} else {
section.hide();
}
});
$('#officialFormulaModal').modal('show');
});
// 100처방 참고자료 저장
$(document).on('click', '#saveOfficialFormulaBtn', function() {
const id = $('#officialFormulaModal').data('formula-id');
$.ajax({
url: `/api/official-formulas/${id}`,
method: 'PUT',
contentType: 'application/json',
data: JSON.stringify({
formula_name_hanja: $('#ofEditHanja').val().trim(),
description: $('#ofEditDescription').val().trim(),
reference_notes: $('#ofEditReferenceNotes').val().trim()
}),
success: function(response) {
if (response.success) {
alert('저장되었습니다.');
$('#officialFormulaModal').modal('hide');
loadOfficialFormulas($('#officialFormulaSearch').val().trim());
}
},
error: function(xhr) {
alert(xhr.responseJSON?.error || '저장 중 오류가 발생했습니다.');
}
});
});
// 100처방 → 내 처방으로 등록
$(document).on('click', '#createFromOfficialBtn', function() {
const id = $('#officialFormulaModal').data('formula-id');
const name = $('#ofModalName').text();
const description = $('#ofEditDescription').val().trim();
// 100처방 모달 닫기
$('#officialFormulaModal').modal('hide');
// 처방 등록 모달 초기화 (신규 모드)
$('#formulaModal').data('edit-mode', false);
$('#formulaModal').data('formula-id', null);
$('#formulaModal .modal-title').text('처방 등록 (원방 기반)');
$('#formulaForm')[0].reset();
$('#formulaIngredients').empty();
// 기본값 세팅
$('#formulaName').val(`어울림 ${name}`);
$('#formulaType').val('CUSTOM');
$('#baseCheop').val(20);
$('#basePouches').val(30);
$('#formulaDescription').val(description);
$('#formulaModal').data('official-formula-id', id);
// 원방 구성 약재 로드
$.get(`/api/official-formulas/${id}/ingredients`, function(res) {
if (res.success && res.data.length > 0) {
formulaIngredientCount = 0;
res.data.forEach(ing => {
formulaIngredientCount++;
$('#formulaIngredients').append(`
<tr data-row="${formulaIngredientCount}">
<td>
<select class="form-control form-control-sm herb-select">
<option value="${ing.ingredient_code}" selected>${ing.herb_name}</option>
</select>
</td>
<td>
<input type="number" class="form-control form-control-sm grams-input"
min="0.1" step="0.1" value="${ing.grams_per_cheop}">
</td>
<td>
<input type="text" class="form-control form-control-sm notes-input" value="${ing.notes || ''}">
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger remove-ingredient">
<i class="bi bi-x"></i>
</button>
</td>
</tr>
`);
// 약재 select에 전체 목록 로드 (현재 값 유지)
const selectEl = $(`#formulaIngredients tr[data-row="${formulaIngredientCount}"] .herb-select`);
loadHerbsForSelectWithCurrent(selectEl, ing.ingredient_code, ing.herb_name);
});
// 삭제 버튼 이벤트
$('.remove-ingredient').on('click', function() {
$(this).closest('tr').remove();
});
}
// 처방 등록 모달 열기
$('#formulaModal').modal('show');
});
});
// 처방 상세 정보 표시 함수
function showFormulaDetail(formulaId, formulaName) {
// 모달에 formulaId 저장
@ -897,7 +1155,8 @@ $(document).ready(function() {
base_pouches: parseInt($('#basePouches').val()),
description: $('#formulaDescription').val(),
efficacy: $('#formulaEfficacy').val(),
ingredients: ingredients
ingredients: ingredients,
official_formula_id: $('#formulaModal').data('official-formula-id') || null
};
// 수정 모드인지 확인
@ -922,6 +1181,7 @@ $(document).ready(function() {
// 수정 모드 초기화
$('#formulaModal').data('edit-mode', false);
$('#formulaModal').data('formula-id', null);
$('#formulaModal').data('official-formula-id', null);
$('#formulaModal .modal-title').text('처방 등록');
loadFormulas();
}
@ -1459,6 +1719,9 @@ $(document).ready(function() {
data-price="${compound.sell_price_total || 0}">
<i class="bi bi-cash-coin"></i>
</button>
<button class="btn btn-sm btn-outline-danger cancel-compound" data-id="${compound.compound_id}">
<i class="bi bi-x-circle"></i>
</button>
` : ''}
${compound.status === 'PAID' ? `
<button class="btn btn-sm btn-outline-primary process-delivery" data-id="${compound.compound_id}">
@ -1498,6 +1761,31 @@ $(document).ready(function() {
const compoundId = $(this).data('id');
processDelivery(compoundId);
});
// 조제 취소 버튼 이벤트
$('.cancel-compound').on('click', function() {
const compoundId = $(this).data('id');
if (confirm('정말 취소하시겠습니까? 사용된 재고가 복원됩니다.')) {
$.ajax({
url: `/api/compounds/${compoundId}/status`,
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ status: 'CANCELLED', reason: '조제 취소 (재고 복원)' }),
success: function(response) {
if (response.success) {
alert('조제가 취소되었고 재고가 복원되었습니다.');
loadCompounds();
} else {
alert(response.error || '취소 처리 중 오류가 발생했습니다.');
}
},
error: function(xhr) {
const err = xhr.responseJSON;
alert(err?.error || '취소 처리 중 오류가 발생했습니다.');
}
});
}
});
} else {
tbody.html('<tr><td colspan="13" class="text-center text-muted">조제 내역이 없습니다.</td></tr>');
$('#todayCompoundCount').text(0);
@ -1754,6 +2042,7 @@ $(document).ready(function() {
<th>수량</th>
<th>단가</th>
<th>입고일</th>
<th>유통기한</th>
<th>도매상</th>
</tr>
</thead>
@ -1776,6 +2065,7 @@ $(document).ready(function() {
<td>${lot.quantity_onhand.toFixed(1)}g</td>
<td>${formatCurrency(lot.unit_price_per_g)}</td>
<td>${lot.received_date}</td>
<td>${lot.expiry_date || '-'}</td>
<td>${lot.supplier_name || '-'}</td>
</tr>`;
});
@ -1887,6 +2177,9 @@ $(document).ready(function() {
<button class="btn btn-sm btn-outline-info view-receipt" data-id="${receipt.receipt_id}">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-sm btn-outline-warning edit-receipt" data-id="${receipt.receipt_id}">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger delete-receipt" data-id="${receipt.receipt_id}">
<i class="bi bi-trash"></i>
</button>
@ -1901,6 +2194,11 @@ $(document).ready(function() {
viewReceiptDetail(receiptId);
});
$('.edit-receipt').on('click', function() {
const receiptId = $(this).data('id');
editReceipt(receiptId);
});
$('.delete-receipt').on('click', function() {
const receiptId = $(this).data('id');
if (confirm('정말 이 입고장을 삭제하시겠습니까? 사용되지 않은 재고만 삭제 가능합니다.')) {
@ -2076,7 +2374,7 @@ $(document).ready(function() {
// ==================== 수동 입고 ====================
// 전체 약재 목록 로드 (입고용 - 재고 필터 없음)
function loadAllHerbsForSelect(selectElement) {
function loadAllHerbsForSelect(selectElement, initialValue) {
$.get('/api/herbs/masters', function(response) {
if (response.success) {
selectElement.empty().append('<option value="">약재 선택</option>');
@ -2087,10 +2385,105 @@ $(document).ready(function() {
}
selectElement.append(`<option value="${herb.ingredient_code}" data-herb-name="${herb.herb_name}">${displayName}</option>`);
});
if (initialValue) {
selectElement.val(initialValue);
}
}
});
}
// 입고장 수정 모드
function editReceipt(receiptId) {
$.get(`/api/purchase-receipts/${receiptId}`, function(response) {
if (!response.success) {
alert('입고장 데이터를 불러올 수 없습니다: ' + response.error);
return;
}
const receipt = response.data;
const modal = $('#manualReceiptModal');
// 수정 모드 플래그 설정
modal.data('edit-mode', true);
modal.data('receipt-id', receiptId);
// 모달 제목 변경
modal.find('.modal-title').html('<i class="bi bi-pencil"></i> 입고장 수정');
modal.find('.modal-header').removeClass('bg-success').addClass('bg-warning');
// 헤더 정보 채우기
$('#manualReceiptDate').val(receipt.receipt_date);
$('#manualReceiptSupplier').val(receipt.supplier_id);
$('#manualReceiptSupplier').prop('disabled', true);
$('#manualReceiptNotes').val(receipt.notes || '');
// 품목 추가 버튼 숨김
$('#addManualReceiptLineBtn').hide();
// 기존 라인 비우기
$('#manualReceiptLines').empty();
manualReceiptLineCount = 0;
// 저장 버튼 텍스트 변경
$('#saveManualReceiptBtn').html('<i class="bi bi-check-circle"></i> 수정 저장');
// 기존 라인 데이터로 행 채우기
receipt.lines.forEach(line => {
manualReceiptLineCount++;
const row = `
<tr data-row="${manualReceiptLineCount}" data-line-id="${line.line_id}">
<td>
<select class="form-control form-control-sm manual-herb-select" disabled>
<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" value="${line.quantity_g || ''}">
</td>
<td>
<input type="number" class="form-control form-control-sm manual-price-input text-end"
min="0" step="0.1" value="${line.unit_price_per_g || ''}">
</td>
<td class="text-end manual-line-total">${(line.line_total || 0).toLocaleString('ko-KR')}</td>
<td>
<select class="form-control form-control-sm manual-origin-input">
<option value="">원산지 선택</option>
${['한국','중국','베트남','인도','태국','페루','일본','기타'].map(c =>
`<option value="${c}" ${(line.origin_country || '') === c ? 'selected' : ''}>${c}</option>`
).join('')}
</select>
</td>
<td>
<input type="text" class="form-control form-control-sm manual-lot-input"
placeholder="로트번호" value="${line.lot_number || ''}">
</td>
<td>
<input type="date" class="form-control form-control-sm manual-expiry-input"
value="${line.expiry_date || ''}">
</td>
<td></td>
</tr>`;
$('#manualReceiptLines').append(row);
const newRow = $(`#manualReceiptLines tr[data-row="${manualReceiptLineCount}"]`);
// 약재 select에 옵션 로드 후 기존 값 선택
loadAllHerbsForSelect(newRow.find('.manual-herb-select'), line.ingredient_code);
// 금액 자동 계산 이벤트
newRow.find('.manual-qty-input, .manual-price-input').on('input', function() {
updateManualReceiptLineTotals();
});
});
updateManualReceiptLineTotals();
// 모달 열기 (show.bs.modal 이벤트 비활성화를 위해 직접 설정)
modal.modal('show');
});
}
let manualReceiptLineCount = 0;
function addManualReceiptLine() {
@ -2112,7 +2505,17 @@ $(document).ready(function() {
</td>
<td class="text-end manual-line-total">0</td>
<td>
<input type="text" class="form-control form-control-sm manual-origin-input" placeholder="예: 중국">
<select class="form-control form-control-sm manual-origin-input">
<option value="">원산지 선택</option>
<option value="한국">한국</option>
<option value="중국">중국</option>
<option value="베트남">베트남</option>
<option value="인도">인도</option>
<option value="태국">태국</option>
<option value="페루">페루</option>
<option value="일본">일본</option>
<option value="기타">기타</option>
</select>
</td>
<td>
<input type="text" class="form-control form-control-sm manual-lot-input" placeholder="로트번호">
@ -2158,8 +2561,20 @@ $(document).ready(function() {
$('#manualReceiptTotalAmount').text(totalAmount.toLocaleString('ko-KR'));
}
// 모달 열릴 때 초기화
// 모달 열릴 때 초기화 (수정 모드가 아닐 때만)
$('#manualReceiptModal').on('show.bs.modal', function() {
const modal = $(this);
if (modal.data('edit-mode')) {
// 수정 모드에서는 초기화하지 않음 (editReceipt에서 이미 설정함)
return;
}
// 새 입고 모드 초기화
modal.find('.modal-title').html('<i class="bi bi-plus-circle"></i> 수동 입고');
modal.find('.modal-header').removeClass('bg-warning').addClass('bg-success');
$('#saveManualReceiptBtn').html('<i class="bi bi-check-circle"></i> 입고 저장');
$('#addManualReceiptLineBtn').show();
$('#manualReceiptSupplier').prop('disabled', false);
const today = new Date().toISOString().split('T')[0];
$('#manualReceiptDate').val(today);
$('#manualReceiptSupplier').val('');
@ -2170,6 +2585,14 @@ $(document).ready(function() {
addManualReceiptLine();
});
// 모달 닫힐 때 수정 모드 플래그 초기화
$('#manualReceiptModal').on('hidden.bs.modal', function() {
const modal = $(this);
modal.removeData('edit-mode');
modal.removeData('receipt-id');
$('#manualReceiptSupplier').prop('disabled', false);
});
// 품목 추가 버튼
$('#addManualReceiptLineBtn').on('click', function() {
addManualReceiptLine();
@ -2192,8 +2615,12 @@ $(document).ready(function() {
$(this).css('z-index', '');
});
// 입고 저장
// 입고 저장 (새 입고 / 수정 공통)
$('#saveManualReceiptBtn').on('click', function() {
const modal = $('#manualReceiptModal');
const isEditMode = modal.data('edit-mode');
const receiptId = modal.data('receipt-id');
const supplierId = $('#manualReceiptSupplier').val();
const receiptDate = $('#manualReceiptDate').val();
const notes = $('#manualReceiptNotes').val();
@ -2230,14 +2657,21 @@ $(document).ready(function() {
return false;
}
lines.push({
const lineData = {
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()
});
};
// 수정 모드에서는 line_id 포함
if (isEditMode) {
lineData.line_id = $(this).data('line-id');
}
lines.push(lineData);
});
if (!valid) return;
@ -2249,33 +2683,63 @@ $(document).ready(function() {
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);
if (isEditMode) {
// 수정 모드: PUT bulk
$.ajax({
url: `/api/purchase-receipts/${receiptId}/bulk`,
method: 'PUT',
contentType: 'application/json',
data: JSON.stringify({
notes: notes,
lines: lines
}),
success: function(response) {
if (response.success) {
alert('입고장이 수정되었습니다.');
$('#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> 수정 저장');
}
},
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> 입고 저장');
}
});
});
} else {
// 새 입고 모드: POST
$.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> 입고 저장');
}
});
}
});
// 입고장 업로드
@ -2635,6 +3099,14 @@ $(document).ready(function() {
typeLabel = '보정';
typeBadge = 'badge bg-warning';
break;
case 'RETURN':
typeLabel = '반환';
typeBadge = 'badge bg-info';
break;
case 'DISCARD':
typeLabel = '폐기';
typeBadge = 'badge bg-dark';
break;
default:
typeLabel = entry.event_type;
typeBadge = 'badge bg-secondary';

View File

@ -355,13 +355,17 @@
<!-- Formulas Page -->
<div id="formulas" class="main-content">
<!-- 내 처방 목록 -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h3>처방 관리</h3>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#formulaModal">
<i class="bi bi-plus-circle"></i> 새 처방 등록
</button>
</div>
<div class="card">
<div class="card mb-4">
<div class="card-header bg-white">
<h6 class="mb-0"><i class="bi bi-journal-medical"></i> 내 처방 목록</h6>
</div>
<div class="card-body">
<table class="table table-hover">
<thead>
@ -380,6 +384,35 @@
</table>
</div>
</div>
<!-- 100처방 원방 마스터 -->
<div class="card">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="bi bi-book"></i> 100처방 원방 마스터 <span class="badge bg-secondary" id="officialFormulaCount">0</span></h6>
<div style="width: 300px;">
<input type="text" class="form-control form-control-sm" id="officialFormulaSearch"
placeholder="처방명/출전 검색...">
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 500px; overflow-y: auto;">
<table class="table table-hover table-sm mb-0">
<thead class="table-light" style="position: sticky; top: 0; z-index: 1;">
<tr>
<th width="60">연번</th>
<th>처방명</th>
<th>한자명</th>
<th>출전</th>
<th width="80">상태</th>
</tr>
</thead>
<tbody id="officialFormulasList">
<!-- Dynamic content -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Compound Page -->
@ -1667,6 +1700,78 @@
</div>
</div>
<!-- 100처방 상세/참고자료 모달 -->
<div class="modal fade" id="officialFormulaModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-info text-white">
<h5 class="modal-title">
<i class="bi bi-book"></i>
<span id="ofModalNumber"></span>. <span id="ofModalName"></span>
<small id="ofModalHanja" class="ms-2"></small>
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label fw-bold">출전</label>
<p id="ofModalSource" class="text-muted">-</p>
</div>
<div class="col-md-6">
<label class="form-label fw-bold">한자명</label>
<input type="text" class="form-control form-control-sm" id="ofEditHanja" placeholder="예: 加味溫膽湯">
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">설명/효능</label>
<textarea class="form-control" id="ofEditDescription" rows="2" placeholder="처방의 주요 효능..."></textarea>
</div>
<!-- 원방 구성 약재 -->
<div class="mb-3" id="ofIngredientsSection" style="display:none;">
<label class="form-label fw-bold">
<i class="bi bi-list-ul"></i> 원방 구성
<span class="badge bg-secondary ms-1" id="ofIngredientCount">0</span>
<span class="badge bg-primary ms-1" id="ofTotalGrams">0</span>g/첩
</label>
<div class="table-responsive" style="max-height: 300px; overflow-y: auto;">
<table class="table table-sm table-hover mb-0">
<thead class="table-light" style="position: sticky; top: 0;">
<tr>
<th width="40">#</th>
<th>약재명</th>
<th width="80" class="text-end">첩당</th>
<th>역할</th>
</tr>
</thead>
<tbody id="ofIngredientsList">
</tbody>
</table>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">
<i class="bi bi-lightbulb"></i> 상담 참고자료
<small class="text-muted fw-normal">(OTC 대비 차별점, 구성 해설, 업셀링 포인트 등)</small>
</label>
<textarea class="form-control" id="ofEditReferenceNotes" rows="10"
placeholder="예: 경악전서의 가미패독산은 일반 패독산과 달리...&#10;&#10;• OTC 대비 차별점&#10;• 구성 약재 해설&#10;• 적응증 상세&#10;• 상담 시 활용 포인트"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" id="createFromOfficialBtn">
<i class="bi bi-plus-circle"></i> 내 처방으로 등록
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
<button type="button" class="btn btn-info text-white" id="saveOfficialFormulaBtn">
<i class="bi bi-check-lg"></i> 저장
</button>
</div>
</div>
</div>
</div>
<!-- Formula Detail Modal (처방 상세 모달) -->
<div class="modal fade" id="formulaDetailModal" tabindex="-1">
<div class="modal-dialog modal-xl">