알림톡 (NCP SENS) — 발송 · 템플릿 코드레벨 동작 흐름

마지막 업데이트 2026-07-22

알림톡 / NCP SENS 카카오 비즈메시지 HmacSHA256 서명 템플릿 5분 캐시 #{} 변수 치환 전화번호 정규화 미접속 트리거 백로그 P0 #5

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 · withAuthMemberNCP 서명 → 템플릿 fetch(캐시) → 변수 치환 → 전화번호 정규화 → NCP messages 전송
템플릿 조회GET /api/alimtalk-templates · withAuthMember로컬 DynamoDB 템플릿 목록
템플릿 생성/수정/삭제/정렬POST/PUT/DELETE … · withAuthAdminOrDevelopercreateAlimtalkTemplate 외. order 드래그&드롭 reorder
DB 레이어db-queries.ts:3006getAlimtalkTemplates·createAlimtalkTemplate (테이블 ppi-alimtalk-template-{stage})
클라 hookentities/alimtalk-template/model/use-alimtalk-templates.ts템플릿 상태 관리
발송 트리거features/monitor/hooks/use-session-checker.ts미접속 5/10분 토스트 + alimtalkAction

발송 흐름 (/api/alimtalk/send)

1
요청 + credential 검증
send/route.ts:88-120
{channelId, templateCode, phone, variables?} 필수 필드 확인. NCP_ACCESS_KEY/NCP_SECRET_KEY/NCP_SENS_SERVICE_ID 없으면 500 NCP_CREDENTIALS_NOT_CONFIGURED.
2
템플릿 본문 fetch (5분 캐시)
fetchTemplateContent :44
${channelId}:${templateCode} 캐시 히트(5분 TTL)면 즉시 반환. 미스면 NCP GET /templates?channelId&templateCodetemplateCode 매칭 content 추출 → 캐시 저장. 못 찾으면 TEMPLATE_CONTENT_NOT_FOUND.
3
변수 치환
:122-127
variables의 각 key에 대해 content.replaceAll("#{key}", value). (단순 문자열 치환 — 프롬프트 변수 엔진과 무관.)
4
전화번호 정규화
formatPhoneNumber :84
숫자만 추출 후 82로 시작하면 0+나머지로. NCP body의 countryCode는 항상 "82" 하드코딩.
5
NCP messages 전송
:129-160
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:

코드 맵 — 파일별 역할

파일역할
app/api/alimtalk/send/route.ts발송 — 서명·템플릿 캐시·치환·정규화·NCP 전송 (전부 한 파일)
app/api/alimtalk-templates/*로컬 템플릿 CRUD + [id]/update + reorder
lib/db-queries.ts:3006getAlimtalkTemplates·createAlimtalkTemplate (DynamoDB)
types/db/alimtalk-template.types.tsAlimtalkTemplate {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만.

관련 문서

모니터 대시보드 — 미접속 감지·토스트가 발송을 트리거하는 곳 아동 알림 프리셋 — 또 다른 알림 시스템(세션 중 환경음 프리셋, 알림톡과 별개) 운영·관리·인프라 문서화 백로그 — 이 문서는 P0 #5 항목