리워드 연동
시청자가 리워드 조건을 달성하고 버튼을 클릭하면 sauceflexMoveReward 이벤트가 전송됩니다. 이 이벤트를 수신하여 자사몰의 리워드 API 호출, 포인트 지급, 링크 이동 등 원하는 처리를 구현할 수 있습니다.
리워드 동작 흐름
리워드는 라이브콘솔에서 등록하고 연결 URL을 설정해야 합니다. 시청자가 리워드에 참가한 후 일정 시간이 지나면 CLICK 버튼이 노출되며, 이를 클릭할 때 이벤트가 전송됩니다.
1
라이브콘솔에서 리워드 등록
리워드 이름, 조건, 연결 URL을 설정합니다. 연결 URL은 필수값이며
linkUrl로 전달됩니다.2
시청자가 리워드 참가
라이브 중 시청자가 리워드 참가 버튼을 클릭합니다.
3
참가 후 1분 경과 — CLICK 버튼 노출
참가 클릭 이후 1분이 지나면 플레이어에 CLICK 버튼이 표시됩니다.
⏱ 1분 대기 필요
4
CLICK 버튼 클릭 → 이벤트 전송
시청자가 버튼을 클릭하면
sauceflexMoveReward 이벤트가 자사몰로 전송됩니다.🎯 브릿지 이벤트 발생
5
자사몰에서 처리
리워드 API 호출, 포인트 지급, 링크 이동 등 자사몰의 비즈니스 로직을 수행합니다.
→
1분 후
⚠️ 연결 URL 필수: 리워드 등록 시 연결 URL이 비어 있으면 CLICK 버튼이 노출되지 않습니다. 라이브콘솔에서 반드시 설정하세요.
이벤트 데이터
sauceflexMoveReward 수신 시 jsonData.params에 아래 필드가 전달됩니다.
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| linkUrl | String | 필수 | 라이브콘솔에서 설정한 리워드 연결 URL |
| broadcastIdx | String | 필수 | 이벤트가 발생한 라이브 ID |
처리 시나리오
리워드 클릭 시 로그인 상태에 따라 처리 방식이 달라집니다. 두 경우를 모두 고려하여 구현하세요.
✅ 로그인 상태
자사몰 리워드 API를 호출하여 포인트·쿠폰 등을 즉시 지급합니다. 회원 식별값을 API에 함께 전달하세요.
⚠️ 로그인 하지 않은 상태
linkUrl로 이동시킵니다. 로그인 후 리워드를 수령하도록 안내하는 페이지로 연결하는 것을 권장합니다.
구현 예시
로그인 여부를 확인하여 분기 처리하는 패턴입니다. YOUR_REWARD_API와 memberData는 자사몰 환경에 맞게 교체하세요.
JavaScript — 기본 패턴
case 'sauceflexMoveReward': { const { linkUrl, broadcastIdx } = jsonData.params // 로그인 상태이면 자사몰 리워드 API 호출 if (isLogin && memberData) { fetch(`${YOUR_REWARD_API}/${memberData}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ broadcastIdx }) }) .then((res) => res.json()) .then((res) => { if (res.success) { window.alert('리워드 포인트가 지급되었습니다.') } else { window.alert('리워드 포인트가 모두 소진되었습니다.') } }) .catch(() => window.alert('리워드 지급 중 오류가 발생했습니다.')) } else { // 게스트 모드이면 연결 URL(로그인 안내 페이지 등)로 이동 location.href = linkUrl } break }
Kotlin — SauceflexBridge.kt
@JavascriptInterface fun sauceflexMoveReward(payload: String?) { val params = JSONObject(payload ?: "{}") val linkUrl = params.optString("linkUrl", "") val broadcastIdx = params.optString("broadcastIdx", "") activity?.runOnUiThread { if (isLoggedIn() && memberData != null) { // 자사몰 리워드 API 호출 (예: Retrofit / OkHttp) // YOUR_REWARD_API + memberData + broadcastIdx } else { // 게스트 모드 → linkUrl로 이동 val intent = Intent(Intent.ACTION_VIEW, Uri.parse(linkUrl)) context.startActivity(intent) } } }
Kotlin — WebView 등록
webView.addJavascriptInterface(SauceflexBridge(context, activity), "sauceflex")
Swift — WKWebView 등록
contentController.add(self, name: "sauceflexMoveReward")
Swift — ViewController.swift
func userContentController( _ userContentController: WKUserContentController, didReceive message: WKScriptMessage ) { switch message.name { case "sauceflexMoveReward": guard let body = message.body as? String, let data = body.data(using: .utf8), let params = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return } let linkUrl = params["linkUrl"] as? String ?? "" let broadcastIdx = params["broadcastIdx"] as? String ?? "" if isLoggedIn, let memberData = self.memberData { // 자사몰 리워드 API 호출 — broadcastIdx, memberData 전달 DispatchQueue.main.async { /* 결과 UI 처리 */ } } else { // 게스트 모드 → linkUrl로 이동 guard let url = URL(string: linkUrl) else { return } DispatchQueue.main.async { UIApplication.shared.open(url) } } default: break } }
⚡ 디버깅 팁: 개발 단계에서는 이벤트 데이터를 먼저 로그로 찍어 전달값을 확인한 뒤 API 연동을 진행하세요.
브릿지 이벤트 리스너 통합 예시
로그인 연동·상품 클릭·쿠폰·리워드를 하나의 리스너로 통합 관리하는 구조입니다. 실제 서비스에서 권장하는 패턴입니다.
JavaScript — 통합 리스너
window.addEventListener('message', (e) => { if (typeof e.data !== 'string') return const { key, params } = JSON.parse(e.data) switch (key) { // 로그인 연동 case 'sauceflexMoveLogin': window.location.href = `/signin?returnUrl=${window.location.href}` break // 상품 클릭 case 'sauceflexMoveProduct': if (params.linkUrl) window.location.href = params.linkUrl break // 쿠폰 case 'sauceflexMoveCoupon': // couponType 에 따라 분기 처리 (쿠폰 연동 가이드 참고) break // 리워드 case 'sauceflexMoveReward': { if (isLogin && memberData) { // 리워드 API 호출 } else { location.href = params.linkUrl } break } } })
Kotlin — SauceflexBridge.kt (전체)
class SauceflexBridge( private val context: Context, private val activity: Activity?, private val webView: WebView ) { // ── 로그인 연동 ───────────────────────────────────── @JavascriptInterface fun sauceflexMoveLogin() { val returnUrl = webView.url ?: "" activity?.runOnUiThread { val intent = Intent(context, LoginActivity::class.java) intent.putExtra("returnUrl", returnUrl) context.startActivity(intent) } } // ── 상품 클릭 ─────────────────────────────────────── @JavascriptInterface fun sauceflexMoveProduct(payload: String?) { val params = JSONObject(payload ?: "{}") val isSoldout = params.optBoolean("isSoldout", false) val externalId = params.optString("externalProductId", "") val linkUrl = params.optString("linkUrl", "") if (isSoldout) { activity?.runOnUiThread { Toast.makeText(context, "품절된 상품입니다.", Toast.LENGTH_SHORT).show() }; return } val url = if (externalId.isNotEmpty()) "https://myshop.com/products/$externalId" else linkUrl if (url.isEmpty()) return activity?.runOnUiThread { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) } } // ── 쿠폰 ──────────────────────────────────────────── @JavascriptInterface fun sauceflexMoveCoupon(payload: String?) { val params = JSONObject(payload ?: "{}") val couponType = params.optString("couponType", "") val linkUrl = params.optString("linkUrl", "") val couponCode = params.optString("couponCode", "") when (couponType) { "link", "newWindow" -> { if (linkUrl.isEmpty()) return activity?.runOnUiThread { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(linkUrl))) } } "download" -> { /* 쿠폰 API 호출 후 Toast 표시 */ } "custom" -> { val meta = JSONObject(params.optString("metaData", "{}")); /* meta["couponId"]로 API 호출 */ } "apiDownload" -> { /* 플레이어가 자동 처리 */ } } } // ── 리워드 ────────────────────────────────────────── @JavascriptInterface fun sauceflexMoveReward(payload: String?) { val params = JSONObject(payload ?: "{}") val linkUrl = params.optString("linkUrl", "") val broadcastIdx = params.optString("broadcastIdx", "") activity?.runOnUiThread { if (isLoggedIn() && memberData != null) { /* 리워드 API 호출 */ } else { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(linkUrl))) } } } }
Kotlin — WebView 등록
webView.addJavascriptInterface(SauceflexBridge(context, activity, webView), "sauceflex")
Swift — WKWebView 전체 핸들러 등록
["sauceflexMoveLogin", "sauceflexMoveProduct", "sauceflexMoveCoupon", "sauceflexMoveReward"] .forEach { contentController.add(self, name: $0) }
Swift — ViewController.swift (전체)
func userContentController( _ userContentController: WKUserContentController, didReceive message: WKScriptMessage ) { func parseParams() -> [String: Any]? { guard let body = message.body as? String, let data = body.data(using: .utf8), let params = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil } return params } switch message.name { // 로그인 연동 — params 없음 case "sauceflexMoveLogin": let returnUrl = webView.url?.absoluteString ?? "" DispatchQueue.main.async { let vc = LoginViewController(); vc.returnUrl = returnUrl self.navigationController?.pushViewController(vc, animated: true) } // 상품 클릭 case "sauceflexMoveProduct": guard let p = parseParams() else { return } let isSoldout = p["isSoldout"] as? Bool ?? false let externalId = p["externalProductId"] as? String ?? "" let linkUrl = p["linkUrl"] as? String ?? "" if isSoldout { return } let target = !externalId.isEmpty() ? "https://myshop.com/products/\(externalId)" : linkUrl guard let url = URL(string: target) else { return } DispatchQueue.main.async { UIApplication.shared.open(url) } // 쿠폰 case "sauceflexMoveCoupon": guard let p = parseParams() else { return } switch p["couponType"] as? String ?? "" { case "link", "newWindow": guard let url = URL(string: p["linkUrl"] as? String ?? "") else { return } DispatchQueue.main.async { UIApplication.shared.open(url) } case "download": break /* 쿠폰 API 호출 */ case "custom": break /* metaData 파싱 후 API 호출 */ case "apiDownload": break /* 플레이어 자동 처리 */ default: break } // 리워드 case "sauceflexMoveReward": guard let p = parseParams() else { return } let rewardUrl = p["linkUrl"] as? String ?? "" let broadcastIdx = p["broadcastIdx"] as? String ?? "" if isLoggedIn, let memberData = self.memberData { /* 리워드 API 호출 — broadcastIdx, memberData 전달 */ } else { guard let url = URL(string: rewardUrl) else { return } DispatchQueue.main.async { UIApplication.shared.open(url) } } default: break } }
✅ 리워드 연동 구현이 완료되었습니다. 라이브 시작 알림이 필요하면 라이브 알림 버튼 연동를 이어서 확인하세요. 각 이벤트의 상세 데이터는 브릿지 이벤트 레퍼런스에 있습니다.
Updated about 24 hours ago