리워드 연동
시청자가 리워드 조건을 달성하고 버튼을 클릭하면 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(payload: String?) { val params = JSONObject(payload ?: "{}") val shopId = params.optString("shopId", "") 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(broadcastId·shopId·shopName) case "sauceflexMoveLogin": let shopId = parseParams()?["shopId"] as? String ?? "" 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 6 days ago
Did this page help you?
