- TypeScript 100%
PaySync API에서 POST /v2/cash-receipts/{id}/send-sms가 제거되어
문자 전송 라우트, 서비스 메서드, 요청 모델, 테스트, README 항목을 삭제합니다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|---|---|---|
| src | ||
| test | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.build.json | ||
| tsconfig.json | ||
PaySync 연동 예제 (TypeScript + Express)
PaySync v2 REST API를 호출하고 PaySync v2 웹훅을 받는 가맹점 백엔드 예제입니다. 주문, 현금영수증, 입금 내역 API를 그대로 중계하고, 웹훅은 Standard Webhooks 서명을 검증한 뒤 처리합니다. API 명세는 PaySync API 문서를 참고합니다.
요구사항
- Node.js 24 이상
- npm
- PaySync API 키와 웹훅 서명 키 (PaySync 대시보드에서 발급)
설정
.env.example을 .env로 복사한 뒤 값을 채웁니다. .env는 Git에 올리지 않습니다.
cp .env.example .env
| 환경 변수 | 필수 | 기본값 | 설명 |
|---|---|---|---|
PAYSYNC_BASE_URL |
https://api.paysync.kr |
PaySync API 주소 | |
PAYSYNC_API_KEY |
O | API 키. Authorization: Bearer <키>로 전송합니다. |
|
PAYSYNC_WEBHOOK_SIGNING_SECRET |
O | 웹훅 서명 키 (whsec_...) |
|
PORT |
8080 |
서버 포트 |
실행
npm install
npm run dev # 개발 모드 (파일 변경 시 자동 재시작)
npm run build # dist/로 컴파일
npm start # 컴파일된 서버 실행
npm test # 테스트 (PaySync API는 가짜 응답으로 대체)
npm run typecheck # 타입 검사
엔드포인트
| 메서드 | 경로 | PaySync API | 응답 |
|---|---|---|---|
| POST | /api/invoices |
POST /v2/invoices |
201 주문 |
| GET | /api/invoices |
GET /v2/invoices |
200 { items, totalItems } |
| GET | /api/invoices/{id} |
GET /v2/invoices/{id} |
200 주문 |
| PATCH | /api/invoices/{id} |
PATCH /v2/invoices/{id} |
200 주문 |
| POST | /api/invoices/{id}/mark-paid |
POST /v2/invoices/{id}/mark-paid |
200 주문 |
| POST | /api/invoices/{id}/cancel |
POST /v2/invoices/{id}/cancel |
200 주문 |
| POST | /api/cash-receipts |
POST /v2/cash-receipts |
201 현금영수증 |
| GET | /api/cash-receipts |
GET /v2/cash-receipts |
200 { items, totalItems } |
| GET | /api/cash-receipts/{id} |
GET /v2/cash-receipts/{id} |
200 현금영수증 |
| POST | /api/cash-receipts/{id}/cancel |
POST /v2/cash-receipts/{id}/cancel |
200 취소 영수증 |
| POST | /api/cash-receipts/{id}/reissue |
POST /v2/cash-receipts/{id}/reissue |
201 새 현금영수증 |
| GET | /api/cash-receipts/{id}/urls |
GET /v2/cash-receipts/{id}/urls |
200 { view, print, pdf } |
| POST | /api/cash-receipts/{id}/send-email |
POST /v2/cash-receipts/{id}/send-email |
204 |
| GET | /api/transactions |
GET /v2/transactions |
200 { items, totalItems } |
| GET | /api/transactions/{id} |
GET /v2/transactions/{id} |
200 입금 내역 |
| POST | /api/webhooks |
(PaySync가 호출) | 200 / 검증 실패 시 400 |
PaySync는 주문 생성에도 HTTP 200과 code: "10001"을 돌려주지만, 이 예제는 생성 API에 201로 응답합니다.
응답의 시각은 Unix 초 정수입니다.
목록 조회 쿼리 파라미터 (offset 기본값 0, limit 기본값 20, 나머지는 선택이며 보낸 값만 전달합니다):
/api/invoices:offset,limit,search,status,due,source,createdBy,dateFrom,dateTo/api/cash-receipts:offset,limit,search,status,type,method,kind,invoiceId,dateFrom,dateTo/api/transactions:offset,limit,search,bankAccountId,status,matchMethod,dateFrom,dateTo
dateFrom, dateTo는 YYYY-MM-DD 형식의 한국 시간 날짜이며 두 날짜 모두 포함합니다.
status, source, bankAccountId는 쉼표로 여러 값을 보낼 수 있습니다 (예: status=PENDING,EXPIRED).
오류 응답은 { "code": "...", "message": "...", "data": ... } 형식입니다. data는 PaySync가 함께 준 경우에만 있습니다.
- 요청 값이 올바르지 않으면
400 BAD_REQUEST - PaySync가 4xx와
code를 돌려주면 같은 상태와code,data(예:404 "20014",409 "220010"과 기존 주문의data.invoiceId) - 그 밖의 PaySync 오류(5xx, JSON이 아닌 응답,
10001이 아닌 200 응답)나 연결 실패는502(code는 PaySync가 준 값 또는UPSTREAM_ERROR)
코드 목록은 에러 코드를 참고합니다.
요청 예시:
curl -X POST http://localhost:8080/api/invoices \
-H 'Content-Type: application/json' \
-d '{"customer":{"name":"홍길동"},"amount":50000,"expireAfterDays":1,"metadata":{"orderId":"ORDER-0001"}}'
주문 수정은 보낸 필드만 바뀝니다. null을 보내면 그 값을 지우므로 본문을 그대로 PaySync에 전달합니다.
curl -X PATCH http://localhost:8080/api/invoices/ivc_xxx \
-H 'Content-Type: application/json' \
-d '{"extendDays":3,"customer":{"email":null}}'
웹훅 로컬 테스트
PaySync는 localhost로 웹훅을 보낼 수 없으므로 ngrok 같은 터널로 로컬 서버를 외부에 엽니다.
npm run dev
ngrok http 8080
PaySync 대시보드의 웹훅 페이지에서 웹훅 추가를 누르고, 출력된 주소 뒤에 경로를 붙여(예: https://abcd-1234.ngrok-free.app/api/webhooks) 받을 URL로 입력합니다. API 버전은 v2를 고릅니다.
웹훅 목록의 보기에서 서명 키(whsec_...)를 확인해 PAYSYNC_WEBHOOK_SIGNING_SECRET에 넣은 뒤 서버를 다시 시작합니다.
주문을 만들면 invoice.created 웹훅이 오고, 서버 로그에 정상 웹훅이 수신되었습니다: evt_... (invoice.created)가 찍힙니다.
터널 없이 직접 서명한 요청을 보내 볼 수도 있습니다. .env의 서명 키로 서명해서 전송합니다.
node --env-file=.env --input-type=module -e '
import { Webhook } from "standardwebhooks";
const body = JSON.stringify({ id: "evt_local", type: "invoice.paid", timestamp: "2026-04-28T03:20:12Z", data: { id: "ivc_test", paidTrigger: "API_CALL" } });
const id = "whm_local", timestamp = new Date();
const signature = new Webhook(process.env.PAYSYNC_WEBHOOK_SIGNING_SECRET).sign(id, timestamp, body);
const res = await fetch("http://localhost:8080/api/webhooks", {
method: "POST",
headers: { "webhook-id": id, "webhook-timestamp": String(Math.floor(timestamp.getTime() / 1000)), "webhook-signature": signature },
body,
});
console.log(res.status);
'
200이 출력되면 성공입니다. PaySync는 200~299 응답을 성공으로 기록하며, 이 예제는 200을 돌려줍니다. 서명이 틀리면 400을 돌려주고 웹훅 검증에 실패했습니다 경고를 남깁니다.
v2 웹훅 본문은 { "id": "evt_...", "type": "...", "timestamp": "<ISO 8601>", "data": { ... } } 형식입니다.
invoice.* 이벤트의 data는 주문이고, invoice.paid의 결제 경로는 data.paidTrigger에 옵니다. transaction.created의 data는 입금입니다. 웹훅 페이로드의 시각은 ISO 8601 문자열입니다.
재시도와 재전송 때문에 같은 이벤트가 여러 번 올 수 있으므로 id(evt_...)로 중복을 걸러야 하며, 이벤트 순서는 보장되지 않습니다.
이벤트별 처리는 src/webhook/service.ts의 dispatch 함수에 추가합니다. 모르는 이벤트는 무시하고 200으로 응답합니다.
구조
src/
├── config.ts 환경 변수 읽기
├── app.ts Express 앱 구성 (포트를 열지 않음)
├── server.ts 서버 시작
├── error-handler.ts 오류 → HTTP 응답 변환
├── paysync/client.ts PaySync API 클라이언트 (인증, 응답 해석, PaySyncApiError)
├── paysync/query.ts 다중 값 쿼리 파라미터 처리
├── invoice/ 주문: model, service, routes
├── cash-receipt/ 현금영수증: model, service, routes
├── transaction/ 입금 내역: model, service, routes
├── bank-account/ 계좌 모델
└── webhook/ 웹훅 서명 검증, 페이로드 모델, 이벤트 처리
test/ node:test + supertest 테스트