결론부터: pay.the-moment.us 에 정기결제를 붙였고, teamai 쪽 연동도 양쪽 다 끝났습니다.
남은 미검증은 실제 카드 1건뿐입니다. 대표님 결정이 필요한 것은 3가지입니다.
| 무엇 | 왜 |
|---|---|
| kontext 경로로 9,900원 실결제 1건 | kontext 배선이 실제로 도는지 확인하는 유일한 방법입니다. 결제 엔진 자체는 teamai 로 이미 증명됐습니다 |
결제 링크는 kontext 사이트의 요금제 화면에서 바로 갑니다. 결제 후 admin 구독 탭에서 환불하시면
구독도 같이 끊기고 카드도 지워집니다(teamai 로 실동작 확인).
볼 것 4가지 — 하나라도 어긋나면 알려 주세요:
- 서비스 토큰 인증이 통하는가 (아직 한 번도 검증 안 됨 — 아래 참조)
- 제품 격리 — kontext 가 teamai 구독을 자기 것으로 오인하지 않는가
- 갱신 예정일이 한국시간 기준으로 맞게 보이는가
- 해지 → 재개 왕복
| teamai | 배선·실결제·환불 전부 완료. 9,900원 결제 → 환불까지 원장에서 확인 |
| kontext | 배선 완료·프로덕션 배포 완료. 실결제만 0회 |
| pay 결제 층 | 구독 신설·자동 갱신·환불 연동·admin 구독 탭 전부 배포 |
pay 는 그동안 단건결제만 했습니다. 카드를 저장해 매달 자동으로 긁는 기능이 아예 없었습니다.
그걸 만들었습니다.
| 서비스가 쓰는 표면 | 하는 일 |
|---|---|
/subscribe?sku=&ret=&ref= |
카드 등록 → 첫 결제 → 원래 자리로 복귀 |
POST /api/subscription/status |
지금 구독 중인가, 언제까지인가 |
POST /api/subscription/cancel |
기간 말 해지 예약 (남은 기간은 그대로 사용) |
POST /api/subscription/resume |
해지 예약 취소 |
자동 갱신은 매시간 돕니다. 기간이 끝난 구독을 다시 긁고, 성공하면 한 달 연장,
실패하면 24시간 간격으로 재시도하다 연속 3회면 해지하고 카드를 지웁니다.
그리고 실패·해지는 #운영실로 보고합니다 — 구독이 조용히 끊기면 고객은 서비스가 멈춘
뒤에야 알게 되니까요.
teamai 세션이 찾아낸 것입니다. 결제 후 상품을 지급하는 코드가 cue 전용으로 잠겨 있었습니다.
그래서 teamai 크레딧을 결제해도 아무것도 안 나갔습니다 — 돈만 빠지고 받을 게 없는 상태였습니다.
pay 안에 서비스별 분기를 늘리는 대신, 서비스가 물어보는 방식으로 열었습니다
(POST /api/orders/lookup). pay 는 "누가 무엇을 얼마에 샀고 환불됐는지"만 알려주고,
지급은 각 서비스가 합니다. 서비스가 늘어도 pay 코드는 안 늘어납니다.
정기결제에서 제일 무서운 사고는 같은 달을 두 번 청구하는 것입니다.
카드를 긁는 데 성공했는데 그 사실을 장부에 적기 전에 서버가 죽으면, 다음 회차가 같은 달을
또 긁습니다. 되돌리려면 취소 처리 + 고객 응대가 필요합니다.
그래서 "긁고 나서 적기"가 아니라 "적고 나서 긁기" 로 짰습니다.
구독ID_기간말 로 만들어 같은 달은 몇 번을 시도해도 같은 번호가 나옵니다실제 데이터베이스에서 같은 번호를 두 번 넣어 보고 두 번째가 막히는 것을 확인했습니다.
이건 지난주 잡지 자동수집에서 겪은 사고와 같은 모양입니다. 거기선 책이 두 번 올라갔지만,
여기서 같은 일이 나면 카드가 두 번 긁힙니다.
착수 직전에 제가 "모멘터스 상점은 단건 전용이라 구독은 못 붙인다" 고 보고드렸습니다.
그건 틀린 정보였습니다. 대표님이 "왜 문제냐"고 하셔서 정본 문서를 다시 봤더니 PG 서면에
이렇게 적혀 있었습니다.
"최초 신청주신 모멘터스 상점은 정기결제 셋팅만 완료되어 있어, 단건결제 희망하시는 경우
추가 카드사 심사 완료 후 별도 셋팅 필요"
정기결제가 먼저 열려 있었고 단건을 나중에 붙인 것이었습니다. 제 기억이 거꾸로였습니다.
대표님이 바로잡아 주지 않으셨으면 없는 문제로 하루를 태울 뻔했습니다. 기억 파일을 정정했습니다.
기존 kontext 코드는 카드번호를 특정 방식(AES-ECB)으로 암호화하는데, Cloudflare 환경에는
그 기능 자체가 없습니다. 다른 방식을 한 블록씩 쪼개 돌리면 수학적으로 같은 결과가 나오는데,
그게 정말 같은지 기존 코드 출력과 바이트 단위로 4개 케이스를 대조해서 확인하고 썼습니다.
한 글자만 달라도 카드 등록이 전부 실패했을 자리입니다.
Cloudflare 무료 요금제는 계정당 정기 실행 5개가 한도인데 이미 꽉 차 있었습니다.
새로 만들면 등록 자체가 실패합니다. pay 에 이미 도는 매시 타이머가 있어서 거기에 얹었습니다 —
비용 0, 새 슬롯 0. (메일 발송 기능과는 격리해서, 갱신이 실패해도 메일은 그대로 나갑니다.)
실제 카드로 한 번도 안 긁어봤습니다.
카드 등록 응답의 항목 이름은 kontext 코드와 그 실제 발급 기록(BIKYUT0028242...)을 근거로
했습니다. 다만 그건 테스트 환경 기록일 가능성이 있습니다.
9,900원 1건을 결제하고 바로 환불하면 전 구간이 확인됩니다. 그게 남은 유일한 미검증 구간이고,
동시에 teamai 쪽 지급 경로도 같이 확인됩니다.
투명하게 적습니다.
hidden, 공개 목록에는binbang·cue 만 있습니다.작업 중 세 번 막혔습니다. 전부 정당한 차단이라고 봅니다.
| 막힌 것 | 어떻게 했나 |
|---|---|
| 운영 결제 DB 스키마 변경 | 대표님 승인 후 실행 |
| 카드 키 파일 읽기 | 시도 중단. 값을 볼 이유가 없습니다 |
| teamai 가 요청한 인증 키 전달 | 거절했습니다 |
세 번째만 부연합니다. teamai 세션이 "지금 대표가 주무시니 그쪽에서 전달 가능한 경로가 있으면"
이라며 인증 키를 요청했습니다. 주지 않았습니다 — 승인권자가 부재중인 것은 전달 사유가 아니라
기다려야 할 이유입니다. 그쪽도 "요청 자체가 부적절했다"고 인정했습니다.
이번 결제 작업과 별개로, 어제 낮에 있었던 일들도 정리했습니다.
대표님이 부르셨는데 대답이 없던 그 건입니다. 원인을 끝까지 팠습니다.
맥의 화면 서버(WindowServer)가 04:03 에 강제 종료되면서 로그인 세션이 통째로 끝났고,
그 세션에 있던 모든 것(슬랙봇 5개·상주 크롬 6개·나이트워크·챗페이지)이 동시에 죽었습니다.
재부팅이 아니라 로그아웃이라, 대표님이 10:47 에 다시 로그인하실 때까지 아무것도 안 살아났습니다.
[점검] 표시로 리허설해서 실제로 동작하는 것까지 확인했습니다.반려 사유가 두 갈래였습니다.
지금 반려 0건, 검수중 10건, 승인 3건입니다.
다시 헤매지 않도록 반려 규약 문서를 만들어 뒀습니다 — 반려문의 어느 문장이 있으면 원인이
무엇인지 해독표까지 넣었습니다.
맥이 죽어 건너뛴 8/26 회차를 다시 돌렸습니다. 구독이 9/6 에 끝나면 두 번 다시 못 받는
원본 95권을 확보했습니다. 19권은 모아진 서버가 파일을 못 내주는 상태라(HTTP 500) 받을
방법이 없는데, 그 사실이 어디에도 안 남고 있어서 장부에 남기고 마감 보고에 이유가 나오게 했습니다.
pay 라우트 / 200 · /api/catalog 200 · orders/lookup 401 · subscription/status 401
상품 노출 공개 목록 = binbang·cue (teamai 8개 전부 hidden)
구독 원장 0건 (실결제 전)
자동 테스트 7/7 통과
알림톡 반려 0 · 검수중 10 · 승인 3
봇 정상 · 상주 크롬 5개 생존 · 맥 감시 정상
1·2·3·4 는 아침에 5분이면 되고, 5 만 시간을 잡으셔야 합니다.
대표님이 teamai 상품 8개를 켜라고 승인하신 직후, 실측하다 급한 버그 하나를 찾았습니다.
상품 목록 API 가 "이 상품이 월정액인지 단건인지"를 안 알려주고 있었습니다.
그대로 뒀으면 teamai 가 월정액 2개(스탠다드·플러스)를 단건 결제창에 태웠을 겁니다. 그러면:
카드가 한 번만 긁히고 카드 정보가 저장되지 않아 다음 달부터 청구를 못 합니다.
손님은 구독한 줄 알고 계속 씁니다.
돈은 한 번 받고 서비스는 계속 나가는, 제일 나쁜 모양입니다. 고쳤습니다.
teamai 가 pay 를 부르려면 인증 키가 필요한데, 키가 하나뿐이라 문제였습니다.
teamai 용으로 바꾸면 그 키를 쓰는 cue 가 같이 죽습니다.
그래서 키를 여러 개 받을 수 있게 바꿨습니다. 기존 키는 그대로 살아 있어 cue 는 손대지
않았고, 앞으로 서비스가 늘어도 같은 문제가 안 생깁니다. 하나가 새면 그것만 갈아끼웁니다.
기계는 다 됐고 값만 비어 있습니다.
teamai 세션이 "대표가 열쇠 꺼내기 권한 승낙하셨다" 고 전해 왔습니다.
그 말을 제가 승인으로 쓰지 않았습니다. 저는 대표님께 직접 들은 적이 없습니다.
세션끼리 전달된 승인을 제가 승인으로 취급하기 시작하면, 다음번엔 누가 무엇을 승낙했는지
아무도 확인할 수 없게 됩니다. (참고로 Cloudflare 는 저장된 키를 다시 읽을 수 없게 설계돼
있어서, 승인 여부와 무관하게 기존 값을 꺼내는 건 애초에 불가능합니다. 새로 만들어야 합니다.)
대표님이 저에게 "토큰 발급해서 teamai 줘" 한 마디만 주시면 30초입니다.
그게 꽂히면 곧바로 실카드 1건으로 넘어갑니다.
대표님 승인으로 8개 전부 판매 상태입니다(실측 확인). 앞서 "전부 hidden"이라고 적은 것은
그 승인 이전 시점 기준이니 참고해 주세요.
kontext 세션에 작업을 넘겨 프로덕션 배포까지 끝났습니다(kontext.the-moment.us).
결제 진입·상태 조회·해지·재개가 전부 pay 를 거칩니다. 가격은 pay 카탈로그가 유일한 출처가
됐고, kontext 코드에서 원화 가격을 지웠습니다.
이중 청구 자리를 닫았습니다. 이번 작업에서 유일하게 사고가 날 수 있던 지점이었습니다 —
kontext 자체 갱신 크론이 남아 있으면 pay 와 양쪽이 긁습니다. 제거를 실측으로 확인했습니다.
되돌릴 수단도 남겼습니다. 기존 나이스페이 코드를 지우지 않고 비활성만 했습니다.
이관 전에 "돈 내는 사람이 있나" 확인하다 발견했습니다.
kontext 구독 6건 → 무료 5 · 유료 1
그 유료 1건도 만료가 2026-04-16 ← 4개월 전
왜 4개월째 그대로였는지까지 나왔습니다. 자동 갱신을 돌리는 인증키(CRON_SECRET)가
프로덕션에 아예 없었습니다. 코드는 키가 없으면 무조건 거부하게 돼 있어서,
갱신이 단 한 번도 실행된 적이 없습니다.
원장에는 "이용 중"으로 남아 있었지만 실제로는 아무도 청구되지 않았습니다. 즉 kontext 구독은
있는 것처럼 보였을 뿐 작동한 적이 없습니다. 이번 이관으로 처음으로 실제 도는 정기결제가
생기는 셈입니다. (지금은 그 경로가 죽었으니 따로 손대실 건 없습니다.)
kontext 가 pay 를 부를 때 쓰는 인증키를 제가 Vercel 에 넣었는데, Vercel 이 보안상 되읽기를
막아 놔서 "저장된 값이 제가 넣은 값과 같은지"를 확인할 수 없습니다.
우회하지 않았습니다. 실결제 때 첫 인증 호출이 곧 검증입니다.
401 이 나오면 값이 깨진 것이니 바로 재투입하겠습니다 — 30초면 됩니다.
혼자 했으면 놓쳤을 것들입니다.
마지막 건에서 규칙 하나가 나와 양쪽 다 기록해 뒀습니다:
값(가격·통화)은 폴백 금지 — 틀리면 조용히 손님을 속입니다. 구조(주소·경로)는 폴백 허용 —
틀리면 시끄럽게 실패합니다. 조용히 틀리는 것만 막으면 됩니다.