기타

토스플레이스 토스포스 영수증 인쇄 오류 해결기

iolate 2024. 11. 9. 14:26

(무려 3년 만의 포스팅.. 요즘엔 그냥 개인 노션에 정리만 하는 편...)

어쩌다보니 식당 한군데의 자잘한 일들을 봐주고 있다.
주로 네트워크, CCTV나 매장에서 메뉴판으로 쓰는 아이패드 관리 따위인데,,
어제는 매니저님에게 전화가 오더니 네트워크 어쩌구 하시더라.

이야기를 들어보니, 포스에서 주문을 넣으면 주방 등에 영수증으로 주문 내역이 출력되는데, 그 출력이 안되더란 이야기.
그래서 포스사 직원을 불렀는데 직원은 네트워크 문제일거라고 하셔서 나에게 도움 요청하신거였다.

내가 한건 아니지만,, 매장 구조상 한 곳으로 보인 랜선을 랜 커플러로 다시 다른 곳으로 보내서 연결이 주렁주렁된 터라 찔리는게 있어서 그러려니 하였고,, 그래도 현상이 이상해서 매장을 방문해봤다.

서론이 꽤나 길 것 같은데...

[토스 결제 시스템에 대한 배경 지식]

토스 플레이스는 위 사진과 같은 조합의 SW/장비들을 제공한다.
왼쪽은 포스 프로그램. 포스 프로그램이란게 늘 그렇듯 필수는 아니다. 있으면 매장 주문 관리 등이 조금 편해지는거고...
윈도우, 맥, 아이패드, 갤럭시 모든 플랫폼을 지원하는게 특이한 포인트 + 보통 사용 수수료를 받는데 토스 포스는 무료로 사용할 수 있다.
토스 포스 프로그램에 대해서도 써볼려면 글이 따로 하나 나올 것 같아서 하략...

그 다음은 토스 프론트. 주로 결제 단말기로 많이 사용하며 간단하게 메뉴를 표시하거나 사용자 입장에서 키오스크 주문기처럼 사용할 수도 있다. 안드로이드 기기임. 영수증 프린터 기능은 없기 때문에 유무선으로 다른 장비를 연결해줘야 한다. 보통 정말 결제만 되는 카드 단말기를 하나 백업으로 구비하고, 그 단말기가 영수증 프린터를 겸하기 때문에 거기다 연결하는 경우가 많은 것 같다.

그 다음은 터미널인데, 이번에 문제 생기면서 직원이 임시로 갖다줘서 처음 봤다. 프론트를 고객 쪽에 향하게 하고, 터미널은 안쪽 직원이 금액 입력, 영수증 출력 등의 용도로 사용하는 느낌인 듯. 생긴걸 보니 결제 단말기로써의 역할도 할 것 같다.

[토스 포스와 영수증 프린터]

토스 포스는 한 매장에서 동시에 여러개 실행할 수 있는데, 매장 현황(테이블 정보, 주문 내역) 등은 공유하지만 연결되는 결제 단말기나 프린터 정보는 공유되지 않는다.
다만, 한쪽에서 주문했을 프린터가 연결된 다른 기기가 켜져있다면, 그 기기가 주문 정보를 받아서 대신 인쇄를 해준다. 그래서 여러 기기에 걸쳐 프린터를 연결해두면 안됨. (이걸 모른 채로 자꾸 지운 프린터로 인쇄가 되어서 한참을 헤맸음ㅠ)

실행 중인 토스 포스에서 영수증 프린터로 인쇄 요청을 보내는 방식이기 때문에, 포스가 실행되는 장비 기준으로 시리얼/USB 연결된 프린터나, 블루투스, 와이파이 프린터로 인쇄시킬 수 있고, 토스 프론트와 연결하고 나면 토스 프론트에 유선 연결된 프린터로 인쇄시키는 것도 가능하다.
또한 A 프린터는 영수증만, B 프린터는 주방에 주문 내역을, C 프린터는 다른 곳에서 특정 주문만을 출력하는 등 프린터별로 인쇄 종류를 나눌 수 있다.

토스 터미널이 아니라면 인쇄는 ESC/POS 프로토콜로 인쇄 요청을 보내게 된다.

이 매장에서는 토스 프론트에 유선 연결된 결제 단말기 하나, ESC/POS 유선 이더넷 영수증 프린터 3대, 그리고 ESC/POS WiFi 프린터 1대를 사용 중이다.

세우테크 SLK-TS400W, 와이파이 영수증 프린터

[문제 상황]

