feat: PAAI 자동인쇄 기능 완성 (EUC-KR 텍스트 방식)

추가:
- 자동인쇄 ON/OFF 토글 (헤더)
- ESC/POS 영수증 인쇄 (EUC-KR 인코딩)
- ESCPOS_TROUBLESHOOTING.md 트러블슈팅 문서

핵심 변경:
- 이미지 방식 -> 텍스트 방식 (socket 직접 전송)
- UTF-8 -> EUC-KR 인코딩
- 이모지 제거 ([V], [!], >> 사용)
- 48자 기준 줄바꿈

인쇄 흐름:
1. PAAI 분석 완료
2. 자동인쇄 ON이면 /pmr/api/paai/print 호출
3. _format_paai_receipt()로 텍스트 생성
4. _print_escpos_text()로 프린터 전송

참고: docs/ESCPOS_TROUBLESHOOTING.md
This commit is contained in:
thug0bin
2026-03-05 12:19:56 +09:00
parent 7ac3f7a8b4
commit 0b17139daa
6 changed files with 939 additions and 27 deletions

View File

@@ -33,6 +33,59 @@
align-items: center;
gap: 15px;
}
.auto-controls {
display: flex;
gap: 8px;
}
.status-badge {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.status-badge .status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-badge.disconnected {
background: #fef2f2;
color: #dc2626;
}
.status-badge.disconnected .status-dot {
background: #ef4444;
}
.status-badge.connected {
background: #ecfdf5;
color: #059669;
}
.status-badge.connected .status-dot {
background: #10b981;
}
.status-badge.auto-print-off {
background: #f3f4f6;
color: #6b7280;
}
.status-badge.auto-print-off .status-dot {
background: #9ca3af;
}
.status-badge.auto-print-on {
background: #dbeafe;
color: #2563eb;
}
.status-badge.auto-print-on .status-dot {
background: #3b82f6;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.date-picker {
padding: 8px 15px;
border: 2px solid #8b5cf6;
@@ -940,6 +993,17 @@
<header class="header">
<h1>💊 조제관리 <span>청춘라벨 v2</span></h1>
<div class="controls">
<!-- 자동감지/자동인쇄 상태 -->
<div class="auto-controls">
<div id="triggerIndicator" class="status-badge disconnected">
<span class="status-dot"></span>
자동감지 OFF
</div>
<div id="autoPrintToggle" class="status-badge auto-print-off" onclick="toggleAutoPrint()">
<span class="status-dot"></span>
자동인쇄 OFF
</div>
</div>
<input type="date" id="dateSelect" class="date-picker">
<div class="stats-box">
<div class="stat-item">
@@ -1947,6 +2011,9 @@
// 토스트 알림
showPaaiToast(patientName, 'PAAI 분석 완료! 클릭하여 확인', 'success', preSerial);
// 자동인쇄 (활성화된 경우)
printPaaiResult(preSerial, patientName, result);
} else {
showPaaiToast(patientName, '분석 실패: ' + (result.error || '알 수 없는 오류'), 'error', preSerial);
}
@@ -2435,6 +2502,13 @@
true // clickable
);
playTriggerSound();
// 자동인쇄 (활성화된 경우)
printPaaiResult(data.pre_serial, data.patient_name, {
success: true,
analysis: data.analysis,
kims_summary: data.kims_summary
});
break;
case 'analysis_failed':
showTriggerToast(data.pre_serial, data.patient_name, 'failed', data.error || '분석 실패', '❌');
@@ -2562,39 +2636,75 @@
// 연결 상태 표시
function updateTriggerIndicator(isConnected) {
let indicator = document.getElementById('triggerIndicator');
if (!indicator) {
const controls = document.querySelector('.header .controls');
if (controls) {
indicator = document.createElement('div');
indicator.id = 'triggerIndicator';
indicator.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: #f1f5f9;
border-radius: 8px;
font-size: 0.8rem;
color: #64748b;
`;
controls.insertBefore(indicator, controls.firstChild);
}
}
const indicator = document.getElementById('triggerIndicator');
if (indicator) {
indicator.className = `status-badge ${isConnected ? 'connected' : 'disconnected'}`;
indicator.innerHTML = `
<span style="
width: 8px;
height: 8px;
border-radius: 50%;
background: ${isConnected ? '#10b981' : '#ef4444'};
"></span>
${isConnected ? '자동감지 ON' : '자동감지 OFF'}
<span class="status-dot"></span>
자동감지 ${isConnected ? 'ON' : 'OFF'}
`;
}
}
// ═══════════════════════════════════════════════════════════════
// 자동인쇄 기능
// ═══════════════════════════════════════════════════════════════
let autoPrintEnabled = localStorage.getItem('pmr_auto_print') === 'true';
// 초기화
function initAutoPrint() {
updateAutoPrintIndicator();
}
// 토글
function toggleAutoPrint() {
autoPrintEnabled = !autoPrintEnabled;
localStorage.setItem('pmr_auto_print', autoPrintEnabled);
updateAutoPrintIndicator();
showToast(autoPrintEnabled ? '🖨️ 자동인쇄 ON' : '🖨️ 자동인쇄 OFF', autoPrintEnabled ? 'success' : 'info');
}
// 표시 업데이트
function updateAutoPrintIndicator() {
const toggle = document.getElementById('autoPrintToggle');
if (toggle) {
toggle.className = `status-badge ${autoPrintEnabled ? 'auto-print-on' : 'auto-print-off'}`;
toggle.innerHTML = `
<span class="status-dot"></span>
자동인쇄 ${autoPrintEnabled ? 'ON' : 'OFF'}
`;
}
}
// PAAI 결과 인쇄
async function printPaaiResult(preSerial, patientName, result) {
if (!autoPrintEnabled) return;
try {
const response = await fetch('/pmr/api/paai/print', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
pre_serial: preSerial,
patient_name: patientName,
result: result
})
});
const data = await response.json();
if (data.success) {
console.log('[AutoPrint] 인쇄 완료:', preSerial);
} else {
console.error('[AutoPrint] 인쇄 실패:', data.error);
}
} catch (err) {
console.error('[AutoPrint] 오류:', err);
}
}
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', initAutoPrint);
// 알림 소리
function playTriggerSound() {
try {