GuidesAPI GuideChangelog
Log In
Guides

리워드 연동

리워드 연동 — 소스라이브 플레이어

시청자가 리워드 조건을 달성하고 버튼을 클릭하면 sauceflexMoveReward 이벤트가 전송됩니다. 이 이벤트를 수신하여 자사몰의 리워드 API 호출, 포인트 지급, 링크 이동 등 원하는 처리를 구현할 수 있습니다.

예상 소요 시간: 20분
📋 선행 조건: 브릿지 이벤트 연동 완료
⚙️ 라이브콘솔에서 리워드 등록 필요
리워드 동작 흐름

리워드는 라이브콘솔에서 등록하고 연결 URL을 설정해야 합니다. 시청자가 리워드에 참가한 후 일정 시간이 지나면 CLICK 버튼이 노출되며, 이를 클릭할 때 이벤트가 전송됩니다.

1
라이브콘솔에서 리워드 등록
리워드 이름, 조건, 연결 URL을 설정합니다. 연결 URL은 필수값이며 linkUrl로 전달됩니다.
2
시청자가 리워드 참가
라이브 중 시청자가 리워드 참가 버튼을 클릭합니다.
3
참가 후 1분 경과 — CLICK 버튼 노출
참가 클릭 이후 1분이 지나면 플레이어에 CLICK 버튼이 표시됩니다.
⏱ 1분 대기 필요
4
CLICK 버튼 클릭 → 이벤트 전송
시청자가 버튼을 클릭하면 sauceflexMoveReward 이벤트가 자사몰로 전송됩니다.
🎯 브릿지 이벤트 발생
5
자사몰에서 처리
리워드 API 호출, 포인트 지급, 링크 이동 등 자사몰의 비즈니스 로직을 수행합니다.
리워드 참가 버튼
① 참가 버튼
1분 후
리워드 CLICK 버튼
② CLICK 버튼
⚠️ 연결 URL 필수: 리워드 등록 시 연결 URL이 비어 있으면 CLICK 버튼이 노출되지 않습니다. 라이브콘솔에서 반드시 설정하세요.
이벤트 데이터

sauceflexMoveReward 수신 시 jsonData.params에 아래 필드가 전달됩니다.

필드타입필수설명
linkUrl String 필수 라이브콘솔에서 설정한 리워드 연결 URL
broadcastIdx String 필수 이벤트가 발생한 라이브 ID
처리 시나리오

리워드 클릭 시 로그인 상태에 따라 처리 방식이 달라집니다. 두 경우를 모두 고려하여 구현하세요.

자사몰 리워드 API를 호출하여 포인트·쿠폰 등을 즉시 지급합니다. 회원 식별값을 API에 함께 전달하세요.
⚠️ 로그인 하지 않은 상태
linkUrl로 이동시킵니다. 로그인 후 리워드를 수령하도록 안내하는 페이지로 연결하는 것을 권장합니다.
구현 예시

로그인 여부를 확인하여 분기 처리하는 패턴입니다. YOUR_REWARD_APImemberData는 자사몰 환경에 맞게 교체하세요.

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
    }
}
리워드 연동 구현이 완료되었습니다. 라이브 시작 알림이 필요하면 라이브 알림 버튼 연동를 이어서 확인하세요. 각 이벤트의 상세 데이터는 브릿지 이벤트 레퍼런스에 있습니다.


bot에 문의하기