알림톡 (NCP SENS) — 발송 · 템플릿 코드레벨 동작 흐름
마지막 업데이트 2026-07-22
TL;DR
아동이 수업에 들어오지 않을 때 보호자에게 카카오 알림톡을 보낸다. 경로는 NCP SENS(Simple & Easy Notification Service). 구조는 두 층 — ① 로컬 템플릿 설정(DynamoDB CRUD: 어떤 카카오 템플릿을 쓸지 title·channelId·templateCode·순서 관리), ② 발송(/api/alimtalk/send: NCP에서 템플릿 본문을 가져와 변수 치환 후 NCP messages API로 전송).
발송은 완전 자동이 아니라 진행자 확인식이다 — 모니터 대시보드가 미접속 5분/10분에 토스트를 띄우고, 진행자가 "발송할까요?"를 클릭해야 보낸다. NCP 호출은 매 요청 HmacSHA256 서명이 필요하고, 템플릿 본문은 5분 메모리 캐시된다.
구성 요소
| 요소 | 경로 / 권한 | 역할 |
|---|---|---|
| 발송 | POST /api/alimtalk/send · withAuthMember | NCP 서명 → 템플릿 fetch(캐시) → 변수 치환 → 전화번호 정규화 → NCP messages 전송 |
| 템플릿 조회 | GET /api/alimtalk-templates · withAuthMember | 로컬 DynamoDB 템플릿 목록 |
| 템플릿 생성/수정/삭제/정렬 | POST/PUT/DELETE … · withAuthAdminOrDeveloper | createAlimtalkTemplate 외. order 드래그&드롭 reorder |
| DB 레이어 | db-queries.ts:3006 | getAlimtalkTemplates·createAlimtalkTemplate (테이블 ppi-alimtalk-template-{stage}) |
| 클라 hook | entities/alimtalk-template/model/use-alimtalk-templates.ts | 템플릿 상태 관리 |
| 발송 트리거 | features/monitor/hooks/use-session-checker.ts | 미접속 5/10분 토스트 + alimtalkAction |
발송 흐름 (/api/alimtalk/send)
{channelId, templateCode, phone, variables?} 필수 필드 확인. NCP_ACCESS_KEY/NCP_SECRET_KEY/NCP_SENS_SERVICE_ID 없으면 500 NCP_CREDENTIALS_NOT_CONFIGURED.${channelId}:${templateCode} 캐시 히트(5분 TTL)면 즉시 반환. 미스면 NCP GET /templates?channelId&templateCode → templateCode 매칭 content 추출 → 캐시 저장. 못 찾으면 TEMPLATE_CONTENT_NOT_FOUND.variables의 각 key에 대해 content.replaceAll("#{key}", value). (단순 문자열 치환 — 프롬프트 변수 엔진과 무관.)82로 시작하면 0+나머지로. NCP body의 countryCode는 항상 "82" 하드코딩.POST /alimtalk/v2/services/{SERVICE_ID}/messages body {plusFriendId:channelId, templateCode, messages:[{countryCode:"82", to, content}]}. 성공 → {success, requestId}, 실패 → ALIMTALK_SEND_FAILED + detail(원 status 전달).NCP SENS 서명 — makeSignature
NCP API Gateway는 모든 호출에 HmacSHA256 서명을 요구한다. send/route.ts:13-30:
function makeSignature(method, url, timestamp) {
const message = `${method} ${url}\n${timestamp}\n${NCP_ACCESS_KEY}`; // url=query 포함 정확히 일치
return crypto.createHmac("sha256", NCP_SECRET_KEY).update(message).digest("base64");
}
// 헤더: x-ncp-apigw-timestamp(ms) · x-ncp-iam-access-key · x-ncp-apigw-signature-v2
timestamp = Date.now()가 message와 헤더 양쪽에 들어가므로 매 요청 새 서명이다. GET(템플릿 조회)·POST(발송) 각각 자기 method·uri로 서명한다.
발송 트리거 — 미접속 토스트
발송은 모니터 대시보드의 미접속 감지에서 출발한다. features/monitor/hooks/use-session-checker.ts:
- 미접속 5분 경과 → 경고 토스트.
- 미접속 10분 경과 → "회차가 종료 처리되며, 채널톡 발송이 필요합니다" 토스트 +
alimtalkAction(발송 액션). - 토스트 문구:
${name} 미접속 ${minutes}분 경과 — ${templateTitle} 알림톡 발송할까요?— 진행자가 확인을 눌러야/api/alimtalk/send가 호출된다.
코드 맵 — 파일별 역할
| 파일 | 역할 |
|---|---|
| app/api/alimtalk/send/route.ts | 발송 — 서명·템플릿 캐시·치환·정규화·NCP 전송 (전부 한 파일) |
| app/api/alimtalk-templates/* | 로컬 템플릿 CRUD + [id]/update + reorder |
| lib/db-queries.ts:3006 | getAlimtalkTemplates·createAlimtalkTemplate (DynamoDB) |
| types/db/alimtalk-template.types.ts | AlimtalkTemplate {title, channelId, templateCode, order?} |
| entities/alimtalk-template/model/use-alimtalk-templates.ts | 클라 템플릿 상태 hook |
| features/monitor/hooks/use-session-checker.ts | 미접속 5/10분 토스트 + 발송 액션 |
읽을 때 주의할 함정
1. "템플릿"이 두 종류다. 로컬 AlimtalkTemplate(DynamoDB)은 어떤 카카오 템플릿을 쓸지를 가리키는 매핑·순서만 담고 본문(content)이 없다. 실제 본문은 발송 시 templateCode로 NCP에서 fetch한다(send/route.ts:44). 본문을 로컬에서 찾으려 하면 안 된다.
2. 발송은 자동이 아니라 진행자 확인식. 미접속 5/10분 토스트의 alimtalkAction을 진행자가 눌러야 발송된다(use-session-checker.ts:83). "왜 자동 발송이 안 되냐"가 아니라 설계가 수동 확인이다.
3. 템플릿 캐시는 인스턴스 메모리 + 5분 stale. templateCache는 모듈 전역 Map이라 서버 인스턴스별로 따로 산다(send/route.ts:41). 카카오에서 템플릿 본문을 바꿔도 최대 5분 + 인스턴스별로 반영이 어긋날 수 있다.
4. 서명의 url은 query까지 정확히 일치해야 한다. makeSignature(method, uri, ...)의 uri가 실제 fetch URL과 다르면(예: query 인코딩 차이) NCP가 서명 불일치로 거부한다. 템플릿 조회 GET은 ?channelId&templateCode query를 포함한 uri로 서명한다.
5. #{} 표기가 프롬프트 변수와 겹치지만 무관. 알림톡 변수 치환은 content.replaceAll("#{key}", value) 단순 문자열 치환이다(send/route.ts:124). 프롬프트 변수 시스템의 #{} 파싱 엔진과는 별개다.
6. 권한이 동작별로 다르다. 발송·템플릿 조회는 withAuthMember(진행자), 템플릿 생성/수정/삭제는 withAuthAdminOrDeveloper다(alimtalk-templates/route.ts). 진행자는 발송만, 템플릿 관리는 admin/dev만.