알림봇-따라하기 시리즈 2편. 주기적으로 조회해서 "지난번 이후 새로 생긴 것"만 알리는 부분을 만든다. 예시는 온라인 판매 채널 세 곳의 새 주문·미답변 문의 알림이다. 모두 2026년 가을에 직접 붙이면서 확인한 내용이다.
상태 파일: 어디까지 봤는가
봇마다 작은 JSON 파일 하나를 볼륨에 둔다. 담는 것은 두 가지다.
{
"lastFrom": "2026-11-01T09:40:00+09:00",
"seen": { "주문번호:옵션번호": "2026-11-01T09:42:10+09:00" }
}
- 조회 시작 시각(
lastFrom): 다음 조회는 여기서부터 한다. - 이미 알린 것의 키(
seen): 조회 구간을 일부러 겹치게 잡아도 중복 알림이 나가지 않게 한다. 오래된 키는 주기적으로 지운다.
규칙은 세 가지다.
- 조회에 실패하면
lastFrom을 전진시키지 않는다. 실패한 구간을 건너뛰면 그 사이의 주문은 영영 알림이 안 온다. 다음 주기에 같은 구간을 다시 본다. - 알림 전송에 성공한 뒤에
seen에 넣는다. 반대로 하면 전송이 실패했을 때 그 건이 사라진다. - 중복 키는 "한 건"의 단위와 맞춘다. 주문 하나에 상품이 여럿이면 주문번호만으로는 부족하다.
주문번호:상품옵션번호처럼 합친다.
상태가 바뀌는 속도가 조회 주기보다 빠를 때
"결제 완료" 상태의 주문만 조회해 새 주문을 알렸는데, 가끔 알림 없이 지나가는 주문이 있었다. 판매자가 주문을 빨리 확인하면 20분 주기의 조회가 오기 전에 상태가 "상품 준비 중"으로 넘어가 버린 것이다. 다음 상태도 함께 조회하고 중복 키로 한 번만 알리게 해서 해결했다. 상태로 걸러 조회하는 API 는 그 상태에 머무는 시간이 내 조회 주기보다 짧을 수 있다는 점을 따져 봐야 한다.
목록은 한 번에 다 오지 않는다
다른 봇에서 겪은 일이다. 체결 내역 조회가 20건씩 끊겨 오는 API 였는데 첫 장만 읽고 있었다. 건수가 적을 때는 문제가 없다가 바쁜 날 21번째부터의 항목을 통째로 놓쳤다. 목록 API 를 붙일 때는 "다음 장이 있다"는 표시(연속 조회 헤더, 페이지 번호, 커서)를 먼저 찾아 끝까지 읽는다.
판매 채널별로 겪은 것
네이버 커머스API
- 토큰을 받을 때 클라이언트 시크릿을 그대로 보내지 않고 bcrypt 로 서명한 값을 보낸다. Node 내장 모듈에는 bcrypt 가 없어서 이 봇만 예외로 외부 라이브러리 하나를 썼다.
- cron 에서 컨테이너 안의 스크립트를 실행할 때, 스크립트를 이미지 밖(마운트한 폴더)에 두면 라이브러리 경로를 못 찾는다.
NODE_PATH로 이미지 안의node_modules를 가리켜야 했다.
11번가 셀러 OpenAPI
- 응답이 EUC-KR 로 인코딩된 XML 이다. UTF-8 로 읽으면 한글이 전부 깨진다. 바이트로 받아
new TextDecoder('euc-kr')로 디코드한다. - 주문 조회의 시각은 분 단위 12자리, 문의 조회는 일 단위 8자리로 형식이 다르고, 한 번에 볼 수 있는 기간도 각각 7일, 30일로 다르다.
- 문의 본문이 HTML 엔티티로 두 번 인코딩돼 온다. 한 번만 풀면
<같은 글자가 남는다. - 문의 목록에는 고객 문의와 시스템 안내가 섞여 있다. 처리 상태 코드로 "아직 답하지 않은 것"만 걸러서 알린다.
- 처음 실데이터가 들어온 한 건은 원본 XML 을 로그에 통째로 남기게 했다. 문서의 필드 이름이 실제와 같은지 확인하는 가장 빠른 방법이다.
쿠팡 오픈API
- 요청마다 HMAC-SHA256 서명을 만든다. 서명할 문자열은
시각 + 메서드 + 경로 + 쿼리이고, 쿼리는 물음표 없이 인코딩된 그대로 붙인다. 시각은 UTC 로yyMMddTHHmmssZ형식이다.const crypto = require('node:crypto'); function sign(method, path, query, accessKey, secretKey) { const d = new Date().toISOString().replace(/[-:]/g, '').slice(2, 15) + 'Z'; // yyMMddTHHmmssZ const sig = crypto.createHmac('sha256', secretKey).update(d + method + path + query).digest('hex'); return `CEA algorithm=HmacSHA256, access-key=${accessKey}, signed-date=${d}, signature=${sig}`; } - 호출하는 서버의 공인 IP 를 판매자 센터에 등록해야 한다. 등록하지 않으면 키가 맞아도 403 이 온다. 반대로 가짜 키로 부르면 401 이 오는데, 이것으로 경로와 헤더 형식이 맞는지는 키를 받기 전에도 확인할 수 있다.
- 분 단위 주문 조회는 범위가 24시간 미만이어야 한다. 넘으면 400.
- 날짜 파라미터는 시간대까지 써야 한다. 날짜만 보내면 형식 오류가 나고,
+09:00의+는 URL 에서%2B로 인코딩한다(안 하면 공백으로 해석된다). - 금액 필드가 숫자가 아니라
{통화, 단위, 나노}객체다. 예전 예제를 보고 숫자로 다루면 알림에[object Object]가 찍힌다. - 상품 문의 응답에는 상품 이름이 없고 상품 번호만 있다. 주문을 조회할 때 본 "번호 → 이름"을 상태 파일에 캐시해 두었다가 문의 알림에 쓴다.
시험 방법
--dry-run: 조회는 하되 채팅에 보내지 않고 상태도 저장하지 않는다. 몇 번을 돌려도 같은 결과가 나오므로 형식을 다듬기 좋다.- 상태 파일 경로를 환경 변수로 바꿀 수 있게 해 두면, 운영 상태를 건드리지 않고 "처음 실행" 상황을 재현할 수 있다.
- 첫 실행에서는 과거 것을 전부 알리지 않도록, 상태 파일이 없으면 "지금"부터 시작한다.
다음 편은 이 알림을 메신저로도 받는 방법이다. 시리즈 전체 목록은 여기.
댓글 없음:
댓글 쓰기