feat: 특이사항(CUSETC) 영수증 인쇄 기능 추가
- pos_printer.py: print_cusetc() 함수 추가 (ESC/POS 영수증 출력)
- admin.html: 회원 상세 모달에 🖨️ 인쇄 버튼 추가
- 즉시 피드백 토스트 + API 응답 후 결과 표시
This commit is contained in:
parent
5074adce20
commit
9ce7e884d7
@ -98,7 +98,7 @@ def print_text(text: str, cut: bool = True) -> bool:
|
|||||||
|
|
||||||
def print_cusetc(customer_name: str, cusetc: str, phone: str = None) -> bool:
|
def print_cusetc(customer_name: str, cusetc: str, phone: str = None) -> bool:
|
||||||
"""
|
"""
|
||||||
특이(참고)사항 영수증 출력
|
특이(참고)사항 영수증 출력 (단순 텍스트 방식)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
customer_name: 고객 이름
|
customer_name: 고객 이름
|
||||||
@ -119,66 +119,27 @@ def print_cusetc(customer_name: str, cusetc: str, phone: str = None) -> bool:
|
|||||||
else:
|
else:
|
||||||
phone_display = phone
|
phone_display = phone
|
||||||
|
|
||||||
try:
|
# 80mm 프린터 = 48자 기준
|
||||||
# ESC/POS 명령어 조합
|
LINE = "=" * 48
|
||||||
commands = bytearray()
|
THIN = "-" * 48
|
||||||
commands.extend(INIT)
|
|
||||||
|
|
||||||
# 헤더 (중앙 정렬, 크게)
|
message = f"""
|
||||||
commands.extend(ALIGN_CENTER)
|
{LINE}
|
||||||
commands.extend(DOUBLE_SIZE)
|
[ 특이사항 ]
|
||||||
commands.extend("[ 특이사항 ]\n".encode('euc-kr'))
|
{LINE}
|
||||||
commands.extend(NORMAL_SIZE)
|
고객: {customer_name}
|
||||||
|
"""
|
||||||
|
if phone_display:
|
||||||
|
message += f"연락처: {phone_display}\n"
|
||||||
|
|
||||||
# 구분선
|
message += f"""출력: {now}
|
||||||
commands.extend("================================\n".encode('euc-kr'))
|
{THIN}
|
||||||
|
{cusetc}
|
||||||
|
{LINE}
|
||||||
|
청춘약국
|
||||||
|
"""
|
||||||
|
|
||||||
# 고객 정보 (왼쪽 정렬)
|
return print_text(message, cut=True)
|
||||||
commands.extend(ALIGN_LEFT)
|
|
||||||
commands.extend(BOLD_ON)
|
|
||||||
commands.extend(f"고객: {customer_name}\n".encode('euc-kr'))
|
|
||||||
commands.extend(BOLD_OFF)
|
|
||||||
|
|
||||||
if phone_display:
|
|
||||||
commands.extend(f"연락처: {phone_display}\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
commands.extend(f"출력: {now}\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
# 구분선
|
|
||||||
commands.extend("--------------------------------\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
# 특이사항 내용 (굵게)
|
|
||||||
commands.extend(BOLD_ON)
|
|
||||||
|
|
||||||
# 긴 텍스트 줄바꿈 처리 (32자 기준)
|
|
||||||
lines = []
|
|
||||||
for line in cusetc.split('\n'):
|
|
||||||
while len(line) > 32:
|
|
||||||
lines.append(line[:32])
|
|
||||||
line = line[32:]
|
|
||||||
lines.append(line)
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
commands.extend(f"{line}\n".encode('euc-kr', errors='replace'))
|
|
||||||
|
|
||||||
commands.extend(BOLD_OFF)
|
|
||||||
|
|
||||||
# 하단 구분선
|
|
||||||
commands.extend("================================\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
# 약국명 (중앙 정렬)
|
|
||||||
commands.extend(ALIGN_CENTER)
|
|
||||||
commands.extend("청춘약국\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
# 피드 + 커트
|
|
||||||
commands.extend(b'\n\n\n')
|
|
||||||
commands.extend(CUT)
|
|
||||||
|
|
||||||
return print_raw(bytes(commands))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"[POS Printer] 특이사항 인쇄 실패: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def test_print() -> bool:
|
def test_print() -> bool:
|
||||||
|
|||||||
@ -20,6 +20,41 @@
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 토스트 알림 */
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.toast {
|
||||||
|
padding: 14px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
animation: toastIn 0.3s ease, toastOut 0.3s ease 2.7s forwards;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.toast.success { background: linear-gradient(135deg, #10b981 0%, #059669 100%); }
|
||||||
|
.toast.error { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); }
|
||||||
|
.toast.info { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); }
|
||||||
|
.toast.printing { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); }
|
||||||
|
@keyframes toastIn {
|
||||||
|
from { opacity: 0; transform: translateX(100px); }
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
@keyframes toastOut {
|
||||||
|
from { opacity: 1; transform: translateX(0); }
|
||||||
|
to { opacity: 0; transform: translateX(100px); }
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||||
padding: 32px 24px;
|
padding: 32px 24px;
|
||||||
@ -944,31 +979,53 @@
|
|||||||
if (editBtn) editBtn.style.display = 'inline-block';
|
if (editBtn) editBtn.style.display = 'inline-block';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 특이사항 인쇄
|
// 토스트 알림 함수
|
||||||
async function printCusetc(customerName, cusetc, phone) {
|
function showToast(message, type = 'info') {
|
||||||
if (!confirm(`${customerName}님의 특이사항을 인쇄하시겠습니까?`)) {
|
let container = document.querySelector('.toast-container');
|
||||||
return;
|
if (!container) {
|
||||||
|
container = document.createElement('div');
|
||||||
|
container.className = 'toast-container';
|
||||||
|
document.body.appendChild(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast ${type}`;
|
||||||
|
|
||||||
|
const icons = { success: '✅', error: '❌', info: 'ℹ️', printing: '🖨️' };
|
||||||
|
toast.innerHTML = `<span>${icons[type] || ''}</span><span>${message}</span>`;
|
||||||
|
|
||||||
|
container.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => toast.remove(), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 인쇄용 전역 변수
|
||||||
|
let printData = { name: '', cusetc: '', phone: '' };
|
||||||
|
|
||||||
|
// 특이사항 인쇄 실행
|
||||||
|
async function doPrintCusetc() {
|
||||||
|
// 즉시 피드백
|
||||||
|
showToast(`${printData.name}님 특이사항 인쇄 중...`, 'printing');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/print/cusetc', {
|
const res = await fetch('/api/print/cusetc', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
customer_name: customerName,
|
customer_name: printData.name,
|
||||||
cusetc: cusetc,
|
cusetc: printData.cusetc,
|
||||||
phone: phone
|
phone: printData.phone
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert('🖨️ ' + data.message);
|
showToast(data.message, 'success');
|
||||||
} else {
|
} else {
|
||||||
alert('❌ 인쇄 실패: ' + (data.error || '알 수 없는 오류'));
|
showToast('인쇄 실패: ' + (data.error || '알 수 없는 오류'), 'error');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('❌ 오류: ' + err.message);
|
showToast('오류: ' + err.message, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1015,7 +1072,7 @@
|
|||||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
|
||||||
<span style="color: #d97706; font-size: 13px;">⚠️ 특이사항</span>
|
<span style="color: #d97706; font-size: 13px;">⚠️ 특이사항</span>
|
||||||
<button onclick="editCusetc('${data.pos_customer.cuscode}', this)" style="background: none; border: 1px solid #d97706; color: #d97706; font-size: 11px; padding: 2px 8px; border-radius: 4px; cursor: pointer;">✏️ 수정</button>
|
<button onclick="editCusetc('${data.pos_customer.cuscode}', this)" style="background: none; border: 1px solid #d97706; color: #d97706; font-size: 11px; padding: 2px 8px; border-radius: 4px; cursor: pointer;">✏️ 수정</button>
|
||||||
${data.pos_customer.cusetc ? `<button onclick="printCusetc('${data.pos_customer.name}', '${(data.pos_customer.cusetc || '').replace(/'/g, "\\'")}', '${user.phone || ''}')" style="background: none; border: 1px solid #6b7280; color: #6b7280; font-size: 11px; padding: 2px 8px; border-radius: 4px; cursor: pointer;">🖨️ 인쇄</button>` : ''}
|
${data.pos_customer.cusetc ? `<button onclick="printData={name:'${data.pos_customer.name}',cusetc:decodeURIComponent('${encodeURIComponent(data.pos_customer.cusetc)}'),phone:'${user.phone||''}'};doPrintCusetc()" style="background: none; border: 1px solid #6b7280; color: #6b7280; font-size: 11px; padding: 2px 8px; border-radius: 4px; cursor: pointer;">🖨️ 인쇄</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div id="cusetc-view" onclick="toggleCusetc(this)" style="color: #92400e; font-size: 14px; font-weight: 500; cursor: ${(data.pos_customer.cusetc || '').length > 30 ? 'pointer' : 'default'}; ${(data.pos_customer.cusetc || '').length > 30 ? 'max-height: 40px; overflow: hidden;' : ''}" title="${(data.pos_customer.cusetc || '').length > 30 ? '클릭하여 펼치기' : ''}">
|
<div id="cusetc-view" onclick="toggleCusetc(this)" style="color: #92400e; font-size: 14px; font-weight: 500; cursor: ${(data.pos_customer.cusetc || '').length > 30 ? 'pointer' : 'default'}; ${(data.pos_customer.cusetc || '').length > 30 ? 'max-height: 40px; overflow: hidden;' : ''}" title="${(data.pos_customer.cusetc || '').length > 30 ? '클릭하여 펼치기' : ''}">
|
||||||
${data.pos_customer.cusetc || '<span style="color: #9ca3af; font-weight: normal;">없음</span>'}
|
${data.pos_customer.cusetc || '<span style="color: #9ca3af; font-weight: normal;">없음</span>'}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user