feat(chat): 채팅 쿼터 광고 충전을 추가한다

This commit is contained in:
Yu Sung
2026-04-30 14:23:15 +09:00
parent 714ad459b0
commit 5823f6ddb2
10 changed files with 423 additions and 133 deletions

View File

@@ -28,7 +28,7 @@ final class ChatRoomViewModel: ObservableObject {
}
}
@Published private(set) var countdownText: String = "00:00:00"
@Published private(set) var totalRemaining: Int = 0
@Published private(set) var showQuotaNoticeView: Bool = false
@Published private(set) var showSendingMessage: Bool = false
@@ -72,8 +72,6 @@ final class ChatRoomViewModel: ObservableObject {
private var hasMoreMessages: Bool = true
private var nextCursor: Int64? = nil
private var timer: Timer?
// MARK: - Actions
func sendMessage() {
guard !messageText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
@@ -125,7 +123,7 @@ final class ChatRoomViewModel: ObservableObject {
let decoded = try jsonDecoder.decode(ApiResponse<SendChatMessageResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
self.messages.append(contentsOf: data.messages)
self.updateQuota(nextRechargeAtEpoch: data.nextRechargeAtEpoch)
self.updateQuota(totalRemaining: data.totalRemaining)
} else {
self.errorMessage = decoded.message ?? I18n.Common.commonError
self.isShowPopup = true
@@ -177,7 +175,7 @@ final class ChatRoomViewModel: ObservableObject {
self?.hasMoreMessages = data.hasMoreMessages
self?.nextCursor = data.messages.last?.messageId
self?.updateQuota(nextRechargeAtEpoch: data.nextRechargeAtEpoch)
self?.updateQuota(totalRemaining: data.totalRemaining)
} else {
if let message = decoded.message {
self?.errorMessage = message
@@ -275,10 +273,25 @@ final class ChatRoomViewModel: ObservableObject {
.store(in: &subscription)
}
func purchaseChatQuota() {
func purchaseChatQuota(canOption: ChatRoomQuotaCanOption) {
purchaseChatQuota(chargeType: .can, canOption: canOption)
}
func showRewardedAdForChatQuota() {
_Concurrency.Task {
await YandexRewardedAdManager.shared.showAdIfAvailable(for: .chatRoomQuota) { [weak self] in
self?.purchaseChatQuota(chargeType: .ad, canOption: nil)
}
}
}
private func purchaseChatQuota(
chargeType: ChatRoomQuotaChargeType,
canOption: ChatRoomQuotaCanOption?
) {
isLoading = true
repository.purchaseChatQuota(roomId: roomId)
repository.purchaseChatQuota(roomId: roomId, chargeType: chargeType, canOption: canOption)
.receive(on: DispatchQueue.main)
.sink { result in
switch result {
@@ -295,10 +308,12 @@ final class ChatRoomViewModel: ObservableObject {
let decoded = try jsonDecoder.decode(ApiResponse<ChatQuotaStatusResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
self?.updateQuota(nextRechargeAtEpoch: data.nextRechargeAtEpoch)
self?.updateQuota(totalRemaining: data.totalRemaining)
let can = UserDefaults.int(forKey: .can)
UserDefaults.set(can - 30, forKey: .can)
if let canOption {
let can = UserDefaults.int(forKey: .can)
UserDefaults.set(can - canOption.needCan, forKey: .can)
}
} else {
if let message = decoded.message {
self?.errorMessage = message
@@ -385,7 +400,7 @@ final class ChatRoomViewModel: ObservableObject {
chatRoomBgImageUrl = nil
roomId = 0
countdownText = "00:00:00"
totalRemaining = 0
showQuotaNoticeView = false
showSendingMessage = false
@@ -421,7 +436,7 @@ final class ChatRoomViewModel: ObservableObject {
let decoded = try jsonDecoder.decode(ApiResponse<ChatQuotaStatusResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
self?.updateQuota(nextRechargeAtEpoch: data.nextRechargeAtEpoch)
self?.updateQuota(totalRemaining: data.totalRemaining)
} else {
if let message = decoded.message {
self?.errorMessage = message
@@ -442,78 +457,18 @@ final class ChatRoomViewModel: ObservableObject {
.store(in: &subscription)
}
private func updateQuota(nextRechargeAtEpoch: Int64?) {
isLoading = true
stopTimer()
// epoch
guard let nextRechargeAtEpoch else {
countdownText = "00:00:00"
showQuotaNoticeView = false
isLoading = false
return
private func updateQuota(totalRemaining: Int) {
self.totalRemaining = totalRemaining
showQuotaNoticeView = totalRemaining <= 0
prepareRewardedAdIfNeeded(totalRemaining: totalRemaining)
}
private func prepareRewardedAdIfNeeded(totalRemaining: Int) {
guard totalRemaining <= 1 else { return }
_Concurrency.Task {
await YandexRewardedAdManager.shared.preloadAd(for: .chatRoomQuota)
}
// 1
let remainMs = remainingMs(to: nextRechargeAtEpoch)
updateCountdownText(remainMs)
// 0
guard remainMs > 0 else {
checkQuotaStatus()
return
}
isLoading = false
showQuotaNoticeView = true
// (1 )
startTimer(targetEpoch: nextRechargeAtEpoch)
}
private func updateCountdownText(_ remainMs: Int64) {
countdownText = remainMs > 0 ? formatMillisToHms(remainMs) : "00:00:00"
}
private func startTimer(targetEpoch: Int64) {
stopTimer()
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self else { return }
let remain = self.remainingMs(to: targetEpoch)
self.updateCountdownText(remain)
if remain == 0 {
self.stopTimer()
self.checkQuotaStatus()
}
}
if let t = timer { RunLoop.main.add(t, forMode: .common) }
}
func stopTimer() {
timer?.invalidate()
timer = nil
}
private func remainingMs(to epoch: Int64) -> Int64 {
let ms = normalizeToMs(epoch)
let nowMs = Int64(Date().timeIntervalSince1970 * 1000)
let fudgeMs: Int64 = 5000
// Kotlin
return max(ms - nowMs + fudgeMs, 0)
}
/// /
private func normalizeToMs(_ epoch: Int64) -> Int64 {
epoch < 1_000_000_000_000 ? epoch * 1000 : epoch
}
private func formatMillisToHms(_ ms: Int64) -> String {
let total = ms / 1000
let h = total / 3600
let m = (total % 3600) / 60
let s = total % 60
return String(format: "%02d:%02d:%02d", h, m, s)
}
private func getSavedBackgroundImageId() -> Int? {