feat: ESC/POS 영수증 프린터로 특이사항 인쇄 기능

- pos_printer.py: ESC/POS 유틸리티 (192.168.0.174:9100)
- POST /api/print/cusetc API 추가
- admin.html: 특이사항 옆 [🖨️ 인쇄] 버튼 추가
- EUC-KR 인코딩으로 한글 지원
This commit is contained in:
thug0bin
2026-03-04 11:46:46 +09:00
parent 50825c597e
commit 5074adce20
3 changed files with 275 additions and 0 deletions

208
backend/pos_printer.py Normal file
View File

@@ -0,0 +1,208 @@
# 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
try:
# ESC/POS 명령어 조합
commands = bytearray()
commands.extend(INIT)
# 헤더 (중앙 정렬, 크게)
commands.extend(ALIGN_CENTER)
commands.extend(DOUBLE_SIZE)
commands.extend("[ 특이사항 ]\n".encode('euc-kr'))
commands.extend(NORMAL_SIZE)
# 구분선
commands.extend("================================\n".encode('euc-kr'))
# 고객 정보 (왼쪽 정렬)
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:
"""테스트 인쇄"""
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 '실패'}")