# 샌드고 (Sendgo) — 모든 SDK 가이드 전문 > 샌드고는 카카오 알림톡·친구톡과 SMS/LMS/MMS를 발송하는 한국 메시지 발송 플랫폼입니다. 20개 언어·프레임워크용 공식 SDK와 REST API를 제공합니다. --- > **PHP에서 카카오 알림톡, 브랜드메시지, SMS를 가장 쉽게 발송하는 순수 PHP SDK** `sendgo/php`는 [Sendgo](https://sendgo.io) 알림 API를 위한 **순수 PHP SDK**입니다. Laravel 등 특정 프레임워크에 의존하지 않으며, `ext-curl`과 `ext-json`만으로 동작합니다. --- ## 설치 ```bash composer require sendgo/php ``` --- ## 빠른 시작 ```php $_ENV['SENDGO_ACCESS_KEY'], 'secret_key' => $_ENV['SENDGO_SECRET_KEY'], 'kakao_sender_key' => $_ENV['SENDGO_KAKAO_SENDER_KEY'], 'sms_sender_key' => $_ENV['SENDGO_SMS_SENDER_KEY'], 'api_version' => 'v2', ]); // 알림톡 발송 $sendgo->alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [ ['contact' => '01012345678', 'name' => '홍길동', 'var1' => 'ORD-001', 'var2' => '29,000원'], ], ]); // SMS 발송 $sendgo->sms->sendSms([ 'content' => '[Sendgo] 인증번호: 123456 (5분 이내 입력)', 'contacts' => [['contact' => '01012345678']], ]); ``` --- ## 알림톡 상세 사용법 ```php alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [ ['contact' => '01011111111', 'name' => '홍길동', 'var1' => 'ORD-001', 'var2' => '29,000원'], ['contact' => '01022222222', 'name' => '김철수', 'var1' => 'ORD-002', 'var2' => '15,000원'], ['contact' => '01033333333', 'name' => '이영희', 'var1' => 'ORD-003', 'var2' => '52,000원'], ], ]); // 예약 발송 $sendgo->alimtalk->send([ 'templateCode' => 'PROMO_SUMMER_2026', 'scheduleType' => 'SCHEDULED', 'at' => '2026-07-28 09:00:00', 'contacts' => [['contact' => '01012345678', 'var1' => '여름 한정 50% 할인']], ]); // 알림톡 실패 시 SMS 자동 대체 발송 $sendgo->alimtalk->send([ 'templateCode' => 'DELIVERY_START_001', 'replaceSms' => 'Y', 'smsSubject' => '[배송 시작 안내]', 'smsContent' => "주문하신 상품이 출고되었습니다.\n송장번호: #{var2}", 'contacts' => [['contact' => '01012345678', 'var1' => 'ORD-001', 'var2' => '1234567890']], ]); ``` --- ## 친구톡 사용법 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```php friendtalk->send([ 'content' => '안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.', 'contacts' => [['contact' => '01012345678']], ]); // 이미지형 $sendgo->friendtalk->send([ 'messageType' => 'FI', 'content' => '이번 주 특가 상품을 확인하세요!', 'imageUrl' => 'https://cdn.example.com/banner.jpg', 'imageLink' => 'https://example.com/event', 'contacts' => [['contact' => '01012345678']], ]); // 버튼 포함 $sendgo->friendtalk->send([ 'content' => '7월 쿠폰이 도착했습니다! 지금 바로 사용하세요.', 'buttons' => [ ['name' => '쿠폰 받기', 'type' => 'WL', 'linkMo' => 'https://example.com/coupon'], ], 'contacts' => [['contact' => '01012345678']], ]); ``` --- ## 브랜드메시지 사용법 브랜드메시지는 친구톡의 후속 채널입니다. 메시지 타입이 친구톡과 1:1 대응되며 (`FT`→`BT`, `FI`→`BI`, `FW`→`BW`, `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`), 요청에는 **친구톡 코드를 그대로** 넘기고 변환은 서버가 처리합니다. 친구톡과 달리 다음이 가능합니다. - 채널 친구가 **아닌** 수신자에게 발송 (`targeting: N`) - 수신 동의한 **전체 채널 친구 동보** 발송 (`targeting: F`, 수신자 목록 불필요) - 리스트·캐러셀·커머스·동영상 등 **템플릿 기반 리치 메시지** > v2 전용입니다. 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보낼 때는 여전히 친구톡 API 를 쓰세요 — 이 엔드포인트는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. 친구톡 요청은 카카오 측에서 브랜드메시지(자유형)로 대체 발송됩니다. ```php brandMessage->send([ 'targeting' => 'M', 'messageType' => 'FL', 'friendTemplateUuid' => '9cd5460b-6458-4edc-9b11-c26d3013c340', 'contacts' => [['contact' => '01012345678', 'var1' => '29,000원']], ]); // 동보 발송 — 수신 동의한 전체 채널 친구 (contacts 불필요) $sendgo->brandMessage->broadcast([ 'messageType' => 'FW', 'friendTemplateUuid' => '9cd5460b-6458-4edc-9b11-c26d3013c340', ]); // 캠페인 조회 $list = $sendgo->brandMessage->campaigns(['count' => 10]); $one = $sendgo->brandMessage->campaign('1f0a6d0e-6b3b-4f0f-9b2f-2f6f6a1b7c11'); ``` --- ## SMS / LMS / MMS 사용법 ```php sms->sendSms([ 'content' => '[Sendgo] 인증번호: 123456 (5분 이내 입력)', 'contacts' => [['contact' => '01012345678']], ]); // LMS (장문, 2,000자 이하) $sendgo->sms->sendLms([ 'subject' => '[중요] 서비스 점검 안내', 'content' => "안녕하세요. 서비스 점검이 예정되어 있습니다.\n\n■ 일시: 2026-07-25 02:00 ~ 06:00\n■ 영향: 전체 서비스", 'contacts' => [['contact' => '01012345678']], ]); // MMS (이미지 포함) $sendgo->sms->sendMms([ 'subject' => '[이벤트] 7월 특가', 'content' => '이번 달 특가 상품을 확인하세요!', 'contacts' => [['contact' => '01012345678']], ]); // 예약 문자 $sendgo->sms->sendSms([ 'content' => '[알림] 예약 미팅을 확인해주세요.', 'scheduleType' => 'SCHEDULED', 'at' => '2026-07-23 08:00:00', 'contacts' => [['contact' => '01012345678']], ]); ``` --- ## 프레임워크 통합 ### Symfony ```php sendgo->alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [['contact' => $phone, 'var1' => $orderNo]], ]); } public function sendShippingAlert(string $phone, string $trackingNo): void { $this->sendgo->alimtalk->send([ 'templateCode' => 'SHIPPING_001', 'replaceSms' => 'Y', 'smsContent' => "배송이 시작되었습니다.\n송장번호: {$trackingNo}", 'contacts' => [['contact' => $phone, 'var1' => $trackingNo]], ]); } } ``` ### Slim Framework ```php set(Sendgo::class, fn() => new Sendgo([ 'access_key' => $_ENV['SENDGO_ACCESS_KEY'], 'secret_key' => $_ENV['SENDGO_SECRET_KEY'], 'kakao_sender_key' => $_ENV['SENDGO_KAKAO_KEY'], 'api_version' => 'v2', ])); ``` ### WordPress / WooCommerce ```php get_option('sendgo_access_key'), 'secret_key' => get_option('sendgo_secret_key'), 'kakao_sender_key' => get_option('sendgo_kakao_key'), 'api_version' => 'v2', ]); } return $instance; } // WooCommerce 주문 완료 시 알림톡 발송 add_action('woocommerce_order_status_completed', function (int $orderId) { $order = wc_get_order($orderId); try { get_sendgo()->alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [ ['contact' => $order->get_billing_phone(), 'var1' => $order->get_order_number()], ], ]); } catch (SendgoException $e) { error_log("Sendgo 알림 실패: {$e->getMessage()}"); } }); ``` --- ## 예외 처리 ```php alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [['contact' => '01012345678']], ]); } catch (SendgoException $e) { echo "발송 실패: HTTP {$e->getStatusCode()} [{$e->getErrorCode()}]" . PHP_EOL; match ($e->getErrorCode()) { 'INVALID_ACCESS_KEY', 'INVALID_SECRET_KEY' => alertOps('Sendgo 인증키를 확인하세요.'), 'INVALID_TEMPLATE_CODE' => logger('존재하지 않는 템플릿'), 'PAYMENT_REQUIRED' => alertOps('Sendgo 크레딧이 부족합니다.'), 'IP_NOT_ALLOWED' => alertOps('허용되지 않은 IP'), default => logger('알 수 없는 오류: ' . $e->getMessage()), }; } ``` --- ## 설정 옵션 | 파라미터 | 타입 | 필수 | 기본값 | 설명 | |---------|------|------|--------|------| | `access_key` | `string` | **필수** | — | Sendgo 액세스 키 | | `secret_key` | `string` | **필수** | — | Sendgo 시크릿 키 | | `kakao_sender_key` | `string\|null` | 선택 | `null` | 카카오 발신프로필 키 | | `sms_sender_key` | `string\|null` | 선택 | `null` | SMS 발신자 키 | | `api_version` | `string` | 선택 | `'v1'` | API 버전 (`v1` \| `v2`) | | `url` | `string` | 선택 | `'https://sendgo.io'` | API 기본 URL | --- ## 짧은 URL 짧은 URL 은 메시지 본문의 링크를 줄이고, 그 링크가 실제로 눌렸는지 집계합니다. 문자는 바이트 수가 요금과 직결되므로 링크를 줄이면 그만큼 본문을 더 쓸 수 있습니다. 같은 원본 URL 을 다시 줄이면 **기존 링크가 그대로 반환**됩니다. 캠페인별로 반응을 따로 집계하려면 `forceNew` 로 새 코드를 만드세요. `deactivate` 는 링크를 삭제하지 않고 리다이렉트만 중지합니다. 이미 발송한 메시지의 링크를 무효화할 때 쓰며, 누적 통계는 남고 이후 접속은 `410 Gone` 이 됩니다. ```php // 짧은 URL 생성 (v2 전용) $short = $sendgo->shortUrl->create([ 'targetUrl' => 'https://example.com/promotions/summer-sale', 'title' => '여름 세일 랜딩', ]); $link = $short['data']['shortUrl']; // 문자/알림톡 본문에 넣을 짧은 링크 $code = $short['data']['code']; // 반응 통계 — 일별 추이 + 디바이스/유입경로/국가별 분해 $stats = $sendgo->shortUrl->stats($code, ['from' => '2026-08-01']); $sendgo->shortUrl->list(['count' => 10]); $sendgo->shortUrl->show($code); $sendgo->shortUrl->deactivate($code); // 리다이렉트만 중지, 통계는 남는다 ``` `stats` 는 일별 추이(`daily`)와 디바이스(`byDevice`)·유입경로(`byReferer`)·국가(`byCountry`)별 분해를 반환합니다. 일별 추이는 사전 집계 표에서 읽으므로 클릭이 많아도 응답 시간이 일정합니다. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ### 1.1.0 (2026-08-11) - 짧은 URL 추가 — `$sendgo->shortUrl` (생성/목록/상세/반응통계/중지) - `HttpClient::delete()` 추가 - `Sendgo::__call` 에 `shortUrl`/`short_url` 포워딩 추가 (Laravel 파사드 대응) ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `sendgo/php` (Packagist) - **저장소**: [send-go/php](https://github.com/send-go/php) - **레지스트리**: https://packagist.org/packages/sendgo/php - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **Laravel에서 카카오 알림톡, 브랜드메시지, SMS를 가장 쉽게 발송하는 공식 Laravel 패키지** `sendgo/laravel`은 [`sendgo/php`](https://github.com/send-go/php) 코어를 확장한 **Laravel 전용 패키지**입니다. ServiceProvider 자동 등록, Facade, Config 게시 등 Laravel 통합을 완벽하게 제공합니다. --- ## 설치 ```bash composer require sendgo/laravel ``` Laravel의 패키지 자동 검색(Package Auto-Discovery)으로 ServiceProvider와 Facade가 자동 등록됩니다. --- ## 빠른 시작 ### 1단계 — 환경변수 설정 (`.env`) ```env SENDGO_ACCESS_KEY=your_access_key SENDGO_SECRET_KEY=your_secret_key SENDGO_KAKAO_SENDER_KEY=your_kakao_key SENDGO_SMS_SENDER_KEY=your_sms_key SENDGO_API_VERSION=v2 ``` ### 2단계 (선택) — 설정 파일 게시 ```bash php artisan vendor:publish --tag=sendgo-config ``` ### 3단계 — 알림톡 전송 ```php sendgo->alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [ [ 'contact' => $order->user->phone, 'name' => $order->user->name, 'var1' => $order->number, 'var2' => number_format($order->total) . '원', ], ], ]); return response()->json(['success' => true]); } } ``` --- ## Facade 사용법 ```php send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [['contact' => '01012345678', 'var1' => 'ORD-001']], ]); // SMS 발송 Sendgo::sms()->sendSms([ 'content' => '[인증] 인증번호: 123456', 'contacts' => [['contact' => '01012345678']], ]); ``` --- ## 상세 사용법 ### 알림톡 ```php alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [ ['contact' => '01011111111', 'name' => '홍길동', 'var1' => 'ORD-001', 'var2' => '29,000원'], ['contact' => '01022222222', 'name' => '김철수', 'var1' => 'ORD-002', 'var2' => '15,000원'], ['contact' => '01033333333', 'name' => '이영희', 'var1' => 'ORD-003', 'var2' => '52,000원'], ], ]); // 예약 발송 app(Sendgo::class)->alimtalk->send([ 'templateCode' => 'PROMO_SUMMER_2026', 'scheduleType' => 'SCHEDULED', 'at' => '2026-07-28 09:00:00', 'contacts' => [['contact' => '01012345678', 'var1' => '여름 한정 50% 할인']], ]); // SMS 자동 대체 발송 app(Sendgo::class)->alimtalk->send([ 'templateCode' => 'DELIVERY_START_001', 'replaceSms' => 'Y', 'smsSubject' => '[배송 시작 안내]', 'smsContent' => "주문하신 상품이 출고되었습니다.\n송장번호: #{var2}", 'contacts' => [['contact' => '01012345678', 'var1' => 'ORD-001', 'var2' => '1234567890']], ]); ``` ### 친구톡 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```php friendtalk->send([ 'content' => '안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.', 'contacts' => [['contact' => '01012345678']], ]); // 이미지형 app(Sendgo::class)->friendtalk->send([ 'messageType' => 'FI', 'content' => '이번 주 특가 상품을 확인하세요!', 'imageUrl' => 'https://cdn.example.com/banner.jpg', 'imageLink' => 'https://example.com/event', 'contacts' => [['contact' => '01012345678']], ]); // 버튼 포함 app(Sendgo::class)->friendtalk->send([ 'content' => '7월 쿠폰이 도착했습니다! 지금 바로 사용하세요.', 'buttons' => [ ['name' => '쿠폰 받기', 'type' => 'WL', 'linkMo' => 'https://example.com/coupon'], ['name' => '고객센터', 'type' => 'WL', 'linkMo' => 'https://example.com/cs'], ], 'contacts' => [['contact' => '01012345678']], ]); ``` ### SMS / LMS / MMS ```php sms->sendSms([ 'content' => '[Sendgo] 인증번호: 123456 (5분 이내 입력)', 'contacts' => [['contact' => '01012345678']], ]); // LMS (장문, 2,000자 이하) app(Sendgo::class)->sms->sendLms([ 'subject' => '[중요] 서비스 점검 안내', 'content' => "안녕하세요. 서비스 점검이 예정되어 있습니다.\n\n■ 일시: 2026-07-25 02:00 ~ 06:00\n■ 영향: 전체 서비스", 'contacts' => [['contact' => '01012345678']], ]); // MMS (이미지 포함) app(Sendgo::class)->sms->sendMms([ 'subject' => '[이벤트] 7월 특가', 'content' => '이번 달 특가 상품을 확인하세요!', 'contacts' => [['contact' => '01011111111'], ['contact' => '01022222222']], ]); ``` --- ## 서비스 클래스 패턴 ```php sendgo->alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [ ['contact' => $phone, 'var1' => $orderNo, 'var2' => number_format($amount) . '원'], ], ]); } public function sendShippingAlert(string $phone, string $trackingNo): void { $this->sendgo->alimtalk->send([ 'templateCode' => 'SHIPPING_001', 'replaceSms' => 'Y', 'smsContent' => "배송이 시작되었습니다.\n송장번호: {$trackingNo}", 'contacts' => [['contact' => $phone, 'var1' => $trackingNo]], ]); } public function sendVerificationCode(string $phone, string $code): void { try { // 알림톡 우선, 실패 시 SMS 대체 $this->sendgo->alimtalk->send([ 'templateCode' => 'VERIFY_CODE_001', 'replaceSms' => 'Y', 'smsContent' => "[인증] 인증번호: {$code} (5분 이내 입력)", 'contacts' => [['contact' => $phone, 'var1' => $code]], ]); } catch (SendgoException $e) { Log::error('Sendgo 인증번호 발송 실패', [ 'phone' => $phone, 'error_code' => $e->getErrorCode(), 'status' => $e->getStatusCode(), ]); throw $e; } } } ``` ```php app->bind(NotificationService::class, function ($app) { return new NotificationService($app->make(Sendgo::class)); }); ``` --- ## Notification Channel ```php 'ORDER_CONFIRM_001', 'contacts' => [ [ 'contact' => $notifiable->phone, 'var1' => $this->orderNo, 'var2' => number_format($this->amount) . '원', ], ], ]; } } ``` --- ## Queue / Job 비동기 발송 ```php alimtalk->send([ 'templateCode' => $this->templateCode, 'contacts' => $this->contacts, ]); } public function failed(SendgoException $e): void { Log::error('알림톡 발송 실패', [ 'templateCode' => $this->templateCode, 'error_code' => $e->getErrorCode(), ]); } } // 디스패치 예시 SendAlimtalkJob::dispatch('ORDER_CONFIRM_001', [ ['contact' => '01012345678', 'var1' => 'ORD-001'], ])->onQueue('notifications'); ``` --- ## 예외 처리 ```php alimtalk->send([...]); } catch (SendgoException $e) { Log::error('Sendgo 발송 실패', [ 'status' => $e->getStatusCode(), 'error_code' => $e->getErrorCode(), 'endpoint' => $e->getEndpoint(), ]); match ($e->getErrorCode()) { 'INVALID_ACCESS_KEY', 'INVALID_SECRET_KEY' => alertOps('Sendgo 인증키 오류'), 'INVALID_TEMPLATE_CODE' => logger()->warning('존재하지 않는 템플릿'), 'PAYMENT_REQUIRED' => alertOps('Sendgo 크레딧 부족'), 'IP_NOT_ALLOWED' => alertOps('허용되지 않은 IP'), default => null, }; } ``` --- ## 설정 옵션 `config/sendgo.php` (vendor:publish 후 커스터마이징 가능): | 키 | 환경변수 | 기본값 | 설명 | |----|---------|--------|------| | `access_key` | `SENDGO_ACCESS_KEY` | — | Sendgo 액세스 키 | | `secret_key` | `SENDGO_SECRET_KEY` | — | Sendgo 시크릿 키 | | `kakao_sender_key` | `SENDGO_KAKAO_SENDER_KEY` | `null` | 카카오 발신프로필 키 | | `sms_sender_key` | `SENDGO_SMS_SENDER_KEY` | `null` | SMS 발신자 키 | | `api_version` | `SENDGO_API_VERSION` | `'v2'` | API 버전 | | `url` | `SENDGO_URL` | `'https://sendgo.io'` | API 기본 URL | --- ## 자주 묻는 질문 (FAQ) **Q. `sendgo/php`와의 차이는 무엇인가요?** A. `sendgo/php`는 프레임워크 독립적인 순수 PHP 코어 패키지입니다. `sendgo/laravel`은 이를 확장해 ServiceProvider 자동 등록, Facade, .env 설정 바인딩 등 Laravel 통합을 추가합니다. **Q. Laravel 10, 11, 12 모두 지원하나요?** A. 네, `illuminate/support` `^10.0|^11.0|^12.0`을 지원합니다. **Q. Facade를 사용하지 않고 DI로만 쓸 수 있나요?** A. 네, `Sendgo\Php\Sendgo`를 생성자에서 타입힌트로 주입받아 사용할 수 있습니다. **Q. 테스트 시 Sendgo를 Mock 처리하려면?** A. `Sendgo\Php\Sendgo`를 Mockery나 PHPUnit Mock으로 교체하면 됩니다. --- ## 브랜드메시지 · 짧은 URL 이 패키지는 코어(`sendgo/php`)의 클라이언트를 그대로 노출하므로, 코어에 있는 채널이 모두 그대로 쓸 수 있습니다. 두 기능 모두 **v2 전용**입니다. | 기능 | 접근 | |------|------| | 카카오 브랜드메시지 (친구톡의 후속 채널) | `Sendgo::brandMessage()` | | 짧은 URL (단축 + 클릭 반응 분석) | `Sendgo::shortUrl()` | 브랜드메시지는 채널 친구가 아닌 수신자에게도 보낼 수 있고(`targeting` = `N`), 수신 동의한 전체 채널 친구에게 동보 발송할 수도 있습니다(`targeting` = `F`). 짧은 URL 은 메시지 본문의 링크를 줄이고 클릭 반응(일별 추이·디바이스·유입경로·국가)을 집계합니다. 사용 예시와 파라미터는 [코어 README](https://github.com/send-go) 와 [SDK 가이드](https://sendgo.io/ko/sdk) 를 참고하세요. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ### 1.1.0 (2026-08-11) - 파사드에 `shortUrl()` 문서화 (`@method` 주석) — IDE 자동완성 대응 ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `sendgo/laravel` (Packagist) - **저장소**: [send-go/laravel](https://github.com/send-go/laravel) - **레지스트리**: https://packagist.org/packages/sendgo/laravel - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **Symfony에서 카카오 알림톡, 브랜드메시지, SMS를 가장 쉽게 발송하는 공식 Symfony 번들** `sendgo/symfony`는 [`sendgo/php`](https://github.com/send-go/php) 코어를 확장한 **Symfony 전용 번들**입니다. DI 컨테이너 서비스 자동 등록, 설정(config) 통합, 오토와이어링을 완벽하게 제공합니다. --- ## 설치 ```bash composer require sendgo/symfony ``` ### 번들 등록 [Symfony Flex](https://symfony.com/doc/current/setup/flex.html)를 사용하는 경우 번들이 자동으로 등록됩니다. Flex를 사용하지 않는다면 `config/bundles.php`에 직접 추가하세요. ```php ['all' => true], ]; ``` --- ## 빠른 시작 ### 1단계 — 환경변수 설정 (`.env`) ```env SENDGO_ACCESS_KEY=your_access_key SENDGO_SECRET_KEY=your_secret_key SENDGO_KAKAO_SENDER_KEY=your_kakao_key SENDGO_SMS_SENDER_KEY=your_sms_key SENDGO_API_VERSION=v2 ``` ### 2단계 — 번들 설정 파일 (`config/packages/sendgo.yaml`) ```yaml # config/packages/sendgo.yaml sendgo: access_key: '%env(SENDGO_ACCESS_KEY)%' secret_key: '%env(SENDGO_SECRET_KEY)%' kakao_sender_key: '%env(SENDGO_KAKAO_SENDER_KEY)%' sms_sender_key: '%env(SENDGO_SMS_SENDER_KEY)%' api_version: '%env(SENDGO_API_VERSION)%' url: 'https://sendgo.io' ``` ### 3단계 — 알림톡 전송 ```php sendgo->alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [ [ 'contact' => $order->getUser()->getPhone(), 'name' => $order->getUser()->getName(), 'var1' => $order->getNumber(), 'var2' => number_format($order->getTotal()) . '원', ], ], ]); return $this->json(['success' => true]); } } ``` --- ## 오토와이어링 사용법 번들이 `Sendgo\Php\Sendgo` 서비스를 컨테이너에 등록하므로, 생성자에 타입힌트만 하면 자동으로 주입됩니다. 별도의 서비스 정의는 필요하지 않습니다. ```php get('sendgo'); // Sendgo\Php\Sendgo 인스턴스 ``` --- ## 상세 사용법 ### 알림톡 ```php alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [ ['contact' => '01011111111', 'name' => '홍길동', 'var1' => 'ORD-001', 'var2' => '29,000원'], ['contact' => '01022222222', 'name' => '김철수', 'var1' => 'ORD-002', 'var2' => '15,000원'], ['contact' => '01033333333', 'name' => '이영희', 'var1' => 'ORD-003', 'var2' => '52,000원'], ], ]); // 예약 발송 $sendgo->alimtalk->send([ 'templateCode' => 'PROMO_SUMMER_2026', 'scheduleType' => 'SCHEDULED', 'at' => '2026-07-28 09:00:00', 'contacts' => [['contact' => '01012345678', 'var1' => '여름 한정 50% 할인']], ]); // SMS 자동 대체 발송 $sendgo->alimtalk->send([ 'templateCode' => 'DELIVERY_START_001', 'replaceSms' => 'Y', 'smsSubject' => '[배송 시작 안내]', 'smsContent' => "주문하신 상품이 출고되었습니다.\n송장번호: #{var2}", 'contacts' => [['contact' => '01012345678', 'var1' => 'ORD-001', 'var2' => '1234567890']], ]); ``` ### 친구톡 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```php friendtalk->send([ 'content' => '안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.', 'contacts' => [['contact' => '01012345678']], ]); // 이미지형 $sendgo->friendtalk->send([ 'messageType' => 'FI', 'content' => '이번 주 특가 상품을 확인하세요!', 'imageUrl' => 'https://cdn.example.com/banner.jpg', 'imageLink' => 'https://example.com/event', 'contacts' => [['contact' => '01012345678']], ]); // 버튼 포함 $sendgo->friendtalk->send([ 'content' => '7월 쿠폰이 도착했습니다! 지금 바로 사용하세요.', 'buttons' => [ ['name' => '쿠폰 받기', 'type' => 'WL', 'linkMo' => 'https://example.com/coupon'], ['name' => '고객센터', 'type' => 'WL', 'linkMo' => 'https://example.com/cs'], ], 'contacts' => [['contact' => '01012345678']], ]); ``` ### SMS / LMS / MMS ```php sms->sendSms([ 'content' => '[Sendgo] 인증번호: 123456 (5분 이내 입력)', 'contacts' => [['contact' => '01012345678']], ]); // LMS (장문, 2,000자 이하) $sendgo->sms->sendLms([ 'subject' => '[중요] 서비스 점검 안내', 'content' => "안녕하세요. 서비스 점검이 예정되어 있습니다.\n\n■ 일시: 2026-07-25 02:00 ~ 06:00\n■ 영향: 전체 서비스", 'contacts' => [['contact' => '01012345678']], ]); // MMS (이미지 포함) $sendgo->sms->sendMms([ 'subject' => '[이벤트] 7월 특가', 'content' => '이번 달 특가 상품을 확인하세요!', 'contacts' => [['contact' => '01011111111'], ['contact' => '01022222222']], ]); ``` --- ## 서비스 클래스 패턴 ```php sendgo->alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [ ['contact' => $phone, 'var1' => $orderNo, 'var2' => number_format($amount) . '원'], ], ]); } public function sendVerificationCode(string $phone, string $code): void { try { // 알림톡 우선, 실패 시 SMS 대체 $this->sendgo->alimtalk->send([ 'templateCode' => 'VERIFY_CODE_001', 'replaceSms' => 'Y', 'smsContent' => "[인증] 인증번호: {$code} (5분 이내 입력)", 'contacts' => [['contact' => $phone, 'var1' => $code]], ]); } catch (SendgoException $e) { $this->logger->error('Sendgo 인증번호 발송 실패', [ 'phone' => $phone, 'error_code' => $e->getErrorCode(), 'status' => $e->getStatusCode(), ]); throw $e; } } } ``` `services.yaml`에서 `autowire: true`가 설정되어 있으면 `Sendgo\Php\Sendgo`가 자동 주입됩니다. --- ## Messenger 비동기 발송 [Symfony Messenger](https://symfony.com/doc/current/messenger.html)로 발송을 비동기 처리할 수 있습니다. ```php sendgo->alimtalk->send([ 'templateCode' => $message->templateCode, 'contacts' => $message->contacts, ]); } } ``` ```php // 디스패치 예시 $bus->dispatch(new SendAlimtalkMessage('ORDER_CONFIRM_001', [ ['contact' => '01012345678', 'var1' => 'ORD-001'], ])); ``` --- ## 예외 처리 ```php alimtalk->send([...]); } catch (SendgoException $e) { $logger->error('Sendgo 발송 실패', [ 'status' => $e->getStatusCode(), 'error_code' => $e->getErrorCode(), 'endpoint' => $e->getEndpoint(), ]); match ($e->getErrorCode()) { 'INVALID_ACCESS_KEY', 'INVALID_SECRET_KEY' => $this->alertOps('Sendgo 인증키 오류'), 'INVALID_TEMPLATE_CODE' => $logger->warning('존재하지 않는 템플릿'), 'PAYMENT_REQUIRED' => $this->alertOps('Sendgo 크레딧 부족'), 'IP_NOT_ALLOWED' => $this->alertOps('허용되지 않은 IP'), default => null, }; } ``` --- ## 설정 옵션 `config/packages/sendgo.yaml` 에서 설정합니다: | 키 | 환경변수 | 기본값 | 설명 | |----|---------|--------|------| | `access_key` | `SENDGO_ACCESS_KEY` | — (필수) | Sendgo 액세스 키 | | `secret_key` | `SENDGO_SECRET_KEY` | — (필수) | Sendgo 시크릿 키 | | `kakao_sender_key` | `SENDGO_KAKAO_SENDER_KEY` | `null` | 카카오 발신프로필 키 | | `sms_sender_key` | `SENDGO_SMS_SENDER_KEY` | `null` | SMS 발신자 키 | | `api_version` | `SENDGO_API_VERSION` | `'v2'` | API 버전 | | `url` | `SENDGO_URL` | `'https://sendgo.io'` | API 기본 URL | --- ## 자주 묻는 질문 (FAQ) **Q. `sendgo/php`와의 차이는 무엇인가요?** A. `sendgo/php`는 프레임워크 독립적인 순수 PHP 코어 패키지입니다. `sendgo/symfony`는 이를 확장해 번들 자동 등록, DI 컨테이너 서비스 등록, config 바인딩, 오토와이어링 등 Symfony 통합을 추가합니다. **Q. Symfony 6.4, 7 모두 지원하나요?** A. 네, `symfony/config`, `symfony/dependency-injection`, `symfony/http-kernel` 모두 `^6.4|^7.0`을 지원합니다. **Q. 오토와이어링 없이 서비스 ID로 접근할 수 있나요?** A. 네, `sendgo` 별칭이 등록되어 있어 `$container->get('sendgo')`로 접근할 수 있습니다. **Q. 테스트 시 Sendgo를 Mock 처리하려면?** A. 테스트 컨테이너에서 `Sendgo\Php\Sendgo` 서비스를 PHPUnit Mock으로 교체하면 됩니다. --- ## 브랜드메시지 · 짧은 URL 이 패키지는 코어(`sendgo/php`)의 클라이언트를 그대로 노출하므로, 코어에 있는 채널이 모두 그대로 쓸 수 있습니다. 두 기능 모두 **v2 전용**입니다. | 기능 | 접근 | |------|------| | 카카오 브랜드메시지 (친구톡의 후속 채널) | `$sendgo->brandMessage` | | 짧은 URL (단축 + 클릭 반응 분석) | `$sendgo->shortUrl` | 브랜드메시지는 채널 친구가 아닌 수신자에게도 보낼 수 있고(`targeting` = `N`), 수신 동의한 전체 채널 친구에게 동보 발송할 수도 있습니다(`targeting` = `F`). 짧은 URL 은 메시지 본문의 링크를 줄이고 클릭 반응(일별 추이·디바이스·유입경로·국가)을 집계합니다. 사용 예시와 파라미터는 [코어 README](https://github.com/send-go) 와 [SDK 가이드](https://sendgo.io/ko/sdk) 를 참고하세요. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. - 짧은 URL 지원 (1.1.0 릴리스 누락분 포함). ### 1.1.0 (2026-08-11) - 브랜드메시지·짧은 URL 접근 방법 문서화 (코어를 그대로 노출) ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `sendgo/symfony` (Packagist) - **저장소**: [send-go/symfony](https://github.com/send-go/symfony) - **레지스트리**: https://packagist.org/packages/sendgo/symfony - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **카카오 알림톡·브랜드메시지와 SMS/LMS/MMS 를 발송하고, WooCommerce 주문 상태에 따라 구매자에게 자동으로 알리는 공식 WordPress 플러그인** Sendgo 코어 SDK([`sendgo/php`](https://github.com/send-go/php))를 번들해 WordPress 에 연결합니다. 관리자 설정 화면에서 키를 넣고, WooCommerce 훅으로 주문 상태가 바뀔 때 구매자에게 알립니다. PHP 8.2 이상이 필요합니다. ## 설치 ### 1. wordpress.org / zip 으로 설치 (권장) 배포 zip 에는 코어 SDK 가 `vendor/` 에 포함되어 있습니다. Composer 없이 그대로 동작합니다. **관리자 > 플러그인 > 새로 추가**에서 zip 을 업로드하거나 `wp-content/plugins/sendgo` 에 풀어 넣으세요. ### 2. Composer 로 설치 ```bash composer require sendgo/wordpress ``` 또는 플러그인 디렉터리에서 직접: ```bash cd wp-content/plugins/sendgo composer install ``` 소스에서 직접 받은 경우에는 `vendor/autoload.php` 가 없으므로 `composer install` 을 실행해야 합니다. 이 파일이 없으면 플러그인은 발송을 하지 못하고 관리자 화면에 원인을 알리는 오류 알림을 띄웁니다. ### 3. 플러그인 활성화 WordPress 관리자 > 플러그인 화면에서 **Sendgo** 를 활성화합니다. ## 설정 관리자 화면의 **Settings > Sendgo** 메뉴에서 다음 값을 입력합니다. 관리자 화면 문자열은 영어를 원본으로 하며, 한국어는 translate.wordpress.org 의 번역을 통해 표시됩니다. | 항목 | 설명 | | --- | --- | | `Access Key` | 샌드고 콘솔에서 발급받은 액세스 키 | | `Secret Key` | 샌드고 콘솔에서 발급받은 시크릿 키 | | `Kakao Sender Key` | 알림톡·브랜드메시지 발신프로필 키 | | `SMS Sender Key` | SMS/LMS/MMS 발신번호 키 | | `API Version` | `v1` 또는 `v2` 선택 | Access Key 와 Secret Key 가 모두 입력되어야 발송 기능이 활성화됩니다. 키는 `sendgo_options` 옵션에 서버 사이드로만 저장되며 프런트엔드에 노출되지 않습니다. API 버전 기본값은 **`v1`** 입니다. 브랜드메시지와 짧은 URL 은 `v2` 에서만 동작하므로 해당 채널을 쓰려면 바꿔야 합니다. > API 베이스 URL 은 `url` 옵션에서 읽지만 설정 화면에 필드가 없습니다. 다른 호스트를 > 가리키려면 코드로 지정하세요. > ```php > $options = get_option('sendgo_options', []); > $options['url'] = 'https://staging.sendgo.io'; > update_option('sendgo_options', $options); > ``` > 1.2.4 이전에는 설정 화면을 한 번 저장하면 이 값이 사라졌습니다. 이제는 저장해도 유지됩니다. ## WooCommerce 연동 WooCommerce가 활성화되어 있으면 다음 주문 상태 변경 시 구매자 청구 연락처로 알림을 자동 발송합니다. - `주문 완료` (`woocommerce_order_status_completed`) - `처리 중` (`woocommerce_order_status_processing`) **Settings > Sendgo > WooCommerce Order Notifications** 섹션에서 **상태별로** 다음을 설정합니다. | 필드 | 설명 | | --- | --- | | `[Order completed] Alimtalk template code` | 완료 상태에서 보낼 템플릿 코드. 첫 번째 변수(`#{var1}`)로 주문 번호가 전달됩니다. | | `[Order completed] SMS text used if Alimtalk fails` | 알림톡이 실패하거나 템플릿 코드가 비어 있을 때 보낼 SMS 본문. `{order_number}` 플레이스홀더 사용 가능. | | `[Processing] Alimtalk template code` | 처리 중 상태에서 보낼 템플릿 코드. | | `[Processing] SMS text used if Alimtalk fails` | 처리 중 상태의 SMS 대체 본문. | **비워둔 상태는 아무것도 발송하지 않습니다.** 두 상태 모두 채우면 주문마다 알림이 두 번(처리 중 + 완료) 발송되므로, 실제로 알려야 하는 상태만 채우세요. 같은 주문·같은 상태로는 한 번만 발송됩니다. 발송에 성공하면 주문 메타 (`_sendgo_notified_completed` / `_sendgo_notified_processing`)에 기록해두기 때문에, 관리자가 주문을 다시 저장하거나 다른 플러그인이 상태를 재설정해도 중복 발송되지 않습니다. 발송이 실패하면 기록하지 않으므로 다음 상태 변경에서 재시도됩니다. 발송 실패는 결제/주문 흐름을 절대 중단시키지 않으며, WooCommerce 로그(source: `sendgo`)에 기록됩니다. > **1.0.x에서 올라오는 경우** — 예전 버전은 두 상태가 `order_template_code` > 하나를 공유했기 때문에 처리 중 → 완료로 넘어가는 주문에 같은 알림이 두 번 > 발송됐습니다. 이제 완료 상태만 기존 키를 그대로 쓰고, 처리 중은 새 필드를 > 채워야 발송됩니다. 기존 설정은 그대로 동작하며 중복 발송만 사라집니다. ### 전화번호 처리 청구 연락처에서 숫자만 남겨 발송합니다(`preg_replace('/[^0-9]/', ...)`). `010-1234-5678` 과 `+82 10 1234 5678` 은 구두점만 제거되며 **국가번호를 변환하지는 않으므로**, API 가 받는 형식의 국내 번호여야 합니다. 청구 연락처가 비어 있는 주문은 조용히 건너뜁니다. ## 사용법 (프로그래밍 방식) 플러그인이 로드된 이후에는 코어 클라이언트를 직접 사용할 수 있습니다. ```php $client = Sendgo_Plugin::instance()->client(); if ($client) { // 알림톡 발송 $client->alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [['contact' => '01012345678', 'var1' => 'ORD-001']], ]); // SMS 발송 $client->sms->sendSms([ 'content' => '인증번호: 123456', 'contacts' => [['contact' => '01012345678']], ]); } ``` **항상 `$client` 를 확인하세요.** 키가 비어 있거나 `vendor/autoload.php` 가 없으면 `null` 이고, `null` 에 메서드를 호출하면 그 훅이 걸린 페이지가 백지가 됩니다. 채널은 클라이언트의 속성으로 접근합니다. | 속성 | 채널 | | --- | --- | | `$client->alimtalk` | 카카오 알림톡 | | `$client->friendtalk` | 카카오 친구톡 | | `$client->brandMessage` | 카카오 브랜드메시지 (v2 전용) | | `$client->sms` | SMS / LMS / MMS | 클라이언트는 요청 단위로 메모이즈되므로 `Sendgo_Plugin::instance()->client()` 를 여러 번 불러도 비용이 없습니다. ### 내 이벤트에 붙이기 ```php add_action('user_register', function (int $user_id): void { $client = Sendgo_Plugin::instance()->client(); if (!$client) { return; } $user = get_userdata($user_id); $phone = preg_replace('/[^0-9]/', '', (string) get_user_meta($user_id, 'billing_phone', true)); if ('' === $phone) { return; } try { $client->alimtalk->send([ 'templateCode' => 'WELCOME_001', 'contacts' => [['contact' => $phone, 'var1' => $user->display_name]], ]); } catch (\Throwable $e) { // 발송 실패가 회원가입을 깨뜨리지 않게 한다. error_log('Sendgo welcome message failed: ' . $e->getMessage()); } }, 10, 1); ``` `try`/`catch` 로 감싸는 것이 핵심입니다. WordPress 훅 안에서 처리되지 않은 `SendgoException` 은 그 훅을 실행한 페이지의 치명적 오류로 드러납니다. ### 브랜드메시지 (친구톡의 후속 채널) > **친구톡은 2025-12-31 종료되었습니다.** 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 > 브랜드메시지(자유형)로 자동 대체 발송됩니다. `friendtalk` 은 여전히 동작하며, 개별 수신자에게 > 보내는 자유 본문 `FT`/`FI`/`FW` 의 유일한 경로이기도 합니다. 템플릿 기반 리치 타입 > (`FL`/`FC`/`FM`/`FP`/`FA`), 친구가 아닌 대상(`N`/`I`), 동보(`F`)에는 브랜드메시지를 쓰세요. 브랜드메시지는 채널 친구가 아닌 수신자에게도 보낼 수 있고(`targeting` = `N`), 수신 동의한 전체 채널 친구에게 동보 발송할 수도 있습니다(`targeting` = `F`). 메시지 타입은 친구톡과 1:1로 대응합니다(`FT`→`BT`, `FI`→`BI`, `FW`→`BW`, `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`). > **v2 전용입니다.** **Settings > Sendgo > API Version** 을 `v2` 로 바꿔야 동작합니다. > 플러그인 기본값은 `v1` 입니다. ```php $client = Sendgo_Plugin::instance()->client(); // 단건 발송 — targeting 이 M/N/I 이면 contacts 가 필요합니다. $client->brandMessage->send([ 'targeting' => 'M', 'messageType' => 'FL', 'friendTemplateUuid' => '9cd5460b-6458-4edc-9b11-c26d3013c340', 'contacts' => [['contact' => '01012345678', 'var1' => '29,000원']], ]); // 동보 발송 — 수신 동의한 전체 채널 친구 (수신자 목록 없음) $result = $client->brandMessage->broadcast([ 'messageType' => 'FW', 'friendTemplateUuid' => '9cd5460b-6458-4edc-9b11-c26d3013c340', ]); // 동보 발송은 업스트림에서 비동기 처리되므로 진행 상황을 조회합니다. $client->brandMessage->campaign($result['data']['campaignId']); $client->brandMessage->campaigns(['count' => 10]); ``` 동보 발송은 친구 목록 전체에 닿을 수 있으므로, 공개 훅이 아니라 의도적인 관리자 동작 (WP-CLI 명령이나 권한 검사를 붙인 admin-post 핸들러)에서만 호출하세요. ### 짧은 URL (클릭 반응 분석) 메시지 본문에 넣는 링크를 줄이고, 실제로 눌렸는지 집계합니다. 문자는 한 통에 들어가는 바이트가 정해져 있어 긴 링크가 그대로 들어가면 LMS 로 넘어가 단가가 올라갑니다. > **v2 전용입니다.** 같은 원본 URL 을 다시 줄이면 **기존 링크를 그대로 반환합니다.** 캠페인별로 반응 수치를 따로 보려면 `forceNew` 를 넘겨 새 코드를 만드세요. `deactivate` 는 링크를 지우지 않고 리다이렉트만 멈춥니다. 이미 발송한 메시지의 링크를 막아야 할 때 쓰며, 쌓인 통계는 남고 방문자는 `410 Gone` 을 받습니다. ```php $client = Sendgo_Plugin::instance()->client(); // 생성 — 같은 원본 URL 이면 기존 코드를 재사용합니다(forceNew 로 강제 신규 생성). $link = $client->shortUrl->create([ 'targetUrl' => 'https://shop.example.com/orders/1024', 'title' => '10월 주문 안내', ]); $link['data']['shortUrl']; // https://sendgo.io/s/aB3xY7z // 클릭 반응 — 일별 추이 · 디바이스 · 유입경로 · 국가 $client->shortUrl->stats($link['data']['uuid'], ['from' => '2026-08-01', 'to' => '2026-08-11']); $client->shortUrl->list(['count' => 20]); $client->shortUrl->show($link['data']['uuid']); // 중지 — 리다이렉트만 멈추고(410 Gone) 통계는 남습니다. $client->shortUrl->deactivate($link['data']['uuid']); ``` ## 오류 처리 ```php use Sendgo\Php\Exception\SendgoException; try { $client->alimtalk->send([...]); } catch (SendgoException $e) { // 발송 실패는 구매자에게 드러내지 않고 로그로 남긴다. if (function_exists('wc_get_logger')) { wc_get_logger()->error( sprintf('Sendgo %d [%s]: %s', $e->getStatusCode(), $e->getErrorCode(), $e->getMessage()), ['source' => 'sendgo'] ); } } ``` 메시지 문자열이 아니라 `getErrorCode()` 로 분기하세요. 메시지는 바뀔 수 있고 코드가 계약입니다. `TOKEN_EXPIRED` 와 `TOKEN_MISMATCH` 는 코어 SDK 안에서 처리됩니다 — 토큰을 재발급하고 요청을 한 번 재시도합니다. ## FAQ **Q. WooCommerce 없이도 사용할 수 있나요?** 네. 코어 클라이언트(`Sendgo_Plugin::instance()->client()`)를 통해 알림톡/SMS를 직접 발송할 수 있습니다. 주문 자동 알림만 WooCommerce에 의존합니다. **Q. 클라이언트가 `null`을 반환합니다.** Access Key 또는 Secret Key 가 설정되지 않았거나, 소스에서 직접 받아 `composer install` 을 실행하지 않아 `vendor/autoload.php` 가 없는 경우입니다. 후자는 관리자 화면에 오류 알림으로 표시됩니다. 배포 zip 에는 `vendor/` 가 포함되어 있으므로 이 경우가 생기지 않습니다. **Q. 인증 키는 어디에 저장되나요?** `sendgo_options` 옵션에 서버 사이드로만 저장됩니다. 프런트엔드에 노출되지 않으며, 시크릿 키는 관리자 화면에서 password 필드로 렌더링됩니다. **Q. 발송이 안 되는데 오류도 없습니다.** 콘솔의 **연동하기 > 연동 정보**에서 호출을 허용할 IP 를 등록했는지 확인하세요. 등록되지 않은 주소에서 온 요청은 거부됩니다. WordPress 사이트가 나가는 IP 를 등록해야 하며, 공유 호스팅이나 CDN 뒤에 있으면 브라우저에 보이는 IP 와 다를 수 있습니다. **Q. 삭제하면 정리가 되나요?** `uninstall.php` 가 `sendgo_options` 옵션을 지웁니다. `_sendgo_notified_*` 주문 메타는 남깁니다 — 무해하고, 지우려면 전체 주문에 대량 쓰기를 해야 합니다. ## 변경 사항 ### 1.2.4 (2026-08-20) - readme 와 플러그인 헤더, 번역 가능한 모든 문자열을 영어로 교체했습니다. msgid 가 한국어라 translate.wordpress.org 에서 번역이 불가능했습니다. - 연동하는 외부 서비스와 전송되는 데이터를 readme 에 명시했습니다(약관·개인정보처리방침 링크 포함). - **버그 수정: `url` 옵션 유실** — 설정 화면에 필드가 없는데 정제 목록에 들어 있어서, 설정을 한 번 저장하면 값이 항상 사라졌습니다. API 베이스 URL 을 재정의해 둔 사이트는 관리자가 저장 버튼을 누르는 순간 기본값으로 되돌아갔습니다. ### 1.2.3 (2026-08-16) - 플러그인 헤더의 Plugin URI 와 Author URI 가 같아 wordpress.org 업로드가 거부되던 문제 수정. ### 1.2.2 (2026-08-16) - wordpress.org 배포 준비 — 코어 SDK(`sendgo/php`)를 `vendor/` 에 번들해 Composer 없이 동작합니다. - 플러그인 헤더(1.2.1)와 `SENDGO_VERSION` 상수(1.1.0)의 버전 불일치 수정. - **코어 SDK 를 찾지 못하면 조용히 아무것도 하지 않던 문제 수정** — 설치·활성화·설정까지 정상으로 보이는데 알림만 안 나가는 상태였습니다. 이제 관리자 알림으로 원인을 표시합니다. ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ### 1.1.0 (2026-08-11) - **버그 수정: WooCommerce 중복 알림 발송** — 주문 상태가 같은 값으로 다시 저장되면 알림이 다시 나갔습니다. `_sendgo_notified_{status}` 주문 메타로 상태별 1회만 발송하도록 막았습니다. 발송이 실패하면 메타를 남기지 않아 다음 상태 변경에서 재시도됩니다. - 주문 상태별로 템플릿을 따로 지정할 수 있게 옵션을 분리했습니다. - 브랜드메시지(친구톡 후속 채널) 사용법 추가 - 짧은 URL 사용법 추가 ## 연동하는 외부 서비스 이 플러그인은 카카오 알림톡·브랜드메시지와 SMS/LMS/MMS 를 발송하기 위해 샌드고 API (https://sendgo.io)에 접속합니다. 발송에는 샌드고 계정이 필요하며, 발송은 유료입니다. - 발송 직전 액세스 키와 시크릿 키로 인증 요청을 보내 단기 토큰을 받습니다. - WooCommerce 주문이 `처리 중`·`주문 완료` 로 바뀌고 해당 상태에 템플릿 코드나 SMS 대체 내용이 설정돼 있으면, 구매자 청구 연락처와 주문 번호를 발신 키·템플릿 코드와 함께 보냅니다. - 코드에서 직접 클라이언트를 호출하면 넘긴 수신 번호와 본문이 그대로 전송됩니다. 설치·활성화만 한 상태나 템플릿 코드·SMS 대체 내용이 모두 비어 있는 상태에서는 아무 데이터도 전송되지 않습니다. - 이용약관: https://sendgo.io/terms-of-service - 개인정보처리방침: https://sendgo.io/privacy-policy ## 라이선스 MIT © amuz — https://sendgo.io --- ## 패키지 정보 - **패키지**: `sendgo/wordpress` (Packagist) - **저장소**: [send-go/wordpress](https://github.com/send-go/wordpress) - **레지스트리**: https://packagist.org/packages/sendgo/wordpress - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **Node.js / TypeScript에서 카카오 알림톡, 브랜드메시지, SMS를 가장 쉽게 발송하는 SDK** `@sendgo/node`는 [Sendgo](https://sendgo.io) 알림 API를 위한 공식 Node.js / TypeScript SDK입니다. **외부 런타임 의존성 없이** Node.js 내장 `fetch`만을 사용하며, 완전한 TypeScript 타입 정의를 제공합니다. Next.js, Express, Fastify, NestJS 등 모든 Node.js 프레임워크에서 사용할 수 있습니다. --- ## Sendgo란? [Sendgo](https://sendgo.io)는 대한민국 기업과 개발자를 위한 **통합 알림 발송 플랫폼**입니다. - **카카오 알림톡**: 카카오톡 채널을 통한 정보성 메시지 (주문 확인, 배송 안내, 인증번호, 예약 확인 등) - **카카오 친구톡**: 카카오톡 채널 친구에게 마케팅/정보성 메시지 (이벤트, 쿠폰, 프로모션) - **SMS / LMS / MMS**: 전통적인 문자 메시지 - **자동 대체 발송**: 알림톡 전송 실패 시 SMS로 자동 전환 --- ## 주요 기능 | 기능 | 설명 | |------|------| | **Zero 런타임 의존성** | Node.js 18+ 내장 `fetch` 사용, 외부 패키지 불필요 | | **완전한 TypeScript 지원** | 모든 요청/응답에 타입 정의 제공 | | **토큰 자동 관리** | 발급·갱신·캐시(50분) 자동 처리 | | **동시 요청 중복 방지** | 여러 요청이 동시에 들어와도 토큰 발급은 1회만 수행 | | **401/403 자동 재시도** | 토큰 만료 시 자동 갱신 후 재발송 | | **다건 동시 발송** | 수신자 배열로 대량 발송 | | **예약 발송** | 원하는 시각에 예약 발송 | | **SMS 자동 대체 발송** | 알림톡 실패 시 SMS로 자동 전환 | | **v1 / v2 API 지원** | 설정 한 줄로 API 버전 전환 | --- ## 지원 메시지 유형 ### 카카오 알림톡 (Alimtalk) - 사전 승인된 템플릿 기반 발송 - 템플릿 변수 `#{var1}` ~ `#{var8}` 지원 - 즉시/예약 발송, SMS 대체 발송 ### 카카오 친구톡 (Friendtalk) > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. - 자유 형식 메시지 발송 - 텍스트(FT), 이미지(FI), 와이드이미지(FW), 리스트(FL), 복합(FM), 커머스(FC) 등 8종 - 버튼, 이미지, 링크 첨부 ### SMS / LMS / MMS - SMS: 단문 (90바이트), LMS: 장문 (2,000바이트), MMS: 이미지 첨부 - 일반/광고/선거 캠페인 유형 --- ## 설치 ```bash # npm npm install @sendgo/node # pnpm pnpm add @sendgo/node # yarn yarn add @sendgo/node # bun bun add @sendgo/node ``` **요구사항:** Node.js 18 이상 (내장 `fetch` 필요) --- ## 빠른 시작 ### 1단계 — 환경변수 설정 ```bash # .env SENDGO_ACCESS_KEY=your_access_key SENDGO_SECRET_KEY=your_secret_key SENDGO_KAKAO_SENDER_KEY=your_kakao_sender_key SENDGO_SMS_SENDER_KEY=your_sms_sender_key SENDGO_API_VERSION=v2 ``` > **카카오 발신프로필 키 발급**: [Sendgo 콘솔](https://sendgo.io) → 카카오 발신프로필 → 등록 ### 2단계 — 클라이언트 초기화 ```typescript import Sendgo from '@sendgo/node'; const sendgo = new Sendgo({ accessKey: process.env.SENDGO_ACCESS_KEY!, secretKey: process.env.SENDGO_SECRET_KEY!, kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY, smsSenderKey: process.env.SENDGO_SMS_SENDER_KEY, apiVersion: 'v2', // 'v1' | 'v2' }); ``` ### 3단계 — 알림톡 전송 ```typescript await sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', // Sendgo 승인 템플릿 코드 contacts: [ { contact: '01012345678', // 수신자 전화번호 (필수) name: '홍길동', // 수신자 이름 (선택) var1: 'ORD-20260723-001', // 템플릿 변수 #{var1} var2: '스프링 부트 가이드', // 템플릿 변수 #{var2} var3: '29,000원', // 템플릿 변수 #{var3} }, ], }); ``` --- ## 상세 사용법 ### 카카오 알림톡 #### 단건 발송 ```typescript await sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [{ contact: '01012345678', name: '홍길동', var1: 'ORD-001', // 주문번호 var2: '맥북 프로', // 상품명 var3: '3,490,000원', // 결제금액 var4: '2026-07-25', // 배송 예정일 }], }); ``` #### 다건 발송 (대량 발송) ```typescript await sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [ { contact: '01011111111', var1: 'ORD-001' }, { contact: '01022222222', var1: 'ORD-002' }, { contact: '01033333333', var1: 'ORD-003' }, ], }); ``` #### 예약 발송 ```typescript await sendgo.alimtalk.send({ templateCode: 'PROMO_SUMMER_2026', scheduleType: 'SCHEDULED', at: '2026-07-28 09:00:00', // 발송 예약 시각 (Y-m-d H:i:s) contacts: [{ contact: '01012345678', var1: '여름 한정 50% 할인' }], }); ``` #### 알림톡 실패 시 SMS 자동 대체 발송 ```typescript await sendgo.alimtalk.send({ templateCode: 'DELIVERY_START_001', replaceSms: 'Y', smsSubject: '[배송 시작 안내]', smsContent: '주문하신 상품이 출고되었습니다.\n송장번호: #{var2}', contacts: [{ contact: '01012345678', var1: 'ORD-001', var2: '1234567890', // 송장번호 }], }); ``` --- ### 카카오 친구톡 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```typescript // 기본 텍스트 await sendgo.friendtalk.send({ content: '안녕하세요! 7월 한정 특가 이벤트를 확인해보세요. 최대 50% 할인!', contacts: [{ contact: '01012345678' }], }); // 이미지 + 버튼 await sendgo.friendtalk.send({ messageType: 'FI', content: '이번 주 특가 상품을 확인하세요!', imageUrl: 'https://cdn.example.com/banner.jpg', imageLink: 'https://example.com/event', buttons: [{ name: '이벤트 보기', type: 'WL', linkMo: 'https://example.com/event', linkPc: 'https://example.com/event', }], contacts: [{ contact: '01012345678' }], }); ``` --- ### SMS / LMS / MMS ```typescript // SMS — 단문 (90자 이하) await sendgo.sms.sendSms({ content: '[Sendgo] 인증번호: 123456 (5분 이내 입력)', contacts: [{ contact: '01012345678' }], }); // LMS — 장문 (제목 포함) await sendgo.sms.sendLms({ subject: '[중요] 서비스 점검 안내', content: `안녕하세요. 서비스 점검이 예정되어 있습니다. ■ 점검 일시: 2026-07-25 02:00 ~ 06:00 ■ 영향 범위: 전체 서비스 이용에 불편을 드려 죄송합니다.`, contacts: [{ contact: '01012345678' }], }); // MMS — 멀티미디어 await sendgo.sms.sendMms({ subject: '[이벤트] 7월 특가', content: '이번 달 특가 상품을 확인하세요!', contacts: [{ contact: '01012345678' }], }); ``` --- ## 브랜드메시지 사용법 브랜드메시지는 친구톡의 후속 채널입니다. 메시지 타입이 친구톡과 1:1 대응되며 (`FT`→`BT`, `FI`→`BI`, `FW`→`BW`, `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`), 요청에는 **친구톡 코드를 그대로** 넘기고 변환은 서버가 처리합니다. 친구톡과 달리 다음이 가능합니다. - 채널 친구가 **아닌** 수신자에게 발송 (`targeting: N`) - 수신 동의한 **전체 채널 친구 동보** 발송 (`targeting: F`, 수신자 목록 불필요) - 리스트·캐러셀·커머스·동영상 등 **템플릿 기반 리치 메시지** > v2 전용입니다. 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보낼 때는 여전히 친구톡 API 를 쓰세요 — 이 엔드포인트는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. 친구톡 요청은 카카오 측에서 브랜드메시지(자유형)로 대체 발송됩니다. ```typescript // 단건 발송 — 채널 친구 대상 await sendgo.brandMessage.send({ targeting: 'M', messageType: 'FL', friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340', contacts: [{ contact: '01012345678', var1: '29,000원' }], }); // 동보 발송 — 수신 동의한 전체 채널 친구 (contacts 불필요) await sendgo.brandMessage.broadcast({ messageType: 'FW', friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340', }); // 캠페인 조회 const list = await sendgo.brandMessage.campaigns({ count: 10 }); const one = await sendgo.brandMessage.campaign('1f0a6d0e-6b3b-4f0f-9b2f-2f6f6a1b7c11'); ``` --- ## 프레임워크 통합 ### Next.js App Router ```typescript // app/api/notify/route.ts import { NextRequest, NextResponse } from 'next/server'; import Sendgo from '@sendgo/node'; // 싱글톤 패턴으로 API Route 간 재사용 const sendgo = new Sendgo({ accessKey: process.env.SENDGO_ACCESS_KEY!, secretKey: process.env.SENDGO_SECRET_KEY!, kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY, apiVersion: 'v2', }); export async function POST(request: NextRequest) { const { phone, orderNumber } = await request.json(); await sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [{ contact: phone, var1: orderNumber }], }); return NextResponse.json({ success: true }); } ``` ```typescript // app/actions/notification.ts — Server Actions 'use server'; import Sendgo from '@sendgo/node'; const sendgo = new Sendgo({ /* ... */ }); export async function sendOrderConfirm(phone: string, orderNumber: string) { return sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [{ contact: phone, var1: orderNumber }], }); } ``` ### Express.js ```typescript import express from 'express'; import Sendgo from '@sendgo/node'; const app = express(); const sendgo = new Sendgo({ accessKey: process.env.SENDGO_ACCESS_KEY!, secretKey: process.env.SENDGO_SECRET_KEY!, kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY, }); app.use(express.json()); app.post('/api/notify', async (req, res) => { const { phone, orderNumber } = req.body; await sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [{ contact: phone, var1: orderNumber }], }); res.json({ success: true }); }); ``` ### NestJS ```typescript // sendgo.module.ts import { Module, Global } from '@nestjs/common'; import Sendgo from '@sendgo/node'; @Global() @Module({ providers: [{ provide: 'SENDGO_CLIENT', useFactory: () => new Sendgo({ accessKey: process.env.SENDGO_ACCESS_KEY!, secretKey: process.env.SENDGO_SECRET_KEY!, kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY, apiVersion: 'v2', }), }], exports: ['SENDGO_CLIENT'], }) export class SendgoModule {} // notification.service.ts @Injectable() export class NotificationService { constructor(@Inject('SENDGO_CLIENT') private readonly sendgo: Sendgo) {} async sendOrderConfirm(phone: string, orderNumber: string) { return this.sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [{ contact: phone, var1: orderNumber }], }); } } ``` ### Fastify ```typescript import Fastify from 'fastify'; import Sendgo from '@sendgo/node'; const app = Fastify(); const sendgo = new Sendgo({ accessKey: '...', secretKey: '...' }); app.post('/notify', async (request, reply) => { const { phone, orderNumber } = request.body as any; await sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [{ contact: phone, var1: orderNumber }], }); return { success: true }; }); ``` --- ## TypeScript 타입 ```typescript import type { SendgoConfig, // 클라이언트 설정 Contact, // 수신자 정보 AlimtalkParams, // 알림톡 발송 파라미터 FriendtalkParams, // 친구톡 발송 파라미터 SmsParams, // SMS 발송 파라미터 SendgoResponse, // API 응답 ScheduleType, // 'DIRECTLY' | 'SCHEDULED' SmsMessageType, // 'SMS' | 'LMS' | 'MMS' FriendtalkMessageType, // 'FT' | 'FI' | 'FW' | ... } from '@sendgo/node'; ``` --- ## 예외 처리 ```typescript import { SendgoError } from '@sendgo/node'; try { await sendgo.alimtalk.send({ ... }); } catch (error) { if (error instanceof SendgoError) { console.error({ statusCode: error.statusCode, // HTTP 상태 코드 errorCode: error.errorCode, // Sendgo 에러 코드 message: error.message, // 에러 메시지 endpoint: error.endpoint, // 호출된 엔드포인트 }); switch (error.errorCode) { case 'INVALID_TEMPLATE_CODE': /* 템플릿 코드 확인 */ break; case 'PAYMENT_REQUIRED': /* 크레딧 충전 알림 */ break; case 'EMPTY_CONTACTS': /* 수신자 확인 */ break; } } } ``` --- ## 설정 옵션 | 옵션 | 타입 | 필수 | 기본값 | 설명 | |------|------|------|--------|------| | `accessKey` | `string` | **필수** | — | Sendgo 액세스 키 | | `secretKey` | `string` | **필수** | — | Sendgo 시크릿 키 | | `kakaoSenderKey` | `string` | 선택 | — | 카카오 발신프로필 키 | | `smsSenderKey` | `string` | 선택 | — | SMS 발신자 키 | | `apiVersion` | `'v1' \| 'v2'` | 선택 | `'v1'` | API 버전 | | `baseUrl` | `string` | 선택 | `'https://sendgo.io'` | API 기본 URL | --- ## 자주 묻는 질문 (FAQ) **Q. CommonJS(`require`)를 지원하나요?** A. 네. `dist/index.js`는 CommonJS 형식으로 빌드되어 `require('@sendgo/node')`로 사용 가능합니다. **Q. Node.js 16에서 사용할 수 있나요?** A. 내장 `fetch`가 Node.js 18에서 안정화되었으므로, 18 이상을 권장합니다. Node.js 16에서는 `node-fetch`를 글로벌로 폴리필해야 합니다. **Q. 토큰은 어떻게 관리되나요?** A. SDK 내부에서 인메모리로 캐싱(50분)합니다. 서버리스(Lambda, Vercel Functions) 환경에서는 콜드 스타트 시 매번 새 토큰이 발급됩니다. **Q. 발송 실패 시 자동 재시도가 되나요?** A. 401/403 응답 시 토큰을 갱신하고 1회 재시도합니다. 그 외 실패는 `SendgoError`로 예외가 발생합니다. **Q. 알림톡 템플릿은 어디서 만드나요?** A. [Sendgo 콘솔](https://sendgo.io) → 알림톡 템플릿 → 템플릿 작성 → 카카오 심사 신청 --- ## 짧은 URL 짧은 URL 은 메시지 본문의 링크를 줄이고, 그 링크가 실제로 눌렸는지 집계합니다. 문자는 바이트 수가 요금과 직결되므로 링크를 줄이면 그만큼 본문을 더 쓸 수 있습니다. 같은 원본 URL 을 다시 줄이면 **기존 링크가 그대로 반환**됩니다. 캠페인별로 반응을 따로 집계하려면 `forceNew` 로 새 코드를 만드세요. `deactivate` 는 링크를 삭제하지 않고 리다이렉트만 중지합니다. 이미 발송한 메시지의 링크를 무효화할 때 쓰며, 누적 통계는 남고 이후 접속은 `410 Gone` 이 됩니다. ```typescript // 짧은 URL 생성 (v2 전용) const created = await sendgo.shortUrl.create({ targetUrl: 'https://example.com/promotions/summer-sale', title: '여름 세일 랜딩', }); const { code, shortUrl } = created.data; // 반응 통계 — 일별 추이 + 디바이스/유입경로/국가별 분해 const stats = await sendgo.shortUrl.stats(code, { from: '2026-08-01' }); await sendgo.shortUrl.list({ count: 10 }); await sendgo.shortUrl.show(code); await sendgo.shortUrl.deactivate(code); // 리다이렉트만 중지, 통계는 남는다 ``` `stats` 는 일별 추이(`daily`)와 디바이스(`byDevice`)·유입경로(`byReferer`)·국가(`byCountry`)별 분해를 반환합니다. 일별 추이는 사전 집계 표에서 읽으므로 클릭이 많아도 응답 시간이 일정합니다. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ### 1.1.0 (2026-08-11) - 짧은 URL 추가 — `sendgo.shortUrl` - `HttpClient.delete()` 추가. `request()` 의 메서드 타입이 `'GET'|'POST'` 로 묶여 DELETE 를 표현할 수 없었다. - `ShortUrlParams` / `ShortUrlListParams` / `ShortUrlStatsParams` 타입 추가 ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `@sendgo/node` (npm) - **저장소**: [send-go/node](https://github.com/send-go/node) - **레지스트리**: https://www.npmjs.com/package/@sendgo/node - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **React / Next.js에서 카카오 알림톡, 브랜드메시지, SMS를 발송하는 공식 React SDK** > **중요**: 이 패키지는 **서버사이드 전용**입니다. > Next.js Server Actions, Route Handlers, API Routes에서만 사용하세요. > 클라이언트 컴포넌트에서 직접 사용하면 API 키가 브라우저에 노출됩니다. --- ## 설치 ```bash npm install @sendgo/react @sendgo/node # 또는 pnpm add @sendgo/react @sendgo/node ``` --- ## 빠른 시작 ### Next.js Server Action ```typescript // app/actions/notify.ts 'use server' import { createSendgoClient } from '@sendgo/react'; const sendgo = createSendgoClient({ accessKey: process.env.SENDGO_ACCESS_KEY!, secretKey: process.env.SENDGO_SECRET_KEY!, kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY, apiVersion: 'v2', }); export async function sendOrderConfirmAction(phone: string, orderNo: string) { return sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [{ contact: phone, var1: orderNo }], }); } ``` ### 클라이언트 컴포넌트에서 훅 사용 ```tsx // app/components/OrderButton.tsx 'use client' import { useAlimtalk } from '@sendgo/react'; import { sendOrderConfirmAction } from '../actions/notify'; export function OrderButton({ phone, orderNo }: { phone: string; orderNo: string }) { const { send, loading, error } = useAlimtalk(sendOrderConfirmAction); return (
{error &&

발송 실패: {error.message}

}
); } ``` --- ## 알림톡 상세 사용법 ```typescript // app/actions/alimtalk.ts 'use server' import { createSendgoClient } from '@sendgo/react'; const sendgo = createSendgoClient({ accessKey: process.env.SENDGO_ACCESS_KEY!, secretKey: process.env.SENDGO_SECRET_KEY!, kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY, smsSenderKey: process.env.SENDGO_SMS_SENDER_KEY, apiVersion: 'v2', }); // 다건 발송 export async function sendBulkAlimtalk() { return sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [ { contact: '01011111111', name: '홍길동', var1: 'ORD-001', var2: '29,000원' }, { contact: '01022222222', name: '김철수', var1: 'ORD-002', var2: '15,000원' }, { contact: '01033333333', name: '이영희', var1: 'ORD-003', var2: '52,000원' }, ], }); } // 예약 발송 export async function sendScheduledAlimtalk(phone: string) { return sendgo.alimtalk.send({ templateCode: 'PROMO_SUMMER_2026', scheduleType: 'SCHEDULED', at: '2026-07-28 09:00:00', contacts: [{ contact: phone, var1: '여름 한정 50% 할인' }], }); } // SMS 대체 발송 export async function sendWithFallback(phone: string, trackingNo: string) { return sendgo.alimtalk.send({ templateCode: 'DELIVERY_START_001', replaceSms: 'Y', smsSubject: '[배송 시작 안내]', smsContent: `주문하신 상품이 출고되었습니다.\n송장번호: ${trackingNo}`, contacts: [{ contact: phone, var1: 'ORD-001', var2: trackingNo }], }); } ``` --- ## SMS / LMS / MMS 사용법 ```typescript // app/actions/sms.ts 'use server' import { createSendgoClient } from '@sendgo/react'; const sendgo = createSendgoClient({ accessKey: '...', secretKey: '...' }); // SMS export async function sendSms(phone: string, code: string) { return sendgo.sms.sendSms({ content: `[Sendgo] 인증번호: ${code} (5분 이내 입력)`, contacts: [{ contact: phone }], }); } // LMS export async function sendLms(phone: string) { return sendgo.sms.sendLms({ subject: '[중요] 서비스 점검 안내', content: '안녕하세요. 서비스 점검이 예정되어 있습니다.\n■ 일시: 2026-07-25 02:00 ~ 06:00', contacts: [{ contact: phone }], }); } ``` --- ## Route Handler (App Router) ```typescript // app/api/notify/order/route.ts import { NextRequest, NextResponse } from 'next/server'; import { createSendgoClient } from '@sendgo/react'; const sendgo = createSendgoClient({ accessKey: process.env.SENDGO_ACCESS_KEY!, secretKey: process.env.SENDGO_SECRET_KEY!, kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY, apiVersion: 'v2', }); export async function POST(request: NextRequest) { const { phone, orderNo, amount } = await request.json(); await sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [{ contact: phone, var1: orderNo, var2: amount }], }); return NextResponse.json({ success: true }); } ``` --- ## useSms 훅 ```tsx 'use client' import { useSms } from '@sendgo/react'; import { sendSmsAction } from '../actions/sms'; export function VerificationForm() { const [phone, setPhone] = useState(''); const { send, loading, error, data } = useSms(sendSmsAction); return (
{ e.preventDefault(); send(phone, '123456'); }}> setPhone(e.target.value)} placeholder="010-0000-0000" /> {error &&

발송 실패: {error.message}

} {data &&

인증번호가 발송되었습니다.

}
); } ``` --- ## 브랜드메시지 · 짧은 URL 이 패키지는 코어(`@sendgo/node`)의 클라이언트를 그대로 노출하므로, 코어에 있는 채널이 모두 그대로 쓸 수 있습니다. 두 기능 모두 **v2 전용**입니다. | 기능 | 접근 | |------|------| | 카카오 브랜드메시지 (친구톡의 후속 채널) | `sendBrandMessage()` | | 짧은 URL (단축 + 클릭 반응 분석) | `createShortUrl() / shortUrlStats()` | 브랜드메시지는 채널 친구가 아닌 수신자에게도 보낼 수 있고(`targeting` = `N`), 수신 동의한 전체 채널 친구에게 동보 발송할 수도 있습니다(`targeting` = `F`). 짧은 URL 은 메시지 본문의 링크를 줄이고 클릭 반응(일별 추이·디바이스·유입경로·국가)을 집계합니다. 사용 예시와 파라미터는 [코어 README](https://github.com/send-go) 와 [SDK 가이드](https://sendgo.io/ko/sdk) 를 참고하세요. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ### 1.1.0 (2026-08-11) - 짧은 URL 서버 액션 추가 — `createShortUrl` / `listShortUrls` / `getShortUrl` / `shortUrlStats` / `deactivateShortUrl` ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `@sendgo/react` (npm) - **저장소**: [send-go/react](https://github.com/send-go/react) - **레지스트리**: https://www.npmjs.com/package/@sendgo/react - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **Vue.js / Nuxt 3에서 카카오 알림톡, 브랜드메시지, SMS를 발송하는 공식 Vue SDK** > **중요**: 이 패키지는 **서버사이드 전용**입니다. > Nuxt 3 Server Routes, API Routes에서만 사용하세요. > 클라이언트 컴포넌트에서 직접 사용하면 API 키가 브라우저에 노출됩니다. --- ## 설치 ```bash npm install @sendgo/vue @sendgo/node # 또는 pnpm add @sendgo/vue @sendgo/node ``` --- ## 빠른 시작 ### Nuxt 3 Server Route에서 알림톡 전송 ```typescript // server/api/notify/order.post.ts import Sendgo from '@sendgo/node'; const sendgo = new Sendgo({ accessKey: process.env.SENDGO_ACCESS_KEY!, secretKey: process.env.SENDGO_SECRET_KEY!, kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY, smsSenderKey: process.env.SENDGO_SMS_SENDER_KEY, apiVersion: 'v2', }); export default defineEventHandler(async (event) => { const { phone, orderNo, amount } = await readBody(event); await sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [{ contact: phone, var1: orderNo, var2: amount }], }); return { success: true }; }); ``` ### Vue 3 컴포저블로 호출 ```vue ``` --- ## 알림톡 상세 사용법 ```typescript // server/api/alimtalk.post.ts import Sendgo from '@sendgo/node'; const sendgo = new Sendgo({ accessKey: process.env.SENDGO_ACCESS_KEY!, secretKey: process.env.SENDGO_SECRET_KEY!, kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY, smsSenderKey: process.env.SENDGO_SMS_SENDER_KEY, apiVersion: 'v2', }); export default defineEventHandler(async (event) => { const body = await readBody(event); switch (body.action) { case 'bulk': // 다건 발송 await sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [ { contact: '01011111111', name: '홍길동', var1: 'ORD-001', var2: '29,000원' }, { contact: '01022222222', name: '김철수', var1: 'ORD-002', var2: '15,000원' }, { contact: '01033333333', name: '이영희', var1: 'ORD-003', var2: '52,000원' }, ], }); break; case 'scheduled': // 예약 발송 await sendgo.alimtalk.send({ templateCode: 'PROMO_SUMMER_2026', scheduleType: 'SCHEDULED', at: '2026-07-28 09:00:00', contacts: [{ contact: body.phone, var1: '여름 한정 50% 할인' }], }); break; case 'with-fallback': // SMS 자동 대체 발송 await sendgo.alimtalk.send({ templateCode: 'DELIVERY_START_001', replaceSms: 'Y', smsSubject: '[배송 시작 안내]', smsContent: `주문하신 상품이 출고되었습니다.\n송장번호: ${body.trackingNo}`, contacts: [{ contact: body.phone, var1: 'ORD-001', var2: body.trackingNo }], }); break; } return { success: true }; }); ``` --- ## SMS / LMS / MMS 사용법 ```typescript // server/api/sms.post.ts import Sendgo from '@sendgo/node'; const sendgo = new Sendgo({ accessKey: '...', secretKey: '...' }); export default defineEventHandler(async (event) => { const { type, phone, content, subject } = await readBody(event); switch (type) { case 'sms': return sendgo.sms.sendSms({ content, contacts: [{ contact: phone }] }); case 'lms': return sendgo.sms.sendLms({ subject, content, contacts: [{ contact: phone }] }); case 'mms': return sendgo.sms.sendMms({ subject, content, contacts: [{ contact: phone }] }); } }); ``` --- ## Nuxt 플러그인으로 전역 등록 ```typescript // plugins/sendgo.server.ts import { SendgoPlugin } from '@sendgo/vue'; export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.use(SendgoPlugin, { accessKey: process.env.SENDGO_ACCESS_KEY!, secretKey: process.env.SENDGO_SECRET_KEY!, kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY, apiVersion: 'v2', }); }); ``` --- ## useBrandMessage 훅 브랜드메시지는 친구톡의 후속 채널입니다. 친구톡은 카카오 정책에 따라 **2025-12-31 종료**되었고, 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 브랜드메시지(자유형)로 자동 대체 발송됩니다. **v2 전용**입니다. ```vue ``` --- ## 브랜드메시지 · 짧은 URL 이 패키지는 코어(`@sendgo/node`)의 클라이언트를 그대로 노출하므로, 코어에 있는 채널이 모두 그대로 쓸 수 있습니다. 두 기능 모두 **v2 전용**입니다. | 기능 | 접근 | |------|------| | 카카오 브랜드메시지 (친구톡의 후속 채널) | `sendgo.brandMessage` | | 짧은 URL (단축 + 클릭 반응 분석) | `sendgo.shortUrl` | 브랜드메시지는 채널 친구가 아닌 수신자에게도 보낼 수 있고(`targeting` = `N`), 수신 동의한 전체 채널 친구에게 동보 발송할 수도 있습니다(`targeting` = `F`). 짧은 URL 은 메시지 본문의 링크를 줄이고 클릭 반응(일별 추이·디바이스·유입경로·국가)을 집계합니다. 사용 예시와 파라미터는 [코어 README](https://github.com/send-go) 와 [SDK 가이드](https://sendgo.io/ko/sdk) 를 참고하세요. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ### 1.1.0 (2026-08-11) - 짧은 URL 타입 재수출 (`ShortUrlParams` 등) ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `@sendgo/vue` (npm) - **저장소**: [send-go/vue](https://github.com/send-go/vue) - **레지스트리**: https://www.npmjs.com/package/@sendgo/vue - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **NestJS에서 카카오 알림톡, 브랜드메시지, SMS를 가장 쉽게 발송하는 공식 NestJS 모듈** `@sendgo/nestjs`는 [`@sendgo/node`](https://github.com/send-go/node) 코어를 확장한 **NestJS 전용 모듈**입니다. `DynamicModule` 기반의 `forRoot` / `forRootAsync` 등록, 전역(Global) 프로바이더, 생성자 주입(DI)을 완벽하게 제공합니다. --- ## 설치 ```bash npm install @sendgo/nestjs @sendgo/node ``` `@nestjs/common`은 peerDependency이므로 NestJS 프로젝트에 이미 설치되어 있어야 합니다. --- ## 빠른 시작 ### 1단계 — 모듈 등록 (`app.module.ts`) ```ts import { Module } from '@nestjs/common'; import { SendgoModule } from '@sendgo/nestjs'; @Module({ imports: [ SendgoModule.forRoot({ accessKey: process.env.SENDGO_ACCESS_KEY!, secretKey: process.env.SENDGO_SECRET_KEY!, kakaoSenderKey: process.env.SENDGO_KAKAO_KEY, smsSenderKey: process.env.SENDGO_SMS_KEY, apiVersion: 'v2', }), ], }) export class AppModule {} ``` `SendgoModule`은 `@Global()`이므로 한 번만 등록하면 어느 모듈에서든 `SendgoService`를 주입할 수 있습니다. ### 2단계 — 서비스 주입 후 발송 ```ts import { Injectable } from '@nestjs/common'; import { SendgoService } from '@sendgo/nestjs'; @Injectable() export class OrderService { constructor(private readonly sendgo: SendgoService) {} async confirm(phone: string, orderNo: string, amount: number) { await this.sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [ { contact: phone, var1: orderNo, var2: `${amount.toLocaleString()}원` }, ], }); } } ``` --- ## 비동기 설정 (ConfigService) 환경변수를 `@nestjs/config`의 `ConfigService`로 주입해 설정을 구성할 수 있습니다. ```ts import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { SendgoModule } from '@sendgo/nestjs'; @Module({ imports: [ ConfigModule.forRoot(), SendgoModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (config: ConfigService) => ({ accessKey: config.getOrThrow('SENDGO_ACCESS_KEY'), secretKey: config.getOrThrow('SENDGO_SECRET_KEY'), kakaoSenderKey: config.get('SENDGO_KAKAO_KEY'), smsSenderKey: config.get('SENDGO_SMS_KEY'), apiVersion: config.get('SENDGO_API_VERSION') ?? 'v1', }), }), ], }) export class AppModule {} ``` --- ## 상세 사용법 모든 발송 메서드는 `SendgoService`의 게터(`alimtalk`, `friendtalk`, `sms`)를 통해 코어 클라이언트에 위임됩니다. ### 알림톡 ```ts // 다건 발송 await this.sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [ { contact: '01011111111', name: '홍길동', var1: 'ORD-001', var2: '29,000원' }, { contact: '01022222222', name: '김철수', var1: 'ORD-002', var2: '15,000원' }, ], }); // 예약 발송 await this.sendgo.alimtalk.send({ templateCode: 'PROMO_SUMMER_2026', scheduleType: 'SCHEDULED', at: '2026-07-28 09:00:00', contacts: [{ contact: '01012345678', var1: '여름 한정 50% 할인' }], }); // SMS 자동 대체 발송 await this.sendgo.alimtalk.send({ templateCode: 'DELIVERY_START_001', replaceSms: 'Y', smsSubject: '[배송 시작 안내]', smsContent: '주문하신 상품이 출고되었습니다.\n송장번호: #{var2}', contacts: [{ contact: '01012345678', var1: 'ORD-001', var2: '1234567890' }], }); ``` ### 친구톡 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```ts // 텍스트형 await this.sendgo.friendtalk.send({ content: '안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.', contacts: [{ contact: '01012345678' }], }); // 이미지형 await this.sendgo.friendtalk.send({ messageType: 'FI', content: '이번 주 특가 상품을 확인하세요!', imageUrl: 'https://cdn.example.com/banner.jpg', imageLink: 'https://example.com/event', contacts: [{ contact: '01012345678' }], }); // 버튼 포함 await this.sendgo.friendtalk.send({ content: '7월 쿠폰이 도착했습니다! 지금 바로 사용하세요.', buttons: [ { name: '쿠폰 받기', type: 'WL', linkMo: 'https://example.com/coupon' }, { name: '고객센터', type: 'WL', linkMo: 'https://example.com/cs' }, ], contacts: [{ contact: '01012345678' }], }); ``` ### SMS / LMS / MMS ```ts // SMS (90자 이하) await this.sendgo.sms.sendSms({ content: '[Sendgo] 인증번호: 123456 (5분 이내 입력)', contacts: [{ contact: '01012345678' }], }); // LMS (장문, 2,000자 이하) await this.sendgo.sms.sendLms({ subject: '[중요] 서비스 점검 안내', content: '안녕하세요. 서비스 점검이 예정되어 있습니다.\n\n■ 일시: 2026-07-25 02:00 ~ 06:00', contacts: [{ contact: '01012345678' }], }); // MMS (멀티미디어) await this.sendgo.sms.sendMms({ subject: '[이벤트] 7월 특가', content: '이번 달 특가 상품을 확인하세요!', contacts: [{ contact: '01011111111' }, { contact: '01022222222' }], }); ``` --- ## 서비스 클래스 패턴 ```ts // notification.service.ts import { Injectable, Logger } from '@nestjs/common'; import { SendgoService, SendgoError } from '@sendgo/nestjs'; @Injectable() export class NotificationService { private readonly logger = new Logger(NotificationService.name); constructor(private readonly sendgo: SendgoService) {} async sendVerificationCode(phone: string, code: string): Promise { try { // 알림톡 우선, 실패 시 SMS 대체 await this.sendgo.alimtalk.send({ templateCode: 'VERIFY_CODE_001', replaceSms: 'Y', smsContent: `[인증] 인증번호: ${code} (5분 이내 입력)`, contacts: [{ contact: phone, var1: code }], }); } catch (e) { if (e instanceof SendgoError) { this.logger.error(`Sendgo 인증번호 발송 실패: ${e.message}`); } throw e; } } } ``` 원본 코어 클라이언트가 필요하면 `this.sendgo.client`로 접근할 수 있습니다. 또한 `Sendgo` 클래스 토큰으로 코어 인스턴스를 직접 주입받는 것도 가능합니다. ```ts import { Injectable } from '@nestjs/common'; import { Sendgo } from '@sendgo/node'; @Injectable() export class OrderService { constructor(private readonly sendgo: Sendgo) {} } ``` --- ## 예외 처리 ```ts import { SendgoError } from '@sendgo/nestjs'; try { await this.sendgo.alimtalk.send({ /* ... */ }); } catch (e) { if (e instanceof SendgoError) { // e.errorCode, e.statusCode, e.endpoint 활용 가능 switch (e.errorCode) { case 'INVALID_ACCESS_KEY': case 'INVALID_SECRET_KEY': // 인증키 오류 처리 break; case 'INVALID_TEMPLATE_CODE': // 존재하지 않는 템플릿 break; case 'PAYMENT_REQUIRED': // 크레딧 부족 break; default: break; } } throw e; } ``` --- ## 설정 옵션 `SendgoModule.forRoot()` / `forRootAsync()`에 전달하는 `SendgoConfig` 값입니다. | 키 | 타입 | 기본값 | 설명 | |----|------|--------|------| | `accessKey` | `string` | — | Sendgo 액세스 키 (필수) | | `secretKey` | `string` | — | Sendgo 시크릿 키 (필수) | | `kakaoSenderKey` | `string` | `''` | 카카오 발신프로필 키 | | `smsSenderKey` | `string` | `''` | SMS 발신자 키 | | `apiVersion` | `'v1' \| 'v2'` | `'v1'` | API 버전 | | `baseUrl` | `string` | `'https://sendgo.io'` | API 기본 URL | --- ## 자주 묻는 질문 (FAQ) **Q. `@sendgo/node`와의 차이는 무엇인가요?** A. `@sendgo/node`는 프레임워크 독립적인 순수 Node.js 코어 SDK입니다. `@sendgo/nestjs`는 이를 확장해 `DynamicModule` 등록, 전역 프로바이더, 생성자 주입 등 NestJS 통합을 추가합니다. **Q. NestJS 10, 11 모두 지원하나요?** A. 네, `@nestjs/common` `>=10.0.0`을 peerDependency로 지원합니다. **Q. `SendgoService` 대신 코어 클라이언트를 직접 주입할 수 있나요?** A. 네, `Sendgo` 클래스를 토큰으로 주입받으면 코어 인스턴스를 직접 사용할 수 있습니다. **Q. 테스트 시 Sendgo를 Mock 처리하려면?** A. 테스트 모듈에서 `SendgoService` 또는 `Sendgo` 프로바이더를 mock 값으로 오버라이드하면 됩니다. --- ## 브랜드메시지 · 짧은 URL 이 패키지는 코어(`@sendgo/node`)의 클라이언트를 그대로 노출하므로, 코어에 있는 채널이 모두 그대로 쓸 수 있습니다. 두 기능 모두 **v2 전용**입니다. | 기능 | 접근 | |------|------| | 카카오 브랜드메시지 (친구톡의 후속 채널) | `sendgo.brandMessage` | | 짧은 URL (단축 + 클릭 반응 분석) | `sendgo.shortUrl` | 브랜드메시지는 채널 친구가 아닌 수신자에게도 보낼 수 있고(`targeting` = `N`), 수신 동의한 전체 채널 친구에게 동보 발송할 수도 있습니다(`targeting` = `F`). 짧은 URL 은 메시지 본문의 링크를 줄이고 클릭 반응(일별 추이·디바이스·유입경로·국가)을 집계합니다. 사용 예시와 파라미터는 [코어 README](https://github.com/send-go) 와 [SDK 가이드](https://sendgo.io/ko/sdk) 를 참고하세요. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ### 1.1.0 (2026-08-11) - `SendgoService.shortUrl` 게터 추가 ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `@sendgo/nestjs` (npm) - **저장소**: [send-go/nestjs](https://github.com/send-go/nestjs) - **레지스트리**: https://www.npmjs.com/package/@sendgo/nestjs - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **Python / Django / FastAPI에서 카카오 알림톡, 브랜드메시지, SMS를 가장 쉽게 발송하는 SDK** `sendgo-python`은 [Sendgo](https://sendgo.io) 알림 API를 위한 공식 Python SDK입니다. **`requests` 하나만 의존하며**, 완전한 타입 힌트(Type Hints)를 제공합니다. Django, FastAPI, Flask, Celery 등 모든 Python 환경에서 사용할 수 있습니다. --- ## Sendgo란? [Sendgo](https://sendgo.io)는 대한민국 기업과 개발자를 위한 **통합 알림 발송 플랫폼**입니다. - **카카오 알림톡**: 카카오톡 채널을 통한 정보성 메시지 (주문 확인, 배송 안내, 예약 확인 등) - **카카오 친구톡**: 마케팅/이벤트 메시지 (쿠폰, 프로모션 등) - **SMS / LMS / MMS**: 전통적인 문자 메시지 - **자동 대체 발송**: 알림톡 실패 시 SMS로 자동 전환 --- ## 주요 기능 | 기능 | 설명 | |------|------| | **최소 의존성** | `requests` 하나만 필요 | | **완전한 타입 힌트** | 모든 파라미터와 반환값에 타입 정의 | | **스레드 안전 토큰 관리** | `threading.Lock` 기반, 멀티스레드 환경 안전 | | **토큰 자동 캐싱(50분)** | 매 요청마다 토큰을 발급하지 않음 | | **401/403 자동 재시도** | 토큰 만료 시 자동 갱신 후 재발송 | | **다건 동시 발송** | 수신자 리스트로 대량 발송 | | **예약 발송** | 원하는 시각에 발송 예약 | | **SMS 자동 대체 발송** | 알림톡 실패 시 SMS로 자동 전환 | | **v1 / v2 API 지원** | 설정 한 줄로 버전 전환 | --- ## 설치 ```bash pip install sendgo-python ``` 또는 `pyproject.toml`: ```toml [project] dependencies = ["sendgo-python>=1.0.0"] ``` --- ## 빠른 시작 ### 1단계 — 환경변수 설정 ```bash # .env SENDGO_ACCESS_KEY=your_access_key SENDGO_SECRET_KEY=your_secret_key SENDGO_KAKAO_SENDER_KEY=your_kakao_key SENDGO_SMS_SENDER_KEY=your_sms_key SENDGO_API_VERSION=v2 ``` ### 2단계 — 클라이언트 초기화 ```python import os from sendgo import Sendgo client = Sendgo( access_key=os.environ["SENDGO_ACCESS_KEY"], secret_key=os.environ["SENDGO_SECRET_KEY"], kakao_sender_key=os.environ.get("SENDGO_KAKAO_SENDER_KEY"), sms_sender_key=os.environ.get("SENDGO_SMS_SENDER_KEY"), api_version="v2", ) ``` ### 3단계 — 알림톡 전송 ```python client.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[ { "contact": "01012345678", # 수신자 전화번호 (필수) "name": "홍길동", # 수신자 이름 (선택) "var1": "ORD-20260723-001", # 템플릿 변수 #{var1} "var2": "스프링 부트 가이드", # 템플릿 변수 #{var2} "var3": "29,000원", # 템플릿 변수 #{var3} } ], ) ``` --- ## 상세 사용법 ### 카카오 알림톡 ```python # 다건 발송 client.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[ {"contact": "01011111111", "name": "홍길동", "var1": "ORD-001"}, {"contact": "01022222222", "name": "김철수", "var1": "ORD-002"}, {"contact": "01033333333", "name": "이영희", "var1": "ORD-003"}, ], ) # 예약 발송 client.alimtalk.send( template_code="PROMO_SUMMER_2026", schedule_type="SCHEDULED", at="2026-07-28 09:00:00", contacts=[{"contact": "01012345678", "var1": "여름 한정 50% 할인"}], ) # 알림톡 실패 시 SMS 자동 대체 발송 client.alimtalk.send( template_code="DELIVERY_START_001", contacts=[{"contact": "01012345678", "var1": "ORD-001", "var2": "1234567890"}], replace_sms="Y", sms_subject="[배송 시작 안내]", sms_content="주문하신 상품이 출고되었습니다.\n송장번호: #{var2}", ) ``` ### 카카오 친구톡 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```python # 텍스트형 client.friendtalk.send( content="안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.", contacts=[{"contact": "01012345678"}], ) # 이미지형 client.friendtalk.send( message_type="FI", content="이번 주 특가 상품을 확인하세요!", image_url="https://cdn.example.com/banner.jpg", image_link="https://example.com/event", contacts=[{"contact": "01012345678"}], ) ``` ### SMS / LMS / MMS ```python # SMS client.sms.send_sms( content="[Sendgo] 인증번호: 123456 (5분 이내 입력)", contacts=[{"contact": "01012345678"}], ) # LMS — 장문 (2,000자 이하) client.sms.send_lms( subject="[중요] 서비스 점검 안내", content="""안녕하세요. 서비스 점검이 예정되어 있습니다. ■ 점검 일시: 2026-07-25 02:00 ~ 06:00 ■ 영향 범위: 전체 서비스 이용에 불편을 드려 죄송합니다.""", contacts=[{"contact": "01012345678"}], ) # MMS — 이미지 포함 client.sms.send_mms( subject="[이벤트] 7월 특가", content="이번 달 특가 상품을 확인하세요!", contacts=[{"contact": "01012345678"}], ) ``` --- ## 브랜드메시지 사용법 브랜드메시지는 친구톡의 후속 채널입니다. 메시지 타입이 친구톡과 1:1 대응되며 (`FT`→`BT`, `FI`→`BI`, `FW`→`BW`, `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`), 요청에는 **친구톡 코드를 그대로** 넘기고 변환은 서버가 처리합니다. 친구톡과 달리 다음이 가능합니다. - 채널 친구가 **아닌** 수신자에게 발송 (`targeting: N`) - 수신 동의한 **전체 채널 친구 동보** 발송 (`targeting: F`, 수신자 목록 불필요) - 리스트·캐러셀·커머스·동영상 등 **템플릿 기반 리치 메시지** > v2 전용입니다. 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보낼 때는 여전히 친구톡 API 를 쓰세요 — 이 엔드포인트는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. 친구톡 요청은 카카오 측에서 브랜드메시지(자유형)로 대체 발송됩니다. ```python # 단건 발송 — 채널 친구 대상 client.brand_message.send( targeting="M", message_type="FL", friend_template_uuid="9cd5460b-6458-4edc-9b11-c26d3013c340", contacts=[{"contact": "01012345678", "var1": "29,000원"}], ) # 동보 발송 — 수신 동의한 전체 채널 친구 (contacts 불필요) client.brand_message.broadcast( message_type="FW", friend_template_uuid="9cd5460b-6458-4edc-9b11-c26d3013c340", ) # 캠페인 조회 (from 은 예약어이므로 from_ 사용) campaigns = client.brand_message.campaigns(from_="2026-08-01", count=10) one = client.brand_message.campaign("1f0a6d0e-6b3b-4f0f-9b2f-2f6f6a1b7c11") ``` --- ## 프레임워크 통합 ### Django ```python # settings.py SENDGO = { "access_key": env("SENDGO_ACCESS_KEY"), "secret_key": env("SENDGO_SECRET_KEY"), "kakao_sender_key": env("SENDGO_KAKAO_SENDER_KEY", default=None), "sms_sender_key": env("SENDGO_SMS_SENDER_KEY", default=None), "api_version": env("SENDGO_API_VERSION", default="v2"), } ``` ```python # apps/notifications/services.py from django.conf import settings from sendgo import Sendgo _sendgo: Sendgo | None = None def get_sendgo() -> Sendgo: global _sendgo if _sendgo is None: _sendgo = Sendgo(**settings.SENDGO) return _sendgo def send_order_confirm(phone: str, order_number: str) -> None: get_sendgo().alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[{"contact": phone, "var1": order_number}], ) ``` ```python # apps/orders/signals.py from django.db.models.signals import post_save from django.dispatch import receiver from .models import Order from apps.notifications.services import send_order_confirm @receiver(post_save, sender=Order) def on_order_created(sender, instance, created, **kwargs): if created: send_order_confirm(instance.user.phone, instance.number) ``` ### FastAPI ```python # core/sendgo.py from functools import lru_cache from sendgo import Sendgo from .config import settings @lru_cache def get_sendgo() -> Sendgo: return Sendgo( access_key=settings.SENDGO_ACCESS_KEY, secret_key=settings.SENDGO_SECRET_KEY, kakao_sender_key=settings.SENDGO_KAKAO_SENDER_KEY, api_version="v2", ) ``` ```python # routers/notify.py from fastapi import APIRouter, Depends from sendgo import Sendgo from core.sendgo import get_sendgo router = APIRouter(prefix="/api") @router.post("/notify/order") async def notify_order( phone: str, order_number: str, sendgo: Sendgo = Depends(get_sendgo), ): sendgo.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[{"contact": phone, "var1": order_number}], ) return {"success": True} ``` ### Celery 비동기 발송 ```python # tasks/notifications.py from celery import shared_task from sendgo import Sendgo, SendgoError import logging logger = logging.getLogger(__name__) @shared_task(bind=True, max_retries=3, default_retry_delay=10) def send_alimtalk_task(self, template_code: str, contacts: list[dict]) -> None: """카카오 알림톡 비동기 발송 Celery 태스크""" sendgo = Sendgo( access_key=settings.SENDGO_ACCESS_KEY, secret_key=settings.SENDGO_SECRET_KEY, kakao_sender_key=settings.SENDGO_KAKAO_SENDER_KEY, ) try: sendgo.alimtalk.send(template_code=template_code, contacts=contacts) except SendgoError as e: logger.error("알림톡 발송 실패: %s [%s]", e, e.error_code) if e.error_code not in ("INVALID_TEMPLATE_CODE", "PAYMENT_REQUIRED"): raise self.retry(exc=e) ``` ```python # 사용 send_alimtalk_task.delay("ORDER_CONFIRM_001", [{"contact": "01012345678", "var1": "ORD-001"}]) ``` --- ## 예외 처리 ```python from sendgo import SendgoError try: client.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[{"contact": "01012345678"}], ) except SendgoError as e: print(f"발송 실패: HTTP {e.status_code} [{e.error_code}]") print(f"엔드포인트: {e.endpoint}, API 버전: {e.api_version}") match e.error_code: case "INVALID_ACCESS_KEY" | "INVALID_SECRET_KEY": alert_ops("Sendgo 인증키를 확인하세요.") case "INVALID_TEMPLATE_CODE": logger.warning("존재하지 않는 템플릿: %s", template_code) case "PAYMENT_REQUIRED": alert_ops("Sendgo 크레딧이 부족합니다.") case "IP_NOT_ALLOWED": alert_ops("허용되지 않은 IP에서 요청이 발생했습니다.") ``` --- ## 설정 옵션 | 파라미터 | 타입 | 필수 | 기본값 | 설명 | |---------|------|------|--------|------| | `access_key` | `str` | **필수** | — | Sendgo 액세스 키 | | `secret_key` | `str` | **필수** | — | Sendgo 시크릿 키 | | `kakao_sender_key` | `str \| None` | 선택 | `None` | 카카오 발신프로필 키 | | `sms_sender_key` | `str \| None` | 선택 | `None` | SMS 발신자 키 | | `api_version` | `str` | 선택 | `'v1'` | API 버전 (`v1` \| `v2`) | | `base_url` | `str` | 선택 | `'https://sendgo.io'` | API 기본 URL | --- ## 자주 묻는 질문 (FAQ) **Q. 비동기(async/await)를 지원하나요?** A. 현재 버전은 동기(`requests` 기반)만 지원합니다. FastAPI 등 비동기 환경에서는 `asyncio.get_event_loop().run_in_executor()`로 스레드풀에서 실행하거나, Celery 태스크로 위임하는 방법을 권장합니다. 비동기 버전(`httpx` 기반)은 향후 추가될 예정입니다. **Q. 멀티스레드 환경에서 안전한가요?** A. 토큰 관리에 `threading.Lock`을 사용하여 멀티스레드 환경에서도 안전합니다. **Q. 알림톡 템플릿은 어디서 등록하나요?** A. [Sendgo 콘솔](https://sendgo.io) → 알림톡 템플릿 → 템플릿 작성 → 카카오 심사 신청 (보통 1~3일 소요) **Q. 대량 발송 시 rate limit이 있나요?** A. Sendgo 플랜별로 TPS 제한이 있습니다. [요금 정책](https://sendgo.io/pricing) 참조. --- ## 짧은 URL 짧은 URL 은 메시지 본문의 링크를 줄이고, 그 링크가 실제로 눌렸는지 집계합니다. 문자는 바이트 수가 요금과 직결되므로 링크를 줄이면 그만큼 본문을 더 쓸 수 있습니다. 같은 원본 URL 을 다시 줄이면 **기존 링크가 그대로 반환**됩니다. 캠페인별로 반응을 따로 집계하려면 `forceNew` 로 새 코드를 만드세요. `deactivate` 는 링크를 삭제하지 않고 리다이렉트만 중지합니다. 이미 발송한 메시지의 링크를 무효화할 때 쓰며, 누적 통계는 남고 이후 접속은 `410 Gone` 이 됩니다. ```python # 짧은 URL 생성 (v2 전용) created = sendgo.short_url.create( target_url="https://example.com/promotions/summer-sale", title="여름 세일 랜딩", ) code = created["data"]["code"] link = created["data"]["shortUrl"] # 반응 통계 — 일별 추이 + 디바이스/유입경로/국가별 분해 # `from` 은 파이썬 예약어이므로 `from_` 을 쓴다 stats = sendgo.short_url.stats(code, from_="2026-08-01") sendgo.short_url.list(count=10) sendgo.short_url.show(code) sendgo.short_url.deactivate(code) # 리다이렉트만 중지, 통계는 남는다 ``` `stats` 는 일별 추이(`daily`)와 디바이스(`byDevice`)·유입경로(`byReferer`)·국가(`byCountry`)별 분해를 반환합니다. 일별 추이는 사전 집계 표에서 읽으므로 클릭이 많아도 응답 시간이 일정합니다. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ### 1.1.0 (2026-08-11) - 짧은 URL 추가 — `client.short_url` - `HttpClient.delete()` 추가 - **버전 단일화** — `__init__.py` 에 하드코딩된 `__version__` 을 제거하고 `importlib.metadata` 로 `pyproject.toml` 값을 읽는다. 두 곳이 어긋나 릴리스가 실패한 적이 있다. ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `sendgo-python` (PyPI) - **저장소**: [send-go/python](https://github.com/send-go/python) - **레지스트리**: https://pypi.org/project/sendgo-python/ - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **Django에서 카카오 알림톡, 브랜드메시지, SMS를 가장 쉽게 발송하는 공식 Django 확장 패키지** `sendgo-django`는 [`sendgo-python`](https://github.com/send-go/python) 코어를 확장한 **Django 전용 패키지**입니다. `settings.SENDGO` 설정 기반의 클라이언트 생성, 지연 로딩 프록시(`client`), AppConfig 통합을 제공합니다. --- ## 설치 ```bash pip install sendgo-django ``` `sendgo-python` 코어는 의존성으로 자동 설치됩니다. --- ## 빠른 시작 ### 1단계 — `INSTALLED_APPS` 등록 (`settings.py`) ```python INSTALLED_APPS += ["sendgo_django"] ``` ### 2단계 — `SENDGO` 설정 추가 (`settings.py`) ```python import os SENDGO = { "ACCESS_KEY": os.environ["SENDGO_ACCESS_KEY"], "SECRET_KEY": os.environ["SENDGO_SECRET_KEY"], "KAKAO_SENDER_KEY": os.environ.get("SENDGO_KAKAO_SENDER_KEY"), "SMS_SENDER_KEY": os.environ.get("SENDGO_SMS_SENDER_KEY"), "API_VERSION": "v2", # "BASE_URL": "https://sendgo.io", # 기본값 } ``` ### 3단계 — 뷰에서 알림톡 전송 ```python # views.py from django.http import JsonResponse from sendgo_django import client def confirm_order(request, order_id): order = get_order(order_id) client.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[ { "contact": order.phone, "name": order.name, "var1": order.number, "var2": f"{order.total:,}원", }, ], ) return JsonResponse({"success": True}) ``` --- ## 프록시 사용법 `sendgo_django.client`는 `SimpleLazyObject` 기반 **지연 로딩 프록시**입니다. 패키지를 임포트하는 시점에는 클라이언트를 생성하지 않고, 실제로 속성에 접근할 때 `get_client()`를 호출합니다. 따라서 설정이 없어도 임포트만으로는 오류가 나지 않습니다. ```python from sendgo_django import client # 알림톡 발송 client.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[{"contact": "01012345678", "var1": "ORD-001"}], ) # SMS 발송 client.sms.send_sms( content="[인증] 인증번호: 123456", contacts=[{"contact": "01012345678"}], ) ``` 명시적으로 인스턴스를 얻고 싶다면 `get_client()`를 직접 호출할 수 있습니다. ```python from sendgo_django import get_client sendgo = get_client() sendgo.friendtalk.send(content="안녕하세요!", contacts=[{"contact": "01012345678"}]) ``` --- ## 상세 사용법 ### 알림톡 ```python from sendgo_django import client # 다건 발송 client.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[ {"contact": "01011111111", "name": "홍길동", "var1": "ORD-001", "var2": "29,000원"}, {"contact": "01022222222", "name": "김철수", "var1": "ORD-002", "var2": "15,000원"}, ], ) ``` ### 친구톡 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```python from sendgo_django import client # 텍스트형 client.friendtalk.send( content="안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.", contacts=[{"contact": "01012345678"}], ) ``` ### SMS / LMS / MMS ```python from sendgo_django import client # SMS (90자 이하) client.sms.send_sms( content="[Sendgo] 인증번호: 123456 (5분 이내 입력)", contacts=[{"contact": "01012345678"}], ) ``` --- ## 서비스 클래스 패턴 ```python # app/services.py import logging from sendgo import SendgoError from sendgo_django import client logger = logging.getLogger(__name__) class NotificationService: """알림 발송 로직을 캡슐화한 서비스 클래스.""" def send_order_confirm(self, phone: str, order_no: str, amount: int) -> None: client.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[{"contact": phone, "var1": order_no, "var2": f"{amount:,}원"}], ) def send_verification_code(self, phone: str, code: str) -> None: try: client.alimtalk.send( template_code="VERIFY_CODE_001", contacts=[{"contact": phone, "var1": code}], ) except SendgoError: logger.exception("Sendgo 인증번호 발송 실패") raise ``` --- ## 예외 처리 ```python from sendgo import SendgoError from sendgo_django import client try: client.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[{"contact": "01012345678", "var1": "ORD-001"}], ) except SendgoError as e: logger.error("Sendgo 발송 실패: %s", e) ``` --- ## 설정 옵션 `settings.SENDGO` 딕셔너리 키: | 키 | 필수 | 기본값 | 설명 | |----|------|--------|------| | `ACCESS_KEY` | ✅ | — | Sendgo 액세스 키 | | `SECRET_KEY` | ✅ | — | Sendgo 시크릿 키 | | `KAKAO_SENDER_KEY` | | `None` | 카카오 발신프로필 키 | | `SMS_SENDER_KEY` | | `None` | SMS 발신자 키 | | `API_VERSION` | | `"v2"` | API 버전 | | `BASE_URL` | | `"https://sendgo.io"` | API 기본 URL | `ACCESS_KEY` 또는 `SECRET_KEY`가 없으면 클라이언트 생성 시 `django.core.exceptions.ImproperlyConfigured`가 발생합니다. --- ## 자주 묻는 질문 (FAQ) **Q. `sendgo-python`과의 차이는 무엇인가요?** A. `sendgo-python`은 프레임워크 독립적인 순수 Python 코어 패키지입니다. `sendgo-django`는 이를 확장해 `settings.SENDGO` 설정 바인딩, 지연 로딩 프록시, AppConfig 통합을 추가합니다. **Q. 설정 없이 임포트하면 오류가 나나요?** A. 아니요. `client`는 지연 프록시이므로 임포트만으로는 오류가 없고, 실제 사용(속성 접근) 시점에 설정을 검증합니다. **Q. 테스트 시 클라이언트를 초기화하려면?** A. `sendgo_django.conf.reset()`을 호출하면 메모이즈된 클라이언트가 초기화됩니다. **Q. Django 4.2, 5.x를 지원하나요?** A. 네, `Django>=4.2`를 지원합니다. --- ## 브랜드메시지 · 짧은 URL 이 패키지는 코어(`sendgo-python`)의 클라이언트를 그대로 노출하므로, 코어에 있는 채널이 모두 그대로 쓸 수 있습니다. 두 기능 모두 **v2 전용**입니다. | 기능 | 접근 | |------|------| | 카카오 브랜드메시지 (친구톡의 후속 채널) | `client.brand_message` | | 짧은 URL (단축 + 클릭 반응 분석) | `client.short_url` | 브랜드메시지는 채널 친구가 아닌 수신자에게도 보낼 수 있고(`targeting` = `N`), 수신 동의한 전체 채널 친구에게 동보 발송할 수도 있습니다(`targeting` = `F`). 짧은 URL 은 메시지 본문의 링크를 줄이고 클릭 반응(일별 추이·디바이스·유입경로·국가)을 집계합니다. 사용 예시와 파라미터는 [코어 README](https://github.com/send-go) 와 [SDK 가이드](https://sendgo.io/ko/sdk) 를 참고하세요. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. - 짧은 URL 지원 (1.1.0 릴리스 누락분 포함). ### 1.1.0 (2026-08-11) - 브랜드메시지·짧은 URL 접근 방법 문서화 (코어를 그대로 노출) ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `sendgo-django` (PyPI) - **저장소**: [send-go/django](https://github.com/send-go/django) - **레지스트리**: https://pypi.org/project/sendgo-django/ - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **FastAPI에서 카카오 알림톡, 브랜드메시지, SMS를 가장 쉽게 발송하는 공식 FastAPI 확장 패키지** `sendgo-fastapi`는 [`sendgo-python`](https://github.com/send-go/python) 코어를 확장한 **FastAPI 전용 패키지**입니다. 환경변수 기반 설정 로딩(pydantic-settings), 의존성 주입(`Depends`), lifespan 초기화 등 FastAPI 통합을 완벽하게 제공합니다. --- ## 설치 ```bash pip install sendgo-fastapi ``` 코어 패키지 `sendgo-python`은 의존성으로 자동 설치됩니다. --- ## 빠른 시작 ### 1단계 — 환경변수 설정 (`.env` 또는 셸 환경) 모든 환경변수는 `SENDGO_` 접두사를 사용하며, `SendgoSettings`가 자동으로 바인딩합니다. ```env SENDGO_ACCESS_KEY=your_access_key SENDGO_SECRET_KEY=your_secret_key SENDGO_KAKAO_SENDER_KEY=your_kakao_key SENDGO_SMS_SENDER_KEY=your_sms_key SENDGO_API_VERSION=v2 SENDGO_BASE_URL=https://sendgo.io ``` ### 2단계 — 알림톡 전송 ```python from fastapi import FastAPI from sendgo_fastapi import SendgoDep app = FastAPI() @app.post("/notify") async def notify(sendgo: SendgoDep): sendgo.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[ {"contact": "01012345678", "name": "홍길동", "var1": "ORD-001", "var2": "29,000원"}, ], ) return {"success": True} ``` `SendgoDep`은 `Annotated[Sendgo, Depends(get_sendgo)]`의 별칭으로, 라우트 인자에 타입힌트만 추가하면 Sendgo 클라이언트가 자동으로 주입됩니다. --- ## 의존성 주입 사용법 ```python from fastapi import FastAPI from sendgo_fastapi import SendgoDep app = FastAPI() @app.post("/verify") async def send_verification(sendgo: SendgoDep): # SMS 발송 sendgo.sms.send_sms( content="[인증] 인증번호: 123456 (5분 이내 입력)", contacts=[{"contact": "01012345678"}], ) return {"success": True} ``` 환경변수 기반 클라이언트는 최초 요청 시 한 번만 생성되어 메모이즈됩니다. --- ## lifespan 초기화 애플리케이션 시작 시점에 클라이언트를 미리 생성해 `app.state.sendgo`에 저장하려면 `init_sendgo(app)`을 lifespan에서 호출하세요. 이 경우 `SendgoDep`/`get_sendgo`는 저장된 인스턴스를 우선 사용합니다. ```python from contextlib import asynccontextmanager from fastapi import FastAPI from sendgo_fastapi import SendgoDep, init_sendgo @asynccontextmanager async def lifespan(app: FastAPI): # 시작 시점에 환경변수 기반 클라이언트를 생성해 app.state.sendgo 에 저장 init_sendgo(app) yield app = FastAPI(lifespan=lifespan) @app.post("/notify") async def notify(sendgo: SendgoDep): sendgo.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[{"contact": "01012345678", "var1": "ORD-001"}], ) return {"success": True} ``` 커스텀 설정을 직접 주입할 수도 있습니다. ```python from sendgo_fastapi import SendgoSettings, init_sendgo init_sendgo(app, SendgoSettings(access_key="...", secret_key="...")) ``` --- ## 상세 사용법 ### 알림톡 ```python from sendgo_fastapi import SendgoDep @app.post("/orders/{order_id}/confirm") async def confirm(order_id: str, sendgo: SendgoDep): # 다건 발송 sendgo.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[ {"contact": "01011111111", "name": "홍길동", "var1": "ORD-001", "var2": "29,000원"}, {"contact": "01022222222", "name": "김철수", "var1": "ORD-002", "var2": "15,000원"}, ], ) # 예약 발송 sendgo.alimtalk.send( template_code="PROMO_SUMMER_2026", schedule_type="SCHEDULED", at="2026-07-28 09:00:00", contacts=[{"contact": "01012345678", "var1": "여름 한정 50% 할인"}], ) # SMS 자동 대체 발송 sendgo.alimtalk.send( template_code="DELIVERY_START_001", replace_sms="Y", sms_subject="[배송 시작 안내]", sms_content="주문하신 상품이 출고되었습니다.\n송장번호: 1234567890", contacts=[{"contact": "01012345678", "var1": "ORD-001", "var2": "1234567890"}], ) return {"success": True} ``` ### 친구톡 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```python # 텍스트형 sendgo.friendtalk.send( content="안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.", contacts=[{"contact": "01012345678"}], ) # 이미지형 sendgo.friendtalk.send( message_type="FI", content="이번 주 특가 상품을 확인하세요!", image_url="https://cdn.example.com/banner.jpg", image_link="https://example.com/event", contacts=[{"contact": "01012345678"}], ) # 버튼 포함 sendgo.friendtalk.send( content="7월 쿠폰이 도착했습니다! 지금 바로 사용하세요.", buttons=[ {"name": "쿠폰 받기", "type": "WL", "linkMo": "https://example.com/coupon"}, {"name": "고객센터", "type": "WL", "linkMo": "https://example.com/cs"}, ], contacts=[{"contact": "01012345678"}], ) ``` ### SMS / LMS / MMS ```python # SMS (90자 이하) sendgo.sms.send_sms( content="[Sendgo] 인증번호: 123456 (5분 이내 입력)", contacts=[{"contact": "01012345678"}], ) # LMS (장문, 2,000자 이하) sendgo.sms.send_lms( subject="[중요] 서비스 점검 안내", content="안녕하세요. 서비스 점검이 예정되어 있습니다.\n\n■ 일시: 2026-07-25 02:00 ~ 06:00", contacts=[{"contact": "01012345678"}], ) # MMS (이미지 포함) sendgo.sms.send_mms( subject="[이벤트] 7월 특가", content="이번 달 특가 상품을 확인하세요!", contacts=[{"contact": "01011111111"}, {"contact": "01022222222"}], ) ``` --- ## 서비스 클래스 패턴 라우트에서 직접 발송하는 대신, 재사용 가능한 서비스 클래스로 분리할 수 있습니다. ```python # app/services/notification.py from sendgo import Sendgo class NotificationService: def __init__(self, sendgo: Sendgo) -> None: self._sendgo = sendgo def send_order_confirm(self, phone: str, order_no: str, amount: int) -> None: self._sendgo.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[{"contact": phone, "var1": order_no, "var2": f"{amount:,}원"}], ) ``` ```python # app/main.py from typing import Annotated from fastapi import Depends, FastAPI from sendgo_fastapi import SendgoDep from app.services.notification import NotificationService app = FastAPI() def get_notification_service(sendgo: SendgoDep) -> NotificationService: return NotificationService(sendgo) NotificationDep = Annotated[NotificationService, Depends(get_notification_service)] @app.post("/orders/{order_id}/confirm") async def confirm(order_id: str, service: NotificationDep): service.send_order_confirm("01012345678", order_id, 29000) return {"success": True} ``` --- ## 백그라운드 태스크 비동기 발송 발송을 요청 응답과 분리하려면 FastAPI의 `BackgroundTasks`를 사용하세요. ```python from fastapi import BackgroundTasks, FastAPI from sendgo_fastapi import SendgoDep app = FastAPI() @app.post("/notify") async def notify(sendgo: SendgoDep, background_tasks: BackgroundTasks): background_tasks.add_task( sendgo.alimtalk.send, template_code="ORDER_CONFIRM_001", contacts=[{"contact": "01012345678", "var1": "ORD-001"}], ) return {"success": True, "queued": True} ``` --- ## 예외 처리 ```python from fastapi import FastAPI, HTTPException from sendgo import SendgoError from sendgo_fastapi import SendgoDep app = FastAPI() @app.post("/notify") async def notify(sendgo: SendgoDep): try: sendgo.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[{"contact": "01012345678", "var1": "ORD-001"}], ) except SendgoError as e: raise HTTPException(status_code=502, detail=f"Sendgo 발송 실패: {e}") return {"success": True} ``` 전역 예외 핸들러로 처리할 수도 있습니다. ```python from fastapi import Request from fastapi.responses import JSONResponse from sendgo import SendgoError @app.exception_handler(SendgoError) async def sendgo_exception_handler(request: Request, exc: SendgoError): return JSONResponse(status_code=502, content={"detail": str(exc)}) ``` --- ## 설정 옵션 `SendgoSettings`(pydantic-settings)가 `SENDGO_` 접두사 환경변수를 자동으로 읽습니다. | 필드 | 환경변수 | 기본값 | 설명 | |------|---------|--------|------| | `access_key` | `SENDGO_ACCESS_KEY` | — | Sendgo 액세스 키 (필수) | | `secret_key` | `SENDGO_SECRET_KEY` | — | Sendgo 시크릿 키 (필수) | | `kakao_sender_key` | `SENDGO_KAKAO_SENDER_KEY` | `None` | 카카오 발신프로필 키 | | `sms_sender_key` | `SENDGO_SMS_SENDER_KEY` | `None` | SMS 발신자 키 | | `api_version` | `SENDGO_API_VERSION` | `"v2"` | API 버전 | | `base_url` | `SENDGO_BASE_URL` | `"https://sendgo.io"` | API 기본 URL | --- ## 자주 묻는 질문 (FAQ) **Q. `sendgo-python`과의 차이는 무엇인가요?** A. `sendgo-python`은 프레임워크 독립적인 순수 Python 코어 패키지입니다. `sendgo-fastapi`는 이를 확장해 pydantic-settings 기반 환경변수 로딩, `Depends` 의존성 주입, lifespan 초기화 등 FastAPI 통합을 추가합니다. **Q. 환경변수 없이 설정을 직접 지정할 수 있나요?** A. 네, `init_sendgo(app, SendgoSettings(access_key="...", secret_key="..."))`처럼 설정 객체를 직접 주입할 수 있습니다. **Q. `get_sendgo`는 요청마다 새 클라이언트를 만드나요?** A. 아니요. 환경변수 기반 싱글턴 또는 `app.state.sendgo`에 저장된 인스턴스를 재사용합니다. **Q. 테스트 시 Sendgo를 Mock 처리하려면?** A. `app.dependency_overrides[get_sendgo] = lambda: mock_client`로 의존성을 교체하면 됩니다. --- ## 브랜드메시지 · 짧은 URL 이 패키지는 코어(`sendgo-python`)의 클라이언트를 그대로 노출하므로, 코어에 있는 채널이 모두 그대로 쓸 수 있습니다. 두 기능 모두 **v2 전용**입니다. | 기능 | 접근 | |------|------| | 카카오 브랜드메시지 (친구톡의 후속 채널) | `sendgo.brand_message` | | 짧은 URL (단축 + 클릭 반응 분석) | `sendgo.short_url` | 브랜드메시지는 채널 친구가 아닌 수신자에게도 보낼 수 있고(`targeting` = `N`), 수신 동의한 전체 채널 친구에게 동보 발송할 수도 있습니다(`targeting` = `F`). 짧은 URL 은 메시지 본문의 링크를 줄이고 클릭 반응(일별 추이·디바이스·유입경로·국가)을 집계합니다. 사용 예시와 파라미터는 [코어 README](https://github.com/send-go) 와 [SDK 가이드](https://sendgo.io/ko/sdk) 를 참고하세요. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. - 짧은 URL 지원 (1.1.0 릴리스 누락분 포함). ### 1.1.0 (2026-08-11) - 브랜드메시지·짧은 URL 접근 방법 문서화 (코어를 그대로 노출) ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `sendgo-fastapi` (PyPI) - **저장소**: [send-go/fastapi](https://github.com/send-go/fastapi) - **레지스트리**: https://pypi.org/project/sendgo-fastapi/ - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **Go에서 카카오 알림톡, 브랜드메시지, SMS를 가장 쉽게 발송하는 공식 Go SDK** `sendgo-go`는 [Sendgo](https://sendgo.io) 알림 API를 위한 공식 Go SDK입니다. **표준 라이브러리만 사용**하며, 완전한 타입 안전성과 동시성 안전 토큰 관리를 제공합니다. --- ## 설치 ```bash go get github.com/send-go/go ``` --- ## 빠른 시작 ```go package main import ( "fmt" "log" "os" sendgo "github.com/send-go/go/sendgo" ) func main() { client, err := sendgo.New(sendgo.Config{ AccessKey: os.Getenv("SENDGO_ACCESS_KEY"), SecretKey: os.Getenv("SENDGO_SECRET_KEY"), KakaoSenderKey: os.Getenv("SENDGO_KAKAO_SENDER_KEY"), SmsSenderKey: os.Getenv("SENDGO_SMS_SENDER_KEY"), ApiVersion: "v2", }) if err != nil { log.Fatal(err) } // 알림톡 발송 result, err := client.Alimtalk.Send(sendgo.AlimtalkRequest{ TemplateCode: "ORDER_CONFIRM_001", Contacts: []sendgo.Contact{ {Contact: "01012345678", Name: "홍길동", Var1: "ORD-001", Var2: "29,000원"}, }, }) if err != nil { log.Fatal(err) } fmt.Printf("발송 결과: %v\n", result) } ``` --- ## 알림톡 상세 사용법 ```go package main import ( "log" "os" sendgo "github.com/send-go/go/sendgo" ) func main() { client, _ := sendgo.New(sendgo.Config{ AccessKey: os.Getenv("SENDGO_ACCESS_KEY"), SecretKey: os.Getenv("SENDGO_SECRET_KEY"), KakaoSenderKey: os.Getenv("SENDGO_KAKAO_SENDER_KEY"), SmsSenderKey: os.Getenv("SENDGO_SMS_SENDER_KEY"), ApiVersion: "v2", }) // 다건 발송 client.Alimtalk.Send(sendgo.AlimtalkRequest{ TemplateCode: "ORDER_CONFIRM_001", Contacts: []sendgo.Contact{ {Contact: "01011111111", Name: "홍길동", Var1: "ORD-001", Var2: "29,000원"}, {Contact: "01022222222", Name: "김철수", Var1: "ORD-002", Var2: "15,000원"}, {Contact: "01033333333", Name: "이영희", Var1: "ORD-003", Var2: "52,000원"}, }, }) // 예약 발송 client.Alimtalk.Send(sendgo.AlimtalkRequest{ TemplateCode: "PROMO_SUMMER_2026", ScheduleType: "SCHEDULED", At: sendgo.String("2026-07-28 09:00:00"), Contacts: []sendgo.Contact{ {Contact: "01012345678", Var1: "여름 한정 50% 할인"}, }, }) // SMS 자동 대체 발송 client.Alimtalk.Send(sendgo.AlimtalkRequest{ TemplateCode: "DELIVERY_START_001", ReplaceSms: "Y", SmsSubject: sendgo.String("[배송 시작 안내]"), SmsContent: sendgo.String("주문하신 상품이 출고되었습니다.\n송장번호: #{var2}"), Contacts: []sendgo.Contact{ {Contact: "01012345678", Var1: "ORD-001", Var2: "1234567890"}, }, }) } ``` --- ## 친구톡 사용법 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```go // 텍스트형 client.Friendtalk.Send(sendgo.FriendtalkRequest{ Content: "안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.", Contacts: []sendgo.Contact{ {Contact: "01012345678"}, }, }) // 이미지형 client.Friendtalk.Send(sendgo.FriendtalkRequest{ MessageType: "FI", Content: "이번 주 특가 상품을 확인하세요!", ImageURL: "https://cdn.example.com/banner.jpg", ImageLink: "https://example.com/event", Contacts: []sendgo.Contact{ {Contact: "01012345678"}, }, }) ``` --- ## 브랜드메시지 사용법 브랜드메시지는 친구톡의 후속 채널입니다. 메시지 타입이 친구톡과 1:1 대응되며 (`FT`→`BT`, `FI`→`BI`, `FW`→`BW`, `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`), 요청에는 **친구톡 코드를 그대로** 넘기고 변환은 서버가 처리합니다. 친구톡과 달리 다음이 가능합니다. - 채널 친구가 **아닌** 수신자에게 발송 (`targeting: N`) - 수신 동의한 **전체 채널 친구 동보** 발송 (`targeting: F`, 수신자 목록 불필요) - 리스트·캐러셀·커머스·동영상 등 **템플릿 기반 리치 메시지** > v2 전용입니다. 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보낼 때는 여전히 친구톡 API 를 쓰세요 — 이 엔드포인트는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. 친구톡 요청은 카카오 측에서 브랜드메시지(자유형)로 대체 발송됩니다. ```go // 단건 발송 — 채널 친구 대상 _, err := client.BrandMessage.Send(sendgo.BrandMessageRequest{ Targeting: "M", MessageType: "FL", FriendTemplateUUID: "9cd5460b-6458-4edc-9b11-c26d3013c340", Contacts: []sendgo.Contact{ {Contact: "01012345678", Var1: "29,000원"}, }, }) // 동보 발송 — 수신 동의한 전체 채널 친구 (Contacts 불필요) _, err = client.BrandMessage.Broadcast(sendgo.BrandMessageRequest{ MessageType: "FW", FriendTemplateUUID: "9cd5460b-6458-4edc-9b11-c26d3013c340", }) // 캠페인 조회 list, err := client.BrandMessage.Campaigns(sendgo.BrandMessageListQuery{Count: 10}) one, err := client.BrandMessage.Campaign("1f0a6d0e-6b3b-4f0f-9b2f-2f6f6a1b7c11") ``` --- ## SMS / LMS / MMS 사용법 ```go // SMS client.SMS.SendSMS(sendgo.SmsRequest{ Content: "[Sendgo] 인증번호: 123456 (5분 이내 입력)", Contacts: []sendgo.Contact{ {Contact: "01012345678"}, }, }) // LMS client.SMS.SendLMS(sendgo.SmsRequest{ Subject: sendgo.String("[중요] 서비스 점검 안내"), Content: "안녕하세요. 서비스 점검이 예정되어 있습니다.\n■ 일시: 2026-07-25 02:00 ~ 06:00", Contacts: []sendgo.Contact{ {Contact: "01012345678"}, }, }) // MMS client.SMS.SendMMS(sendgo.SmsRequest{ Subject: sendgo.String("[이벤트] 7월 특가"), Content: "이번 달 특가 상품을 확인하세요!", Contacts: []sendgo.Contact{ {Contact: "01011111111"}, {Contact: "01022222222"}, }, }) // 예약 문자 client.SMS.SendSMS(sendgo.SmsRequest{ Content: "[알림] 예약 미팅을 확인해주세요.", ScheduleType: "SCHEDULED", At: sendgo.String("2026-07-23 08:00:00"), Contacts: []sendgo.Contact{{Contact: "01012345678"}}, }) ``` --- ## 프레임워크 통합 ### net/http 서버 ```go package main import ( "encoding/json" "net/http" "os" sendgo "github.com/send-go/go/sendgo" ) var sg *sendgo.Client func init() { var err error sg, err = sendgo.New(sendgo.Config{ AccessKey: os.Getenv("SENDGO_ACCESS_KEY"), SecretKey: os.Getenv("SENDGO_SECRET_KEY"), KakaoSenderKey: os.Getenv("SENDGO_KAKAO_SENDER_KEY"), ApiVersion: "v2", }) if err != nil { panic(err) } } func notifyOrderHandler(w http.ResponseWriter, r *http.Request) { var req struct { Phone string `json:"phone"` OrderNo string `json:"order_no"` } json.NewDecoder(r.Body).Decode(&req) _, err := sg.Alimtalk.Send(sendgo.AlimtalkRequest{ TemplateCode: "ORDER_CONFIRM_001", Contacts: []sendgo.Contact{{Contact: req.Phone, Var1: req.OrderNo}}, }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(map[string]bool{"success": true}) } func main() { http.HandleFunc("/api/notify/order", notifyOrderHandler) http.ListenAndServe(":8080", nil) } ``` ### Gin Framework ```go package main import ( "net/http" "os" "github.com/gin-gonic/gin" sendgo "github.com/send-go/go/sendgo" ) func main() { sg, _ := sendgo.New(sendgo.Config{ AccessKey: os.Getenv("SENDGO_ACCESS_KEY"), SecretKey: os.Getenv("SENDGO_SECRET_KEY"), KakaoSenderKey: os.Getenv("SENDGO_KAKAO_SENDER_KEY"), ApiVersion: "v2", }) r := gin.Default() r.POST("/api/notify/order", func(c *gin.Context) { var req struct { Phone string `json:"phone"` OrderNo string `json:"order_no"` } c.ShouldBindJSON(&req) _, err := sg.Alimtalk.Send(sendgo.AlimtalkRequest{ TemplateCode: "ORDER_CONFIRM_001", Contacts: []sendgo.Contact{{Contact: req.Phone, Var1: req.OrderNo}}, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true}) }) r.Run(":8080") } ``` --- ## 예외 처리 ```go import "github.com/send-go/go/sendgo" _, err := client.Alimtalk.Send(req) if err != nil { var se *sendgo.SendgoError if errors.As(err, &se) { fmt.Printf("발송 실패: HTTP %d [%s]\n", se.StatusCode, se.ErrorCode) switch se.ErrorCode { case "INVALID_ACCESS_KEY", "INVALID_SECRET_KEY": alertOps("Sendgo 인증키를 확인하세요.") case "INVALID_TEMPLATE_CODE": log.Printf("존재하지 않는 템플릿: %s", se.Message) case "PAYMENT_REQUIRED": alertOps("Sendgo 크레딧이 부족합니다.") } } } ``` --- ## 포인터 필드와 `sendgo.String` `AlimtalkRequest` 의 `At`/`SmsSubject`/`SmsContent` 와 `SmsRequest` 의 `Subject` 는 "설정하지 않음"(JSON `null`)과 빈 문자열을 구분해야 하므로 `*string` 입니다. Go에서는 리터럴의 주소를 얻을 수 없으므로(`&"..."` 는 컴파일 에러) 변수를 따로 선언하거나 `sendgo.String(...)` 헬퍼를 사용하세요. ```go err := client.SMS.SendLMS(sendgo.SmsRequest{ Subject: sendgo.String("[중요] 서비스 점검 안내"), Content: "...", Contacts: []sendgo.Contact{{Contact: "01012345678"}}, }) ``` ## 설정 옵션 | 필드 | 타입 | 필수 | 기본값 | 설명 | |------|------|------|--------|------| | `AccessKey` | `string` | **필수** | — | Sendgo 액세스 키 | | `SecretKey` | `string` | **필수** | — | Sendgo 시크릿 키 | | `KakaoSenderKey` | `string` | 선택 | `""` | 카카오 발신프로필 키 | | `SmsSenderKey` | `string` | 선택 | `""` | SMS 발신자 키 | | `ApiVersion` | `string` | 선택 | `"v2"` | API 버전 (`v1` \| `v2`) | | `BaseURL` | `string` | 선택 | `"https://sendgo.io"` | API 기본 URL | --- ## 짧은 URL 짧은 URL 은 메시지 본문의 링크를 줄이고, 그 링크가 실제로 눌렸는지 집계합니다. 문자는 바이트 수가 요금과 직결되므로 링크를 줄이면 그만큼 본문을 더 쓸 수 있습니다. 같은 원본 URL 을 다시 줄이면 **기존 링크가 그대로 반환**됩니다. 캠페인별로 반응을 따로 집계하려면 `forceNew` 로 새 코드를 만드세요. `deactivate` 는 링크를 삭제하지 않고 리다이렉트만 중지합니다. 이미 발송한 메시지의 링크를 무효화할 때 쓰며, 누적 통계는 남고 이후 접속은 `410 Gone` 이 됩니다. ```go // 짧은 URL 생성 (v2 전용) created, err := client.ShortURL.Create(sendgo.ShortURLRequest{ TargetURL: "https://example.com/promotions/summer-sale", Title: "여름 세일 랜딩", }) if err != nil { log.Fatal(err) } code := created["data"].(map[string]any)["code"].(string) // 반응 통계 — 일별 추이 + 디바이스/유입경로/국가별 분해 stats, err := client.ShortURL.Stats(code, sendgo.ShortURLStatsQuery{From: "2026-08-01"}) client.ShortURL.List(sendgo.ShortURLListQuery{Count: 10}) client.ShortURL.Show(code) client.ShortURL.Deactivate(code) // 리다이렉트만 중지, 통계는 남는다 ``` `stats` 는 일별 추이(`daily`)와 디바이스(`byDevice`)·유입경로(`byReferer`)·국가(`byCountry`)별 분해를 반환합니다. 일별 추이는 사전 집계 표에서 읽으므로 클릭이 많아도 응답 시간이 일정합니다. ## 변경 사항 ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ### 1.1.0 (2026-08-11) - 짧은 URL 추가 — `client.ShortURL` - **모듈 경로 수정** — `github.com/sendgo-dev/sendgo-go` → `github.com/send-go/go`. README·문서가 모두 후자를 쓰고 있어 `go get` 이 동작할 수 없었다. - `sendgo.String()` 헬퍼 추가. `At`/`SmsSubject`/`SmsContent`/`Subject` 가 `*string` 이라 리터럴을 직접 넘길 수 없었다. - `httpClient.delete()` 추가 ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `github.com/send-go/go` (Go Modules) - **저장소**: [send-go/go](https://github.com/send-go/go) - **레지스트리**: https://pkg.go.dev/github.com/send-go/go - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **Java에서 카카오 알림톡, 브랜드메시지, SMS를 가장 쉽게 발송하는 순수 Java SDK** `sendgo-java`는 [Sendgo](https://sendgo.io) 알림 API를 위한 **순수 Java 코어 SDK**입니다. Spring, Quarkus 등 특정 프레임워크에 의존하지 않으며, `java.net.http.HttpClient`와 Jackson만 사용합니다. Spring Boot 프로젝트라면 [`sendgo-spring`](https://github.com/send-go/spring) 패키지를 사용하세요. --- ## 설치 ### Maven ```xml io.sendgo sendgo-java 1.1.0 ``` ### Gradle ```groovy implementation 'io.sendgo:sendgo-java:1.1.0' ``` --- ## 빠른 시작 ```java import io.sendgo.*; import io.sendgo.model.*; SendgoClient sendgo = new SendgoClient(SendgoConfig.builder() .accessKey(System.getenv("SENDGO_ACCESS_KEY")) .secretKey(System.getenv("SENDGO_SECRET_KEY")) .kakaoSenderKey(System.getenv("SENDGO_KAKAO_SENDER_KEY")) .smsSenderKey(System.getenv("SENDGO_SMS_SENDER_KEY")) .apiVersion("v2") .build()); // 알림톡 발송 sendgo.alimtalk().send(AlimtalkRequest.builder() .templateCode("ORDER_CONFIRM_001") .contacts(List.of( Contact.builder() .contact("01012345678") .name("홍길동") .var1("ORD-001") .var2("29,000원") .build() )) .build()); ``` --- ## 알림톡 상세 사용법 ```java import io.sendgo.*; import io.sendgo.model.*; import java.util.List; // 다건 발송 sendgo.alimtalk().send(AlimtalkRequest.builder() .templateCode("ORDER_CONFIRM_001") .contacts(List.of( Contact.builder().contact("01011111111").name("홍길동").var1("ORD-001").var2("29,000원").build(), Contact.builder().contact("01022222222").name("김철수").var1("ORD-002").var2("15,000원").build(), Contact.builder().contact("01033333333").name("이영희").var1("ORD-003").var2("52,000원").build() )) .build()); // 예약 발송 sendgo.alimtalk().send(AlimtalkRequest.builder() .templateCode("PROMO_SUMMER_2026") .scheduleType("SCHEDULED") .at("2026-07-28 09:00:00") .contacts(List.of( Contact.builder().contact("01012345678").var1("여름 한정 50% 할인").build() )) .build()); // SMS 자동 대체 발송 sendgo.alimtalk().send(AlimtalkRequest.builder() .templateCode("DELIVERY_START_001") .replaceSms("Y") .smsSubject("[배송 시작 안내]") .smsContent("주문하신 상품이 출고되었습니다.\n송장번호: #{var2}") .contacts(List.of( Contact.builder().contact("01012345678").var1("ORD-001").var2("1234567890").build() )) .build()); ``` --- ## 친구톡 사용법 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```java // 텍스트형 sendgo.friendtalk().send(FriendtalkRequest.builder() .content("안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.") .contacts(List.of(Contact.builder().contact("01012345678").build())) .build()); // 이미지형 sendgo.friendtalk().send(FriendtalkRequest.builder() .messageType("FI") .content("이번 주 특가 상품을 확인하세요!") .imageUrl("https://cdn.example.com/banner.jpg") .imageLink("https://example.com/event") .contacts(List.of(Contact.builder().contact("01012345678").build())) .build()); ``` --- ## 브랜드메시지 사용법 브랜드메시지는 친구톡의 후속 채널입니다. 메시지 타입이 친구톡과 1:1 대응되며 (`FT`→`BT`, `FI`→`BI`, `FW`→`BW`, `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`), 요청에는 **친구톡 코드를 그대로** 넘기고 변환은 서버가 처리합니다. 친구톡과 달리 다음이 가능합니다. - 채널 친구가 **아닌** 수신자에게 발송 (`targeting: N`) - 수신 동의한 **전체 채널 친구 동보** 발송 (`targeting: F`, 수신자 목록 불필요) - 리스트·캐러셀·커머스·동영상 등 **템플릿 기반 리치 메시지** > v2 전용입니다. 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보낼 때는 여전히 친구톡 API 를 쓰세요 — 이 엔드포인트는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. 친구톡 요청은 카카오 측에서 브랜드메시지(자유형)로 대체 발송됩니다. ```java import io.sendgo.model.BrandMessageRequest; import io.sendgo.model.Contact; // 단건 발송 — 채널 친구 대상 sendgo.brandMessage().send(BrandMessageRequest.builder() .targeting("M") .messageType("FL") .friendTemplateUuid("9cd5460b-6458-4edc-9b11-c26d3013c340") .contact(Contact.builder().contact("01012345678").var1("29,000원").build()) .build()); // 동보 발송 — 수신 동의한 전체 채널 친구 (contacts 불필요) sendgo.brandMessage().broadcast(BrandMessageRequest.builder() .messageType("FW") .friendTemplateUuid("9cd5460b-6458-4edc-9b11-c26d3013c340") .build()); // 캠페인 조회 var list = sendgo.brandMessage().campaigns(null, null, 10); var one = sendgo.brandMessage().campaign("1f0a6d0e-6b3b-4f0f-9b2f-2f6f6a1b7c11"); ``` --- ## SMS / LMS / MMS 사용법 ```java // SMS sendgo.sms().sendSms(SmsRequest.sms() .content("[Sendgo] 인증번호: 123456 (5분 이내 입력)") .contact(Contact.builder().contact("01012345678").build())); // LMS sendgo.sms().sendLms(SmsRequest.lms() .subject("[중요] 서비스 점검 안내") .content("안녕하세요. 서비스 점검이 예정되어 있습니다.\n■ 일시: 2026-07-25 02:00 ~ 06:00") .contact(Contact.builder().contact("01012345678").build())); // MMS sendgo.sms().sendMms(SmsRequest.mms() .subject("[이벤트] 7월 특가") .content("이번 달 특가 상품을 확인하세요!") .contacts(List.of( Contact.builder().contact("01011111111").build(), Contact.builder().contact("01022222222").build() ))); ``` --- ## 프레임워크 통합 ### Quarkus ```java // src/main/java/org/acme/SendgoProducer.java import io.sendgo.*; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; import org.eclipse.microprofile.config.inject.ConfigProperty; @ApplicationScoped public class SendgoProducer { @ConfigProperty(name = "sendgo.access-key") String accessKey; @ConfigProperty(name = "sendgo.secret-key") String secretKey; @ConfigProperty(name = "sendgo.kakao-sender-key") String kakaoKey; @Produces @ApplicationScoped public SendgoClient sendgoClient() { return new SendgoClient(SendgoConfig.builder() .accessKey(accessKey) .secretKey(secretKey) .kakaoSenderKey(kakaoKey) .apiVersion("v2") .build()); } } // 서비스에서 주입받아 사용 @ApplicationScoped public class NotificationService { @Inject SendgoClient sendgo; public void sendOrderConfirm(String phone, String orderNo) { sendgo.alimtalk().send(AlimtalkRequest.builder() .templateCode("ORDER_CONFIRM_001") .contact(Contact.builder().contact(phone).var1(orderNo).build()) .build()); } } ``` ### Micronaut ```java import io.micronaut.context.annotation.Factory; import io.micronaut.context.annotation.Value; import io.sendgo.*; import jakarta.inject.Singleton; @Factory public class SendgoFactory { @Value("${sendgo.access-key}") String accessKey; @Value("${sendgo.secret-key}") String secretKey; @Singleton public SendgoClient sendgoClient() { return new SendgoClient(SendgoConfig.builder() .accessKey(accessKey) .secretKey(secretKey) .apiVersion("v2") .build()); } } ``` ### 순수 Java (싱글톤 패턴) ```java public final class SendgoHolder { private static volatile SendgoClient instance; public static SendgoClient get() { if (instance == null) { synchronized (SendgoHolder.class) { if (instance == null) { instance = new SendgoClient(SendgoConfig.builder() .accessKey(System.getenv("SENDGO_ACCESS_KEY")) .secretKey(System.getenv("SENDGO_SECRET_KEY")) .kakaoSenderKey(System.getenv("SENDGO_KAKAO_KEY")) .apiVersion("v2") .build()); } } } return instance; } } // 사용 SendgoHolder.get().alimtalk().send(AlimtalkRequest.builder() .templateCode("ORDER_CONFIRM_001") .contact(Contact.builder().contact("01012345678").var1("ORD-001").build()) .build()); ``` --- ## 예외 처리 ```java import io.sendgo.exception.SendgoException; try { sendgo.alimtalk().send(...); } catch (SendgoException e) { System.err.printf("발송 실패: HTTP %d [%s]%n", e.getStatusCode(), e.getErrorCode()); switch (e.getErrorCode()) { case "INVALID_ACCESS_KEY", "INVALID_SECRET_KEY" -> alertOps("Sendgo 인증키를 확인하세요."); case "INVALID_TEMPLATE_CODE" -> log.warn("존재하지 않는 템플릿: {}", e.getMessage()); case "PAYMENT_REQUIRED" -> alertOps("Sendgo 크레딧이 부족합니다."); case "IP_NOT_ALLOWED" -> alertOps("허용되지 않은 IP"); default -> log.error("알 수 없는 오류", e); } } ``` --- ## 설정 옵션 | 파라미터 | 타입 | 필수 | 기본값 | 설명 | |---------|------|------|--------|------| | `accessKey` | `String` | **필수** | — | Sendgo 액세스 키 | | `secretKey` | `String` | **필수** | — | Sendgo 시크릿 키 | | `kakaoSenderKey` | `String` | 선택 | `null` | 카카오 발신프로필 키 | | `smsSenderKey` | `String` | 선택 | `null` | SMS 발신자 키 | | `apiVersion` | `String` | 선택 | `"v2"` | API 버전 (`v1` \| `v2`) | | `baseUrl` | `String` | 선택 | `"https://sendgo.io"` | API 기본 URL | --- ## 1.1.0 변경 사항 - **`contact(...)` 가 누적된다.** 이전에는 `contacts = List.of(v)` 로 리스트를 통째로 교체했기 때문에 `.contact(a).contact(b)` 로 다건을 넣으면 `a` 가 조용히 사라졌다. 이제 호출할 때마다 추가된다. 한 번만 호출하던 기존 코드는 동작이 같다. 전체를 한 번에 지정하려면 여전히 `contacts(List.of(...))` 를 쓴다. - **`sendSms` / `sendLms` / `sendMms` 가 messageType 을 강제한다.** 이전에는 세 메서드가 모두 요청을 그대로 넘겼기 때문에 `sendLms(SmsRequest.sms()...)` 가 SMS 로 발송됐다. 요청 자체의 타입을 그대로 쓰려면 `send(...)` 를 사용한다. - **`Contact` 에 `var6` ~ `var8` 이 추가됐다.** 다른 언어 SDK(Node/Python/.NET/Go/Flutter)와 동일한 범위를 지원하도록 맞췄다. 그 이상은 `variable("name", "value")` 를 사용한다. ## 짧은 URL 짧은 URL 은 메시지 본문의 링크를 줄이고, 그 링크가 실제로 눌렸는지 집계합니다. 문자는 바이트 수가 요금과 직결되므로 링크를 줄이면 그만큼 본문을 더 쓸 수 있습니다. 같은 원본 URL 을 다시 줄이면 **기존 링크가 그대로 반환**됩니다. 캠페인별로 반응을 따로 집계하려면 `forceNew` 로 새 코드를 만드세요. `deactivate` 는 링크를 삭제하지 않고 리다이렉트만 중지합니다. 이미 발송한 메시지의 링크를 무효화할 때 쓰며, 누적 통계는 남고 이후 접속은 `410 Gone` 이 됩니다. ```java // 짧은 URL 생성 (v2 전용) Map created = sendgo.shortUrl().create(ShortUrlRequest.builder() .targetUrl("https://example.com/promotions/summer-sale") .title("여름 세일 랜딩") .build()); @SuppressWarnings("unchecked") Map data = (Map) created.get("data"); String code = (String) data.get("code"); // 반응 통계 — 일별 추이 + 디바이스/유입경로/국가별 분해 Map stats = sendgo.shortUrl().stats(code, "2026-08-01", null); sendgo.shortUrl().list(null, null, 10); sendgo.shortUrl().show(code); sendgo.shortUrl().deactivate(code); // 리다이렉트만 중지, 통계는 남는다 ``` `stats` 는 일별 추이(`daily`)와 디바이스(`byDevice`)·유입경로(`byReferer`)·국가(`byCountry`)별 분해를 반환합니다. 일별 추이는 사전 집계 표에서 읽으므로 클릭이 많아도 응답 시간이 일정합니다. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `io.sendgo:sendgo-java` (Maven Central) - **저장소**: [send-go/java](https://github.com/send-go/java) - **레지스트리**: https://central.sonatype.com/artifact/io.sendgo/sendgo-java - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **Spring Boot에서 카카오 알림톡, 브랜드메시지, SMS를 가장 쉽게 발송하는 공식 Spring Boot Starter** `sendgo-spring`은 [`sendgo-java`](https://github.com/send-go/java) 코어를 확장한 **Spring Boot 전용 스타터**입니다. `application.yml` 설정만으로 `SendgoClient` 빈이 자동 등록됩니다. --- ## 설치 ### Maven ```xml io.sendgo sendgo-spring 1.0.0 ``` ### Gradle ```groovy implementation 'io.sendgo:sendgo-spring:1.0.0' ``` --- ## 빠른 시작 (3단계) ### 1단계 — application.yml 설정 ```yaml sendgo: access-key: ${SENDGO_ACCESS_KEY} secret-key: ${SENDGO_SECRET_KEY} kakao-sender-key: ${SENDGO_KAKAO_SENDER_KEY} sms-sender-key: ${SENDGO_SMS_SENDER_KEY} api-version: v2 ``` ### 2단계 — 서비스에서 SendgoClient 주입 ```java @Service public class NotificationService { private final SendgoClient sendgo; public NotificationService(SendgoClient sendgo) { this.sendgo = sendgo; } public void sendOrderConfirm(String phone, String orderNo, String amount) { sendgo.alimtalk().send(AlimtalkRequest.builder() .templateCode("ORDER_CONFIRM_001") .contacts(List.of( Contact.builder() .contact(phone) .var1(orderNo) .var2(amount) .build() )) .build()); } } ``` ### 3단계 — 컨트롤러에서 사용 ```java @RestController @RequestMapping("/api/orders") public class OrderController { private final NotificationService notificationService; public OrderController(NotificationService notificationService) { this.notificationService = notificationService; } @PostMapping("/{id}/confirm") public ResponseEntity> confirmOrder(@PathVariable Long id) { Order order = orderService.findById(id); notificationService.sendOrderConfirm( order.getPhone(), order.getNumber(), order.getFormattedAmount() ); return ResponseEntity.ok(Map.of("success", true)); } } ``` --- ## 알림톡 상세 사용법 ```java import io.sendgo.*; import io.sendgo.model.*; import java.util.List; @Service public class AlimtalkExamples { private final SendgoClient sendgo; public AlimtalkExamples(SendgoClient sendgo) { this.sendgo = sendgo; } // 다건 발송 public void sendBulk() { sendgo.alimtalk().send(AlimtalkRequest.builder() .templateCode("ORDER_CONFIRM_001") .contacts(List.of( Contact.builder().contact("01011111111").name("홍길동").var1("ORD-001").var2("29,000원").build(), Contact.builder().contact("01022222222").name("김철수").var1("ORD-002").var2("15,000원").build(), Contact.builder().contact("01033333333").name("이영희").var1("ORD-003").var2("52,000원").build() )) .build()); } // 예약 발송 public void sendScheduled() { sendgo.alimtalk().send(AlimtalkRequest.builder() .templateCode("PROMO_SUMMER_2026") .scheduleType("SCHEDULED") .at("2026-07-28 09:00:00") .contacts(List.of( Contact.builder().contact("01012345678").var1("여름 한정 50% 할인").build() )) .build()); } // SMS 자동 대체 발송 public void sendWithFallback() { sendgo.alimtalk().send(AlimtalkRequest.builder() .templateCode("DELIVERY_START_001") .replaceSms("Y") .smsSubject("[배송 시작 안내]") .smsContent("주문하신 상품이 출고되었습니다.\n송장번호: #{var2}") .contacts(List.of( Contact.builder().contact("01012345678").var1("ORD-001").var2("1234567890").build() )) .build()); } } ``` --- ## 친구톡 사용법 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```java @Service public class FriendtalkExamples { private final SendgoClient sendgo; public FriendtalkExamples(SendgoClient sendgo) { this.sendgo = sendgo; } // 텍스트형 public void sendText() { sendgo.friendtalk().send(FriendtalkRequest.builder() .content("안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.") .contacts(List.of(Contact.builder().contact("01012345678").build())) .build()); } // 이미지형 public void sendImage() { sendgo.friendtalk().send(FriendtalkRequest.builder() .messageType("FI") .content("이번 주 특가 상품을 확인하세요!") .imageUrl("https://cdn.example.com/banner.jpg") .imageLink("https://example.com/event") .contacts(List.of(Contact.builder().contact("01012345678").build())) .build()); } } ``` --- ## SMS / LMS / MMS 사용법 ```java @Service public class SmsExamples { private final SendgoClient sendgo; public SmsExamples(SendgoClient sendgo) { this.sendgo = sendgo; } // SMS (90자 이하) public void sendSms(String phone, String code) { sendgo.sms().sendSms(SmsRequest.sms() .content("[Sendgo] 인증번호: " + code + " (5분 이내 입력)") .contact(Contact.builder().contact(phone).build())); } // LMS (장문) public void sendLms(String phone) { sendgo.sms().sendLms(SmsRequest.lms() .subject("[중요] 서비스 점검 안내") .content("안녕하세요. 서비스 점검이 예정되어 있습니다.\n\n■ 일시: 2026-07-25 02:00 ~ 06:00\n■ 영향: 전체 서비스") .contact(Contact.builder().contact(phone).build())); } // MMS (이미지) public void sendMms(List phones) { List contacts = phones.stream() .map(p -> Contact.builder().contact(p).build()) .toList(); sendgo.sms().sendMms(SmsRequest.mms() .subject("[이벤트] 7월 특가") .content("이번 달 특가 상품을 확인하세요!") .contacts(contacts)); } } ``` --- ## Spring Events 통합 ```java // 이벤트 정의 public record OrderConfirmedEvent(String phone, String orderNo, String amount) {} // 이벤트 리스너 @Component public class OrderEventListener { private final SendgoClient sendgo; public OrderEventListener(SendgoClient sendgo) { this.sendgo = sendgo; } @EventListener @Async public void handleOrderConfirmed(OrderConfirmedEvent event) { sendgo.alimtalk().send(AlimtalkRequest.builder() .templateCode("ORDER_CONFIRM_001") .contacts(List.of( Contact.builder() .contact(event.phone()) .var1(event.orderNo()) .var2(event.amount()) .build() )) .build()); } } // 이벤트 발행 @Service public class OrderService { private final ApplicationEventPublisher publisher; public void confirmOrder(Order order) { // ... 주문 처리 로직 publisher.publishEvent(new OrderConfirmedEvent( order.getPhone(), order.getNumber(), order.getFormattedAmount() )); } } ``` --- ## 예외 처리 ```java import io.sendgo.exception.SendgoException; @Service public class SafeNotificationService { private final SendgoClient sendgo; private static final Logger log = LoggerFactory.getLogger(SafeNotificationService.class); public SafeNotificationService(SendgoClient sendgo) { this.sendgo = sendgo; } public void sendSafely(String templateCode, String phone, String var1) { try { sendgo.alimtalk().send(AlimtalkRequest.builder() .templateCode(templateCode) .contact(Contact.builder().contact(phone).var1(var1).build()) .build()); } catch (SendgoException e) { log.error("알림톡 발송 실패: HTTP {} [{}] endpoint={}", e.getStatusCode(), e.getErrorCode(), e.getEndpoint()); switch (e.getErrorCode()) { case "INVALID_ACCESS_KEY", "INVALID_SECRET_KEY" -> alertOps("Sendgo 인증키 오류"); case "INVALID_TEMPLATE_CODE" -> log.warn("존재하지 않는 템플릿: {}", templateCode); case "PAYMENT_REQUIRED" -> alertOps("Sendgo 크레딧 부족"); case "IP_NOT_ALLOWED" -> alertOps("허용되지 않은 IP"); default -> log.error("알 수 없는 오류", e); } } } } ``` --- ## 설정 옵션 (application.yml) | 키 | 필수 | 기본값 | 설명 | |----|------|--------|------| | `sendgo.access-key` | **필수** | — | Sendgo 액세스 키 | | `sendgo.secret-key` | **필수** | — | Sendgo 시크릿 키 | | `sendgo.kakao-sender-key` | 선택 | `null` | 카카오 발신프로필 키 | | `sendgo.sms-sender-key` | 선택 | `null` | SMS 발신자 키 | | `sendgo.api-version` | 선택 | `v2` | API 버전 | | `sendgo.url` | 선택 | `https://sendgo.io` | API 기본 URL | --- ## 브랜드메시지 · 짧은 URL 이 패키지는 코어(`io.sendgo:sendgo-java`)의 클라이언트를 그대로 노출하므로, 코어에 있는 채널이 모두 그대로 쓸 수 있습니다. 두 기능 모두 **v2 전용**입니다. | 기능 | 접근 | |------|------| | 카카오 브랜드메시지 (친구톡의 후속 채널) | `sendgo.brandMessage()` | | 짧은 URL (단축 + 클릭 반응 분석) | `sendgo.shortUrl()` | 브랜드메시지는 채널 친구가 아닌 수신자에게도 보낼 수 있고(`targeting` = `N`), 수신 동의한 전체 채널 친구에게 동보 발송할 수도 있습니다(`targeting` = `F`). 짧은 URL 은 메시지 본문의 링크를 줄이고 클릭 반응(일별 추이·디바이스·유입경로·국가)을 집계합니다. 사용 예시와 파라미터는 [코어 README](https://github.com/send-go) 와 [SDK 가이드](https://sendgo.io/ko/sdk) 를 참고하세요. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ### 1.1.0 (2026-08-11) - **`sendgo-java` 의존을 1.1.0 으로 올림** — 1.0.1 로 고정돼 있어 Spring 사용자에게 `shortUrl()` 이 노출되지 않았다. ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `io.sendgo:sendgo-spring` (Maven Central) - **저장소**: [send-go/spring](https://github.com/send-go/spring) - **레지스트리**: https://central.sonatype.com/artifact/io.sendgo/sendgo-spring - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **Ruby에서 카카오 알림톡, 브랜드메시지, SMS를 가장 쉽게 발송하는 공식 Ruby SDK** `sendgo`는 [Sendgo](https://sendgo.io) 알림 API를 위한 공식 Ruby SDK입니다. **표준 라이브러리만 사용**하며, MonitorMixin 기반 스레드 안전 토큰 관리를 제공합니다. Rails, Sinatra, Hanami 등 모든 Ruby 환경에서 사용할 수 있습니다. --- ## 설치 ```bash gem install sendgo ``` 또는 Gemfile: ```ruby gem 'sendgo', '~> 1.0' ``` --- ## 빠른 시작 ```ruby require 'sendgo' client = Sendgo::Client.new( access_key: ENV['SENDGO_ACCESS_KEY'], secret_key: ENV['SENDGO_SECRET_KEY'], kakao_sender_key: ENV['SENDGO_KAKAO_SENDER_KEY'], sms_sender_key: ENV['SENDGO_SMS_SENDER_KEY'], api_version: 'v2' ) # 알림톡 발송 client.alimtalk.send( template_code: 'ORDER_CONFIRM_001', contacts: [ { contact: '01012345678', name: '홍길동', var1: 'ORD-001', var2: '29,000원' } ] ) ``` --- ## 알림톡 상세 사용법 ```ruby require 'sendgo' client = Sendgo::Client.new( access_key: ENV['SENDGO_ACCESS_KEY'], secret_key: ENV['SENDGO_SECRET_KEY'], kakao_sender_key: ENV['SENDGO_KAKAO_SENDER_KEY'], sms_sender_key: ENV['SENDGO_SMS_SENDER_KEY'], api_version: 'v2' ) # 다건 발송 client.alimtalk.send( template_code: 'ORDER_CONFIRM_001', contacts: [ { contact: '01011111111', name: '홍길동', var1: 'ORD-001', var2: '29,000원' }, { contact: '01022222222', name: '김철수', var1: 'ORD-002', var2: '15,000원' }, { contact: '01033333333', name: '이영희', var1: 'ORD-003', var2: '52,000원' } ] ) # 예약 발송 client.alimtalk.send( template_code: 'PROMO_SUMMER_2026', schedule_type: 'SCHEDULED', at: '2026-07-28 09:00:00', contacts: [ { contact: '01012345678', var1: '여름 한정 50% 할인' } ] ) # SMS 자동 대체 발송 client.alimtalk.send( template_code: 'DELIVERY_START_001', replace_sms: 'Y', sms_subject: '[배송 시작 안내]', sms_content: "주문하신 상품이 출고되었습니다.\n송장번호: \#{var2}", contacts: [ { contact: '01012345678', var1: 'ORD-001', var2: '1234567890' } ] ) ``` --- ## 친구톡 사용법 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```ruby # 텍스트형 client.friendtalk.send( content: '안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.', contacts: [{ contact: '01012345678' }] ) # 이미지형 client.friendtalk.send( message_type: 'FI', content: '이번 주 특가 상품을 확인하세요!', image_url: 'https://cdn.example.com/banner.jpg', image_link: 'https://example.com/event', contacts: [{ contact: '01012345678' }] ) # 버튼 포함 client.friendtalk.send( content: '7월 쿠폰이 도착했습니다! 지금 바로 사용하세요.', buttons: [{ name: '쿠폰 받기', type: 'WL', link_mo: 'https://example.com/coupon' }], contacts: [{ contact: '01012345678' }] ) ``` --- ## 브랜드메시지 사용법 브랜드메시지는 친구톡의 후속 채널입니다. 메시지 타입이 친구톡과 1:1 대응되며 (`FT`→`BT`, `FI`→`BI`, `FW`→`BW`, `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`), 요청에는 **친구톡 코드를 그대로** 넘기고 변환은 서버가 처리합니다. 친구톡과 달리 다음이 가능합니다. - 채널 친구가 **아닌** 수신자에게 발송 (`targeting: N`) - 수신 동의한 **전체 채널 친구 동보** 발송 (`targeting: F`, 수신자 목록 불필요) - 리스트·캐러셀·커머스·동영상 등 **템플릿 기반 리치 메시지** > v2 전용입니다. 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보낼 때는 여전히 친구톡 API 를 쓰세요 — 이 엔드포인트는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. 친구톡 요청은 카카오 측에서 브랜드메시지(자유형)로 대체 발송됩니다. ```ruby # 단건 발송 — 채널 친구 대상 client.brand_message.send( targeting: "M", message_type: "FL", friend_template_uuid: "9cd5460b-6458-4edc-9b11-c26d3013c340", contacts: [{ contact: "01012345678", var1: "29,000원" }] ) # 동보 발송 — 수신 동의한 전체 채널 친구 (contacts 불필요) client.brand_message.broadcast( message_type: "FW", friend_template_uuid: "9cd5460b-6458-4edc-9b11-c26d3013c340" ) # 캠페인 조회 campaigns = client.brand_message.campaigns(from: "2026-08-01", count: 10) one = client.brand_message.campaign("1f0a6d0e-6b3b-4f0f-9b2f-2f6f6a1b7c11") ``` --- ## SMS / LMS / MMS 사용법 ```ruby # SMS (90자 이하) client.sms.send_sms( content: '[Sendgo] 인증번호: 123456 (5분 이내 입력)', contacts: [{ contact: '01012345678' }] ) # LMS (장문) client.sms.send_lms( subject: '[중요] 서비스 점검 안내', content: "안녕하세요. 서비스 점검이 예정되어 있습니다.\n■ 일시: 2026-07-25 02:00 ~ 06:00", contacts: [{ contact: '01012345678' }] ) # MMS (이미지 포함) client.sms.send_mms( subject: '[이벤트] 7월 특가', content: '이번 달 특가 상품을 확인하세요!', contacts: [{ contact: '01011111111' }, { contact: '01022222222' }] ) ``` --- ## Rails 통합 ```ruby # config/initializers/sendgo.rb SENDGO_CLIENT = Sendgo::Client.new( access_key: ENV['SENDGO_ACCESS_KEY'], secret_key: ENV['SENDGO_SECRET_KEY'], kakao_sender_key: ENV['SENDGO_KAKAO_SENDER_KEY'], sms_sender_key: ENV['SENDGO_SMS_SENDER_KEY'], api_version: 'v2' ) # app/services/notification_service.rb class NotificationService def initialize(client = SENDGO_CLIENT) @client = client end def send_order_confirm(phone:, order_no:, amount:) @client.alimtalk.send( template_code: 'ORDER_CONFIRM_001', contacts: [{ contact: phone, var1: order_no, var2: amount }] ) end def send_verification_code(phone:, code:) @client.alimtalk.send( template_code: 'VERIFY_CODE_001', replace_sms: 'Y', sms_content: "[인증] 인증번호: #{code} (5분 이내 입력)", contacts: [{ contact: phone, var1: code }] ) end end # app/models/order.rb class Order < ApplicationRecord after_create :send_confirmation private def send_confirmation NotificationService.new.send_order_confirm( phone: user.phone, order_no: number, amount: "#{total.to_i.to_s(:delimited)}원" ) end end ``` ### Sidekiq 비동기 발송 ```ruby # app/workers/alimtalk_worker.rb class AlimtalkWorker include Sidekiq::Worker sidekiq_options retry: 3 def perform(template_code, contacts) SENDGO_CLIENT.alimtalk.send( template_code: template_code, contacts: contacts ) rescue Sendgo::Error => e logger.error "알림톡 발송 실패: #{e.message} [#{e.error_code}]" raise e unless %w[INVALID_TEMPLATE_CODE PAYMENT_REQUIRED].include?(e.error_code) end end # 사용 AlimtalkWorker.perform_async('ORDER_CONFIRM_001', [ { 'contact' => '01012345678', 'var1' => 'ORD-001' } ]) ``` --- ## 예외 처리 ```ruby require 'sendgo' begin client.alimtalk.send(template_code: 'ORDER_CONFIRM_001', contacts: [...]) rescue Sendgo::Error => e puts "발송 실패: HTTP #{e.status_code} [#{e.error_code}]" case e.error_code when 'INVALID_ACCESS_KEY', 'INVALID_SECRET_KEY' alert_ops('Sendgo 인증키를 확인하세요.') when 'INVALID_TEMPLATE_CODE' Rails.logger.warn("존재하지 않는 템플릿: #{e.message}") when 'PAYMENT_REQUIRED' alert_ops('Sendgo 크레딧이 부족합니다.') when 'IP_NOT_ALLOWED' alert_ops('허용되지 않은 IP') end end ``` --- ## 설정 옵션 | 파라미터 | 타입 | 필수 | 기본값 | 설명 | |---------|------|------|--------|------| | `access_key` | `String` | **필수** | — | Sendgo 액세스 키 | | `secret_key` | `String` | **필수** | — | Sendgo 시크릿 키 | | `kakao_sender_key` | `String` | 선택 | `nil` | 카카오 발신프로필 키 | | `sms_sender_key` | `String` | 선택 | `nil` | SMS 발신자 키 | | `api_version` | `String` | 선택 | `'v1'` | API 버전 (`v1` \| `v2`) | | `base_url` | `String` | 선택 | `'https://sendgo.io'` | API 기본 URL | --- ## 짧은 URL 짧은 URL 은 메시지 본문의 링크를 줄이고, 그 링크가 실제로 눌렸는지 집계합니다. 문자는 바이트 수가 요금과 직결되므로 링크를 줄이면 그만큼 본문을 더 쓸 수 있습니다. 같은 원본 URL 을 다시 줄이면 **기존 링크가 그대로 반환**됩니다. 캠페인별로 반응을 따로 집계하려면 `forceNew` 로 새 코드를 만드세요. `deactivate` 는 링크를 삭제하지 않고 리다이렉트만 중지합니다. 이미 발송한 메시지의 링크를 무효화할 때 쓰며, 누적 통계는 남고 이후 접속은 `410 Gone` 이 됩니다. ```ruby # 짧은 URL 생성 (v2 전용) created = sendgo.short_url.create( target_url: "https://example.com/promotions/summer-sale", title: "여름 세일 랜딩" ) code = created.dig("data", "code") link = created.dig("data", "shortUrl") # 반응 통계 — 일별 추이 + 디바이스/유입경로/국가별 분해 stats = sendgo.short_url.stats(code, from: "2026-08-01") sendgo.short_url.list(count: 10) sendgo.short_url.show(code) sendgo.short_url.deactivate(code) # 리다이렉트만 중지, 통계는 남는다 ``` `stats` 는 일별 추이(`daily`)와 디바이스(`byDevice`)·유입경로(`byReferer`)·국가(`byCountry`)별 분해를 반환합니다. 일별 추이는 사전 집계 표에서 읽으므로 클릭이 많아도 응답 시간이 일정합니다. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ### 1.1.0 (2026-08-11) - 짧은 URL 추가 — `client.short_url` - `HttpClient#delete` 추가 - **버그 수정** — `request()` 가 Get/Post 만 만들어 `:delete` 가 조용히 POST 로 나가고 있었다. `Net::HTTP::Delete` 분기 추가. ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `sendgo` (RubyGems) - **저장소**: [send-go/ruby](https://github.com/send-go/ruby) - **레지스트리**: https://rubygems.org/gems/sendgo - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **Rails에서 카카오 알림톡, 브랜드메시지, SMS를 가장 쉽게 발송하는 공식 Rails 확장 젬** `sendgo-rails`는 [`sendgo`](https://github.com/send-go/ruby) 코어 젬을 확장한 **Rails 전용 확장 젬**입니다. Railtie 자동 등록, `config.sendgo` 설정 바인딩, 초기화 파일 제너레이터, 메모이즈된 클라이언트를 제공합니다. --- ## 설치 Gemfile에 추가합니다. ```ruby gem "sendgo-rails", "~> 1.0" ``` 그리고 설치합니다. ```bash bundle install ``` --- ## 빠른 시작 ### 1단계 — 초기화 파일 생성 ```bash bin/rails g sendgo:install ``` `config/initializers/sendgo.rb` 파일이 생성됩니다. ### 2단계 — 환경변수 설정 (`.env` 또는 배포 환경) ```env SENDGO_ACCESS_KEY=your_access_key SENDGO_SECRET_KEY=your_secret_key SENDGO_KAKAO_SENDER_KEY=your_kakao_key SENDGO_SMS_SENDER_KEY=your_sms_key SENDGO_API_VERSION=v2 ``` ### 3단계 — 초기화 파일 확인 (`config/initializers/sendgo.rb`) ```ruby Rails.application.config.sendgo.tap do |config| config.access_key = ENV["SENDGO_ACCESS_KEY"] config.secret_key = ENV["SENDGO_SECRET_KEY"] config.kakao_sender_key = ENV["SENDGO_KAKAO_SENDER_KEY"] config.sms_sender_key = ENV["SENDGO_SMS_SENDER_KEY"] config.api_version = ENV.fetch("SENDGO_API_VERSION", "v2") config.url = ENV.fetch("SENDGO_URL", "https://sendgo.io") end ``` > 설정값을 지정하지 않으면 동일한 이름의 ENV 환경변수로 자동 폴백합니다. > 즉, 초기화 파일 없이 환경변수만으로도 동작합니다. ### 4단계 — 컨트롤러에서 알림톡 발송 ```ruby class OrdersController < ApplicationController def confirm order = Order.find(params[:id]) Sendgo::Rails.client.alimtalk.send( template_code: "ORDER_CONFIRM_001", contacts: [ { contact: order.user.phone, name: order.user.name, var1: order.number, var2: "#{order.total}원" } ] ) render json: { success: true } end end ``` --- ## 상세 사용법 `Sendgo::Rails.client`는 코어 `Sendgo::Client` 인스턴스를 메모이즈하여 반환합니다. `.alimtalk`, `.friendtalk`, `.sms` 서비스를 그대로 사용할 수 있습니다. ### 알림톡 ```ruby # 다건 발송 Sendgo::Rails.client.alimtalk.send( template_code: "ORDER_CONFIRM_001", contacts: [ { contact: "01011111111", name: "홍길동", var1: "ORD-001", var2: "29,000원" }, { contact: "01022222222", name: "김철수", var1: "ORD-002", var2: "15,000원" }, { contact: "01033333333", name: "이영희", var1: "ORD-003", var2: "52,000원" } ] ) # 예약 발송 Sendgo::Rails.client.alimtalk.send( template_code: "PROMO_SUMMER_2026", schedule_type: "SCHEDULED", at: "2026-07-28 09:00:00", contacts: [{ contact: "01012345678", var1: "여름 한정 50% 할인" }] ) # SMS 자동 대체 발송 Sendgo::Rails.client.alimtalk.send( template_code: "DELIVERY_START_001", replace_sms: "Y", sms_subject: "[배송 시작 안내]", sms_content: "주문하신 상품이 출고되었습니다.", contacts: [{ contact: "01012345678", var1: "ORD-001", var2: "1234567890" }] ) ``` ### 친구톡 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```ruby # 텍스트형 Sendgo::Rails.client.friendtalk.send( content: "안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.", contacts: [{ contact: "01012345678" }] ) # 이미지형 Sendgo::Rails.client.friendtalk.send( message_type: "FI", content: "이번 주 특가 상품을 확인하세요!", image_url: "https://cdn.example.com/banner.jpg", image_link: "https://example.com/event", contacts: [{ contact: "01012345678" }] ) # 버튼 포함 Sendgo::Rails.client.friendtalk.send( content: "7월 쿠폰이 도착했습니다! 지금 바로 사용하세요.", buttons: [{ name: "쿠폰 받기", type: "WL", link_mo: "https://example.com/coupon" }], contacts: [{ contact: "01012345678" }] ) ``` ### SMS / LMS / MMS ```ruby # SMS (90자 이하) Sendgo::Rails.client.sms.send_sms( content: "[Sendgo] 인증번호: 123456 (5분 이내 입력)", contacts: [{ contact: "01012345678" }] ) # LMS (장문, 2,000자 이하) Sendgo::Rails.client.sms.send_lms( subject: "[중요] 서비스 점검 안내", content: "안녕하세요. 서비스 점검이 예정되어 있습니다.\n■ 일시: 2026-07-25 02:00 ~ 06:00", contacts: [{ contact: "01012345678" }] ) # MMS (이미지 포함) Sendgo::Rails.client.sms.send_mms( subject: "[이벤트] 7월 특가", content: "이번 달 특가 상품을 확인하세요!", contacts: [{ contact: "01011111111" }, { contact: "01022222222" }] ) ``` --- ## Active Job 비동기 발송 발송은 외부 API 호출이므로 백그라운드 잡으로 처리하는 것을 권장합니다. ```ruby # app/jobs/send_alimtalk_job.rb class SendAlimtalkJob < ApplicationJob queue_as :notifications retry_on Sendgo::Error, wait: 10.seconds, attempts: 3 def perform(template_code, contacts) Sendgo::Rails.client.alimtalk.send( template_code: template_code, contacts: contacts ) end end # 디스패치 예시 SendAlimtalkJob.perform_later("ORDER_CONFIRM_001", [ { contact: "01012345678", var1: "ORD-001" } ]) ``` 서비스 클래스 패턴도 자연스럽게 사용할 수 있습니다. ```ruby # app/services/notification_service.rb class NotificationService def initialize(client = Sendgo::Rails.client) @client = client end def send_order_confirm(phone:, order_no:, amount:) @client.alimtalk.send( template_code: "ORDER_CONFIRM_001", contacts: [{ contact: phone, var1: order_no, var2: amount }] ) end end ``` --- ## 예외 처리 ```ruby begin Sendgo::Rails.client.alimtalk.send(template_code: "ORDER_CONFIRM_001", contacts: [...]) rescue Sendgo::Error => e Rails.logger.error "Sendgo 발송 실패: HTTP #{e.status_code} [#{e.error_code}]" case e.error_code when "INVALID_ACCESS_KEY", "INVALID_SECRET_KEY" alert_ops("Sendgo 인증키를 확인하세요.") when "INVALID_TEMPLATE_CODE" Rails.logger.warn("존재하지 않는 템플릿: #{e.message}") when "PAYMENT_REQUIRED" alert_ops("Sendgo 크레딧이 부족합니다.") when "IP_NOT_ALLOWED" alert_ops("허용되지 않은 IP") end end ``` --- ## 설정 옵션 `config/initializers/sendgo.rb`에서 `Rails.application.config.sendgo`로 설정합니다. 각 값이 비어 있으면 동일 이름의 환경변수로 폴백합니다. | 키 | 환경변수 | 기본값 | 설명 | |----|---------|--------|------| | `access_key` | `SENDGO_ACCESS_KEY` | — | Sendgo 액세스 키 | | `secret_key` | `SENDGO_SECRET_KEY` | — | Sendgo 시크릿 키 | | `kakao_sender_key` | `SENDGO_KAKAO_SENDER_KEY` | `nil` | 카카오 발신프로필 키 | | `sms_sender_key` | `SENDGO_SMS_SENDER_KEY` | `nil` | SMS 발신자 키 | | `api_version` | `SENDGO_API_VERSION` | `"v2"` | API 버전 | | `url` | `SENDGO_URL` | `"https://sendgo.io"` | API 기본 URL | > 테스트에서 설정을 바꾼 뒤에는 `Sendgo::Rails.reset!`을 호출해 메모이즈된 클라이언트를 초기화하세요. --- ## 자주 묻는 질문 (FAQ) **Q. `sendgo`(코어 젬)와의 차이는 무엇인가요?** A. `sendgo`는 프레임워크 독립적인 순수 Ruby 코어 젬입니다. `sendgo-rails`는 이를 확장해 Railtie 자동 등록, `config.sendgo` 설정 바인딩, 초기화 제너레이터, 메모이즈된 `Sendgo::Rails.client`를 추가합니다. **Q. 어떤 Rails 버전을 지원하나요?** A. `railties >= 6.1`을 지원합니다. (Rails 6.1, 7.x 이상) **Q. 초기화 파일 없이 환경변수만으로 쓸 수 있나요?** A. 네. 설정값이 없으면 `SENDGO_*` 환경변수로 자동 폴백하므로 `bin/rails g sendgo:install` 없이도 동작합니다. **Q. 테스트 시 클라이언트를 초기화하려면?** A. `Sendgo::Rails.reset!`을 호출하면 메모이즈된 클라이언트가 초기화되어 다음 호출 시 다시 생성됩니다. --- ## 브랜드메시지 · 짧은 URL 이 패키지는 코어(`sendgo`)의 클라이언트를 그대로 노출하므로, 코어에 있는 채널이 모두 그대로 쓸 수 있습니다. 두 기능 모두 **v2 전용**입니다. | 기능 | 접근 | |------|------| | 카카오 브랜드메시지 (친구톡의 후속 채널) | `client.brand_message` | | 짧은 URL (단축 + 클릭 반응 분석) | `client.short_url` | 브랜드메시지는 채널 친구가 아닌 수신자에게도 보낼 수 있고(`targeting` = `N`), 수신 동의한 전체 채널 친구에게 동보 발송할 수도 있습니다(`targeting` = `F`). 짧은 URL 은 메시지 본문의 링크를 줄이고 클릭 반응(일별 추이·디바이스·유입경로·국가)을 집계합니다. 사용 예시와 파라미터는 [코어 README](https://github.com/send-go) 와 [SDK 가이드](https://sendgo.io/ko/sdk) 를 참고하세요. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ### 1.1.0 (2026-08-11) - `Sendgo::Rails.client.short_url` 사용법 문서화 ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `sendgo-rails` (RubyGems) - **저장소**: [send-go/rails](https://github.com/send-go/rails) - **레지스트리**: https://rubygems.org/gems/sendgo-rails - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **.NET / ASP.NET Core에서 카카오 알림톡, 브랜드메시지, SMS를 가장 쉽게 발송하는 공식 .NET SDK** `Sendgo.SDK`는 [Sendgo](https://sendgo.io) 알림 API를 위한 공식 .NET SDK입니다. `HttpClient` 기반의 완전한 비동기(`async/await`) 지원, ASP.NET Core DI 통합을 제공합니다. --- ## 설치 ```bash dotnet add package Sendgo.SDK ``` 또는 NuGet Package Manager: ``` Install-Package Sendgo.SDK ``` --- ## 빠른 시작 ### 1단계 — appsettings.json 설정 ```json { "Sendgo": { "AccessKey": "your_access_key", "SecretKey": "your_secret_key", "KakaoSenderKey": "your_kakao_key", "SmsSenderKey": "your_sms_key", "ApiVersion": "v2" } } ``` ### 2단계 — 클라이언트 초기화 ```csharp using Sendgo; using Sendgo.Models; var client = new SendgoClient(new SendgoOptions { AccessKey = Environment.GetEnvironmentVariable("SENDGO_ACCESS_KEY")!, SecretKey = Environment.GetEnvironmentVariable("SENDGO_SECRET_KEY")!, KakaoSenderKey = Environment.GetEnvironmentVariable("SENDGO_KAKAO_KEY"), SmsSenderKey = Environment.GetEnvironmentVariable("SENDGO_SMS_KEY"), ApiVersion = "v2", }); ``` ### 3단계 — 알림톡 전송 ```csharp await client.SendAlimtalkAsync(new AlimtalkRequest { TemplateCode = "ORDER_CONFIRM_001", Contacts = [ new Contact { PhoneNumber = "01012345678", Name = "홍길동", Var1 = "ORD-001", Var2 = "29,000원" } ], }); ``` --- ## 알림톡 상세 사용법 ```csharp using Sendgo; using Sendgo.Models; // 다건 발송 await client.SendAlimtalkAsync(new AlimtalkRequest { TemplateCode = "ORDER_CONFIRM_001", Contacts = [ new Contact { PhoneNumber = "01011111111", Name = "홍길동", Var1 = "ORD-001", Var2 = "29,000원" }, new Contact { PhoneNumber = "01022222222", Name = "김철수", Var1 = "ORD-002", Var2 = "15,000원" }, new Contact { PhoneNumber = "01033333333", Name = "이영희", Var1 = "ORD-003", Var2 = "52,000원" }, ], }); // 예약 발송 await client.SendAlimtalkAsync(new AlimtalkRequest { TemplateCode = "PROMO_SUMMER_2026", ScheduleType = "SCHEDULED", At = "2026-07-28 09:00:00", Contacts = [new Contact { PhoneNumber = "01012345678", Var1 = "여름 한정 50% 할인" }], }); // SMS 자동 대체 발송 await client.SendAlimtalkAsync(new AlimtalkRequest { TemplateCode = "DELIVERY_START_001", ReplaceSms = "Y", SmsSubject = "[배송 시작 안내]", SmsContent = "주문하신 상품이 출고되었습니다.\n송장번호: #{var2}", Contacts = [new Contact { PhoneNumber = "01012345678", Var1 = "ORD-001", Var2 = "1234567890" }], }); ``` --- ## 친구톡 사용법 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```csharp // 텍스트형 await client.SendFriendtalkAsync(new FriendtalkRequest { Content = "안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.", Contacts = [new Contact { PhoneNumber = "01012345678" }], }); // 이미지형 await client.SendFriendtalkAsync(new FriendtalkRequest { MessageType = "FI", Content = "이번 주 특가 상품을 확인하세요!", ImageUrl = "https://cdn.example.com/banner.jpg", ImageLink = "https://example.com/event", Contacts = [new Contact { PhoneNumber = "01012345678" }], }); ``` --- ## 브랜드메시지 사용법 브랜드메시지는 친구톡의 후속 채널입니다. 메시지 타입이 친구톡과 1:1 대응되며 (`FT`→`BT`, `FI`→`BI`, `FW`→`BW`, `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`), 요청에는 **친구톡 코드를 그대로** 넘기고 변환은 서버가 처리합니다. 친구톡과 달리 다음이 가능합니다. - 채널 친구가 **아닌** 수신자에게 발송 (`targeting: N`) - 수신 동의한 **전체 채널 친구 동보** 발송 (`targeting: F`, 수신자 목록 불필요) - 리스트·캐러셀·커머스·동영상 등 **템플릿 기반 리치 메시지** > v2 전용입니다. 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보낼 때는 여전히 친구톡 API 를 쓰세요 — 이 엔드포인트는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. 친구톡 요청은 카카오 측에서 브랜드메시지(자유형)로 대체 발송됩니다. ```csharp using Sendgo.Models; // 단건 발송 — 채널 친구 대상 await client.SendBrandMessageAsync(new BrandMessageRequest { Targeting = "M", MessageType = "FL", FriendTemplateUuid = "9cd5460b-6458-4edc-9b11-c26d3013c340", Contacts = new[] { new Contact { PhoneNumber = "01012345678", Var1 = "29,000원" } }, }); // 동보 발송 — 수신 동의한 전체 채널 친구 (Contacts 불필요) await client.BroadcastBrandMessageAsync(new BrandMessageRequest { MessageType = "FW", FriendTemplateUuid = "9cd5460b-6458-4edc-9b11-c26d3013c340", }); // 캠페인 조회 var list = await client.GetBrandMessagesAsync(count: 10); var one = await client.GetBrandMessageAsync("1f0a6d0e-6b3b-4f0f-9b2f-2f6f6a1b7c11"); ``` --- ## SMS / LMS / MMS 사용법 ```csharp // SMS await client.SendSmsAsync(new SmsRequest { Content = "[Sendgo] 인증번호: 123456 (5분 이내 입력)", Contacts = [new Contact { PhoneNumber = "01012345678" }], }); // LMS await client.SendLmsAsync(new SmsRequest { Subject = "[중요] 서비스 점검 안내", Content = "안녕하세요. 서비스 점검이 예정되어 있습니다.\n■ 일시: 2026-07-25 02:00 ~ 06:00", Contacts = [new Contact { PhoneNumber = "01012345678" }], }); // MMS await client.SendMmsAsync(new SmsRequest { Subject = "[이벤트] 7월 특가", Content = "이번 달 특가 상품을 확인하세요!", Contacts = [ new Contact { PhoneNumber = "01011111111" }, new Contact { PhoneNumber = "01022222222" }, ], }); ``` --- ## ASP.NET Core DI 통합 ```csharp // Program.cs builder.Services.AddSingleton(sp => { var config = sp.GetRequiredService(); return new SendgoClient(config.GetSection("Sendgo").Get()!); }); ``` ```csharp // Services/NotificationService.cs public class NotificationService { private readonly SendgoClient _sendgo; private readonly ILogger _logger; public NotificationService(SendgoClient sendgo, ILogger logger) { _sendgo = sendgo; _logger = logger; } public async Task SendOrderConfirmAsync(string phone, string orderNo, string amount) { await _sendgo.SendAlimtalkAsync(new AlimtalkRequest { TemplateCode = "ORDER_CONFIRM_001", Contacts = [new Contact { PhoneNumber = phone, Var1 = orderNo, Var2 = amount }], }); } public async Task SendVerificationCodeAsync(string phone, string code) { await _sendgo.SendAlimtalkAsync(new AlimtalkRequest { TemplateCode = "VERIFY_CODE_001", ReplaceSms = "Y", SmsContent = $"[인증] 인증번호: {code} (5분 이내 입력)", Contacts = [new Contact { PhoneNumber = phone, Var1 = code }], }); } } ``` ```csharp // Controllers/NotifyController.cs [ApiController] [Route("api/[controller]")] public class NotifyController(SendgoClient sendgo) : ControllerBase { [HttpPost("order")] public async Task Order([FromBody] OrderNotifyRequest req) { await sendgo.SendAlimtalkAsync(new AlimtalkRequest { TemplateCode = "ORDER_CONFIRM_001", Contacts = [new Contact { PhoneNumber = req.Phone, Var1 = req.OrderNo, Var2 = req.Amount }], }); return Ok(new { success = true }); } [HttpPost("sms/verify")] public async Task SendVerification([FromBody] VerifyRequest req) { await sendgo.SendSmsAsync(new SmsRequest { Content = $"[인증] 인증번호: {req.Code} (5분 이내 입력)", Contacts = [new Contact { PhoneNumber = req.Phone }], }); return Ok(new { success = true }); } } ``` --- ## Hangfire 비동기 발송 ```csharp // Jobs/NotificationJobs.cs public class NotificationJobs { private readonly SendgoClient _sendgo; public NotificationJobs(SendgoClient sendgo) => _sendgo = sendgo; [AutomaticRetry(Attempts = 3)] public async Task SendOrderConfirmJob(string phone, string orderNo) { await _sendgo.SendAlimtalkAsync(new AlimtalkRequest { TemplateCode = "ORDER_CONFIRM_001", Contacts = [new Contact { PhoneNumber = phone, Var1 = orderNo }], }); } } // 사용 BackgroundJob.Enqueue(j => j.SendOrderConfirmJob(phone, orderNo)); ``` --- ## 예외 처리 ```csharp using Sendgo.Exceptions; try { await client.SendAlimtalkAsync(new AlimtalkRequest { ... }); } catch (SendgoException ex) { logger.LogError("알림톡 발송 실패: status={Status}, code={Code}", ex.StatusCode, ex.ErrorCode); switch (ex.ErrorCode) { case "INVALID_ACCESS_KEY": case "INVALID_SECRET_KEY": AlertOps("Sendgo 인증키를 확인하세요."); break; case "INVALID_TEMPLATE_CODE": logger.LogWarning("존재하지 않는 템플릿: {Template}", ex.Message); break; case "PAYMENT_REQUIRED": AlertOps("Sendgo 크레딧이 부족합니다."); break; case "IP_NOT_ALLOWED": AlertOps("허용되지 않은 IP에서 요청이 발생했습니다."); break; } } ``` --- ## 설정 옵션 | 프로퍼티 | 타입 | 필수 | 기본값 | 설명 | |---------|------|------|--------|------| | `AccessKey` | `string` | **필수** | — | Sendgo 액세스 키 | | `SecretKey` | `string` | **필수** | — | Sendgo 시크릿 키 | | `KakaoSenderKey` | `string?` | 선택 | `null` | 카카오 발신프로필 키 | | `SmsSenderKey` | `string?` | 선택 | `null` | SMS 발신자 키 | | `ApiVersion` | `string` | 선택 | `"v2"` | API 버전 (`v1` \| `v2`) | | `BaseUrl` | `string` | 선택 | `"https://sendgo.io"` | API 기본 URL | --- ## 짧은 URL 짧은 URL 은 메시지 본문의 링크를 줄이고, 그 링크가 실제로 눌렸는지 집계합니다. 문자는 바이트 수가 요금과 직결되므로 링크를 줄이면 그만큼 본문을 더 쓸 수 있습니다. 같은 원본 URL 을 다시 줄이면 **기존 링크가 그대로 반환**됩니다. 캠페인별로 반응을 따로 집계하려면 `forceNew` 로 새 코드를 만드세요. `deactivate` 는 링크를 삭제하지 않고 리다이렉트만 중지합니다. 이미 발송한 메시지의 링크를 무효화할 때 쓰며, 누적 통계는 남고 이후 접속은 `410 Gone` 이 됩니다. ```csharp // 짧은 URL 생성 (v2 전용) var created = await sendgo.CreateShortUrlAsync(new ShortUrlRequest { TargetUrl = "https://example.com/promotions/summer-sale", Title = "여름 세일 랜딩", }, ct); // 반응 통계 — 일별 추이 + 디바이스/유입경로/국가별 분해 var stats = await sendgo.GetShortUrlStatsAsync(code, from: "2026-08-01", ct: ct); await sendgo.GetShortUrlsAsync(count: 10, ct: ct); await sendgo.GetShortUrlAsync(code, ct); await sendgo.DeactivateShortUrlAsync(code, ct); // 리다이렉트만 중지, 통계는 남는다 ``` `stats` 는 일별 추이(`daily`)와 디바이스(`byDevice`)·유입경로(`byReferer`)·국가(`byCountry`)별 분해를 반환합니다. 일별 추이는 사전 집계 표에서 읽으므로 클릭이 많아도 응답 시간이 일정합니다. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ### 1.1.0 (2026-08-11) - 짧은 URL 추가 — `CreateShortUrlAsync` / `GetShortUrlsAsync` / `GetShortUrlAsync` / `GetShortUrlStatsAsync` / `DeactivateShortUrlAsync` - `ShortUrlRequest` record 추가 - `DeleteAsync` 헬퍼 추가 ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `Sendgo.SDK` (NuGet) - **저장소**: [send-go/dotnet](https://github.com/send-go/dotnet) - **레지스트리**: https://www.nuget.org/packages/Sendgo.SDK - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **ASP.NET Core에서 카카오 알림톡, 브랜드메시지, SMS를 가장 쉽게 발송하는 공식 DI 확장 패키지** `Sendgo.AspNetCore`는 [`Sendgo.SDK`](https://www.nuget.org/packages/Sendgo.SDK) 코어를 확장한 **ASP.NET Core 전용 패키지**입니다. `IServiceCollection` 확장 메서드로 `SendgoClient`를 싱글턴으로 등록하고, `appsettings.json` 또는 코드로 손쉽게 설정 바인딩을 제공합니다. --- ## 설치 ```bash dotnet add package Sendgo.AspNetCore ``` `Sendgo.SDK` 코어가 자동으로 함께 설치됩니다. --- ## 빠른 시작 ### 1단계 — 설정 등록 (`Program.cs`) ```csharp var builder = WebApplication.CreateBuilder(args); // appsettings.json의 "Sendgo" 섹션에서 바인딩 builder.Services.AddSendgo(builder.Configuration.GetSection("Sendgo")); builder.Services.AddControllers(); var app = builder.Build(); ``` ### 2단계 — `appsettings.json` ```json { "Sendgo": { "AccessKey": "your_access_key", "SecretKey": "your_secret_key", "KakaoSenderKey": "your_kakao_key", "SmsSenderKey": "your_sms_key", "ApiVersion": "v2" } } ``` > `BaseUrl`을 지정하지 않으면 기본값 `https://sendgo.io`가 사용됩니다. ### 3단계 — 컨트롤러에서 주입받아 발송 ```csharp using Microsoft.AspNetCore.Mvc; using Sendgo; using Sendgo.Models; [ApiController] [Route("orders")] public class OrderController : ControllerBase { private readonly SendgoClient _sendgo; public OrderController(SendgoClient sendgo) => _sendgo = sendgo; [HttpPost("{orderNo}/confirm")] public async Task Confirm(string orderNo) { await _sendgo.SendAlimtalkAsync(new AlimtalkRequest { TemplateCode = "ORDER_CONFIRM_001", Contacts = [new Contact { PhoneNumber = "01012345678", Var1 = orderNo }], }); return Ok(new { success = true }); } } ``` --- ## 설정 방법 ### appsettings.json 바인딩 호출 측에서 이미 스코프가 지정된 섹션을 전달합니다. ```csharp builder.Services.AddSendgo(builder.Configuration.GetSection("Sendgo")); ``` 환경별 오버라이드는 `appsettings.Development.json`, 환경변수, User Secrets 등 ASP.NET Core의 표준 구성 소스를 그대로 활용할 수 있습니다. ```bash # User Secrets로 민감 정보 관리 (개발 환경 권장) dotnet user-secrets set "Sendgo:AccessKey" "your_access_key" dotnet user-secrets set "Sendgo:SecretKey" "your_secret_key" ``` ### 람다(코드)로 설정 ```csharp builder.Services.AddSendgo(options => { options.AccessKey = builder.Configuration["SENDGO_ACCESS_KEY"]!; options.SecretKey = builder.Configuration["SENDGO_SECRET_KEY"]!; options.KakaoSenderKey = builder.Configuration["SENDGO_KAKAO_KEY"]; options.SmsSenderKey = builder.Configuration["SENDGO_SMS_KEY"]; options.ApiVersion = "v2"; }); ``` 두 방식 모두 `SendgoClient`를 **싱글턴**으로 등록하므로, 생성자에서 `SendgoClient`를 타입힌트로 주입받아 사용할 수 있습니다. --- ## 상세 사용법 ### 알림톡 ```csharp // 다건 발송 await _sendgo.SendAlimtalkAsync(new AlimtalkRequest { TemplateCode = "ORDER_CONFIRM_001", Contacts = [ new Contact { PhoneNumber = "01011111111", Name = "홍길동", Var1 = "ORD-001", Var2 = "29,000원" }, new Contact { PhoneNumber = "01022222222", Name = "김철수", Var1 = "ORD-002", Var2 = "15,000원" }, ], }); ``` ### 친구톡 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```csharp await _sendgo.SendFriendtalkAsync(new { content = "안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.", contacts = new[] { new { contact = "01012345678" } }, }); ``` ### SMS / LMS / MMS ```csharp // SMS (90자 이하) await _sendgo.SendSmsAsync(new SmsRequest { Content = "[Sendgo] 인증번호: 123456 (5분 이내 입력)", Contacts = [new Contact { PhoneNumber = "01012345678" }], }); // LMS (장문, 2,000자 이하) await _sendgo.SendLmsAsync(new SmsRequest { Subject = "[중요] 서비스 점검 안내", Content = "안녕하세요. 서비스 점검이 예정되어 있습니다.", Contacts = [new Contact { PhoneNumber = "01012345678" }], }); // MMS (이미지 포함) await _sendgo.SendMmsAsync(new SmsRequest { Subject = "[이벤트] 7월 특가", Content = "이번 달 특가 상품을 확인하세요!", Contacts = [new Contact { PhoneNumber = "01011111111" }], }); ``` --- ## 서비스 클래스 패턴 `SendgoClient`가 싱글턴으로 등록되어 있으므로, 도메인 서비스에 그대로 주입할 수 있습니다. ```csharp // Services/NotificationService.cs using Sendgo; using Sendgo.Models; public class NotificationService { private readonly SendgoClient _sendgo; private readonly ILogger _logger; public NotificationService(SendgoClient sendgo, ILogger logger) { _sendgo = sendgo; _logger = logger; } public Task SendOrderConfirmAsync(string phone, string orderNo, int amount) => _sendgo.SendAlimtalkAsync(new AlimtalkRequest { TemplateCode = "ORDER_CONFIRM_001", Contacts = [new Contact { PhoneNumber = phone, Var1 = orderNo, Var2 = $"{amount:N0}원" }], }); } ``` ```csharp // Program.cs builder.Services.AddScoped(); ``` --- ## 예외 처리 ```csharp using Sendgo.Exceptions; try { await _sendgo.SendAlimtalkAsync(request); } catch (SendgoException e) { _logger.LogError("Sendgo 발송 실패 (status={Status}, code={Code}, endpoint={Endpoint})", e.StatusCode, e.ErrorCode, e.Endpoint); throw; } ``` --- ## 설정 옵션 `SendgoOptions` (섹션 키: `Sendgo`): | 키 | 기본값 | 설명 | |----|--------|------| | `AccessKey` | — | Sendgo 액세스 키 (필수) | | `SecretKey` | — | Sendgo 시크릿 키 (필수) | | `KakaoSenderKey` | `null` | 카카오 발신프로필 키 | | `SmsSenderKey` | `null` | SMS 발신자 키 | | `ApiVersion` | `v1` | API 버전 (v1 \| v2) | | `BaseUrl` | `https://sendgo.io` | API 기본 URL | --- ## 자주 묻는 질문 (FAQ) **Q. `Sendgo.SDK`와의 차이는 무엇인가요?** A. `Sendgo.SDK`는 프레임워크 독립적인 순수 .NET 코어 패키지입니다. `Sendgo.AspNetCore`는 이를 확장해 `IServiceCollection` 확장 메서드, `appsettings.json` 설정 바인딩 등 ASP.NET Core 통합을 추가합니다. **Q. `SendgoClient`는 어떤 수명(lifetime)으로 등록되나요?** A. 싱글턴으로 등록됩니다. `SendgoClient`는 내부적으로 `HttpClient`와 토큰을 재사용하도록 설계되어 있어 싱글턴이 적합합니다. **Q. DI를 쓰지 않고 직접 생성할 수도 있나요?** A. 네, `new SendgoClient(new SendgoOptions { ... })`로 직접 생성할 수 있습니다. 이 패키지는 그 등록을 자동화할 뿐입니다. **Q. 테스트 시 Sendgo를 Mock 처리하려면?** A. `SendgoClient`는 `sealed`이므로, 도메인 서비스가 의존하는 인터페이스를 별도로 두거나 통합 테스트에서 실제 클라이언트를 사용하는 것을 권장합니다. --- ## 브랜드메시지 · 짧은 URL 이 패키지는 코어(`Sendgo.SDK`)의 클라이언트를 그대로 노출하므로, 코어에 있는 채널이 모두 그대로 쓸 수 있습니다. 두 기능 모두 **v2 전용**입니다. | 기능 | 접근 | |------|------| | 카카오 브랜드메시지 (친구톡의 후속 채널) | `SendBrandMessageAsync()` | | 짧은 URL (단축 + 클릭 반응 분석) | `CreateShortUrlAsync()` | 브랜드메시지는 채널 친구가 아닌 수신자에게도 보낼 수 있고(`targeting` = `N`), 수신 동의한 전체 채널 친구에게 동보 발송할 수도 있습니다(`targeting` = `F`). 짧은 URL 은 메시지 본문의 링크를 줄이고 클릭 반응(일별 추이·디바이스·유입경로·국가)을 집계합니다. 사용 예시와 파라미터는 [코어 README](https://github.com/send-go) 와 [SDK 가이드](https://sendgo.io/ko/sdk) 를 참고하세요. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ### 1.1.0 (2026-08-11) - **`Sendgo.SDK` 의존을 1.1.0 으로 올림** — 1.0.1 로 고정돼 있어 짧은 URL 메서드가 노출되지 않았다. ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `Sendgo.AspNetCore` (NuGet) - **저장소**: [send-go/aspnetcore](https://github.com/send-go/aspnetcore) - **레지스트리**: https://www.nuget.org/packages/Sendgo.AspNetCore - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- > **Flutter / Dart 서버에서 카카오 알림톡, 브랜드메시지, SMS를 발송하는 공식 Dart SDK** > **중요**: 이 패키지는 **서버사이드 전용**입니다 (Dart 백엔드, Shelf, Serverpod 등). > API 키가 클라이언트(Flutter 앱)에 노출되지 않도록 반드시 서버에서만 사용하세요. --- ## 설치 ```yaml # pubspec.yaml dependencies: sendgo_flutter: ^1.1.0 ``` ```bash dart pub get ``` --- ## 빠른 시작 ```dart import 'package:sendgo_flutter/sendgo_flutter.dart'; void main() async { final client = SendgoClient( accessKey: Platform.environment['SENDGO_ACCESS_KEY']!, secretKey: Platform.environment['SENDGO_SECRET_KEY']!, kakaoSenderKey: Platform.environment['SENDGO_KAKAO_SENDER_KEY'], smsSenderKey: Platform.environment['SENDGO_SMS_SENDER_KEY'], apiVersion: 'v2', ); // 알림톡 발송 await client.alimtalk.send( templateCode: 'ORDER_CONFIRM_001', contacts: [ Contact(contact: '01012345678', name: '홍길동', var1: 'ORD-001', var2: '29,000원'), ], ); } ``` --- ## 알림톡 상세 사용법 ```dart import 'package:sendgo_flutter/sendgo_flutter.dart'; final client = SendgoClient( accessKey: Platform.environment['SENDGO_ACCESS_KEY']!, secretKey: Platform.environment['SENDGO_SECRET_KEY']!, kakaoSenderKey: Platform.environment['SENDGO_KAKAO_SENDER_KEY'], smsSenderKey: Platform.environment['SENDGO_SMS_SENDER_KEY'], apiVersion: 'v2', ); // 다건 발송 await client.alimtalk.send( templateCode: 'ORDER_CONFIRM_001', contacts: [ Contact(contact: '01011111111', name: '홍길동', var1: 'ORD-001', var2: '29,000원'), Contact(contact: '01022222222', name: '김철수', var1: 'ORD-002', var2: '15,000원'), Contact(contact: '01033333333', name: '이영희', var1: 'ORD-003', var2: '52,000원'), ], ); // 예약 발송 await client.alimtalk.send( templateCode: 'PROMO_SUMMER_2026', scheduleType: 'SCHEDULED', at: '2026-07-28 09:00:00', contacts: [Contact(contact: '01012345678', var1: '여름 한정 50% 할인')], ); // SMS 자동 대체 발송 await client.alimtalk.send( templateCode: 'DELIVERY_START_001', replaceSms: 'Y', smsSubject: '[배송 시작 안내]', smsContent: '주문하신 상품이 출고되었습니다.\n송장번호: #{var2}', contacts: [Contact(contact: '01012345678', var1: 'ORD-001', var2: '1234567890')], ); ``` --- ## 친구톡 사용법 > ⚠️ **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 친구톡 발송 요청은 카카오 측에서 **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. > 호출은 계속 성공하며, 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보내는 경로는 > 현재 이것뿐이므로 기존 코드를 당장 바꿀 필요는 없습니다. > > 다음의 경우에는 **브랜드메시지**를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응되며 변환은 서버가 처리합니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, > `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. ```dart // 텍스트형 await client.friendtalk.send( content: '안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.', contacts: [Contact(contact: '01012345678')], ); // 이미지형 await client.friendtalk.send( messageType: 'FI', content: '이번 주 특가 상품을 확인하세요!', imageUrl: 'https://cdn.example.com/banner.jpg', imageLink: 'https://example.com/event', contacts: [Contact(contact: '01012345678')], ); ``` --- ## 브랜드메시지 사용법 브랜드메시지는 친구톡의 후속 채널입니다. 메시지 타입이 친구톡과 1:1 대응되며 (`FT`→`BT`, `FI`→`BI`, `FW`→`BW`, `FL`→`BL`, `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`), 요청에는 **친구톡 코드를 그대로** 넘기고 변환은 서버가 처리합니다. 친구톡과 달리 다음이 가능합니다. - 채널 친구가 **아닌** 수신자에게 발송 (`targeting: N`) - 수신 동의한 **전체 채널 친구 동보** 발송 (`targeting: F`, 수신자 목록 불필요) - 리스트·캐러셀·커머스·동영상 등 **템플릿 기반 리치 메시지** > v2 전용입니다. 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 보낼 때는 여전히 친구톡 API 를 쓰세요 — 이 엔드포인트는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. 친구톡 요청은 카카오 측에서 브랜드메시지(자유형)로 대체 발송됩니다. ```dart // 단건 발송 — 채널 친구 대상 await client.brandMessage.send(BrandMessageRequest( targeting: 'M', messageType: 'FL', friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340', contacts: [Contact(contact: '01012345678', var1: '29,000원')], )); // 동보 발송 — 수신 동의한 전체 채널 친구 (contacts 불필요) await client.brandMessage.broadcast(BrandMessageRequest( messageType: 'FW', friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340', )); // 캠페인 조회 final list = await client.brandMessage.campaigns(count: 10); final one = await client.brandMessage.campaign('1f0a6d0e-6b3b-4f0f-9b2f-2f6f6a1b7c11'); ``` --- ## SMS / LMS / MMS 사용법 ```dart // SMS await client.sms.sendSms( content: '[Sendgo] 인증번호: 123456 (5분 이내 입력)', contacts: [Contact(contact: '01012345678')], ); // LMS await client.sms.sendLms( subject: '[중요] 서비스 점검 안내', content: '안녕하세요. 서비스 점검이 예정되어 있습니다.\n■ 일시: 2026-07-25 02:00 ~ 06:00', contacts: [Contact(contact: '01012345678')], ); // MMS await client.sms.sendMms( subject: '[이벤트] 7월 특가', content: '이번 달 특가 상품을 확인하세요!', contacts: [ Contact(contact: '01011111111'), Contact(contact: '01022222222'), ], ); ``` --- ## Shelf 서버 통합 ```dart // bin/server.dart import 'dart:convert'; import 'dart:io'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as io; import 'package:sendgo_flutter/sendgo_flutter.dart'; final sendgo = SendgoClient( accessKey: Platform.environment['SENDGO_ACCESS_KEY']!, secretKey: Platform.environment['SENDGO_SECRET_KEY']!, kakaoSenderKey: Platform.environment['SENDGO_KAKAO_SENDER_KEY'], apiVersion: 'v2', ); Response handler(Request request) async { if (request.url.path == 'api/notify/order' && request.method == 'POST') { final body = jsonDecode(await request.readAsString()); await sendgo.alimtalk.send( templateCode: 'ORDER_CONFIRM_001', contacts: [Contact(contact: body['phone'], var1: body['orderNo'])], ); return Response.ok(jsonEncode({'success': true}), headers: {'content-type': 'application/json'}); } return Response.notFound('Not found'); } void main() async { final server = await io.serve(handler, InternetAddress.anyIPv4, 8080); print('서버 시작: ${server.port}'); } ``` --- ## 예외 처리 ```dart import 'package:sendgo_flutter/sendgo_flutter.dart'; try { await client.alimtalk.send( templateCode: 'ORDER_CONFIRM_001', contacts: [Contact(contact: '01012345678')], ); } on SendgoException catch (e) { print('발송 실패: HTTP ${e.statusCode} [${e.errorCode}]'); switch (e.errorCode) { case 'INVALID_ACCESS_KEY': case 'INVALID_SECRET_KEY': alertOps('Sendgo 인증키를 확인하세요.'); case 'INVALID_TEMPLATE_CODE': logger.warn('존재하지 않는 템플릿: ${e.message}'); case 'PAYMENT_REQUIRED': alertOps('Sendgo 크레딧이 부족합니다.'); case 'IP_NOT_ALLOWED': alertOps('허용되지 않은 IP'); } } ``` --- ## 설정 옵션 | 파라미터 | 타입 | 필수 | 기본값 | 설명 | |---------|------|------|--------|------| | `accessKey` | `String` | **필수** | — | Sendgo 액세스 키 | | `secretKey` | `String` | **필수** | — | Sendgo 시크릿 키 | | `kakaoSenderKey` | `String?` | 선택 | `null` | 카카오 발신프로필 키 | | `smsSenderKey` | `String?` | 선택 | `null` | SMS 발신자 키 | | `apiVersion` | `String` | 선택 | `'v2'` | API 버전 (`v1` \| `v2`) | | `baseUrl` | `String` | 선택 | `'https://sendgo.io'` | API 기본 URL | --- ## 짧은 URL 짧은 URL 은 메시지 본문의 링크를 줄이고, 그 링크가 실제로 눌렸는지 집계합니다. 문자는 바이트 수가 요금과 직결되므로 링크를 줄이면 그만큼 본문을 더 쓸 수 있습니다. 같은 원본 URL 을 다시 줄이면 **기존 링크가 그대로 반환**됩니다. 캠페인별로 반응을 따로 집계하려면 `forceNew` 로 새 코드를 만드세요. `deactivate` 는 링크를 삭제하지 않고 리다이렉트만 중지합니다. 이미 발송한 메시지의 링크를 무효화할 때 쓰며, 누적 통계는 남고 이후 접속은 `410 Gone` 이 됩니다. ```dart // 짧은 URL 생성 (v2 전용) final created = await sendgo.shortUrl.create(const ShortUrlRequest( targetUrl: 'https://example.com/promotions/summer-sale', title: '여름 세일 랜딩', )); final code = created['data']['code'] as String; // 반응 통계 — 일별 추이 + 디바이스/유입경로/국가별 분해 final stats = await sendgo.shortUrl.stats(code, from: '2026-08-01'); await sendgo.shortUrl.list(count: 10); await sendgo.shortUrl.show(code); await sendgo.shortUrl.deactivate(code); // 리다이렉트만 중지, 통계는 남는다 ``` `stats` 는 일별 추이(`daily`)와 디바이스(`byDevice`)·유입경로(`byReferer`)·국가(`byCountry`)별 분해를 반환합니다. 일별 추이는 사전 집계 표에서 읽으므로 클릭이 많아도 응답 시간이 일정합니다. ## 변경 사항 ### 1.2.1 (2026-08-14) - 레지스트리 목록에 노출되는 패키지 설명에서 친구톡을 브랜드메시지로 교체했습니다. npm/PyPI/Packagist/Maven/NuGet/RubyGems 검색 결과에 그대로 찍히는 문자열이라 종료된 채널을 계속 홍보하고 있었습니다. - 검색 키워드에 `brand-message` 를 추가했습니다 (`friendtalk` 은 유입 검색어라 유지). ### 1.2.0 (2026-08-14) - **친구톡 Deprecated 표기** — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었고, 2026-01-01 부터 발송 요청이 브랜드메시지(자유형)로 자동 대체 발송됩니다. 관련 API 에 각 언어의 표준 deprecation 표기를 달았습니다. - 자유 본문 타입(`FT`/`FI`/`FW`)의 개별 발송 경로는 아직 친구톡 API 뿐이라는 점을 문서에 명시했습니다 — 브랜드메시지 API 는 그 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. - 브랜드메시지 전환 안내와 메시지 타입 1:1 대응표를 README 에 추가했습니다. ## 라이선스 MIT License © 2026 [Sendgo](https://sendgo.io) --- ## 패키지 정보 - **패키지**: `sendgo_flutter` (pub.dev) - **저장소**: [send-go/flutter](https://github.com/send-go/flutter) - **레지스트리**: https://pub.dev/packages/sendgo_flutter - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- Sendgo API의 OpenAPI 3.0.3 스펙입니다. 코드 생성기, API 클라이언트, AI 코딩 도구에 그대로 넣어 쓸 수 있는 기계 판독용 계약입니다. - 최신 스펙: - 서버: `https://api.sendgo.io/api` ## 엔드포인트 | 채널 | 메서드 | 경로 | | --- | --- | --- | | 토큰 발급 | `POST` | `/{version}/token` | | 알림톡 (Alimtalk) | `POST` | `/{version}/notices/send` | | 친구톡 (Friendtalk) — **Deprecated** | `POST` | `/{version}/friends/send` | | 브랜드메시지 발송 | `POST` | `/{version}/brand-messages/send` | | 브랜드메시지 캠페인 목록 | `GET` | `/{version}/brand-messages` | | 브랜드메시지 캠페인 상세 | `GET` | `/{version}/brand-messages/{campaign_id}` | | SMS/LMS/MMS | `POST` | `/{version}/messages/send` | `{version}` 은 `v1` 또는 `v2` 입니다. 브랜드메시지는 **v2 전용**입니다. > ⚠️ **친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.** > 2026-01-01 부터 `/{version}/friends/send` 로 들어온 요청은 카카오 측에서 > **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. 호출은 계속 성공하지만 실제로 > 나가는 것은 브랜드메시지입니다. > > 엔드포인트는 제거되지 않습니다 — 자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게 > 보내는 경로는 현재 이것뿐이며, `/{version}/brand-messages/send` 는 같은 조합에 대해 > `NOT_A_BRAND_MESSAGE` 를 반환합니다. > > 다음의 경우에는 브랜드메시지를 사용하세요. > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`) > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`) > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`) > > 메시지 타입은 1:1 대응됩니다 — `FT`→`BT`, `FI`→`BI`, `FW`→`BW`, `FL`→`BL`, > `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`. 변환은 서버가 처리하므로 요청에는 > 친구톡 코드를 그대로 넘깁니다. ## 인증 2단계입니다. 1. **토큰 발급** — `accessKey:secretKey` 를 Base64로 인코딩해 Basic 인증으로 `POST /{version}/token` 호출 2. **API 호출** — 발급받은 토큰으로 Bearer 인증 - v1: `Authorization: Bearer base64(token)` - v2: `Authorization: Bearer token` 토큰은 발급 후 **24시간** 유효합니다. 응답의 `expiresAt`(v2) / `expires_at`(v1) 값을 보고 만료 전에 재발급하세요. ## 사용법 ### 브라우저에서 살펴보기 [Swagger Editor](https://editor.swagger.io)에 `openapi.yaml` 을 붙여넣으면 엔드포인트와 스키마를 시각적으로 탐색할 수 있습니다. ### 클라이언트 코드 생성 ```bash # TypeScript npx @openapitools/openapi-generator-cli generate \ -i https://sendgo.io/openapi.yaml -g typescript-fetch -o ./sendgo-client # Python openapi-generator-cli generate \ -i https://sendgo.io/openapi.yaml -g python -o ./sendgo-client ``` 공식 SDK가 이미 있는 언어라면 생성된 클라이언트보다 SDK를 쓰는 게 낫습니다. SDK는 토큰 캐싱과 만료 시 자동 재발급을 처리해주지만, 생성된 클라이언트는 직접 구현해야 합니다. 지원 언어 목록은 를 참고하세요. ### 스펙 검증 ```bash npx @redocly/cli lint openapi.yaml ``` ## 변경 사항 ### 1.1.0 (2026-08-11) - 짧은 URL 5개 경로 추가 - 토큰 유효시간을 '약 50분' → '24시간' 으로 수정 (실제 구현은 `Carbon::now()->addDay()`) - paths 에서 쓰이는데 선언되지 않았던 `브랜드메시지` 태그 선언 추가 ## 라이선스 MIT © Sendgo — https://sendgo.io --- ## 패키지 정보 - **패키지**: `send-go/openapi` (GitHub) - **저장소**: [send-go/openapi](https://github.com/send-go/openapi) - **레지스트리**: https://github.com/send-go/openapi - **라이선스**: MIT ### API 키 발급 방법 샌드고에 로그인한 뒤 **연동하기 → 연동 정보** 메뉴에서 액세스 키와 시크릿 키를 발급받고, 같은 화면에서 호출을 허용할 IP 를 등록합니다 — 등록되지 않은 주소에서 온 요청은 거부됩니다. 알림톡·브랜드메시지를 사용하려면 **발신프로필 관리** 메뉴에서 발신프로필을 등록해 `kakao_sender_key`를 발급받고, 문자를 사용하려면 **발신번호 관리** 메뉴에서 발신번호를 등록해야 합니다. --- 카카오 알림톡을 처음 보내기까지 필요한 건 다섯 단계입니다. 그중 코드는 마지막 하나뿐이고, 앞의 넷은 **한 번만 하면 되는 계정 설정**입니다. > 급하다면: 계정 설정(1~3단계)이 이미 끝나 있으면 [5단계](#5단계-알림톡-발송)로 바로 가세요. ## 1단계 — 액세스 키와 시크릿 키 발급 [샌드고 콘솔](https://sendgo.io)에 로그인한 뒤 **연동 관리 → 앱**에서 앱을 만들면 `accessKey` 와 `secretKey` 가 발급됩니다. 이 두 값은 계정의 발송 권한 전체를 가집니다. 환경변수에 넣으세요. ```bash # .env SENDGO_ACCESS_KEY=your_access_key SENDGO_SECRET_KEY=your_secret_key SENDGO_KAKAO_SENDER_KEY=your_kakao_sender_key SENDGO_SMS_SENDER_KEY=your_sms_sender_key SENDGO_API_VERSION=v2 ``` 앱에 **허용 IP** 를 걸어두면 키가 유출돼도 다른 곳에서는 쓸 수 없습니다. 서버 IP 가 고정이라면 걸어두는 편이 좋습니다. ## 2단계 — 발신번호와 카카오 발신프로필 등록 한국에서는 **전기통신사업법**상 사전등록된 번호로만 문자를 보낼 수 있습니다. 등록되지 않은 번호는 가입할 때가 아니라 **발송할 때** 실패하므로, 코드를 쓰기 전에 끝내야 합니다. - **발신번호(SMS)**: 콘솔 → 발신번호에서 통신서비스 이용증명원 등을 제출해 등록합니다. 승인까지 영업일 기준 시간이 걸립니다. - **카카오 발신프로필**: 카카오톡 채널을 샌드고에 연결하면 `kakaoSenderKey` 가 발급됩니다. 채널은 카카오 비즈니스에서 **비즈니스 채널**로 전환돼 있어야 합니다. 자세한 절차는 [발신번호 사전등록](/ko/cookbook/sender-number)에 정리돼 있습니다. ## 3단계 — 알림톡 템플릿 등록과 승인 알림톡의 본문은 **미리 승인받은 템플릿**입니다. 발송할 때 하는 일은 템플릿의 변수를 채우는 것뿐입니다. ```text [#{var2}] 주문이 확인되었습니다. 주문번호: #{var1} 결제금액: #{var3} ``` 이런 템플릿을 등록해 `ORDER_CONFIRM_001` 같은 **템플릿 코드**를 받으면 준비가 끝납니다. 심사에서 자주 반려되는 이유와 통과 요령은 [알림톡 템플릿 등록과 심사 통과](/ko/cookbook/alimtalk-template)에 있습니다. > 광고성 문구는 알림톡 템플릿으로 승인되지 않습니다. 정보성만 가능합니다. 홍보 메시지는 [브랜드메시지](/ko/cookbook/send-brand-message)를 쓰세요. ## 4단계 — SDK 설치 쓰고 있는 언어의 공식 패키지를 설치합니다. 프레임워크 패키지(Laravel, Spring, NestJS 등)는 코어를 자동으로 끌어오므로 **둘 다 설치하지 마세요**. ```bash composer require sendgo/laravel # Laravel composer require sendgo/php # 순수 PHP npm install @sendgo/node # Node.js / TypeScript pip install sendgo-python # Python go get github.com/send-go/go # Go gem install sendgo # Ruby dotnet add package Sendgo.SDK # .NET ``` 전체 목록과 선택 기준은 [내 언어에 맞는 SDK 고르기](/ko/cookbook/choose-sdk)에 있습니다. ## 5단계 — 알림톡 발송 클라이언트를 한 번 만들어 두고 재사용합니다. 발신 키는 **클라이언트 생성 시점에** 넣어두면 발송할 때마다 다시 쓰지 않아도 됩니다. ### Node.js / TypeScript ```typescript import Sendgo from '@sendgo/node'; const sendgo = new Sendgo({ accessKey: process.env.SENDGO_ACCESS_KEY!, secretKey: process.env.SENDGO_SECRET_KEY!, kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY, smsSenderKey: process.env.SENDGO_SMS_SENDER_KEY, apiVersion: 'v2', }); await sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [ { contact: '01012345678', name: '홍길동', var1: 'ORD-001', var2: '주문', var3: '29,000원' }, ], }); ``` `@sendgo/node` 는 **기본 내보내기(default export)** 입니다. `import { Sendgo } from '@sendgo/node'` 는 동작하지 않습니다. ### Python ```python import os from sendgo import Sendgo client = Sendgo( access_key=os.environ["SENDGO_ACCESS_KEY"], secret_key=os.environ["SENDGO_SECRET_KEY"], kakao_sender_key=os.environ.get("SENDGO_KAKAO_SENDER_KEY"), sms_sender_key=os.environ.get("SENDGO_SMS_SENDER_KEY"), api_version="v2", ) client.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[ {"contact": "01012345678", "name": "홍길동", "var1": "ORD-001", "var3": "29,000원"} ], ) ``` ### PHP ```php $_ENV['SENDGO_ACCESS_KEY'], 'secret_key' => $_ENV['SENDGO_SECRET_KEY'], 'kakao_sender_key' => $_ENV['SENDGO_KAKAO_SENDER_KEY'], 'sms_sender_key' => $_ENV['SENDGO_SMS_SENDER_KEY'], 'api_version' => 'v2', ]); $sendgo->alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [ ['contact' => '01012345678', 'name' => '홍길동', 'var1' => 'ORD-001', 'var3' => '29,000원'], ], ]); ``` ### Laravel `sendgo/laravel` 은 ServiceProvider 를 자동 등록하므로 컨테이너에서 바로 주입받습니다. ```php sendgo->alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [[ 'contact' => $order->user->phone, 'name' => $order->user->name, 'var1' => $order->number, 'var3' => number_format($order->total).'원', ]], ]); return response()->json(['success' => true]); } } ``` ### Java ```java import io.sendgo.*; import io.sendgo.model.*; import java.util.List; SendgoClient sendgo = new SendgoClient(SendgoConfig.builder() .accessKey(System.getenv("SENDGO_ACCESS_KEY")) .secretKey(System.getenv("SENDGO_SECRET_KEY")) .kakaoSenderKey(System.getenv("SENDGO_KAKAO_SENDER_KEY")) .smsSenderKey(System.getenv("SENDGO_SMS_SENDER_KEY")) .apiVersion("v2") .build()); sendgo.alimtalk().send(AlimtalkRequest.builder() .templateCode("ORDER_CONFIRM_001") .contacts(List.of( Contact.builder().contact("01012345678").name("홍길동").var1("ORD-001").build() )) .build()); ``` ### Go ```go package main import ( "log" "os" sendgo "github.com/send-go/go/sendgo" ) func main() { client, err := sendgo.New(sendgo.Config{ AccessKey: os.Getenv("SENDGO_ACCESS_KEY"), SecretKey: os.Getenv("SENDGO_SECRET_KEY"), KakaoSenderKey: os.Getenv("SENDGO_KAKAO_SENDER_KEY"), SmsSenderKey: os.Getenv("SENDGO_SMS_SENDER_KEY"), ApiVersion: "v2", }) if err != nil { log.Fatal(err) } _, err = client.Alimtalk.Send(sendgo.AlimtalkRequest{ TemplateCode: "ORDER_CONFIRM_001", Contacts: []sendgo.Contact{ {Contact: "01012345678", Name: "홍길동", Var1: "ORD-001"}, }, }) if err != nil { log.Fatal(err) } } ``` ## 첫 발송이 실패했다면 | 오류 코드 | 뜻 | 할 일 | | --- | --- | --- | | `INVALID_ACCESS_KEY` | 액세스 키가 틀렸다 | 환경변수가 실제로 로드됐는지 확인 | | `ACCESS_KEY_NOT_APPROVED` | 앱이 아직 승인되지 않았다 | 콘솔에서 앱 승인 상태 확인 | | `IP_NOT_ALLOWED` | 허용 IP 밖에서 호출했다 | 앱의 허용 IP 목록에 서버 IP 추가 | | `INVALID_TEMPLATE_CODE` | 없는 템플릿 코드 | 콘솔의 템플릿 코드와 대조 (승인 완료 상태여야 함) | | `INVALID_KAKAO_SENDER_KEY` | 카카오 발신프로필 키가 틀렸다 | 콘솔 → 카카오 발신프로필에서 키 재확인 | | `EMPTY_CONTACTS` | 수신자가 비었다 | `contacts` 배열이 실제로 채워졌는지 확인 | | `PAYMENT_REQUIRED` | 크레딧 부족 | 충전 후 재시도 (재시도만으로는 해결되지 않음) | 전체 목록과 재시도 전략은 [오류 코드와 재시도 전략](/ko/cookbook/error-handling)에 있습니다. ## 다음 단계 - 수신자마다 다른 값을 넣어 한 번에 보내기 → [대량 발송과 치환 변수](/ko/cookbook/bulk-send) - 알림톡이 실패하면 문자로 대신 보내기 → [SMS 대체 발송](/ko/cookbook/sms-fallback) - 정해진 시각에 보내기 → [예약 발송](/ko/cookbook/scheduled-send) - 템플릿 없이 자유 본문 보내기 → [SMS·LMS·MMS 보내기](/ko/cookbook/send-sms) --- 세 채널은 **보낼 수 있는 내용**이 다릅니다. 비용이나 도달률보다 이 제약이 먼저입니다. ## 한 장 요약 | | 카카오 알림톡 | 카카오 브랜드메시지 | SMS · LMS · MMS | | --- | --- | --- | --- | | 보낼 수 있는 내용 | **정보성만** | 정보성 + **광고 가능** | 정보성 + **광고 가능** | | 본문 | 사전 승인 템플릿 | 사전 등록 템플릿(리치 타입) | **자유 작성** | | 수신 대상 | 전화번호 아는 누구나 | 채널 친구 / 비친구 / 전체 동보 | 전화번호 아는 누구나 | | 사전 준비 | 카카오 발신프로필 + **템플릿 심사** | 카카오 발신프로필 + 템플릿 등록 | **발신번호 등록**만 | | 도달 조건 | 수신자가 카카오톡 사용 | 수신자가 카카오톡 사용 | 통신망, 사실상 전부 | | 상대 단가 | 가장 낮음 | 중간 | 가장 높음 | | 엔드포인트 | `/api/v2/notices/send` | `/api/v2/brand-messages/send` | `/api/v2/messages/send` | ## 상황별로 고르기 ```text 보내려는 내용이 광고·홍보인가? ├─ 예 → 카카오 채널 친구 기반으로 보내고 싶은가? │ ├─ 예 → 브랜드메시지 (adFlag: Y) │ └─ 아니오 → 광고 문자 (SMS/LMS) — (광고) 표기 + 수신거부 필수 │ ※ 어느 쪽이든 21시~익일 08시 발송 금지 │ └─ 아니오(정보성) → 같은 형태가 반복되는 알림인가? ├─ 예 → 알림톡 (템플릿 심사 필요, 가장 저렴) │ + 반드시 도달해야 하면 replaceSms: Y └─ 아니오 → 문자 (인증번호·일회성 안내, 템플릿 불필요) ``` ### 주문 확인, 배송 안내, 예약 확인 **알림톡.** 문구가 정형화돼 있고 반복되므로 템플릿 심사를 한 번 통과시켜 두면 단가가 가장 낮습니다. 도달이 중요하니 [SMS 대체 발송](/ko/cookbook/sms-fallback)을 함께 켜세요. ### 인증번호 **문자(SMS).** 알림톡으로도 가능하지만, 인증번호는 즉시 도달이 생명이고 수신자가 카카오톡을 쓰는지 알 수 없습니다. 문자가 단순하고 확실합니다. 실패를 삼키지 말고 사용자에게 알려야 합니다. ### 할인·이벤트·쿠폰 홍보 **브랜드메시지** 또는 **광고 문자.** 알림톡 템플릿으로는 승인되지 않습니다. 카카오 채널을 운영 중이라면 브랜드메시지가 표현력(리스트·캐러셀·커머스)과 단가 면에서 유리합니다. ### 서비스 점검 공지처럼 긴 안내문 **LMS.** 2,000바이트까지 자유 작성이고 제목을 붙일 수 있습니다. 알림톡 템플릿으로 만들기엔 매번 문안이 달라 재심사가 걸립니다. ### 채널 친구가 아닌 사람에게 홍보 **브랜드메시지 `targeting: N`.** 친구톡으로는 불가능했던 것이고, 광고 문자보다 표현력이 좋습니다. ## 자주 하는 오판 - **"알림톡이 싸니까 홍보도 알림톡으로."** 승인되지 않습니다. 우회하려 하면 반려되고, 반복되면 채널에 제재가 갑니다. - **"문자는 옛날 방식이니 전부 알림톡으로."** 알림톡은 카카오톡 사용자에게만 도달합니다. 대체 발송 없이 전환하면 일정 비율이 조용히 사라집니다. - **"친구톡을 쓰면 되지."** [2025-12-31 종료](/ko/cookbook/friendtalk-sunset)되었습니다. 신규 개발은 브랜드메시지입니다. - **"템플릿 심사는 금방 되겠지."** 영업일이 걸립니다. [발신번호 등록](/ko/cookbook/sender-number)과 함께 가장 먼저 시작해야 하는 일입니다. ## 섞어 쓰기 실무에서는 보통 셋 다 씁니다. ```php alimtalk->send([ 'templateCode' => config('sendgo.templates.order_confirm'), 'replaceSms' => 'Y', 'smsSubject' => '[주문 확인]', 'smsContent' => '주문 #{var1} 이 확인되었습니다.', 'contacts' => [['contact' => $phone, 'var1' => $orderNo]], ]); // 인증번호 — 문자 $sendgo->sms->sendSms([ 'content' => "[서비스명] 인증번호: {$code} (5분 이내 입력)", 'contacts' => [['contact' => $phone]], ]); // 가을 세일 — 브랜드메시지 (광고, 야간 금지 확인 후) $sendgo->brandMessage->send([ 'targeting' => 'M', 'messageType' => 'FL', 'friendTemplateUuid' => config('sendgo.templates.autumn_sale'), 'adFlag' => 'Y', 'contacts' => $optedInContacts, ]); ``` ## 다음 단계 - [카카오 알림톡 보내기](/ko/cookbook/send-alimtalk) - [SMS · LMS · MMS 보내기](/ko/cookbook/send-sms) - [브랜드메시지 보내기](/ko/cookbook/send-brand-message) --- 샌드고는 **20개**의 공식 패키지를 제공합니다. 고르는 기준은 간단합니다. 1. 프레임워크 전용 패키지가 있으면 그걸 쓴다 (설정과 DI 가 이미 붙어 있다). 2. 없으면 해당 언어의 코어 패키지를 쓴다. 3. 프론트엔드·모바일에서는 **직접 호출하지 않고** 서버를 거친다. ## 서버 코어 SDK 프레임워크에 묶이지 않은 순수 클라이언트입니다. 어디서든 동작합니다. | 언어 | 패키지 | 설치 | 가이드 | | --- | --- | --- | --- | | PHP | `sendgo/php` | `composer require sendgo/php` | [PHP](/ko/sdk/php) | | Node.js / TypeScript | `@sendgo/node` | `npm install @sendgo/node` | [Node.js](/ko/sdk/node) | | Python | `sendgo-python` | `pip install sendgo-python` | [Python](/ko/sdk/python) | | Go | `github.com/send-go/go` | `go get github.com/send-go/go` | [Go](/ko/sdk/go) | | Java | `io.sendgo:sendgo-java` | Maven Central | [Java](/ko/sdk/java) | | Ruby | `sendgo` | `gem install sendgo` | [Ruby](/ko/sdk/ruby) | | C# / .NET | `Sendgo.SDK` | `dotnet add package Sendgo.SDK` | [.NET](/ko/sdk/dotnet) | ## 프레임워크 확장 코어 위에 설정 파일, 서비스 등록, 의존성 주입을 얹은 패키지입니다. **코어를 따로 설치할 필요 없습니다.** | 프레임워크 | 패키지 | 얹혀 있는 코어 | 얻는 것 | | --- | --- | --- | --- | | Laravel | `sendgo/laravel` | `sendgo/php` | ServiceProvider 자동 등록, Facade, config 게시, Notification 채널 | | Symfony | `sendgo/symfony` | `sendgo/php` | Bundle, DI Extension, `services.yaml` 설정 | | WordPress / WooCommerce | `sendgo/wordpress` | `sendgo/php` | 설정 화면, 주문 상태 훅 | | NestJS | `@sendgo/nestjs` | `@sendgo/node` | `SendgoModule.forRoot()`, `SendgoService` | | Django | `sendgo-django` | `sendgo-python` | `settings.SENDGO`, 앱 로딩 시 초기화 | | FastAPI | `sendgo-fastapi` | `sendgo-python` | `SendgoDep` 의존성 주입, pydantic 설정 | | Spring Boot | `io.sendgo:sendgo-spring` | `io.sendgo:sendgo-java` | 자동 구성, `application.yml` 바인딩 | | Ruby on Rails | `sendgo-rails` | `sendgo` | Railtie, `sendgo:install` 제너레이터 | | ASP.NET Core | `Sendgo.AspNetCore` | `Sendgo.SDK` | `services.AddSendgo(...)` DI | ## 프론트엔드 · 모바일 이 패키지들은 **서버 경유**를 전제로 만들어졌습니다. 키를 클라이언트 번들에 넣지 마세요. | 대상 | 패키지 | 권장 사용법 | | --- | --- | --- | | React / Next.js | `@sendgo/react` | Next.js Route Handler(서버)에서 `@sendgo/node` 호출, 클라이언트는 그 라우트만 호출 | | Vue / Nuxt | `@sendgo/vue` | Nuxt server route 에서 호출 | | Flutter / Dart | `sendgo_flutter` | 자체 백엔드를 경유. 앱에 키를 넣지 않는다 | ## 어느 것도 맞지 않는다면 REST API 를 직접 호출하세요. OpenAPI 3.0.3 스펙이 공개돼 있습니다. ```bash curl -O https://sendgo.io/openapi.yaml openapi-generator generate -i openapi.yaml -g rust -o ./sendgo-rust ``` ## 선택 요약 ```text PHP 프로젝트인가? ├─ Laravel → sendgo/laravel ├─ Symfony → sendgo/symfony ├─ WordPress → sendgo/wordpress └─ 그 외 → sendgo/php JS/TS 프로젝트인가? ├─ NestJS → @sendgo/nestjs ├─ Next.js → @sendgo/react (+ 서버에서 @sendgo/node) ├─ Nuxt → @sendgo/vue (+ 서버에서 @sendgo/node) └─ 그 외 → @sendgo/node Python 프로젝트인가? ├─ Django → sendgo-django ├─ FastAPI → sendgo-fastapi └─ 그 외 → sendgo-python Java? → Spring Boot 면 sendgo-spring, 아니면 sendgo-java Ruby? → Rails 면 sendgo-rails, 아니면 sendgo .NET? → ASP.NET Core 면 Sendgo.AspNetCore, 아니면 Sendgo.SDK Go? → github.com/send-go/go Flutter? → sendgo_flutter (서버 경유) 그 외? → https://sendgo.io/openapi.yaml 로 클라이언트 생성 ``` 설치를 끝냈다면 [5분 만에 첫 알림톡 발송하기](/ko/cookbook/quickstart)로 이어집니다. --- 샌드고 인증은 두 단계입니다. **키로 토큰을 받고, 토큰으로 발송한다.** 거의 모든 경우 이 문서를 읽을 필요가 없습니다 — 공식 SDK 가 전부 대신 처리합니다. REST 를 직접 호출하거나, SDK 가 없는 언어에서 클라이언트를 만들 때만 필요합니다. ## 준비물 샌드고 콘솔 → **연동 관리 → 앱**에서 발급받은 `accessKey` 와 `secretKey`. ```bash export SENDGO_ACCESS_KEY=your_access_key export SENDGO_SECRET_KEY=your_secret_key ``` 두 값은 계정의 발송 권한 전체를 가집니다. 저장소에 커밋하지 마세요. 실수로 커밋했다면 콘솔에서 **폐기 후 재발급**해야 합니다 — 커밋을 되돌리는 것만으로는 노출이 사라지지 않습니다. ## 토큰 발급 `accessKey:secretKey` 를 Base64 로 인코딩해 Basic 인증 헤더로 보냅니다. ```bash curl -X POST https://sendgo.io/api/v2/token \ -H "Authorization: Basic $(printf '%s:%s' "$SENDGO_ACCESS_KEY" "$SENDGO_SECRET_KEY" | base64)" ``` ```json { "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } } ``` ## 발급받은 토큰으로 호출 이후 모든 요청은 Bearer 인증을 씁니다. **v1 과 v2 의 값이 다릅니다.** | 버전 | 헤더 | | --- | --- | | v1 | `Authorization: Bearer base64(token)` | | v2 | `Authorization: Bearer token` | v1 에서 토큰을 한 번 더 인코딩하지 않으면 401 이 돌아옵니다. 신규 연동은 **v2** 를 쓰세요. ```bash curl -X POST https://sendgo.io/api/v2/notices/send \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "templateCode": "ORDER_CONFIRM_001", "scheduleType": "DIRECTLY", "kakaoSenderKey": "your_kakao_sender_key", "senderKey": "your_sms_sender_key", "contacts": [{ "contact": "01012345678", "var1": "ORD-001" }] }' ``` ## SDK 를 쓸 때 키만 넘기면 끝입니다. 토큰 발급, 캐싱, 만료 시 재발급, 401/403 재시도가 모두 안에서 처리됩니다. ```typescript import Sendgo from '@sendgo/node'; const sendgo = new Sendgo({ accessKey: process.env.SENDGO_ACCESS_KEY!, secretKey: process.env.SENDGO_SECRET_KEY!, apiVersion: 'v2', }); // 토큰 관련 코드는 없습니다. 첫 발송 때 알아서 받아옵니다. ``` ```python from sendgo import Sendgo client = Sendgo( access_key=os.environ["SENDGO_ACCESS_KEY"], secret_key=os.environ["SENDGO_SECRET_KEY"], api_version="v2", ) ``` ```php $_ENV['SENDGO_ACCESS_KEY'], 'secret_key' => $_ENV['SENDGO_SECRET_KEY'], 'api_version' => 'v2', ]); ``` ```go client, err := sendgo.New(sendgo.Config{ AccessKey: os.Getenv("SENDGO_ACCESS_KEY"), SecretKey: os.Getenv("SENDGO_SECRET_KEY"), ApiVersion: "v2", }) ``` **클라이언트는 한 번 만들어 재사용하세요.** 요청마다 새로 만들면 캐시된 토큰이 버려지고 매번 토큰을 새로 발급받게 됩니다. Laravel·Spring·NestJS 확장은 싱글턴으로 등록하므로 이 문제가 없습니다. ## 인증 오류 | 코드 | HTTP | 원인 | | --- | --- | --- | | `INVALID_ACCESS_KEY` | 401 | 액세스 키가 존재하지 않거나 시크릿이 틀림 | | `ACCESS_KEY_NOT_APPROVED` | 403 | 앱이 아직 승인 대기 상태 | | `IP_NOT_ALLOWED` | 403 | 앱에 설정한 허용 IP 밖에서 호출 | `IP_NOT_ALLOWED` 는 로컬에서 개발할 때 자주 만납니다. 개발 환경에서는 허용 IP 를 비워 두거나 개발자 IP 를 추가하세요. 서버 IP 는 배포 후 실제 아웃바운드 IP 로 확인해야 합니다 — NAT 게이트웨이나 로드밸런서 뒤라면 인스턴스 IP 와 다릅니다. ## 다음 단계 - [5분 만에 첫 알림톡 발송하기](/ko/cookbook/quickstart) - [오류 코드와 재시도 전략](/ko/cookbook/error-handling) --- 한국에서 문자·알림톡을 보내려면 **발신번호를 미리 등록**해야 합니다. 선택이 아니라 법령 요구사항입니다(전기통신사업법 제84조의2, 전화번호 거짓표시 금지). 이 절차가 연동 일정에서 가장 자주 병목이 됩니다. 서류 검토에 시간이 걸리는데, 많은 팀이 코드를 다 짜고 나서야 시작하기 때문입니다. **가장 먼저 시작하세요.** ## 두 종류의 발신 키 | 키 | 무엇을 가리키나 | 어디에 쓰이나 | | --- | --- | --- | | `senderKey` | 등록된 **문자 발신번호** | SMS · LMS · MMS, 알림톡의 SMS 대체 발송 | | `kakaoSenderKey` | 연결된 **카카오톡 채널**(발신프로필) | 알림톡, 브랜드메시지 | 알림톡만 보낼 거라면 `kakaoSenderKey` 만 있어도 되지만, [SMS 대체 발송](/ko/cookbook/sms-fallback)을 켜는 순간 `senderKey` 도 필요합니다. 실무에서는 대체 발송을 켜는 경우가 대부분이므로 **둘 다 준비**하는 편이 낫습니다. ## 문자 발신번호 등록 1. 샌드고 콘솔 → **발신번호** → 등록 2. 등록할 번호와 명의 정보 입력 3. 증빙 서류 제출 - 통신서비스 이용증명원 (해당 번호가 신청인 명의임을 증명) - 사업자등록증 (법인 명의인 경우) - 위임장 (타인 명의 번호를 쓰는 경우) 4. 검토 후 승인 **자주 막히는 지점** - 증명원의 명의와 계정의 사업자 정보가 다르면 반려됩니다. 지사·계열사 번호를 본사 계정으로 등록할 때 자주 발생합니다. - 증명원은 발급일이 최근이어야 합니다. 몇 년 전 서류는 다시 발급받으세요. - 대표번호(1588, 1600 등)는 회선 명의 확인이 추가로 필요할 수 있습니다. ## 카카오 발신프로필 연결 알림톡은 문자 발신번호와 별개로, **카카오톡 채널**이 있어야 합니다. 1. [카카오 비즈니스](https://business.kakao.com)에서 카카오톡 채널 개설 2. 채널을 **비즈니스 채널**로 전환 (사업자 인증 필요) 3. 샌드고 콘솔 → **카카오 발신프로필** → 등록 4. 채널 검색용 아이디 입력 → 관리자 휴대폰으로 인증 5. 발급된 `kakaoSenderKey` 를 환경변수에 저장 ```bash SENDGO_KAKAO_SENDER_KEY=your_kakao_sender_key SENDGO_SMS_SENDER_KEY=your_sms_sender_key ``` **자주 막히는 지점** - **일반 채널은 안 됩니다.** 비즈니스 채널로 전환하지 않으면 발신프로필을 만들 수 없습니다. - 채널 관리자 권한이 있는 카카오 계정으로 인증해야 합니다. - 채널 검색용 아이디는 채널명이 아니라 `@` 로 시작하는 식별자입니다. ## 등록 전에 발송하면 발신번호가 등록되지 않은 상태에서 호출하면 **가입 시점이 아니라 발송 시점에** 실패합니다. | 오류 코드 | 원인 | | --- | --- | | `INVALID_KAKAO_SENDER_KEY` | 카카오 발신프로필 키가 존재하지 않거나 이 계정 소유가 아님 | | `SENDER_NOT_REGISTERED` 계열 | 문자 발신번호가 미등록 또는 승인 대기 | 콘솔에서 상태가 **승인 완료**인지 확인하세요. "검토 중" 상태의 번호로는 발송되지 않습니다. ## 다음 단계 - [5분 만에 첫 알림톡 발송하기](/ko/cookbook/quickstart) - [알림톡 템플릿 등록과 심사 통과](/ko/cookbook/alimtalk-template) --- 알림톡은 **승인된 템플릿에 변수를 채워 보내는** 방식입니다. 본문을 코드에서 만드는 게 아니라, 이미 심사를 통과한 문안의 빈칸만 채웁니다. ```text [#{var2}] 주문이 확인되었습니다. 주문번호: #{var1} 결제금액: #{var3} ``` 이 템플릿이 `ORDER_CONFIRM_001` 로 승인돼 있다면, 코드는 `var1`, `var2`, `var3` 만 넘기면 됩니다. ## 준비물 - 승인 완료된 **템플릿 코드** → [알림톡 템플릿 등록과 심사 통과](/ko/cookbook/alimtalk-template) - **카카오 발신프로필 키**(`kakaoSenderKey`) → [발신번호 사전등록](/ko/cookbook/sender-number) - 액세스 키 / 시크릿 키 ## 필수 파라미터 `POST /api/v2/notices/send` | 파라미터 | 필수 | 설명 | | --- | --- | --- | | `templateCode` | ✅ | 승인된 템플릿 코드 | | `contacts` | ✅ | 수신자 배열. 비어 있으면 `EMPTY_CONTACTS` | | `contacts[].contact` | ✅ | 수신 번호. **하이픈 없이 숫자만** (`01012345678`) | | `contacts[].name` | | 수신자 이름 | | `contacts[].var1`~`var8` | | 템플릿 변수 | | `kakaoSenderKey` | ✅ | 카카오 발신프로필 키 (클라이언트에 넣어두면 생략 가능) | | `senderKey` | | 문자 발신번호 키. SMS 대체 발송을 켤 때 필요 | | `scheduleType` | | `DIRECTLY`(기본) 또는 `SCHEDULED` | | `at` | | 예약 시각 `Y-m-d H:i:s`. `scheduleType: SCHEDULED` 일 때 | | `replaceSms` | | `Y` 면 실패 시 SMS 대체 발송 | | `smsSubject` / `smsContent` | | 대체 발송 본문. `replaceSms: Y` 면 필수 | ## 언어별 예제 ### Node.js / TypeScript ```typescript import Sendgo from '@sendgo/node'; const sendgo = new Sendgo({ accessKey: process.env.SENDGO_ACCESS_KEY!, secretKey: process.env.SENDGO_SECRET_KEY!, kakaoSenderKey: process.env.SENDGO_KAKAO_SENDER_KEY, smsSenderKey: process.env.SENDGO_SMS_SENDER_KEY, apiVersion: 'v2', }); await sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [{ contact: '01012345678', name: '홍길동', var1: 'ORD-001', var2: '맥북 프로', var3: '3,490,000원', }], }); ``` ### Python ```python from sendgo import Sendgo client = Sendgo( access_key=os.environ["SENDGO_ACCESS_KEY"], secret_key=os.environ["SENDGO_SECRET_KEY"], kakao_sender_key=os.environ.get("SENDGO_KAKAO_SENDER_KEY"), sms_sender_key=os.environ.get("SENDGO_SMS_SENDER_KEY"), api_version="v2", ) client.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[ {"contact": "01012345678", "name": "홍길동", "var1": "ORD-001", "var2": "맥북 프로"} ], ) ``` Django 라면 `sendgo-django`, FastAPI 라면 `sendgo-fastapi` 를 쓰면 설정과 주입이 붙어 있습니다. ### PHP ```php $_ENV['SENDGO_ACCESS_KEY'], 'secret_key' => $_ENV['SENDGO_SECRET_KEY'], 'kakao_sender_key' => $_ENV['SENDGO_KAKAO_SENDER_KEY'], 'sms_sender_key' => $_ENV['SENDGO_SMS_SENDER_KEY'], 'api_version' => 'v2', ]); $sendgo->alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [ ['contact' => '01012345678', 'name' => '홍길동', 'var1' => 'ORD-001', 'var2' => '29,000원'], ], ]); ``` ### Laravel ```php sendgo->alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [[ 'contact' => $order->user->phone, 'name' => $order->user->name, 'var1' => $order->number, 'var2' => $order->items->first()->name, 'var3' => number_format($order->total).'원', ]], ]); } catch (SendgoException $e) { // 발송 실패가 주문 처리를 막지 않도록 로깅만 하고 넘어간다. Log::error('알림톡 발송 실패', ['order' => $order->id, 'message' => $e->getMessage()]); } } } ``` 발송은 **큐에 넣는 편이 낫습니다.** 외부 API 호출이 HTTP 응답 시간에 들어가면 카카오 쪽이 느릴 때 사용자 요청까지 함께 느려집니다. ### Java / Spring Boot ```java import io.sendgo.*; import io.sendgo.model.*; import java.util.List; sendgo.alimtalk().send(AlimtalkRequest.builder() .templateCode("ORDER_CONFIRM_001") .contacts(List.of( Contact.builder() .contact("01012345678") .name("홍길동") .var1("ORD-001") .var2("29,000원") .build() )) .build()); ``` ### Go ```go result, err := client.Alimtalk.Send(sendgo.AlimtalkRequest{ TemplateCode: "ORDER_CONFIRM_001", Contacts: []sendgo.Contact{ {Contact: "01012345678", Name: "홍길동", Var1: "ORD-001", Var2: "29,000원"}, }, }) if err != nil { log.Printf("알림톡 발송 실패: %v", err) } ``` ### Ruby ```ruby client.alimtalk.send( template_code: 'ORDER_CONFIRM_001', contacts: [ { contact: '01012345678', name: '홍길동', var1: 'ORD-001', var2: '29,000원' } ] ) ``` ### C# / .NET ```csharp await client.SendAlimtalkAsync(new AlimtalkRequest { TemplateCode = "ORDER_CONFIRM_001", Contacts = [ new Contact { PhoneNumber = "01012345678", Name = "홍길동", Var1 = "ORD-001" } ], }); ``` ### REST 직접 호출 ```bash curl -X POST https://sendgo.io/api/v2/notices/send \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "templateCode": "ORDER_CONFIRM_001", "scheduleType": "DIRECTLY", "replaceSms": "N", "kakaoSenderKey": "your_kakao_sender_key", "senderKey": "your_sms_sender_key", "contacts": [ { "contact": "01012345678", "name": "홍길동", "var1": "ORD-001" } ] }' ``` ## 발송이 실패하는 이유 | 오류 코드 | 원인 | 재시도로 해결되나 | | --- | --- | --- | | `INVALID_TEMPLATE_CODE` | 없는 코드이거나 아직 승인되지 않음 | ❌ | | `INVALID_KAKAO_SENDER_KEY` | 발신프로필 키가 틀림 | ❌ | | `EMPTY_CONTACTS` | 수신자 배열이 비어 있음 | ❌ | | `PAYMENT_REQUIRED` | 크레딧 부족 | ❌ (충전 필요) | | `ACCESS_KEY_NOT_APPROVED` | 앱 미승인 | ❌ | | `IP_NOT_ALLOWED` | 허용 IP 밖 | ❌ | | 네트워크 타임아웃 | 일시적 | ✅ | 거의 모든 실패는 **재시도해도 똑같이 실패**합니다. 무한 재시도 대신 로깅하고 넘어가세요. 자세한 전략은 [오류 코드와 재시도 전략](/ko/cookbook/error-handling)에 있습니다. ## 놓치기 쉬운 것 - **전화번호에 하이픈을 넣지 마세요.** `010-1234-5678` 이 아니라 `01012345678` 입니다. - **템플릿 변수를 빠뜨리면 치환되지 않은 채 발송됩니다.** 템플릿에 `#{var4}` 가 있는데 `var4` 를 안 넘기면 수신자가 그 문자열을 그대로 봅니다. - **광고 문구는 알림톡으로 나가지 않습니다.** 정보성만 승인됩니다. 홍보는 [브랜드메시지](/ko/cookbook/send-brand-message)입니다. - **클라이언트를 요청마다 새로 만들지 마세요.** 캐시된 토큰이 버려집니다. ## 다음 단계 - [대량 발송과 치환 변수](/ko/cookbook/bulk-send) - [SMS 대체 발송](/ko/cookbook/sms-fallback) - [예약 발송](/ko/cookbook/scheduled-send) --- 문자는 **템플릿 승인이 필요 없습니다.** 본문을 코드에서 자유롭게 만들 수 있어서, 알림톡 템플릿으로 커버되지 않는 일회성 안내나 인증번호에 적합합니다. ## 준비물 - 사전등록된 **문자 발신번호**(`senderKey`) → [발신번호 사전등록](/ko/cookbook/sender-number) - 액세스 키 / 시크릿 키 카카오 발신프로필은 필요 없습니다. ## SMS, LMS, MMS — 어떤 걸 써야 하나 | 타입 | 본문 한도 | 제목 | 첨부 | 언제 | | --- | --- | --- | --- | --- | | **SMS** | 90바이트 (한글 ~45자) | ❌ | ❌ | 인증번호, 짧은 알림 | | **LMS** | 2,000바이트 (한글 ~1,000자) | ✅ | ❌ | 공지, 안내문 | | **MMS** | 2,000바이트 | ✅ | 이미지 | 이벤트 배너, 쿠폰 이미지 | **바이트 계산**: 한글 2바이트, 영문·숫자·기호 1바이트, 줄바꿈 1~2바이트. 90바이트를 넘기면 SMS 로 보낼 수 없습니다. 길이가 유동적인 본문이라면 처음부터 LMS 를 쓰거나, 길이에 따라 분기하세요. ## 언어별 예제 ### Node.js / TypeScript ```typescript // SMS — 단문 await sendgo.sms.sendSms({ content: '[샌드고] 인증번호: 123456 (5분 이내 입력)', contacts: [{ contact: '01012345678' }], }); // LMS — 장문 (제목 포함) await sendgo.sms.sendLms({ subject: '[중요] 서비스 점검 안내', content: `안녕하세요. 서비스 점검이 예정되어 있습니다. ■ 점검 일시: 2026-09-01 02:00 ~ 06:00 ■ 영향 범위: 전체 서비스 이용에 불편을 드려 죄송합니다.`, contacts: [{ contact: '01012345678' }], }); // MMS — 이미지 첨부 await sendgo.sms.sendMms({ subject: '[이벤트] 9월 특가', content: '이번 달 특가 상품을 확인하세요!', contacts: [{ contact: '01012345678' }], }); ``` ### Python ```python client.sms.send_sms( content="[샌드고] 인증번호: 123456 (5분 이내 입력)", contacts=[{"contact": "01012345678"}], ) client.sms.send_lms( subject="[중요] 서비스 점검 안내", content="안녕하세요.\n\n서비스 점검이 예정되어 있습니다.\n일시: 2026-09-01 02:00 ~ 06:00", contacts=[{"contact": "01012345678"}], ) client.sms.send_mms( subject="[이벤트] 9월 특가", content="이번 달 특가 상품을 확인하세요!", contacts=[{"contact": "01012345678"}], ) ``` ### PHP · Laravel ```php sms->sendSms([ 'content' => '[샌드고] 인증번호: 123456 (5분 이내 입력)', 'contacts' => [['contact' => '01012345678']], ]); $sendgo->sms->sendLms([ 'subject' => '[중요] 서비스 점검 안내', 'content' => "안녕하세요.\n\n서비스 점검이 예정되어 있습니다.", 'contacts' => [['contact' => '01012345678']], ]); $sendgo->sms->sendMms([ 'subject' => '[이벤트] 9월 특가', 'content' => '이번 달 특가 상품을 확인하세요!', 'contacts' => [['contact' => '01011111111'], ['contact' => '01022222222']], ]); ``` Laravel 에서는 `app(Sendgo::class)->sms->sendSms([...])` 또는 생성자 주입으로 씁니다. ### REST 직접 호출 ```bash curl -X POST https://sendgo.io/api/v2/messages/send \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "campaignType": "MESSAGE", "messageType": "SMS", "scheduleType": "DIRECTLY", "content": "인증번호: 123456", "contacts": [{ "contact": "01012345678" }], "senderKey": "your_sms_sender_key" }' ``` `messageType` 을 `LMS` 나 `MMS` 로 바꾸면 해당 타입으로 나갑니다. LMS/MMS 는 `subject` 를 함께 보냅니다. ## 인증번호 발송 패턴 인증번호는 문자의 가장 흔한 용도입니다. 몇 가지 주의점이 있습니다. ```typescript // 인증번호는 서버에서 생성하고, 발송 실패를 사용자에게 알려야 한다. const code = String(Math.floor(100000 + Math.random() * 900000)); await redis.setex(`verify:${phone}`, 300, code); // 5분 만료 try { await sendgo.sms.sendSms({ content: `[서비스명] 인증번호: ${code} (5분 이내 입력)`, contacts: [{ contact: phone }], }); } catch (error) { // 인증번호는 실패를 삼키면 안 된다 — 사용자가 오지 않는 문자를 기다린다. throw new Error('인증번호 발송에 실패했습니다. 잠시 후 다시 시도해 주세요.'); } ``` - **발송 실패를 삼키지 마세요.** 주문 알림과 달리 인증번호는 도착하지 않으면 사용자가 다음 단계로 갈 수 없습니다. - **재발송에 제한을 거세요.** 번호당 분당 횟수를 제한하지 않으면 문자 폭탄에 악용되고 크레딧이 소진됩니다. - **본문에 서비스명을 넣으세요.** 어디서 온 인증번호인지 모르면 사용자가 입력하지 않습니다. ## 광고 문자 규칙 홍보성 문자에는 정보통신망법이 적용됩니다. ```text (광고)[브랜드명] 가을 세일 최대 50%! ... 무료수신거부 080-000-0000 ``` - 본문 **맨 앞**에 `(광고)` 표기 - **무료 수신거부** 번호 또는 방법 명시 - **21시 ~ 익일 08시** 발송 금지 - 사전 수신동의를 받은 대상에게만 자세한 내용은 [광고성 메시지 규칙](/ko/cookbook/ad-message-rules)에 있습니다. 인증번호·주문 알림 같은 정보성 문자에는 적용되지 않습니다. ## 다음 단계 - [알림톡이 실패하면 문자로 대체 발송](/ko/cookbook/sms-fallback) - [짧은주소로 클릭 추적하기](/ko/cookbook/short-url) - [대량 발송과 치환 변수](/ko/cookbook/bulk-send) --- 브랜드메시지는 카카오 채널을 통한 **마케팅·홍보 메시지** 채널입니다. 정보성만 가능한 알림톡과 달리 광고를 보낼 수 있고, 친구톡과 달리 **채널 친구가 아닌 사람에게도** 보낼 수 있습니다. > **v2 전용입니다.** v1 에는 이 엔드포인트가 없습니다. ## 준비물 - 카카오 발신프로필 키(`kakaoSenderKey`) → [발신번호 사전등록](/ko/cookbook/sender-number) - 콘솔에 등록한 브랜드메시지 템플릿의 **`friendTemplateUuid`** - 광고성 메시지라면 `adFlag: "Y"` 와 [광고 규칙](/ko/cookbook/ad-message-rules) 준수 ## `targeting` 이 경로를 가른다 | `targeting` | 대상 | 발송 방식 | `contacts` | | --- | --- | --- | --- | | `M` | 채널 친구 | 단건(`BRAND_BASIC`) | 필수 | | `N` | 채널 친구가 **아닌** 수신자 | 단건(`BRAND_BASIC`) | 필수 | | `I` | 지정 대상 | 단건(`BRAND_BASIC`) | 필수 | | `F` | 수신 동의한 **전체** 채널 친구 | 동보(`BRAND_GROUP`) | 불필요 | `F` 는 수신자를 지정하지 않으므로 응답에 발송 건수가 없고 **접수 여부만** 돌아옵니다. 결과는 캠페인 조회로 확인합니다. ## 메시지 타입 요청에는 **친구톡 코드를 그대로** 넘깁니다. 서버가 브랜드메시지 코드로 변환합니다. | 넘기는 값 | 변환 결과 | 내용 | | --- | --- | --- | | `FT` | `BT` | 텍스트 | | `FI` | `BI` | 이미지 | | `FW` | `BW` | 와이드 이미지 | | `FL` | `BL` | 리스트 | | `FC` | `BC` | 커머스 | | `FM` | `BM` | 복합 | | `FP` | `BP` | 프리미엄 동영상 | | `FA` | `BA` | 캐러셀 | **중요한 예외**: `FT`/`FI`/`FW` 를 `M`/`N`/`I` 로 보내면 이 엔드포인트는 `NOT_A_BRAND_MESSAGE` 를 반환합니다. 자유 본문을 개별 수신자에게 보내는 경로는 여전히 `/api/v2/friends/send` 입니다. 자세한 내용은 [친구톡 종료 대응](/ko/cookbook/friendtalk-sunset)에 있습니다. ## 언어별 예제 ### Node.js / TypeScript ```typescript // 단건 발송 — 채널 친구 대상 await sendgo.brandMessage.send({ targeting: 'M', messageType: 'FL', friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340', adFlag: 'Y', contacts: [{ contact: '01012345678', var1: '29,000원' }], }); // 단건 발송 — 채널 친구가 아닌 수신자 await sendgo.brandMessage.send({ targeting: 'N', messageType: 'FM', friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340', adFlag: 'Y', contacts: [{ contact: '01012345678' }], }); // 동보 발송 — 수신 동의한 전체 채널 친구 (contacts 없음) await sendgo.brandMessage.broadcast({ messageType: 'FW', friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340', adFlag: 'Y', }); // 캠페인 결과 확인 const list = await sendgo.brandMessage.campaigns({ count: 10 }); const one = await sendgo.brandMessage.campaign(campaignId); ``` ### Python ```python client.brand_message.send( targeting="M", message_type="FL", friend_template_uuid="9cd5460b-6458-4edc-9b11-c26d3013c340", ad_flag="Y", contacts=[{"contact": "01012345678", "var1": "29,000원"}], ) ``` ### PHP · Laravel ```php brandMessage->send([ 'targeting' => 'M', 'messageType' => 'FL', 'friendTemplateUuid' => '9cd5460b-6458-4edc-9b11-c26d3013c340', 'adFlag' => 'Y', 'contacts' => [['contact' => '01012345678', 'var1' => '29,000원']], ]); ``` ### REST 직접 호출 ```bash curl -X POST https://sendgo.io/api/v2/brand-messages/send \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "kakaoSenderKey": "your_kakao_sender_key", "targeting": "F", "messageType": "FW", "friendTemplateUuid": "9cd5460b-6458-4edc-9b11-c26d3013c340", "adFlag": "Y", "scheduleType": "DIRECTLY" }' ``` ## SMS 대체 발송 브랜드메시지도 `replaceSms: "Y"` 로 실패 시 문자 대체가 가능합니다. `smsSubject` 와 `smsContent`, 그리고 `senderKey` 를 함께 넘겨야 합니다. ```typescript await sendgo.brandMessage.send({ targeting: 'M', messageType: 'FL', friendTemplateUuid: '...', replaceSms: 'Y', smsSubject: '[여름 특가]', smsContent: '여름 한정 특가를 확인하세요.', contacts: [{ contact: '01012345678' }], }); ``` ## 캠페인 결과 조회 동보 발송은 접수만 확인되므로 결과를 따로 조회합니다. ```bash # 목록 (기본 최근 90일) curl "https://sendgo.io/api/v2/brand-messages?count=30" -H "Authorization: Bearer $TOKEN" # 상세 — 발송 응답의 campaignId 사용 curl "https://sendgo.io/api/v2/brand-messages/$CAMPAIGN_ID" -H "Authorization: Bearer $TOKEN" ``` 이 엔드포인트는 브랜드메시지 캠페인(`BRAND_GROUP`, `BRAND_BASIC`)만 반환합니다. 친구톡 캠페인은 `/api/v2/friends` 를 쓰세요. ## 다음 단계 - [친구톡 종료 대응 마이그레이션](/ko/cookbook/friendtalk-sunset) - [광고성 메시지 규칙](/ko/cookbook/ad-message-rules) --- 알림톡은 카카오톡을 쓰는 사람에게만 도달합니다. 카카오톡을 안 쓰거나, 채널을 차단했거나, 전달에 실패하면 메시지가 사라집니다. **주문 확인이나 배송 안내처럼 반드시 도달해야 하는 알림**이라면 문자로 대체하도록 켜 두세요. ## 필요한 것 대체 발송에는 **문자 발신번호가 추가로** 필요합니다. 알림톡만 보낼 때는 `kakaoSenderKey` 만 있으면 되지만, 대체 발송을 켜면 `senderKey`(문자 발신번호)도 등록돼 있어야 합니다. ## 켜는 법 세 개를 함께 넘깁니다. **하나라도 빠지면 대체 발송이 조용히 실패합니다.** | 파라미터 | 값 | | --- | --- | | `replaceSms` | `"Y"` | | `smsSubject` | 문자 제목 (LMS 로 나갈 때 사용) | | `smsContent` | 문자 본문 | ### Node.js / TypeScript ```typescript await sendgo.alimtalk.send({ templateCode: 'DELIVERY_START_001', replaceSms: 'Y', smsSubject: '[배송 시작 안내]', smsContent: '주문하신 상품이 출고되었습니다.\n송장번호: #{var2}', contacts: [{ contact: '01012345678', var1: 'ORD-001', var2: '1234567890', }], }); ``` ### Python ```python client.alimtalk.send( template_code="DELIVERY_START_001", replace_sms="Y", sms_subject="[배송 시작 안내]", sms_content="주문하신 상품이 출고되었습니다.\n송장번호: #{var2}", contacts=[{"contact": "01012345678", "var1": "ORD-001", "var2": "1234567890"}], ) ``` ### PHP · Laravel ```php alimtalk->send([ 'templateCode' => 'DELIVERY_START_001', 'replaceSms' => 'Y', 'smsSubject' => '[배송 시작 안내]', 'smsContent' => "주문하신 상품이 출고되었습니다.\n송장번호: #{var2}", 'contacts' => [ ['contact' => '01012345678', 'var1' => 'ORD-001', 'var2' => '1234567890'], ], ]); ``` ### REST 직접 호출 ```bash curl -X POST https://sendgo.io/api/v2/notices/send \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "templateCode": "DELIVERY_001", "scheduleType": "DIRECTLY", "replaceSms": "Y", "smsSubject": "[배송 시작 안내]", "smsContent": "주문하신 상품이 출고되었습니다.\n송장번호: #{var2}", "kakaoSenderKey": "your_kakao_sender_key", "senderKey": "your_sms_sender_key", "contacts": [{ "contact": "01012345678", "var1": "ORD-001", "var2": "1234567890" }] }' ``` ## 대체 본문에서 변수 쓰기 `smsContent` 안의 `#{var1}` 형태 자리표시자는 각 수신자의 값으로 치환됩니다. 알림톡 템플릿과 같은 변수를 그대로 쓸 수 있어서, 두 본문을 따로 관리할 필요가 없습니다. ```text 알림톡 템플릿: [#{var2}] 주문 #{var1} 이 출고되었습니다. smsContent: [#{var2}] 주문 #{var1} 출고. 송장 #{var3} ``` 다만 문자에는 **바이트 제한**이 적용됩니다. 90바이트를 넘으면 LMS 로 나가고 요금이 달라집니다. ## 흔한 실수 - **`smsContent` 를 빠뜨림.** `replaceSms: 'Y'` 만 켜고 본문을 안 넣으면 대체할 내용이 없어서 아무것도 나가지 않습니다. 알림톡도 실패했는데 문자도 안 가는 최악의 조합이라, 실제로 가장 자주 나오는 사고입니다. - **문자 발신번호 미등록.** 알림톡만 테스트할 때는 문제가 없다가, 실제 대체가 발생하는 순간 실패합니다. - **비용 예측 누락.** 문자는 알림톡보다 단가가 높습니다. 대체 비율이 10%만 돼도 예상 비용이 눈에 띄게 올라갑니다. - **광고성 메시지에 대체 발송.** 알림톡은 정보성만 나가므로 이 조합이 나올 일이 거의 없지만, 브랜드메시지에서 대체 발송을 켤 때는 문자 쪽에 `(광고)` 표기와 수신거부 안내가 필요합니다. ## 다음 단계 - [SMS · LMS · MMS 보내기](/ko/cookbook/send-sms) - [오류 코드와 재시도 전략](/ko/cookbook/error-handling) --- 대량 발송의 핵심은 **한 템플릿, 여러 값**입니다. `contacts` 배열의 각 항목이 자기 변수를 가지므로, 사람마다 다른 주문번호·금액·날짜를 같은 문안으로 보낼 수 있습니다. ## 기본형 ```typescript await sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: [ { contact: '01011111111', name: '홍길동', var1: 'ORD-001', var2: '29,000원' }, { contact: '01022222222', name: '김철수', var1: 'ORD-002', var2: '15,000원' }, { contact: '01033333333', name: '이영희', var1: 'ORD-003', var2: '52,000원' }, ], }); ``` ```python client.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[ {"contact": "01011111111", "name": "홍길동", "var1": "ORD-001", "var2": "29,000원"}, {"contact": "01022222222", "name": "김철수", "var1": "ORD-002", "var2": "15,000원"}, ], ) ``` ```php alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => [ ['contact' => '01011111111', 'name' => '홍길동', 'var1' => 'ORD-001', 'var2' => '29,000원'], ['contact' => '01022222222', 'name' => '김철수', 'var1' => 'ORD-002', 'var2' => '15,000원'], ], ]); ``` ## 배치로 나누기 요청 하나에 수만 건을 담으면 타임아웃이 났을 때 어디까지 처리됐는지 알 수 없고, 재시도 비용이 커집니다. 수백 건 단위로 나누세요. ### Node.js ```typescript const BATCH = 500; async function sendInBatches(recipients: Recipient[]) { const failures: Recipient[] = []; for (let i = 0; i < recipients.length; i += BATCH) { const chunk = recipients.slice(i, i + BATCH); try { await sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts: chunk.map((r) => ({ contact: r.phone, name: r.name, var1: r.orderNo, var2: r.amount, })), }); } catch (error) { // 배치 하나가 실패해도 나머지는 계속 보낸다. console.error(`배치 ${i / BATCH} 실패`, error); failures.push(...chunk); } } return failures; } ``` ### PHP · Laravel ```php chunk(500)->each(function (Collection $chunk) use ($sendgo) { try { $sendgo->alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => $chunk->map(fn ($r) => [ 'contact' => $r->phone, 'name' => $r->name, 'var1' => $r->order_no, 'var2' => number_format($r->amount).'원', ])->values()->all(), ]); } catch (\Sendgo\Php\Exception\SendgoException $e) { Log::error('배치 발송 실패', ['message' => $e->getMessage()]); } }); ``` ### Python ```python BATCH = 500 for i in range(0, len(recipients), BATCH): chunk = recipients[i:i + BATCH] try: client.alimtalk.send( template_code="ORDER_CONFIRM_001", contacts=[ {"contact": r.phone, "name": r.name, "var1": r.order_no} for r in chunk ], ) except SendgoError as e: logger.error("배치 발송 실패: %s", e) ``` ## 큐에서 처리하기 대량 발송은 웹 요청 안에서 하지 마세요. 사용자는 응답을 기다리고, 타임아웃이 나면 중간부터 다시 보낼 방법이 없습니다. ```php alimtalk->send([ 'templateCode' => 'ORDER_CONFIRM_001', 'contacts' => $this->contacts, ]); } public function failed(SendgoException $e): void { // 실패한 배치를 기록해 두면 나중에 실패분만 재발송할 수 있다. FailedDispatch::create(['contacts' => $this->contacts, 'reason' => $e->getMessage()]); } } ``` ## 부분 실패 다루기 요청이 200 으로 돌아와도 **개별 수신자는 실패할 수 있습니다.** 없는 번호, 차단된 수신자, 카카오톡 미사용자 등입니다. - 전체를 재발송하지 마세요. 성공한 사람에게 같은 메시지가 두 번 갑니다. - 실패분만 골라 다시 보내거나, [SMS 대체 발송](/ko/cookbook/sms-fallback)을 켜서 자동으로 문자로 넘기세요. - 건별 결과는 콘솔의 발송 내역에서 확인합니다. ## 놓치기 쉬운 것 - **번호 정규화를 먼저 하세요.** DB 에 `010-1234-5678`, `+821012345678`, `01012345678` 이 섞여 있는 경우가 흔합니다. 하이픈과 국가번호를 제거해 숫자만 남기세요. - **중복 번호를 제거하세요.** 같은 사람에게 두 번 나가고 크레딧도 두 번 빠집니다. - **크레딧을 미리 확인하세요.** 1만 건 발송 도중 잔액이 떨어지면 `PAYMENT_REQUIRED` 가 나면서 나머지가 통째로 실패합니다. - **광고성이라면 야간 발송 금지가 적용됩니다.** 배치가 21시를 넘겨 실행되지 않도록 하세요 → [광고성 메시지 규칙](/ko/cookbook/ad-message-rules) ## 다음 단계 - [예약 발송](/ko/cookbook/scheduled-send) - [오류 코드와 재시도 전략](/ko/cookbook/error-handling) --- 발송 시각을 지정하려면 `scheduleType` 을 `SCHEDULED` 로 두고 `at` 에 시각을 넣습니다. 기본값은 `DIRECTLY`(즉시)입니다. ## 형식 | 파라미터 | 값 | | --- | --- | | `scheduleType` | `"SCHEDULED"` | | `at` | `"2026-09-01 09:00:00"` — `Y-m-d H:i:s`, **한국 표준시(KST)** | ISO 8601(`2026-09-01T09:00:00Z`)이나 유닉스 타임스탬프는 받지 않습니다. ## 예제 ### Node.js / TypeScript ```typescript await sendgo.alimtalk.send({ templateCode: 'PROMO_SUMMER_2026', scheduleType: 'SCHEDULED', at: '2026-09-01 09:00:00', contacts: [{ contact: '01012345678', var1: '가을 한정 30% 할인' }], }); ``` ### Python ```python client.alimtalk.send( template_code="PROMO_SUMMER_2026", schedule_type="SCHEDULED", at="2026-09-01 09:00:00", contacts=[{"contact": "01012345678", "var1": "가을 한정 30% 할인"}], ) ``` ### PHP · Laravel ```php alimtalk->send([ 'templateCode' => 'PROMO_SUMMER_2026', 'scheduleType' => 'SCHEDULED', 'at' => '2026-09-01 09:00:00', 'contacts' => [['contact' => '01012345678', 'var1' => '가을 한정 30% 할인']], ]); ``` ### 문자 예약 같은 파라미터가 문자에도 적용됩니다. ```bash curl -X POST https://sendgo.io/api/v2/messages/send \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "campaignType": "MESSAGE", "messageType": "LMS", "scheduleType": "SCHEDULED", "at": "2026-09-01 09:00:00", "subject": "[공지] 서비스 점검 안내", "content": "9월 1일 02:00~06:00 점검이 예정되어 있습니다.", "contacts": [{ "contact": "01012345678" }], "senderKey": "your_sms_sender_key" }' ``` ## 시간대 함정 서버가 UTC 로 동작하는 경우(도커 컨테이너, AWS 기본 설정 등)가 흔합니다. **날짜를 만드는 코드가 KST 를 쓰는지 확인하세요.** UTC 시각을 그대로 넘기면 9시간 이른 시각에 발송됩니다. ```php now()->addDay()->setTime(9, 0)->format('Y-m-d H:i:s'), // 올바른 예 'at' => now('Asia/Seoul')->addDay()->setTime(9, 0)->format('Y-m-d H:i:s'), ``` ```python from datetime import datetime, time from zoneinfo import ZoneInfo at = datetime.now(ZoneInfo("Asia/Seoul")).replace(hour=9, minute=0, second=0) client.alimtalk.send( template_code="PROMO_001", schedule_type="SCHEDULED", at=at.strftime("%Y-%m-%d %H:%M:%S"), contacts=[...], ) ``` ```typescript // Node.js — 서버 시간대에 의존하지 말고 명시적으로 KST 로 포맷한다. const at = new Intl.DateTimeFormat('sv-SE', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }).format(target).replace('T', ' '); ``` ## 야간 광고 제한과의 관계 광고성 메시지는 **실제 발송 시각** 기준으로 21시~익일 08시에 나갈 수 없습니다. 예약을 그 구간으로 잡으면 안 됩니다. 주의할 조합: 저녁에 배치를 돌리면서 예약 시각을 "지금부터 3시간 뒤"로 계산하면, 실행 시각에 따라 제한 구간에 들어갈 수 있습니다. 절대 시각으로 잡거나, 제한 구간이면 다음 날 08시로 밀어내세요. ```php addHours(3); // 21시~08시 구간이면 다음 날 아침으로 미룬다. if ($at->hour >= 21 || $at->hour < 8) { $at = $at->copy()->addDay()->setTime(8, 0); } ``` 자세한 규칙은 [광고성 메시지 규칙](/ko/cookbook/ad-message-rules)에 있습니다. ## 다음 단계 - [대량 발송과 치환 변수](/ko/cookbook/bulk-send) - [광고성 메시지 규칙](/ko/cookbook/ad-message-rules) --- 알림톡은 **본문을 미리 심사받는** 채널입니다. 코드를 아무리 잘 짜도 승인된 템플릿이 없으면 한 통도 나가지 않습니다. 연동 일정에서 [발신번호 등록](/ko/cookbook/sender-number)과 함께 가장 먼저 시작해야 하는 일입니다. ## 정보성만 승인된다 알림톡의 전제는 **수신자가 예상하고 있는 메시지**입니다. | 승인되는 것 | 승인되지 않는 것 | | --- | --- | | 주문·결제 확인 | 할인·세일 안내 | | 배송 상태 변경 | 신규 상품 출시 | | 예약 확인·변경·취소 | 이벤트 참여 유도 | | 인증번호 | 쿠폰 발급 홍보 | | 결제 실패·연체 안내 | 재구매 권유 | | 회원가입·탈퇴 완료 | 앱 설치 유도 | 경계가 애매한 경우도 있습니다. "장바구니에 담아두신 상품이 품절 임박입니다"는 사실 안내처럼 보이지만 구매 유도이므로 광고성으로 판단됩니다. **광고를 보내고 싶다면** 알림톡이 아니라 [브랜드메시지](/ko/cookbook/send-brand-message)나 [광고 문자](/ko/cookbook/send-sms)를 쓰세요. 알림톡 템플릿으로 우회하려는 시도는 반려되고, 반복되면 채널 자체에 제재가 갑니다. ## 변수 설계 바뀌는 값만 변수로 두고, 나머지는 본문에 고정합니다. ```text [#{var2}] 주문이 확인되었습니다. ■ 주문번호: #{var1} ■ 결제금액: #{var3} ■ 배송예정일: #{var4} 배송이 시작되면 다시 안내드립니다. ``` - `#{var1}` ~ `#{var8}`, 최대 여덟 개 - **본문 전체를 변수로 채우지 마세요.** `#{var1}` 하나뿐인 템플릿은 심사할 내용이 없어 반려됩니다. - 변수 자리에 들어갈 값의 성격이 일정해야 합니다. `#{var3}` 이 어떤 발송에서는 금액, 다른 발송에서는 주소가 되면 안 됩니다. - 발송할 때 **정의된 변수를 모두 채우세요.** 빠뜨리면 수신자가 `#{var4}` 를 그대로 봅니다. ## 등록 절차 1. 샌드고 콘솔 → **카카오 알림톡 템플릿** → 등록 2. 연결할 카카오 발신프로필 선택 3. 템플릿 이름과 **템플릿 코드** 지정 — 코드가 코드에서 쓸 식별자입니다 (`ORDER_CONFIRM_001`) 4. 카테고리 선택 (주문/배송/예약 등 실제 용도와 맞게) 5. 본문 작성, 변수 배치 6. 버튼 추가 (선택) — 채널 추가, 웹링크, 배송조회 등 7. 제출 → 심사 승인 상태가 **승인 완료**가 되어야 발송됩니다. "검토 중"인 템플릿 코드로 호출하면 `INVALID_TEMPLATE_CODE` 가 돌아옵니다. ## 자주 반려되는 문안과 고치는 법 | 반려된 문안 | 왜 | 고친 문안 | | --- | --- | --- | | `지금 구매하시면 20% 할인!` | 광고성 | (알림톡 대신 브랜드메시지) | | `#{var1}` | 심사할 본문이 없음 | 고정 문구 + 변수 조합으로 재작성 | | `안녕하세요 고객님` | 발송 목적이 불명확 | 무슨 알림인지 제목에 명시 | | `문의는 카톡 주세요` | 외부 채널 유도 | 공식 고객센터 번호나 버튼으로 대체 | | `#{var1}님, 이벤트 참여하세요` | 참여 유도 = 광고성 | 브랜드메시지로 이동 | ## 운영 중 템플릿 수정 본문을 바꾸면 **재심사**입니다. 운영 중인 템플릿을 고치면 승인 전까지 발송이 막힐 수 있습니다. 안전한 방법은 버전을 나누는 것입니다. ```text ORDER_CONFIRM_001 ← 현재 운영 중 ORDER_CONFIRM_002 ← 새 문안, 심사 중 ``` `002` 가 승인되면 코드에서 템플릿 코드만 교체하고, 한동안 두고 보다가 `001` 을 정리합니다. 템플릿 코드를 환경변수나 설정으로 빼 두면 배포 없이 전환할 수 있습니다. ```php config('sendgo.templates.order_confirm'), ``` ## 다음 단계 - [카카오 알림톡 보내기](/ko/cookbook/send-alimtalk) - [발신번호 사전등록](/ko/cookbook/sender-number) --- 샌드고 API 오류의 대부분은 **재시도로 해결되지 않습니다.** 요청이 잘못됐거나 계정 상태가 문제이기 때문입니다. 이 구분을 먼저 잡아야 무의미한 재시도 루프와 크레딧 낭비를 피할 수 있습니다. ## 응답 형태 ```json { "code": "INVALID_TEMPLATE_CODE", "message": "존재하지 않는 템플릿 코드입니다." } ``` 검증 실패는 필드별 오류가 함께 옵니다. ```json { "code": "VALIDATION_FAILED", "message": "The given data was invalid.", "errors": { "targetUrl": ["The target url field must be a valid URL."] } } ``` ## 오류 코드 전체 | HTTP | 코드 | 뜻 | 재시도 | | --- | --- | --- | --- | | 400 | `EMPTY_CONTACTS` | 수신자 배열이 비었다 | ❌ | | 400 | `INVALID_TEMPLATE_CODE` | 없거나 미승인 템플릿 | ❌ | | 400 | `VALIDATION_FAILED` | 입력값 검증 실패 | ❌ | | 400 | `NOT_A_BRAND_MESSAGE` | 자유형(FT/FI/FW)+개별 수신자를 브랜드메시지로 보냄 | ❌ | | 401 | `INVALID_ACCESS_KEY` | 액세스 키/시크릿이 틀림 | ❌ | | 402 | `PAYMENT_REQUIRED` | 크레딧 부족 | ❌ (충전 필요) | | 403 | `ACCESS_KEY_NOT_APPROVED` | 앱이 승인되지 않음 | ❌ | | 403 | `IP_NOT_ALLOWED` | 허용 IP 밖에서 호출 | ❌ | | 404 | `NOT_FOUND` | 캠페인 등 대상 없음 | ❌ | | 404 | `INVALID_KAKAO_SENDER_KEY` | 카카오 발신프로필 키가 틀림 | ❌ | | — | 타임아웃 / 5xx | 일시적 장애 | ✅ | **재시도할 가치가 있는 건 마지막 한 줄뿐입니다.** ## 언어별 처리 ### Node.js / TypeScript ```typescript import Sendgo, { SendgoError } from '@sendgo/node'; try { await sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts }); } catch (error) { if (error instanceof SendgoError) { switch (error.code) { case 'PAYMENT_REQUIRED': // 재시도해도 소용없다. 운영자에게 알린다. await notifyOps('샌드고 크레딧이 소진되었습니다'); break; case 'INVALID_TEMPLATE_CODE': case 'INVALID_KAKAO_SENDER_KEY': // 설정 오류 — 배포된 코드가 잘못됐다는 뜻이므로 크게 알린다. logger.error('샌드고 설정 오류', { code: error.code }); break; default: logger.warn('알림톡 발송 실패', { code: error.code, message: error.message }); } return; // 재시도하지 않는다 } throw error; // 네트워크 오류 등은 상위 재시도 로직으로 } ``` ### Python ```python from sendgo import SendgoError NON_RETRYABLE = { "EMPTY_CONTACTS", "INVALID_TEMPLATE_CODE", "VALIDATION_FAILED", "INVALID_ACCESS_KEY", "PAYMENT_REQUIRED", "ACCESS_KEY_NOT_APPROVED", "IP_NOT_ALLOWED", "INVALID_KAKAO_SENDER_KEY", } try: client.alimtalk.send(template_code="ORDER_CONFIRM_001", contacts=contacts) except SendgoError as e: if e.code in NON_RETRYABLE: logger.error("알림톡 발송 실패(재시도 불가): %s", e.code) return raise # 일시적 오류만 상위로 올려 재시도 ``` ### PHP · Laravel ```php alimtalk->send([ 'templateCode' => config('sendgo.templates.order_confirm'), 'contacts' => $contacts, ]); } catch (SendgoException $e) { // 발송 실패가 주문 처리를 되돌리게 하지 않는다. Log::error('알림톡 발송 실패', [ 'code' => $e->getCode(), 'message' => $e->getMessage(), 'order' => $order->id, ]); } ``` ## 큐에서의 재시도 Laravel 큐나 Celery 를 쓴다면 **재시도 횟수를 낮게 잡으세요.** 기본값을 그대로 두면 재시도로 해결되지 않는 오류에 대해 수십 번 같은 요청을 보냅니다. ```php addMinutes(5); } } ``` 크레딧 부족(`PAYMENT_REQUIRED`)에서는 특히 조심해야 합니다. 큐에 1만 건이 쌓인 상태에서 잔액이 떨어지면, 재시도 설정에 따라 수만 번의 실패 호출이 발생합니다. ## 요청은 성공, 메시지는 미도달 HTTP 200 은 **접수 성공**이지 도달 성공이 아닙니다. - 수신자가 카카오톡을 안 쓴다 → [SMS 대체 발송](/ko/cookbook/sms-fallback)으로 커버 - 채널을 차단했다 → 대체 발송으로 커버 - 없는 번호다 → 데이터 정합성 문제. 발송 전 번호 검증 필요 건별 결과는 콘솔의 발송 내역에서 확인합니다. ## 발송 전 점검 가장 좋은 오류 처리는 발송 전에 막는 것입니다. ```php map(fn ($r) => preg_replace('/\D/', '', $r->phone)) ->map(fn ($p) => str_starts_with($p, '82') ? '0'.substr($p, 2) : $p) ->unique() // 중복 제거 — 두 번 나가고 두 번 과금된다 ->filter(fn ($p) => strlen($p) >= 10) ->values(); ``` ## 다음 단계 - [SMS 대체 발송](/ko/cookbook/sms-fallback) - [대량 발송과 치환 변수](/ko/cookbook/bulk-send) --- 문자 본문에 긴 URL 을 그대로 넣으면 두 가지 손해를 봅니다. **바이트를 잡아먹고**, **누가 눌렀는지 알 수 없습니다.** ```text [이벤트] 가을 특가! https://shop.example.com/promotions/autumn-2026?utm_source=sms&utm_campaign=autumn → 90바이트를 훌쩍 넘겨 LMS 로 승격되고, 반응은 측정되지 않는다. [이벤트] 가을 특가! https://sendgo.io/s/k7Rm2xQ → SMS 안에 들어가고, 클릭이 집계된다. ``` ## 짧은주소 만들기 `POST /api/v2/short-urls` | 파라미터 | 필수 | 설명 | | --- | --- | --- | | `targetUrl` | ✅ | 원본 URL. `http`/`https` 만 허용, 최대 2,048자 | | `title` | | 관리 화면에서 구분할 이름 | | `expiresAt` | | 이 시각 이후 `410 Gone`. `Y-m-d H:i:s` | | `forceNew` | | `true` 면 같은 URL 이어도 새 코드 발급 | ### Node.js / TypeScript ```typescript const created = await sendgo.shortUrl.create({ targetUrl: 'https://shop.example.com/promotions/autumn-2026', title: '가을 세일 랜딩', expiresAt: '2026-09-30 23:59:59', }); const { code, shortUrl } = created.data; await sendgo.sms.sendSms({ content: `[이벤트] 가을 특가! ${shortUrl}`, contacts: [{ contact: '01012345678' }], }); ``` ### Python ```python created = client.short_url.create( target_url="https://shop.example.com/promotions/autumn-2026", title="가을 세일 랜딩", ) link = created["data"]["shortUrl"] client.sms.send_sms( content=f"[이벤트] 가을 특가! {link}", contacts=[{"contact": "01012345678"}], ) ``` ### PHP ```php shortUrl->create([ 'targetUrl' => 'https://shop.example.com/promotions/autumn-2026', 'title' => '가을 세일 랜딩', ]); $link = $short['data']['shortUrl']; $sendgo->sms->sendSms([ 'content' => "[이벤트] 가을 특가! {$link}", 'contacts' => [['contact' => '01012345678']], ]); ``` ## 클릭 통계 조회 ```typescript const stats = await sendgo.shortUrl.stats(code, { from: '2026-09-01' }); await sendgo.shortUrl.list({ count: 10 }); // 목록 await sendgo.shortUrl.show(code); // 상세 await sendgo.shortUrl.deactivate(code); // 리다이렉트 중지 (통계는 남는다) ``` ```python stats = client.short_url.stats(code, from_="2026-09-01") client.short_url.list(count=10) client.short_url.show(code) ``` ```php shortUrl->stats($code, ['from' => '2026-09-01']); $sendgo->shortUrl->list(['count' => 10]); $sendgo->shortUrl->show($code); $sendgo->shortUrl->deactivate($code); ``` ## 캠페인별로 통계 나누기 같은 랜딩 페이지를 여러 캠페인에서 쓴다면 `forceNew` 로 코드를 따로 발급받으세요. 그러지 않으면 통계가 한 코드에 합쳐져 어떤 캠페인이 효과가 있었는지 알 수 없습니다. ```typescript const augustLink = await sendgo.shortUrl.create({ targetUrl: 'https://shop.example.com/sale', title: '8월 세일 - 알림톡', forceNew: true, }); const septemberLink = await sendgo.shortUrl.create({ targetUrl: 'https://shop.example.com/sale', // 같은 목적지 title: '9월 세일 - 문자', forceNew: true, // 다른 코드 }); ``` ## 놓치기 쉬운 것 - **알림톡 템플릿에는 링크를 변수로 넣어야 합니다.** 템플릿 본문에 URL 을 고정하면 캠페인마다 재심사를 받아야 합니다. `#{var3}` 자리에 짧은주소를 넣으세요. - **만료 시각을 정하세요.** 지난 이벤트 링크가 계속 살아 있으면 뒤늦게 눌린 사용자가 종료된 페이지를 봅니다. - **짧은주소를 사이트맵에 넣지 마세요.** 추측 불가능한 코드가 곧 접근 통제인 경우가 있습니다. ## 다음 단계 - [SMS · LMS · MMS 보내기](/ko/cookbook/send-sms) - [광고성 메시지 규칙](/ko/cookbook/ad-message-rules) --- 홍보·마케팅 메시지에는 **정보통신망법**이 적용됩니다. 지키지 않으면 과태료 대상이고, 반복되면 발신 채널 자체가 제재를 받습니다. 정보성 메시지(주문 확인, 배송 안내, 인증번호)에는 적용되지 않습니다. 알림톡은 정보성만 승인되므로 실질적으로 이 규칙은 **광고 문자와 브랜드메시지**의 문제입니다. ## 지켜야 할 네 가지 ### 1. 사전 수신 동의 수신자에게 **미리 동의를 받은 경우에만** 보낼 수 있습니다. 회원가입 시 마케팅 수신 동의 체크박스가 그것입니다. 동의는 기본 선택 상태로 두면 안 되고, 동의 시점과 내역을 보관해야 합니다. ### 2. (광고) 표기 본문 **맨 앞**에 붙입니다. ```text (광고)[브랜드명] 가을 세일 최대 50% ... ``` LMS 처럼 제목이 있으면 제목 맨 앞에 넣습니다. 중간이나 끝에 넣으면 인정되지 않습니다. ### 3. 무료 수신거부 방법 수신자가 **비용을 부담하지 않고** 거부할 수 있어야 합니다. 080 무료 수신거부 번호가 일반적입니다. ```text (광고)[브랜드명] 가을 세일 최대 50% 자세히 보기 https://sendgo.io/s/k7Rm2xQ 무료수신거부 080-000-0000 ``` ### 4. 야간 발송 금지 (21:00 ~ 08:00) **21시부터 익일 08시까지** 광고성 메시지를 보낼 수 없습니다. 이 시간대에 보내려면 야간 수신에 대한 **별도 동의**를 따로 받아야 합니다. 샌드고는 이 구간의 광고성 발송 요청을 검증합니다. 다만 코드 쪽에서도 막아 두는 편이 안전합니다 — 배치 작업이 예정보다 늦게 실행돼 21시를 넘기는 일이 흔합니다. ## 코드에서 강제하기 ### 발송 시각 가드 ```php hour >= 8 && $at->hour < 21; } /** 제한 구간이면 다음 허용 시각(익일 08:00)으로 미룬다. */ public static function nextAllowed(CarbonImmutable $at): CarbonImmutable { if (self::isAllowed($at)) { return $at; } return $at->hour >= 21 ? $at->addDay()->setTime(8, 0) : $at->setTime(8, 0); } } ``` ```typescript // 서버 시간대에 의존하지 않고 KST 기준으로 판단한다. function kstHour(date = new Date()): number { return Number( new Intl.DateTimeFormat('en-GB', { timeZone: 'Asia/Seoul', hour: '2-digit', hour12: false, }).format(date), ); } const canSendAd = () => { const h = kstHour(); return h >= 8 && h < 21; }; ``` ```python from datetime import datetime from zoneinfo import ZoneInfo def can_send_ad(at: datetime | None = None) -> bool: at = at or datetime.now(ZoneInfo("Asia/Seoul")) return 8 <= at.hour < 21 ``` ### 본문 검증 ```php addHours(3)); $sendgo->brandMessage->send([ 'targeting' => 'M', 'scheduleType' => 'SCHEDULED', 'at' => $at->format('Y-m-d H:i:s'), 'adFlag' => 'Y', // ... ]); ``` ## 다음 단계 - [브랜드메시지 보내기](/ko/cookbook/send-brand-message) - [예약 발송](/ko/cookbook/scheduled-send) - [SMS · LMS · MMS 보내기](/ko/cookbook/send-sms) --- 카카오 친구톡은 **2025-12-31 로 종료**되었습니다. 후속 채널은 브랜드메시지입니다. 가장 먼저 알아야 할 것: **기존 코드는 깨지지 않습니다.** 엔드포인트는 유지되고 호출도 성공합니다. 하지만 실제로 나가는 것은 카카오가 자동 대체한 브랜드메시지이므로, 무엇이 달라지는지는 알고 있어야 합니다. ## 지금 무슨 일이 일어나고 있나 | | 2025-12-31 이전 | 2026-01-01 이후 | | --- | --- | --- | | `/api/v2/friends/send` 호출 | 친구톡 발송 | 호출 성공, **브랜드메시지(자유형)로 자동 대체 발송** | | 엔드포인트 제거 여부 | — | 제거되지 않음 | | 신규 개발 | 친구톡 | **브랜드메시지** | ## 언제 옮겨야 하나 ### 지금 옮겨야 하는 경우 - **템플릿 기반 리치 타입**이 필요하다 — `FL`(리스트), `FC`(커머스), `FM`(복합), `FP`(프리미엄 동영상), `FA`(캐러셀) - **채널 친구가 아닌 수신자**에게 보내야 한다 — `targeting: N` 또는 `I` - **수신 동의한 전체 채널 친구에게 동보**를 보내야 한다 — `targeting: F`, 수신자 목록 없이 이 셋은 친구톡으로는 애초에 불가능했던 것들입니다. 브랜드메시지로 옮기는 이유가 "종료 대응"이라기보다 **기능 확장**에 가깝습니다. ### 당장 안 옮겨도 되는 경우 - **자유 본문 타입(`FT`/`FI`/`FW`)을 개별 수신자에게** 보내는 코드 이 조합은 오히려 친구톡 엔드포인트를 **계속 써야 합니다.** 브랜드메시지 엔드포인트는 같은 조합에 `NOT_A_BRAND_MESSAGE` 를 반환합니다. ```typescript // 이건 그대로 둔다 — 브랜드메시지로 옮기면 NOT_A_BRAND_MESSAGE 가 난다. await sendgo.friendtalk.send({ messageType: 'FT', content: '안녕하세요! 이벤트 안내드립니다.', contacts: [{ contact: '01012345678' }], }); ``` ### 대체 발송 자체를 원하지 않는다면 친구톡 요청이 브랜드메시지로 대체되는 것을 원하지 않으면, 친구톡을 시도하지 말고 [문자](/ko/cookbook/send-sms)로 보내세요. ## 메시지 타입 대응표 **요청에는 친구톡 코드를 그대로 넘깁니다.** 변환은 서버가 합니다. | 친구톡 | 브랜드메시지 | 내용 | | --- | --- | --- | | `FT` | `BT` | 텍스트 | | `FI` | `BI` | 이미지 | | `FW` | `BW` | 와이드 이미지 | | `FL` | `BL` | 리스트 | | `FC` | `BC` | 커머스 | | `FM` | `BM` | 복합 | | `FP` | `BP` | 프리미엄 동영상 | | `FA` | `BA` | 캐러셀 | 코드에서 `BT`, `BI` 로 바꿔 쓰지 마세요. 넘기는 값은 `FT`, `FI` 그대로입니다. ## 옮기는 법 ### 옮기기 전 (친구톡, 개별 수신자) ```typescript await sendgo.friendtalk.send({ messageType: 'FI', content: '이번 주 특가 상품을 확인하세요!', imageUrl: 'https://cdn.example.com/banner.jpg', adFlag: 'Y', contacts: [{ contact: '01012345678' }], }); ``` ### 옮긴 뒤 (브랜드메시지, 템플릿 기반) 브랜드메시지는 콘솔에 등록한 템플릿을 참조합니다. `friendTemplateUuid` 가 필요합니다. ```typescript await sendgo.brandMessage.send({ targeting: 'M', // 채널 친구 messageType: 'FL', // 리스트형 리치 템플릿 friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340', adFlag: 'Y', contacts: [{ contact: '01012345678', var1: '29,000원' }], }); // 전체 친구 동보 — 수신자 목록이 필요 없다 await sendgo.brandMessage.broadcast({ messageType: 'FW', friendTemplateUuid: '9cd5460b-6458-4edc-9b11-c26d3013c340', adFlag: 'Y', }); ``` 가장 큰 차이는 **본문을 요청에 싣지 않는다**는 점입니다. 친구톡은 `content` 를 그때그때 넘겼지만, 브랜드메시지의 리치 타입은 등록된 템플릿을 참조하고 변수만 채웁니다. ## 캠페인 조회도 갈라진다 ```bash # 브랜드메시지 캠페인 (BRAND_GROUP, BRAND_BASIC) GET /api/v2/brand-messages # 친구톡 캠페인 GET /api/v2/friends ``` 브랜드메시지 목록 엔드포인트는 친구톡 캠페인을 반환하지 않습니다. 두 채널을 함께 쓰고 있다면 리포트 코드에서 둘 다 조회해야 합니다. ## 체크리스트 - [ ] 친구톡 발송 코드를 전부 찾았다 (`friendtalk`, `friends/send`) - [ ] 각 호출이 `FT`/`FI`/`FW` + 개별 수신자인지 확인했다 → 그렇다면 **그대로 둔다** - [ ] 리치 타입·비친구·동보가 필요한 곳을 골라냈다 → 브랜드메시지로 이전 - [ ] 콘솔에 브랜드메시지 템플릿을 등록하고 `friendTemplateUuid` 를 확보했다 - [ ] 광고성이라면 `adFlag: 'Y'` 와 [광고 규칙](/ko/cookbook/ad-message-rules)을 확인했다 - [ ] 리포트·통계 코드가 두 엔드포인트를 모두 조회한다 - [ ] SDK 를 1.2.1 이상으로 올렸다 (친구톡 종료가 반영된 버전) ## 다음 단계 - [브랜드메시지 보내기](/ko/cookbook/send-brand-message) - [광고성 메시지 규칙](/ko/cookbook/ad-message-rules) ---