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(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
    }
}
라이브 시작 알림은 라이브 알림 버튼 연동에서 다룹니다. 각 이벤트의 상세 데이터는 브릿지 이벤트 레퍼런스에 있습니다.

Did this page help you?
🏠 소스라이브 🎬 소스클립 🔗 소스링크 📢 소스애드
🧩 API 가이드
🆕 최근 업데이트
💬 도움이 더 필요하신가요?
메일로 문의 카카오톡 채널로 문의
담당자에게 문의하기

궁금한 점이나 불편했던 점을 남겨주시면 담당자가 확인 후 답변드릴게요.

가이드 챗봇 BETA
* AI를 활용해 답변해서 사실과 다를 수도 있어요. 더 궁금한 사항은 '담당자에게 문의하기'를 이용해주세요.
💡 궁금한 솔루션(예: 라이브, 링크 등)을 함께 적어주시면 더 정확한 답변을 받을 수 있어요.