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

@@ -1443,3 +1443,193 @@ def paai_admin_feedback_stats():
except Exception as e:
logging.error(f"피드백 통계 조회 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
# ─────────────────────────────────────────────────────────────
# ESC/POS 자동인쇄 API (EUC-KR 텍스트 방식)
# ─────────────────────────────────────────────────────────────
import socket
# 프린터 설정
ESCPOS_PRINTER_IP = "192.168.0.174"
ESCPOS_PRINTER_PORT = 9100
# ESC/POS 명령어
_ESC = b'\x1b'
_INIT = _ESC + b'@' # 프린터 초기화
_CUT = _ESC + b'd\x03' # 피드 + 커트
@pmr_bp.route('/api/paai/print', methods=['POST'])
def paai_print():
"""PAAI 분석 결과 ESC/POS 인쇄"""
try:
data = request.get_json()
pre_serial = data.get('pre_serial', '')
patient_name = data.get('patient_name', '')
result = data.get('result', {})
analysis = result.get('analysis', {})
kims_summary = result.get('kims_summary', {})
# 영수증 텍스트 생성
message = _format_paai_receipt(pre_serial, patient_name, analysis, kims_summary)
# 인쇄
success = _print_escpos_text(message)
if success:
logging.info(f"PAAI 인쇄 완료: {pre_serial} ({patient_name})")
return jsonify({'success': True, 'message': '인쇄 완료'})
else:
return jsonify({'success': False, 'error': '프린터 연결 실패'}), 500
except Exception as e:
logging.error(f"PAAI 인쇄 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
def _print_escpos_text(message: str) -> bool:
"""ESC/POS 프린터로 텍스트 전송 (EUC-KR)"""
try:
# EUC-KR 인코딩
text_bytes = message.encode('euc-kr', errors='replace')
# 명령어 조합
command = _INIT + text_bytes + b'\n\n\n' + _CUT
# 소켓 전송
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10)
sock.connect((ESCPOS_PRINTER_IP, ESCPOS_PRINTER_PORT))
sock.sendall(command)
sock.close()
return True
except Exception as e:
logging.error(f"ESC/POS 전송 실패: {e}")
return False
def _format_paai_receipt(pre_serial: str, patient_name: str,
analysis: dict, kims_summary: dict) -> str:
"""PAAI 복약안내 영수증 텍스트 생성 (48자 기준)"""
LINE = "=" * 48
THIN = "-" * 48
now = datetime.now().strftime("%Y-%m-%d %H:%M")
# 헤더
msg = f"\n{LINE}\n"
msg += _center_text("[ PAAI 복약안내 ]") + "\n"
msg += f"{LINE}\n"
# 환자 정보
msg += f"환자: {patient_name}\n"
msg += f"처방번호: {pre_serial}\n"
msg += f"출력: {now}\n"
msg += f"{THIN}\n"
# 상호작용 요약
interaction_count = kims_summary.get('interaction_count', 0)
has_severe = kims_summary.get('has_severe', False)
if has_severe:
msg += "[!!] 중증 상호작용 있음!\n"
elif interaction_count > 0:
msg += f"[!] 약물 상호작용: {interaction_count}\n"
else:
msg += "[V] 상호작용 없음\n"
msg += "\n"
# 처방 해석
insight = analysis.get('prescription_insight', '')
if insight:
msg += f"{THIN}\n"
msg += ">> 처방 해석\n"
for line in _wrap_text(insight, 44):
msg += f" {line}\n"
msg += "\n"
# 복용 주의사항
cautions = analysis.get('cautions', [])
if cautions:
msg += f"{THIN}\n"
msg += ">> 복용 주의사항\n"
for i, caution in enumerate(cautions[:4], 1):
first_line = True
for line in _wrap_text(f"{i}. {caution}", 44):
if first_line:
msg += f" {line}\n"
first_line = False
else:
msg += f" {line}\n"
msg += "\n"
# 상담 포인트
counseling = analysis.get('counseling_points', [])
if counseling:
msg += f"{THIN}\n"
msg += ">> 상담 포인트\n"
for i, point in enumerate(counseling[:3], 1):
first_line = True
for line in _wrap_text(f"{i}. {point}", 44):
if first_line:
msg += f" {line}\n"
first_line = False
else:
msg += f" {line}\n"
msg += "\n"
# OTC 추천
otc_recs = analysis.get('otc_recommendations', [])
if otc_recs:
msg += f"{THIN}\n"
msg += ">> OTC 추천\n"
for rec in otc_recs[:2]:
product = rec.get('product', '')
reason = rec.get('reason', '')
msg += f" - {product}\n"
for line in _wrap_text(reason, 42):
msg += f" {line}\n"
msg += "\n"
# 푸터
msg += f"{LINE}\n"
msg += _center_text("양구청춘약국 PAAI") + "\n"
msg += _center_text("Tel: 033-481-5222") + "\n"
msg += "\n"
return msg
def _center_text(text: str, width: int = 48) -> str:
"""중앙 정렬"""
text_len = len(text)
if text_len >= width:
return text
spaces = (width - text_len) // 2
return " " * spaces + text
def _wrap_text(text: str, width: int = 44) -> list:
"""텍스트 줄바꿈"""
if not text:
return []
lines = []
words = text.split()
current = ""
for word in words:
if len(current) + len(word) + 1 <= width:
current = current + " " + word if current else word
else:
if current:
lines.append(current)
current = word
if current:
lines.append(current)
return lines if lines else [text[:width]]