다시 매장으로 돌아와서, 영수증 프린터 한대의 파손과 다른 프린터가 모두 작동을 하지 않아서 토스 플레이스 대리점 직원을 부른 상황.
직원 입장에선 토스 프론트와 직접 연결된 결제 단말기는 인쇄가 되고, 네트워크로 연결된 영수증 프린터가 작동하지 않으니 네트워크 탓을 할 수 밖에 없었을터다.

나도 현상이 너무 이상해서 이런저런 테스트를 해봤는데, 아무리 봐도 네트워크 문제는 아니였다.
그러다 내가 ESC/POS 프로토콜로 프린터에 직접 명령을 전송할 경우 인쇄가 잘 되는걸 발견하였다!!!

임시로 가져오신 토스 터미널 2대는 인쇄가 잘 되었다. 토스 터미널은 ESC/POS 프로토콜이 아니라 별도 방식을 사용하는 듯.

대리점에서 오신 분도 계속 네트워크만 의심하시다가, 내가 테스트한걸 보시더니 토스 포스 앱의 문제임을 99% 확신하시고 영상을 찍어가셨다. 토스 포스 앱이 갑자기 문제 생겼으리라곤 누가 의심했을까...
다른 매장에선 별도로 안된다는 리폿이 없어서 더 네트워크만 의심했던건데, 왜 다른 매장 리폿이 없었는지는 의문이다. 그냥 그러려니 하고 쓰거나... 토스 포스를 그만큼 안쓰거나....??

(수정) 다음날 대리점 직원으로부터 전달받았는데 다른 매장은 잘 된다고 한다... 저 매장도 전체 기기에서 프린터를 삭제하고 다시 등록하니 또 인쇄가 되기도 하는걸 발견....

대리점 직원분도 예전과 다르게 최근에는 매장에 설치해주러 갔을 때 프린터 연결이 잘 안되어서 힘들었는데, 포스 프로그램 탓이었던 것인가?! 하면서 한탄하시더라..ㅠㅠ 그건 아마 다른 문제일 것 같긴 하지만 그냥 그러려니 했다.

여튼 당장 할 수 있는게 없으니 둘 다 퇴근.

 

[해결 과정]

집에 가는 길에 문득 토스 포스에서 인쇄 요청을 어떻게 잘 못 보내는지 궁금해졌다.
위에서 얘기했듯, 포스 프로그램을 별도로 실행시키면 매장 영업에 큰 영향 없이 이런저런 테스트를 할 수 있다.
로컬에 소켓 서버를 열고, 뭐라고 오는지 받아봤다.

테스트 출력을 누르면 '테스트 프린트' 라고만 출력이 되는데, 그 요청을 받아봤다.
ESC/POS 프로토콜은 검색해봤음. ESC 는 0x1B 를, GS 는 0x1D 를 의미한다.
요청 받은 패킷을 해석하면 아래와 같다.

1B 52 0D
	ESC R n: Select international characters
    	n 13: Korea
1D 21 11
	GS ! n: Select character size
    	0 <= n <= 255
C5 D7 BD BA C6 AE 20 C7 C1 B8 B0 C6 AE 0A
	'테스트 프린트\n'
1B 21 00
	ESC ! n: Batch specify print mode
1D 21 00
	GS ! n: Select character size
1B 61 00
	ESC a n: Position alignment
    	00 or 48: left
        01 or 49: center
        02 or 50: right
1D 42 00
	GS B n: Specify/cancel white/black inverted printing

0A 0A 0A 0A 0A 0A
	'\n\n\n\n\n\n'
1D 56 31
	GS V: Cut paper
    
(아래 4개 중복)
1B 21 00
1D 21 00
1B 61 00
1D 42 00
(----------)
1B 40
	ESC @: Initialize printer

 

소켓을 제대로 못 열거나, 실수로 프린터 주소를 하드 코딩 해뒀거나.. 따위를 의심했는데 그런건 아니였다.
그저 소켓 연결 수립 직후 보내야하는 Initialize printer 패킷 (0x1B 0x40)을, 가장 먼저 보내지 않고 가장 늦게 보내고 있었다...ㅡㅡ;;

그래서 매장에 따로 오드로이드(라즈베리파이 같은거) 한대 꽂아두고, 파이썬 서버를 띄워서 1B 40 으로 시작하지 않는 인쇄 요청은 내가 붙여서 다시 프린터로 보내도록 간단하게 프로그램을 짜두고, 토스 포스 프린터 설정을 다 바꾸었다.

그러니 정상 작동!

코드는 오래 쓸게 아니니 대충 작성해서 돌렸다.

import socket
import binascii
import traceback
import os

