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:
159
backend/paai_printer.py
Normal file
159
backend/paai_printer.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""PAAI ESC/POS 프린터 모듈"""
|
||||
import json
|
||||
from datetime import datetime
|
||||
from escpos.printer import Network
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
# 프린터 설정
|
||||
PRINTER_IP = "192.168.0.174"
|
||||
PRINTER_PORT = 9100
|
||||
THERMAL_WIDTH = 576
|
||||
|
||||
|
||||
def print_paai_result(pre_serial: str, patient_name: str, analysis: dict, kims_summary: dict) -> dict:
|
||||
"""PAAI 분석 결과 인쇄"""
|
||||
try:
|
||||
# 이미지 생성
|
||||
img = create_receipt_image(pre_serial, patient_name, analysis, kims_summary)
|
||||
|
||||
# 프린터 연결 및 출력
|
||||
p = Network(PRINTER_IP, port=PRINTER_PORT, timeout=15)
|
||||
p.image(img)
|
||||
p.text('\n\n\n')
|
||||
p.cut()
|
||||
|
||||
return {'success': True, 'message': '인쇄 완료'}
|
||||
except Exception as e:
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
|
||||
def create_receipt_image(pre_serial: str, patient_name: str, analysis: dict, kims_summary: dict) -> Image:
|
||||
"""영수증 이미지 생성"""
|
||||
# 폰트
|
||||
try:
|
||||
font_title = ImageFont.truetype('malgun.ttf', 28)
|
||||
font_section = ImageFont.truetype('malgunbd.ttf', 20)
|
||||
font_normal = ImageFont.truetype('malgun.ttf', 18)
|
||||
font_small = ImageFont.truetype('malgun.ttf', 15)
|
||||
except:
|
||||
font_title = ImageFont.load_default()
|
||||
font_section = font_title
|
||||
font_normal = font_title
|
||||
font_small = font_title
|
||||
|
||||
width = THERMAL_WIDTH
|
||||
padding = 20
|
||||
y = padding
|
||||
|
||||
# 이미지 생성
|
||||
img = Image.new('RGB', (width, 1000), 'white')
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# 헤더
|
||||
draw.text((width//2, y), 'PAAI 복약안내', font=font_title, fill='black', anchor='mt')
|
||||
y += 40
|
||||
draw.line([(padding, y), (width-padding, y)], fill='black', width=1)
|
||||
y += 15
|
||||
|
||||
# 환자 정보
|
||||
draw.text((padding, y), f'환자: {patient_name}', font=font_normal, fill='black')
|
||||
y += 25
|
||||
draw.text((padding, y), f'처방번호: {pre_serial}', font=font_small, fill='black')
|
||||
y += 20
|
||||
now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
draw.text((padding, y), f'출력: {now_str}', font=font_small, fill='black')
|
||||
y += 25
|
||||
draw.line([(padding, y), (width-padding, y)], fill='black', width=1)
|
||||
y += 15
|
||||
|
||||
# 상호작용
|
||||
interaction_count = kims_summary.get('interaction_count', 0)
|
||||
has_severe = kims_summary.get('has_severe', False)
|
||||
|
||||
if has_severe:
|
||||
draw.text((padding, y), '[주의] 중증 상호작용 있음!', font=font_section, fill='black')
|
||||
elif interaction_count > 0:
|
||||
draw.text((padding, y), f'약물 상호작용: {interaction_count}건', font=font_normal, fill='black')
|
||||
else:
|
||||
draw.text((padding, y), '상호작용 없음', font=font_normal, fill='black')
|
||||
y += 30
|
||||
|
||||
# 처방 해석
|
||||
insight = analysis.get('prescription_insight', '')
|
||||
if insight:
|
||||
draw.text((padding, y), '[처방 해석]', font=font_section, fill='black')
|
||||
y += 28
|
||||
for line in wrap_text(insight, 40)[:3]:
|
||||
draw.text((padding, y), line, font=font_small, fill='black')
|
||||
y += 20
|
||||
y += 10
|
||||
|
||||
# 주의사항
|
||||
cautions = analysis.get('cautions', [])
|
||||
if cautions:
|
||||
draw.text((padding, y), '[복용 주의사항]', font=font_section, fill='black')
|
||||
y += 28
|
||||
for i, c in enumerate(cautions[:3], 1):
|
||||
for line in wrap_text(f'{i}. {c}', 40)[:2]:
|
||||
draw.text((padding, y), line, font=font_small, fill='black')
|
||||
y += 20
|
||||
y += 10
|
||||
|
||||
# 상담 포인트
|
||||
counseling = analysis.get('counseling_points', [])
|
||||
if counseling:
|
||||
draw.text((padding, y), '[상담 포인트]', font=font_section, fill='black')
|
||||
y += 28
|
||||
for i, c in enumerate(counseling[:2], 1):
|
||||
for line in wrap_text(f'{i}. {c}', 40)[:2]:
|
||||
draw.text((padding, y), line, font=font_small, fill='black')
|
||||
y += 20
|
||||
y += 10
|
||||
|
||||
# 푸터
|
||||
y += 10
|
||||
draw.line([(padding, y), (width-padding, y)], fill='black', width=1)
|
||||
y += 15
|
||||
draw.text((width//2, y), '양구청춘약국 PAAI', font=font_small, fill='black', anchor='mt')
|
||||
|
||||
return img.crop((0, 0, width, y + 30))
|
||||
|
||||
|
||||
def wrap_text(text: str, max_chars: int = 40) -> list:
|
||||
"""텍스트 줄바꿈"""
|
||||
lines = []
|
||||
words = text.split()
|
||||
current = ""
|
||||
|
||||
for word in words:
|
||||
if len(current) + len(word) + 1 <= max_chars:
|
||||
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[:max_chars]]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# CLI 테스트
|
||||
import sys
|
||||
if len(sys.argv) > 1:
|
||||
pre_serial = sys.argv[1]
|
||||
else:
|
||||
pre_serial = '20260305000075'
|
||||
|
||||
# 테스트 데이터
|
||||
analysis = {
|
||||
'prescription_insight': '테스트 처방입니다.',
|
||||
'cautions': ['주의사항 1', '주의사항 2'],
|
||||
'counseling_points': ['상담 포인트 1']
|
||||
}
|
||||
kims_summary = {'interaction_count': 0, 'has_severe': False}
|
||||
|
||||
result = print_paai_result(pre_serial, '테스트환자', analysis, kims_summary)
|
||||
print(result)
|
||||
Reference in New Issue
Block a user