- pos_printer.py: print_cusetc() 함수 추가 (ESC/POS 영수증 출력)
- admin.html: 회원 상세 모달에 🖨️ 인쇄 버튼 추가
- 즉시 피드백 토스트 + API 응답 후 결과 표시
170 lines
4.2 KiB
Python
170 lines
4.2 KiB
Python
# pos_printer.py - ESC/POS 영수증 프린터 유틸리티
|
|
# 0bin-label-app/src/pos_settings_dialog.py 기반
|
|
|
|
import socket
|
|
import logging
|
|
from datetime import datetime
|
|
|
|
# 프린터 설정 (config에서 불러올 수도 있음)
|
|
POS_PRINTER_IP = "192.168.0.174"
|
|
POS_PRINTER_PORT = 9100
|
|
POS_PRINTER_NAME = "올댓포스 오른쪽"
|
|
|
|
# ESC/POS 명령어
|
|
ESC = b'\x1b'
|
|
GS = b'\x1d'
|
|
|
|
# 기본 명령
|
|
INIT = ESC + b'@' # 프린터 초기화
|
|
CUT = ESC + b'd\x03' # 피드 + 커트 (원본 방식)
|
|
FEED = b'\n\n\n' # 줄바꿈
|
|
|
|
# 정렬
|
|
ALIGN_LEFT = ESC + b'a\x00'
|
|
ALIGN_CENTER = ESC + b'a\x01'
|
|
ALIGN_RIGHT = ESC + b'a\x02'
|
|
|
|
# 폰트 스타일
|
|
BOLD_ON = ESC + b'E\x01'
|
|
BOLD_OFF = ESC + b'E\x00'
|
|
DOUBLE_HEIGHT = ESC + b'!\x10'
|
|
DOUBLE_WIDTH = ESC + b'!\x20'
|
|
DOUBLE_SIZE = ESC + b'!\x30' # 가로세로 2배
|
|
NORMAL_SIZE = ESC + b'!\x00'
|
|
|
|
# 로깅
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
def print_raw(data: bytes, ip: str = None, port: int = None) -> bool:
|
|
"""
|
|
ESC/POS 바이트 데이터를 프린터로 전송
|
|
|
|
Args:
|
|
data: ESC/POS 명령어 + 텍스트 바이트
|
|
ip: 프린터 IP (기본값: POS_PRINTER_IP)
|
|
port: 프린터 포트 (기본값: POS_PRINTER_PORT)
|
|
|
|
Returns:
|
|
bool: 성공 여부
|
|
"""
|
|
ip = ip or POS_PRINTER_IP
|
|
port = port or POS_PRINTER_PORT
|
|
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(5)
|
|
sock.connect((ip, port))
|
|
sock.sendall(data)
|
|
sock.close()
|
|
logging.info(f"[POS Printer] 전송 성공: {ip}:{port}")
|
|
return True
|
|
except socket.timeout:
|
|
logging.error(f"[POS Printer] 연결 시간 초과: {ip}:{port}")
|
|
return False
|
|
except ConnectionRefusedError:
|
|
logging.error(f"[POS Printer] 연결 거부됨: {ip}:{port}")
|
|
return False
|
|
except Exception as e:
|
|
logging.error(f"[POS Printer] 전송 실패: {e}")
|
|
return False
|
|
|
|
|
|
def print_text(text: str, cut: bool = True) -> bool:
|
|
"""
|
|
텍스트를 영수증 프린터로 출력
|
|
|
|
Args:
|
|
text: 출력할 텍스트 (한글 지원)
|
|
cut: 출력 후 용지 커트 여부
|
|
|
|
Returns:
|
|
bool: 성공 여부
|
|
"""
|
|
try:
|
|
# EUC-KR 인코딩 (한글 지원)
|
|
text_bytes = text.encode('euc-kr', errors='replace')
|
|
|
|
# 명령어 조합
|
|
command = INIT + text_bytes + b'\n\n\n'
|
|
if cut:
|
|
command += CUT
|
|
|
|
return print_raw(command)
|
|
except Exception as e:
|
|
logging.error(f"[POS Printer] 텍스트 인쇄 실패: {e}")
|
|
return False
|
|
|
|
|
|
def print_cusetc(customer_name: str, cusetc: str, phone: str = None) -> bool:
|
|
"""
|
|
특이(참고)사항 영수증 출력 (단순 텍스트 방식)
|
|
|
|
Args:
|
|
customer_name: 고객 이름
|
|
cusetc: 특이사항 내용
|
|
phone: 전화번호 (선택)
|
|
|
|
Returns:
|
|
bool: 성공 여부
|
|
"""
|
|
now = datetime.now().strftime('%Y-%m-%d %H:%M')
|
|
|
|
# 전화번호 포맷팅
|
|
phone_display = ""
|
|
if phone:
|
|
phone_clean = phone.replace("-", "").replace(" ", "")
|
|
if len(phone_clean) == 11:
|
|
phone_display = f"{phone_clean[:3]}-{phone_clean[3:7]}-{phone_clean[7:]}"
|
|
else:
|
|
phone_display = phone
|
|
|
|
# 80mm 프린터 = 48자 기준
|
|
LINE = "=" * 48
|
|
THIN = "-" * 48
|
|
|
|
message = f"""
|
|
{LINE}
|
|
[ 특이사항 ]
|
|
{LINE}
|
|
고객: {customer_name}
|
|
"""
|
|
if phone_display:
|
|
message += f"연락처: {phone_display}\n"
|
|
|
|
message += f"""출력: {now}
|
|
{THIN}
|
|
{cusetc}
|
|
{LINE}
|
|
청춘약국
|
|
"""
|
|
|
|
return print_text(message, cut=True)
|
|
|
|
|
|
def test_print() -> bool:
|
|
"""테스트 인쇄"""
|
|
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
test_message = f"""
|
|
================================
|
|
POS 프린터 테스트
|
|
================================
|
|
|
|
IP: {POS_PRINTER_IP}
|
|
Port: {POS_PRINTER_PORT}
|
|
Time: {now}
|
|
|
|
청춘약국 마일리지 시스템
|
|
ESC/POS 정상 작동!
|
|
================================
|
|
"""
|
|
return print_text(test_message, cut=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# 테스트
|
|
print("POS 프린터 테스트 인쇄...")
|
|
result = test_print()
|
|
print(f"결과: {'성공' if result else '실패'}")
|