2026년 11월 13일 금요일

내 일을 대신 지켜보는 알림 봇 만들기 (2) 새 것만 알리기와 오픈마켓 API 함정

알림봇-따라하기 시리즈 2편. 주기적으로 조회해서 "지난번 이후 새로 생긴 것"만 알리는 부분을 만든다. 예시는 온라인 판매 채널 세 곳의 새 주문·미답변 문의 알림이다. 모두 2026년 가을에 직접 붙이면서 확인한 내용이다.

상태 파일: 어디까지 봤는가

봇마다 작은 JSON 파일 하나를 볼륨에 둔다. 담는 것은 두 가지다.

{
  "lastFrom": "2026-11-01T09:40:00+09:00",
  "seen": { "주문번호:옵션번호": "2026-11-01T09:42:10+09:00" }
}
  • 조회 시작 시각(lastFrom): 다음 조회는 여기서부터 한다.
  • 이미 알린 것의 키(seen): 조회 구간을 일부러 겹치게 잡아도 중복 알림이 나가지 않게 한다. 오래된 키는 주기적으로 지운다.

규칙은 세 가지다.

  1. 조회에 실패하면 lastFrom 을 전진시키지 않는다. 실패한 구간을 건너뛰면 그 사이의 주문은 영영 알림이 안 온다. 다음 주기에 같은 구간을 다시 본다.
  2. 알림 전송에 성공한 뒤에 seen 에 넣는다. 반대로 하면 전송이 실패했을 때 그 건이 사라진다.
  3. 중복 키는 "한 건"의 단위와 맞춘다. 주문 하나에 상품이 여럿이면 주문번호만으로는 부족하다. 주문번호:상품옵션번호 처럼 합친다.

상태가 바뀌는 속도가 조회 주기보다 빠를 때

"결제 완료" 상태의 주문만 조회해 새 주문을 알렸는데, 가끔 알림 없이 지나가는 주문이 있었다. 판매자가 주문을 빨리 확인하면 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: 조회는 하되 채팅에 보내지 않고 상태도 저장하지 않는다. 몇 번을 돌려도 같은 결과가 나오므로 형식을 다듬기 좋다.
  • 상태 파일 경로를 환경 변수로 바꿀 수 있게 해 두면, 운영 상태를 건드리지 않고 "처음 실행" 상황을 재현할 수 있다.
  • 첫 실행에서는 과거 것을 전부 알리지 않도록, 상태 파일이 없으면 "지금"부터 시작한다.

다음 편은 이 알림을 메신저로도 받는 방법이다. 시리즈 전체 목록은 여기.

댓글 없음:

댓글 쓰기

반도체 주식을 보기 전에 그려 두는 지도 — 설계, 파운드리, 메모리, 장비, 소재, 후공정

"반도체주"라고 한 묶음으로 부르지만, 그 안의 회사들은 하는 일도 돈 버는 방식도 전혀 다르다. 같은 뉴스에 어떤 회사는 오르고 어떤 회사는 내리는 이유가 여기에 있다. 종목을 들여다보기 전에 머릿속에 그려 두면 좋은 지도를 정리한...