PORT = int(os.environ.get('PORT', '9100'))
TARGET = '192.168.20.' + os.environ.get('TARGET', '250')

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('0.0.0.0', PORT))
sock.listen(10)

while True:
	conn, client_addr = sock.accept()
	print('connection from', client_addr)
	
	proxy = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	is_first = True

	try:
		proxy.connect((TARGET, 9100))
		while True:
			data = conn.recv(1024)
			
			if data:
				if is_first:
					is_first = False			
					if data[:2] != b'\x1b\x40':
						proxy.send(b'\x1b\x40')
				
				# print('received', binascii.hexlify(data, ' ').upper())
				proxy.send(data)
			else:
				# print('no more data from', client_addr)
				break
	except:
		traceback.print_exc()
	finally:
		try:
			conn.close()
			proxy.close()
		except: pass

 

[추가 문제]

인쇄가 잘 되어서 싱글벙글하고 있었는데,, 어느순간 또 먹통이 되는 문제가 발생..
토스 터미널은 여전히 잘 되고... 토스 포스 프로그램을 완전히 종료하고 나면 또 잘 된다.
영상을 올리고 싶은데... 매장명이 노출되는데 영상 편집은 귀찮으니 생략....

매장 직원들에게 물어보니 예전부터 자주 그랬다고.... 직원들은 맺힌게 많았는지 포스 안 만들던 회사가 만드니 너무 불안정하다고 아예 시스템을 갈아버렸으면 좋겠다고 얘기하시더라.... (하지만... 토스 프론트가 예쁜걸.... 매장 주문 내역을 가져갈 수 있는 API 같은 것도 전혀 없어서... 바꿔야 한다는 의견도 프론트 예쁘다 원툴로 버텼는걸....)

그동안 안되는게 네트워크 문제일 줄 알았는데 전부 토스 문제였다니... 허허....

안될 때는 프린터로 소켓 연결 수립 조차 안한다. 이건 내가 어떻게 할 수 없으니 GG...
그냥 안되면 포스 앱을 한번 종료했다가 다시 실행해달라고만 전해뒀다.


방문하셨던 담당자분에겐 이 문제를 전달해드렸는데... 이게 과연 토스 포스 개발팀에게 전달이 될지.. 되면 언제 수정이 될지...
토스 포스 앱 자체는 업데이트가 되게 잦던데, 금방 해결되길 바라본다.

문제를 파악하고 해결하는 과정이 나름 재밌기도 했고,
누군가는 또 오작동이 자기 매장의 문제일거라 생각하고 고통받고 있을 것 같아서,, 혹시나 하는 마음에 짧게 남겨 보았음.


(추가 수정) 프린터로 요청 자체를 보내지 않는 문제 때문에 사용을 거의 못하셨다고 한다.
그래서 오늘은 토스 터미널 요청을 역분석해서, 터미널로 등록 → ESC/POS 프린터로 전송하도록 수정하였다.

토스 터미널은 :14555 포트에 http 서버가 띄워져 있으며, 토스 포스 앱은 3개의 엔드포인트를 요청한다.
- GET /: 404 반환됨
- GET /sn: { "serialNumber": "..." } 반환. 아무 값이나 뱉어도 무관함
- POST /print: 인쇄 요청, { "data": "base64 encoded..", "count": 1 }

헤더로도 몇가지 오지만 일단 무시.

base64 인코딩 된 문자열은 ESC/POS 명령어셋과 거의 동일하지만, 지원 인코딩 셋이 좀 다르다.
코드 상으론 좀 더 다양하게 지원하는 것 같긴 한데, 일단 ESC R n 에서 n = 0x0D Korean 표준을 따르지 않고, 0x0F 를 사용, 이는 GB18030 셋으로 인코딩 된다. 중국어 코드 집합인데,, 한국어도 잘 됨.

조금 귀찮지만 GB18030 을 디코딩한 후, cp949 에 없는 캐릭터셋은 대충 ? 로 치환해준 뒤, cp949 로 보내면 된다.

이를 위해 ESC/POS 명령어셋 파싱이 필요한데, 일단 내가 확인한 바로는 아래 명령어셋만 사용함.
- ESC @, ESC R, ESC !, ESC a, ESC E
- GS !, GS B, GS V

추가로 따로 포트번호가 없기 때문에 여러 대의 프린터랑 연결하기가 어렵다.
서버에 IP 를 여러개 할당한 후, 요청의 Host 헤더를 보고 적절한 프린터로 연결되도록 수정함.
도메인 주소는 토스 포스 앱에서 올바른 주소로 인정해주지 않는다...ㅎㅎ;;

조금 더 코드가 길어졌지만... 일단 이렇게라도 잘 되길 바라며....